This commit is contained in:
Tim Griesser 2018-11-08 09:04:48 -08:00
parent 3d5387bcf1
commit 446047a918
67 changed files with 3064 additions and 946 deletions

View File

@ -6,5 +6,6 @@
"**/CVS": true,
"**/.DS_Store": true,
"**/node_modules": true
}
},
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -68,7 +68,7 @@ type EnumMemberList = ["", "", ""];
### `GQLiteralScalar(typeName, GraphQLScalarTypeConfig)`
The `GQLiteralScalar` doesn't provide much added value over the graphql-js `GraphQLScalarType` object, it mainly exists for consistency with the rest of the GQLiteral library.
The `GQLiteralScalar` doesn't provide any added value over the graphql-js `GraphQLScalarType` object, it only exists for consistency with the rest of the GQLiteral library.
```js
const DateScalar = GQLiteralScalar("Date", {
@ -152,7 +152,7 @@ type DefinitionBlockObject = {
/**
* Specify at the type-level whether fields are considered null by default.
* Takes precedence over `defaultNull` if set on the schema.
* Takes precedence over `defaultNull` if it is set on the schema.
*/
defaultNull(nullable: boolean): void;
};

View File

@ -23,9 +23,9 @@ code chunks. The most common approach is to break up types into files, either on
However you end up structuring your files, they ultimately all need to be imported and passed to the `GQLiteralSchema` function, and keeping a consistent approach to file naming makes it simpler
```
import * as userTypes from './gqltieral/user'
import * as postTypes from './gqltieral/post'
import * as commentTypes from './gqltieral/comment'
import * as userTypes from './gqliteral/user'
import * as postTypes from './gqliteral/post'
import * as commentTypes from './gqliteral/comment'
```
You could also consolidate this in an `index.js` or similar export file:
@ -36,7 +36,7 @@ export * from './post'
export * from './comment'
```
Using that file to build the schema
Using that file to build the schema:
```
import * as allTypes from './gqliteral'

View File

@ -4,4 +4,4 @@ title: Frequently Asked Questions
sidebar_label: FAQ
---
## Why
## Why isn't this a class-based API

View File

@ -34,28 +34,42 @@ The schema requires that an Object named `Query` be provided at the top-level.
const Query = GQLiteralObject("Query", (t) => {
t.field("account", "Account", {
args: {
name: t.stringArg({
name: GQLiteralArg("String", {
description:
"Providing the name of the account holder will search for accounts matching that name",
}),
status: t.fieldArg("StatusEnum"),
status: GQLiteralArg("StatusEnum"),
},
});
t.field("accountsById", "Account", {
list: true,
args: {
ids: t.intArg({ list: true }),
ids: GQLiteralArg("Int", { list: true }),
},
});
t.field("accounts", "AccountConnection", {
args: {
limit: t.intArg({ required: true }),
limit: GQLiteralArg("Int", { required: true }),
},
});
});
const Node = GQLiteralObject("Node", (t) => {
t.id("id", { description: "Node ID" });
});
const UserFields = GQLiteralAbstractType((t) => {
t.string("username");
t.string("email");
});
const Account = GQLiteralObject("Account", (t) => {
t.int("id", { description: "Primary key of the account" });
t.implements("Node");
t.mix(UserFields);
});
const schema = GQLiteralSchema({
types: [Account, Node, Query],
});
```
@ -67,7 +81,7 @@ One benefit of GraphQL is the strict enforcement and guarentees of null values i
`GQLiteral` breaks slightly from this convention, and instead assumes by all fields are "non-null" unless otherwise specified with a `nullable` option set to `true`. It also assumes all arguments are nullable unless `required` is set to true.
The rationale being that for most applications, the case of returning `null` to mask errors and still properly handle this partial response is exceptional, and should be handled as such by manually defining these places where the schema could break in this regard.
The rationale being that for most applications, the case of returning `null` to mask errors and still properly handle this partial response is exceptional, and should be handled as such by manually defining these places where a schema could break in this regard.
If you find yourself wanting this the other way around, there is a `defaultNull` option for the `GQLiteralSchema` which will make all fields nullable unless `nullable: false` is specified during field definition.
@ -92,7 +106,7 @@ When hand constructing schemas in a GraphQL Interface Definition Language (IDL)
Have you ever found yourself re-using the same set of fields in multiple types, but it doesn't necessarily warrant the ceremony of being a named `interface` type? Or maybe you have a set of fields that are mirrored in both the input & output.
## Resolving Props
## Resolving: Property
One common idiom in GraphQL is exposing fields that mask or rename the property name on the backing object. GQLiteral provides a `property` option on the field configuration object, for conveniently accessing an object property without needing to define a resolver function.
@ -106,6 +120,10 @@ const User = GQLiteralObject("User", (t) => {
When using the TypeScript, configuring the [backing object type](typescript-setup.md) definitions will check for the existence of the property on the object, and error if a non-existent property is referenced.
## 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
## Generating the IDL file
When making a change to GraphQL it is often beneficial to see how exactly this changes the output types. GQLiteral makes this simple, provide a path to where you want the schema file to be output and this file will automatically be generated when `process.env.NODE_ENV === "development"`.

View File

@ -1,10 +0,0 @@
{
"name": "gqliteral-simple-app",
"dependencies": {
"express": "^4.16.4",
"apollo-server-express": "^2.1.0"
},
"devDependencies": {
"@types/express": "^4.16.0"
}
}

View File

@ -1 +0,0 @@
import * as types from "./types";

View File

@ -1,7 +0,0 @@
import { GQLiteralObject } from "../../../../src";
export const Blog = GQLiteralObject("Blog", (t) => {
t.field("id", "ID");
t.field("title", "String", { description: "The title of the blog" });
t.field("posts", "Post", { list: true });
});

View File

@ -1,12 +0,0 @@
import { GQLiteralObject, GQLiteralInputObject } from "../../../../src";
export const Comment = GQLiteralObject("Comment", t => {
t.field("id", "ID");
});
export const CreateCommentInput = GQLiteralInputObject(
"CreateCommentInput",
t => {
t.mix("Comment");
}
);

View File

@ -1,5 +0,0 @@
export * from "./blog";
export * from "./comment";
export * from "./node";
export * from "./post";
export * from "./user";

View File

@ -1,5 +0,0 @@
import { GQLiteralInterface } from "../../../../src";
export const Node = GQLiteralInterface("Node", t => {
t.resolveType(() => {});
});

View File

@ -1,5 +0,0 @@
import { GQLiteralObject } from "../../../../src";
export const Post = GQLiteralObject("Post", t => {
t.implements("Node");
});

View File

@ -1 +0,0 @@
// export const Query = GQLitQuery();

View File

@ -1,5 +0,0 @@
import { GQLiteralObject } from "../../../../src";
export const User = GQLiteralObject("User", t => {
t.mix("Node");
});

View File

@ -0,0 +1,12 @@
# GQLiteral Star Wars:
This is meant to demonstrate some of the features of GQLiteral.
### Changes from the graphql-js example:
- Backing data types are changed to `snake_case` to demonstrate the `property` feature.
- Removes the "secretBackstory" field which always throws an Error
## License
MIT

View File

@ -0,0 +1,12 @@
{
"name": "gqliteral-swapi-example",
"version": "0.0.0",
"dependencies": {
"graphql": "^14.0.2",
"gqliteral": "1.0.0"
},
"devDependencies": {
"jest": "^23.6.0",
"ts-jest": "^23.10.4"
}
}

View File

@ -0,0 +1,20 @@
/**
* These are Flow types which correspond to the schema.
* They represent the shape of the data visited during field resolution.
*/
export interface Character {
id: string;
name: string;
friends: string[];
appears_in: number[];
}
export interface Human extends Character {
type: "Human";
home_planet?: string;
}
export interface Droid extends Character {
type: "Droid";
primary_function: string;
}

View File

@ -0,0 +1,134 @@
import { Character, Human, Droid } from "./backingTypes";
/**
* Copied from GraphQL JS:
*
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* This defines a basic set of data for our Star Wars Schema.
*
* This data is hard coded for the sake of the demo, but you could imagine
* fetching this data from a backend service rather than from hardcoded
* JSON objects in a more complex demo.
*/
const luke = {
type: "Human",
id: "1000",
name: "Luke Skywalker",
friends: ["1002", "1003", "2000", "2001"],
appears_in: [4, 5, 6],
home_planet: "Tatooine",
};
const vader = {
type: "Human",
id: "1001",
name: "Darth Vader",
friends: ["1004"],
appears_in: [4, 5, 6],
home_planet: "Tatooine",
};
const han = {
type: "Human",
id: "1002",
name: "Han Solo",
friends: ["1000", "1003", "2001"],
appears_in: [4, 5, 6],
};
const leia = {
type: "Human",
id: "1003",
name: "Leia Organa",
friends: ["1000", "1002", "2000", "2001"],
appears_in: [4, 5, 6],
home_planet: "Alderaan",
};
const tarkin = {
type: "Human",
id: "1004",
name: "Wilhuff Tarkin",
friends: ["1001"],
appears_in: [4],
};
const humanData = {
"1000": luke,
"1001": vader,
"1002": han,
"1003": leia,
"1004": tarkin,
} as { [key in string]: Human };
const threepio = {
type: "Droid",
id: "2000",
name: "C-3PO",
friends: ["1000", "1002", "1003", "2001"],
appears_in: [4, 5, 6],
primary_function: "Protocol",
};
const artoo = {
type: "Droid",
id: "2001",
name: "R2-D2",
friends: ["1000", "1002", "1003"],
appears_in: [4, 5, 6],
primary_function: "Astromech",
};
const droidData = {
"2000": threepio,
"2001": artoo,
} as { [key in string]: Droid };
/**
* Helper function to get a character by ID.
*/
function getCharacter(id: string) {
// Returning a promise just to illustrate GraphQL.js's support.
return Promise.resolve(humanData[id] || droidData[id]);
}
/**
* Allows us to query for a character's friends.
*/
export function getFriends(character: Character): Array<Promise<Character>> {
// Notice that GraphQL accepts Arrays of Promises.
return character.friends.map((id) => getCharacter(id));
}
/**
* Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2.
*/
export function getHero(episode: number): Character {
if (episode === 5) {
// Luke is the hero of Episode V.
return luke;
}
// Artoo is the hero otherwise.
return artoo;
}
/**
* Allows us to query for the human with the given id.
*/
export function getHuman(id: string): Human {
return humanData[id];
}
/**
* Allows us to query for the droid with the given id.
*/
export function getDroid(id: string): Droid {
return droidData[id];
}

View File

@ -0,0 +1,11 @@
import * as path from "path";
import { GQLiteralSchema } from "../../../../src";
/**
* Finally, we construct our schema (whose starting query type is the query
* type we defined above) and export it.
*/
export const StarWarsSchema = GQLiteralSchema({
types: [],
definitionFilePath: path.join(__dirname, "../schema.graphql"),
});

View File

@ -0,0 +1,21 @@
import { GQLiteralInterface } from "../../../../../src";
import { getFriends } from "../data";
export const Character = GQLiteralInterface("Character", (t) => {
t.description("A character in the Star Wars Trilogy");
t.string("id", { description: "The id of the character" });
t.string("name", { description: "The name of the character" });
t.field("friends", "Character", {
list: true,
description:
"The friends of the character, or an empty list if they have none.",
resolve: (character) => getFriends(character),
});
t.field("appearsIn", "Episode", {
list: true,
description: "Which movies they appear in.",
});
t.resolveType((character) => {
return character.type;
});
});

View File

@ -0,0 +1,10 @@
import { GQLiteralObject } from "../../../../../src";
export const Droid = GQLiteralObject("Droid", (t) => {
t.description("A mechanical creature in the Star Wars universe.");
t.implements("Character");
t.string("primaryFunction", {
description: "The primary function of the droid.",
defaultValue: "N/A",
});
});

View File

@ -0,0 +1,33 @@
import { GQLiteralEnum } from "../../../../../src";
/**
* Note: this could also be:
*
* GQLiteralEnum("Episode", {
* NEWHOPE: 4,
* EMPIRE: 5,
* JEDI: 6
* })
*
* if we chose to omit the descriptions.
*/
export const Episode = GQLiteralEnum("Episode", (t) => {
t.description("One of the films in the Star Wars Trilogy");
t.members([
{
name: "NEWHOPE",
value: 4,
description: "Released in 1977.",
},
{
name: "EMPIRE",
value: 5,
description: "Released in 1980.",
},
{
name: "JEDI",
value: 6,
description: "Released in 1983",
},
]);
});

View File

@ -0,0 +1,11 @@
import { GQLiteralObject } from "../../../../../src";
export const Human = GQLiteralObject("Human", (t) => {
t.description("A humanoid creature in the Star Wars universe.");
t.implements("Character");
t.string("homePlanet", {
nullable: true,
description: "The home planet of the human, or null if unknown.",
property: "home_planet",
});
});

View File

@ -0,0 +1,31 @@
import { GQLiteralObject, GQLiteralArg } from "../../../../../src";
import { getHero, getHuman, getDroid } from "../data";
const characterIdArg = GQLiteralArg("String", {
required: true,
description: "id of the character",
});
export const Query = GQLiteralObject("Query", (t) => {
t.field("hero", "Character", {
args: {
episode: GQLiteralArg("Episode", {
description:
"If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.",
}),
},
resolve: (root, { episode }) => getHero(episode),
});
t.field("human", "Human", {
args: {
id: characterIdArg,
},
resolve: (root, { id }) => getHuman(id),
});
t.field("droid", "Droid", {
args: {
id: characterIdArg,
},
resolve: (root, { id }) => getDroid(id),
});
});

View File

@ -1,8 +0,0 @@
{
"name": "gqliteral-swapi-example",
"version": "0.0.0",
"dependencies": {
"swapi-graphql": "^0.0.6"
},
"devDependencies": {}
}

View File

@ -1,31 +0,0 @@
import { GQLiteralObject } from "../../../../../src";
export const Film = GQLiteralObject("Film", (t) => {
t.description("A single film");
t.string("title", { description: "The title of the film" });
t.int("episodeID", {
description: "The episode number of this film.",
property: "episode_id",
});
t.string("openingCrawl", {
description: "The opening paragraphs at the beginning of this film.",
});
t.string("director", {
description: "The name of the director of this film.",
});
t.field("producers", "String", {
list: true,
description: "The name(s) of the producer(s) of this film.",
resolve: (film) => {
return film.producer.split(",").map((s: string) => s.trim());
},
});
t.string("releaseDate", {
property: "release_date",
description:
"The ISO 8601 date format of film release at original creator country.",
});
t.field("speciesConnection", "FilmSpeciesConnection");
t.field("starshipConnection", "StarshipConnection");
t.field("starshipConnection", "StarshipConnection");
});

View File

@ -1,59 +0,0 @@
import { GQLiteralObject } from "../../../../../src";
export const Person = GQLiteralObject("Person", t => {
t.string("name", { description: "The name of this person" });
t.string("birthYear", {
property: "birth_year",
description: `The birth year of the person, using the in-universe standard of BBY or ABY -
Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is
a battle that occurs at the end of Star Wars episode IV: A New Hope.`,
});
t.string("eyeColor", {
property: "eye_color",
description: `The eye color of this person. Will be "unknown" if not known or "n/a" if the
person does not have an eye.`,
});
t.string("gender", {
description: `The gender of this person. Either "Male", "Female" or "unknown",
"n/a" if the person does not have a gender.`,
defaultValue: "n/a",
});
t.string("hairColor", {
property: "hair_color",
description: `The hair color of this person. Will be "unknown" if not known or "n/a" if the
person does not have hair.`,
});
t.int("height", {
resolve: person => convertToNumber(person.height),
description: "The height of the person in centimeters.",
});
t.float("mass", {
resolve: person => convertToNumber(person.mass),
description: "The mass of the person in kilograms.",
});
t.string("skinColor", {
property: "skin_color",
description: "The skin color of this person.",
});
t.field("homeworld", "Planet", {
resolve: person =>
person.homeworld ? getObjectFromUrl(person.homeworld) : null,
description: "A planet that this person was born on or inhabits.",
});
});
/**
* Given a string, convert it to a number
*/
function convertToNumber(value: string): number | null {
if (["unknown", "n/a"].indexOf(value) !== -1) {
return null;
}
// remove digit grouping
const numberString = value.replace(/,/, "");
return Number(numberString);
}
function getObjectFromUrl(item: string) {
return {};
}

View File

@ -1,8 +0,0 @@
import { GQLiteralObject } from "../../../../../src";
export const Planet = GQLiteralObject("Planet", t => {
t.description(`A large mass, planet or planetoid in the Star Wars Universe, at the time of
0 ABY.`);
t.string("name", { description: "The name of this planet." });
t.int("diameter");
});

View File

@ -1,25 +0,0 @@
import { GQLiteralObject } from "../../../../../src";
export const Vehicle = GQLiteralObject("Vehicle", (t) => {
t.description(
"A single transport craft that does not have hyperdrive capability"
);
t.string("name", {
description: `The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder
bike".`,
});
t.string("model", {
description: `The model or official name of this vehicle. Such as "All-Terrain Attack
Transport".`,
});
t.string("vehicleClass", {
property: "vehicle_class",
description:
'The class of this vehicle, such as "Wheeled" or "Repulsorcraft".',
});
t.field("manufacturers", "String", {
// resolve: () => {},
list: true,
description: "The manufacturers of this vehicle.",
});
});

View File

@ -1,18 +1,14 @@
import { GenTypesShape } from "./src/types";
import { GenTypesShape } from "./src/gen";
export interface GeneratedSchemaTypes extends GenTypesShape {
interfaces: "a" | "b";
enums: "One" | "Two" | "Three";
enumTypes: {
OneThroughThree: "NEWHOPE" | "EMPIRE" | "JEDI";
};
objectTypes: {
a: {
root: {
a: {};
};
args: {};
};
};
inputObjectTypes: {};
}
export type GQLiteralGen = any;
// export interface GQLiteralGen extends GenTypesShape {
// enumTypes: {
// PostStatuse: "PENDING" | "DELETED";
// NewEnum: "SOMETHING";
// };
// objectTypes: {};
// inputObjectTypes: {};
// interfaceTypes: {};
// scalarTypes: {};
// }

View File

@ -8,6 +8,7 @@
"graphql-codegen-typescript-template": "^0.13.0"
},
"devDependencies": {
"@types/node": "^10.12.2",
"@types/jest": "^23.3.7",
"@types/graphql": "14.0.3",
"graphql": "^14.0.2",

View File

@ -1,47 +1,43 @@
/// <reference types="jest" />
import {
gqlitBuildTypes,
GQLiteralEnum,
GQLiteralObject,
} from "../definitions";
import { GQLiteralEnum, GQLiteralObject } from "../definitions";
import { GraphQLEnumType, GraphQLObjectType } from "graphql";
import { buildTypes } from "../utils";
describe("gqlit", () => {
describe("GQLiteralEnum", () => {
const PrimaryColors = GQLiteralEnum("PrimaryColors", t => {
t.member({ value: "RED" });
t.member({ value: "YELLOW" });
t.member({ value: "BLUE" });
const PrimaryColors = GQLiteralEnum("PrimaryColors", (t) => {
t.members(["RED", "YELLOW", "BLUE"]);
});
const RainbowColors = GQLiteralEnum("RainbowColors", t => {
const RainbowColors = GQLiteralEnum("RainbowColors", (t) => {
t.mix("PrimaryColors");
t.member({ value: "ORANGE" });
t.member({ value: "GREEN" });
t.member({ value: "VIOLET" });
t.members(["ORANGE", "GREEN", "VIOLET"]);
});
const AdditivePrimaryColors = GQLiteralEnum("AdditivePrimaryColors", t => {
t.mix("PrimaryColors", { omit: ["YELLOW"] });
t.member({ value: "GREEN" });
});
const AdditivePrimaryColors = GQLiteralEnum(
"AdditivePrimaryColors",
(t) => {
t.mix("PrimaryColors", { omit: ["YELLOW"] });
t.members(["GREEN"]);
}
);
const CircularRefTestA = GQLiteralEnum("CircularA", t => {
const CircularRefTestA = GQLiteralEnum("CircularA", (t) => {
t.mix("CircularB");
t.member({ value: "A" });
t.members(["A"]);
});
const CircularRefTestB = GQLiteralEnum("CircularA", t => {
const CircularRefTestB = GQLiteralEnum("CircularA", (t) => {
t.mix("CircularA");
t.member({ value: "B" });
t.members(["B"]);
});
it("builds an enum", () => {
const types: { PrimaryColors: GraphQLEnumType } = gqlitBuildTypes([
const types: { PrimaryColors: GraphQLEnumType } = buildTypes([
PrimaryColors,
]) as any;
expect(types.PrimaryColors).toBeInstanceOf(GraphQLEnumType);
expect(types.PrimaryColors.getValues().map(v => v.value)).toEqual([
expect(types.PrimaryColors.getValues().map((v) => v.value)).toEqual([
"RED",
"YELLOW",
"BLUE",
@ -49,12 +45,12 @@ describe("gqlit", () => {
});
it("can mix enums", () => {
const types: { RainbowColors: GraphQLEnumType } = gqlitBuildTypes([
const types: { RainbowColors: GraphQLEnumType } = buildTypes([
PrimaryColors,
RainbowColors,
]) as any;
expect(types.RainbowColors).toBeInstanceOf(GraphQLEnumType);
expect(types.RainbowColors.getValues().map(v => v.value)).toEqual([
expect(types.RainbowColors.getValues().map((v) => v.value)).toEqual([
"RED",
"YELLOW",
"BLUE",
@ -65,41 +61,43 @@ describe("gqlit", () => {
});
it("can omit with mix", () => {
const types: { AdditivePrimaryColors: GraphQLEnumType } = gqlitBuildTypes(
[PrimaryColors, AdditivePrimaryColors]
) as any;
const types: {
AdditivePrimaryColors: GraphQLEnumType;
} = buildTypes([PrimaryColors, AdditivePrimaryColors]) as any;
expect(types.AdditivePrimaryColors).toBeInstanceOf(GraphQLEnumType);
expect(types.AdditivePrimaryColors.getValues().map(v => v.value)).toEqual(
["RED", "BLUE", "GREEN"]
);
expect(
types.AdditivePrimaryColors.getValues().map((v) => v.value)
).toEqual(["RED", "BLUE", "GREEN"]);
});
it("can pick with mix", () => {
const FavoriteColors = GQLiteralEnum("FavoriteColors", t => {
const FavoriteColors = GQLiteralEnum("FavoriteColors", (t) => {
t.mix("RainbowColors", { pick: ["RED", "GREEN"] });
});
const types: { FavoriteColors: GraphQLEnumType } = gqlitBuildTypes([
const types: { FavoriteColors: GraphQLEnumType } = buildTypes([
PrimaryColors,
RainbowColors,
FavoriteColors,
]) as any;
expect(types.FavoriteColors).toBeInstanceOf(GraphQLEnumType);
expect(types.FavoriteColors.getValues().map(v => v.value)).toEqual([
expect(types.FavoriteColors.getValues().map((v) => v.value)).toEqual([
"RED",
"GREEN",
]);
});
it("can map internal values", () => {
const Internal = GQLiteralEnum("Internal", t => {
t.member({ value: "A", internalValue: "--A--" });
t.member({ value: "B", internalValue: "--B--" });
const Internal = GQLiteralEnum("Internal", (t) => {
t.members([
{ name: "A", value: "--A--" },
{ name: "B", value: "--B--" },
]);
});
const types: { Internal: GraphQLEnumType } = gqlitBuildTypes([
const types: { Internal: GraphQLEnumType } = buildTypes([
Internal,
]) as any;
expect(types.Internal.getValues().map(v => v.name)).toEqual(["A", "B"]);
expect(types.Internal.getValues().map(v => v.value)).toEqual([
expect(types.Internal.getValues().map((v) => v.name)).toEqual(["A", "B"]);
expect(types.Internal.getValues().map((v) => v.value)).toEqual([
"--A--",
"--B--",
]);
@ -114,16 +112,22 @@ describe("gqlit", () => {
const types: {
MappedObj: GraphQLEnumType;
MappedArr: GraphQLEnumType;
} = gqlitBuildTypes([MappedObj, MappedArr]) as any;
expect(types.MappedArr.getValues().map(v => v.name)).toEqual(["A", "B"]);
expect(types.MappedObj.getValues().map(v => v.name)).toEqual(["a", "b"]);
expect(types.MappedObj.getValues().map(v => v.value)).toEqual([1, 2]);
} = buildTypes([MappedObj, MappedArr]) as any;
expect(types.MappedArr.getValues().map((v) => v.name)).toEqual([
"A",
"B",
]);
expect(types.MappedObj.getValues().map((v) => v.name)).toEqual([
"a",
"b",
]);
expect(types.MappedObj.getValues().map((v) => v.value)).toEqual([1, 2]);
});
it("throws if the enum has no members", () => {
const NoMembers = GQLiteralEnum("NoMembers", () => {});
expect(() => {
const types: { NoMembers: GraphQLEnumType } = gqlitBuildTypes([
const types: { NoMembers: GraphQLEnumType } = buildTypes([
NoMembers,
]) as any;
expect(types.NoMembers.getValues()).toHaveLength(0);
@ -132,7 +136,7 @@ describe("gqlit", () => {
it("throws when building with a circular reference", () => {
expect(() => {
gqlitBuildTypes([CircularRefTestA, CircularRefTestB]);
buildTypes([CircularRefTestA, CircularRefTestB]);
}).toThrowError(
"Circular dependency mixin detected when building GQLit Enum"
);
@ -140,16 +144,14 @@ describe("gqlit", () => {
});
describe("GQLiteralObject", () => {
const Account = GQLiteralObject("Account", t => {
const Account = GQLiteralObject("Account", (t) => {
t.id("id", { description: "The ID of the account" });
t.string("name", { description: "Holder of the account" });
t.string("email", {
description: "The email of the person whos account this is",
});
});
const type: { Account: GraphQLObjectType } = gqlitBuildTypes([
Account,
]) as any;
const type: { Account: GraphQLObjectType } = buildTypes([Account]) as any;
expect(Object.keys(type.Account.getFields()).sort()).toEqual([
"id",
"email",

477
src/builder.ts Normal file
View File

@ -0,0 +1,477 @@
import {
isNamedType,
GraphQLNonNull,
isOutputType,
GraphQLFieldConfig,
GraphQLInputFieldConfigMap,
GraphQLFieldConfigMap,
GraphQLFieldConfigArgumentMap,
GraphQLInputFieldConfig,
defaultFieldResolver,
isInputObjectType,
GraphQLNamedType,
GraphQLUnionType,
GraphQLInterfaceType,
GraphQLInputObjectType,
GraphQLObjectType,
GraphQLEnumType,
GraphQLScalarType,
GraphQLUnionTypeConfig,
isUnionType,
isObjectType,
isInterfaceType,
isEnumType,
GraphQLFieldResolver,
isInputType,
GraphQLInputType,
GraphQLList,
} from "graphql";
import * as Types from "./types";
import { GQLiteralTypeWrapper } from "./definitions";
interface BuildConfig {
union: {
name: string;
members: Types.UnionTypeDef[];
typeConfig: Types.UnionTypeConfig;
};
object: {
name: string;
fields: Types.FieldDefType[];
interfaces: string[];
typeConfig: Types.ObjectTypeConfig;
};
input: {
name: string;
fields: Types.FieldDefType[];
typeConfig: Types.InputTypeConfig;
};
interface: {
name: string;
fields: Types.FieldDefType[];
typeConfig: Types.UnionTypeConfig;
};
enum: {
name: string;
members: Types.EnumDefType[];
typeConfig: Types.EnumTypeConfig;
};
}
const NULL_DEFAULTS = {
output: false,
outputList: false,
outputListItem: false,
input: true,
inputList: true,
inputListItem: false,
};
/**
* Builds all of the types, properly accounts for any using "mix".
* Since the enum types are resolved synchronously, these need to guard for
* circular references at this step, while fields will guard for it during lazy evaluation.
*/
export class SchemaBuilder {
protected buildingTypes: Set<string> = new Set();
protected finalTypeMap: Record<string, GraphQLNamedType> = {};
protected pendingTypeMap: Record<string, GQLiteralTypeWrapper<any>> = {};
constructor(
protected schemaConfig: Types.Omit<Types.SchemaConfig, "types">
) {}
addType(typeDef: GQLiteralTypeWrapper<any> | GraphQLNamedType) {
if (this.finalTypeMap[typeDef.name] || this.pendingTypeMap[typeDef.name]) {
throw new Error(`Named type ${typeDef.name} declared more than once`);
}
if (isNamedType(typeDef)) {
this.finalTypeMap[typeDef.name] = typeDef;
} else {
this.pendingTypeMap[typeDef.name] = typeDef;
}
}
getFinalTypeMap(): Record<string, GraphQLNamedType> {
Object.keys(this.pendingTypeMap).forEach((key) => {
// If we've already constructed the type by this point,
// via circular dependency resolution don't worry about building it.
if (this.finalTypeMap[key]) {
return;
}
this.finalTypeMap[key] = this.getOrBuildType(key);
this.buildingTypes.clear();
});
return {};
}
inputObjectType(config: BuildConfig["input"]): GraphQLInputObjectType {
const { name, fields, typeConfig } = config;
return new GraphQLInputObjectType({
name,
fields: () => this.buildInputObjectFields(name, fields),
description: config.typeConfig.description,
});
}
objectType(config: BuildConfig["object"]) {
const { fields, interfaces, name, typeConfig } = config;
return new GraphQLObjectType({
name,
interfaces: () => interfaces.map((i) => this.getInterface(i)),
fields: () => this.buildObjectFields(name, fields, typeConfig),
});
}
interfaceType(config: BuildConfig["interface"]) {
let description;
const { name, fields, typeConfig } = config;
return new GraphQLInterfaceType({
name,
fields: () => this.buildObjectFields(name, fields, typeConfig),
resolveType: typeConfig.resolveType,
description,
// astNode?: Maybe<InterfaceTypeDefinitionNode>;
// extensionASTNodes?: Maybe<ReadonlyArray<InterfaceTypeExtensionNode>>;
});
}
enumType(config: BuildConfig["enum"]) {
const { name, typeConfig, members } = config;
let values: GraphQLEnumValueConfigMap = {},
description;
config.members.forEach((member) => {
switch (member.item) {
case Types.NodeType.ENUM_MEMBER:
values[member.info.name] = {
value: member.info.value,
description: member.info.description,
};
break;
case Types.NodeType.MIX:
const { pick, omit } = member.mixOptions;
enumToMix.getValues().forEach((val) => {
if (pick && pick.indexOf(val.name) === -1) {
return;
}
if (omit && omit.indexOf(val.name) !== -1) {
return;
}
values[val.name] = {
description: val.description,
deprecationReason: val.deprecationReason,
value: val.value,
astNode: val.astNode,
};
});
break;
}
});
if (Object.keys(values).length === 0) {
throw new Error(
`GQLiteralEnum ${this.name} must have at least one member`
);
}
return new GraphQLEnumType({
name,
values,
description,
});
}
unionType(config: BuildConfig["union"]) {
return new GraphQLUnionType({
name: config.name,
types: () => {
return config.members.reduce((result: GraphQLObjectType[], member) => {
switch (member.item) {
case Types.NodeType.MIX:
break;
case Types.NodeType.UNION_MEMBER:
const type = this.getOrBuildType(member.typeName);
if (!isObjectType(type)) {
throw new Error(
`Expected ${member.typeName} to be an ObjectType, saw ${
type.constructor.name
}`
);
}
return result.concat(type);
}
return result;
}, []);
},
resolveType: config.typeConfig,
});
}
protected missingType(typeName: string): GraphQLNamedType {
const suggestions = suggestionList(
typeName,
Object.keys(this.buildingTypes).concat(Object.keys(this.finalTypeMap))
);
let suggestionsString = "";
if (suggestions.length > 0) {
suggestionsString = ` or mean ${suggestions.join(", ")}`;
}
throw new Error(
`Missing type ${typeName}, did you forget to import a type${suggestionsString}?`
);
}
protected buildObjectFields(
typeName: string,
fields: Types.FieldDefType[],
typeConfig: Types.ObjectTypeConfig
): GraphQLFieldConfigMap<any, any> {
const fieldMap: GraphQLFieldConfigMap<any, any> = {};
fields.forEach((field) => {
switch (field.item) {
case Types.NodeType.MIX:
case Types.NodeType.MIX_ABSTRACT:
throw new Error("TODO");
break;
case Types.NodeType.FIELD:
fieldMap[field.fieldName] = this.buildObjectField(field, typeConfig);
break;
}
});
return fieldMap;
}
protected buildObjectField(
field: Types.FieldDef,
typeConfig: Types.ObjectTypeConfig
): GraphQLFieldConfig<any, any> {
return {
type: this.getOutputType(field.fieldType),
resolve: this.getResolver(field.fieldOptions, typeConfig),
description: typeConfig.description,
args: this.buildArgs(field.fieldOptions.args || {}, typeConfig),
// subscribe?: GraphQLFieldResolver<TSource, TContext, TArgs>;
// deprecationReason?: Maybe<string>;
// description?: Maybe<string>;
// astNode?: Maybe<FieldDefinitionNode>;
};
}
protected buildInputObjectFields(
typeName: string,
fields: Types.FieldDefType[]
): GraphQLInputFieldConfigMap {
return {};
}
protected buildInputObjectField(): GraphQLInputFieldConfig<any, any> {
return {};
}
protected buildArgs(
args: Types.OutputFieldArgs,
typeConfig: Types.InputTypeConfig
): GraphQLFieldConfigArgumentMap {
const allArgs: GraphQLFieldConfigArgumentMap = {};
Object.keys(allArgs).forEach((argName) => {
const argDef = args[argName];
allArgs[argName] = {
type: this.decorateInputType(
this.getInputType(argDef.type),
argDef,
typeConfig
),
description: argDef.description,
};
});
return {};
}
protected decorateOutputType(
type: GraphQLInputType,
typeOpts: Types.FieldOpts,
typeConfig: Types.ObjectTypeConfig
) {
return this.decorateType(type, typeOpts, typeConfig, false);
}
protected decorateInputType(
type: GraphQLInputType,
argOpts: Types.ArgOpts,
typeConfig: Types.InputTypeConfig
) {
const { required: _required, requiredListItem, ...rest } = argOpts;
const newOpts = rest;
if (typeof _required !== "undefined") {
newOpts.nullable = !_required;
}
if (typeof requiredListItem !== "undefined") {
if (rest.list) {
newOpts.listItemNullable = !requiredListItem;
}
}
return this.decorateType(type, newOpts, typeConfig, true);
}
/**
* Adds the null / list configuration to the type.
*/
protected decorateType(
type: GraphQLInputType,
fieldOpts: Types.FieldOpts,
typeConfig: Types.ObjectTypeConfig,
isInput: boolean
): GraphQLInputType {
let finalType = type;
const nullConfig = {
...NULL_DEFAULTS,
...this.schemaConfig.nullabilityConfig,
...typeConfig.nullabilityConfig,
};
const { list, nullable, listItemNullable } = fieldOpts;
const isNullable =
typeof nullable !== "undefined"
? nullable
: list
? isInput
? nullConfig.inputList
: nullConfig.outputList
: isInput
? nullConfig.input
: nullConfig.output;
// TODO: Figure out how lists of lists will be represented.
if (list) {
const nullableItem =
typeof listItemNullable !== "undefined"
? listItemNullable
: isInput
? nullConfig.inputListItem
: nullConfig.outputListItem;
if (nullableItem) {
finalType = GraphQLNonNull(finalType);
}
finalType = GraphQLList(finalType);
} else if (typeof listItemNullable !== "undefined") {
console.log(
"listItemNullable should only be set with list: true, this option is ignored"
);
}
if (isNullable) {
return GraphQLNonNull(finalType);
}
return finalType;
}
protected getInterface(name: string): GraphQLInterfaceType {
const type = this.getOrBuildType(name);
if (!isInterfaceType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLInterfaceType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getEnum(name: string): GraphQLEnumType {
const type = this.getOrBuildType(name);
if (!isEnumType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLEnumType, saw ${type.constructor.name}`
);
}
return type;
}
protected getUnion(name: string): GraphQLUnionType {
const type = this.getOrBuildType(name);
if (!isUnionType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLUnionType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getInputType(name: string) {
const type = this.getOrBuildType(name);
if (!isInputType(type)) {
throw new Error(
`Expected ${name} to be a valid input type, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getOutputType(name: string) {
const type = this.getOrBuildType(name);
if (!isOutputType(type)) {
throw new Error(
`Expected ${name} to be a valid output type, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getObjectType(name: string) {
const type = this.getOrBuildType(name);
if (!isObjectType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLObjectType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getOrBuildType(name: string): GraphQLNamedType {
if (this.finalTypeMap[name]) {
return this.finalTypeMap[name];
}
if (this.buildingTypes.has(name)) {
throw new Error(
`GQLiteral: Circular dependency detected, while building types ${Array.from(
this.buildingTypes
)}`
);
}
const pendingType = this.pendingTypeMap[name];
if (pendingType) {
this.buildingTypes.add(name);
return pendingType.type.buildType(name, this);
}
return this.missingType(name);
}
protected getResolver(
fieldOptions: Types.OutputFieldOpts,
typeConfig: Types.ObjectTypeConfig
) {
if (fieldOptions.resolve) {
if (typeof fieldOptions.property !== "undefined") {
console.warn(
`Both resolve and property should not be supplied, property will be ignored`
);
}
return fieldOptions.resolve;
}
if (fieldOptions.property) {
return propertyFieldResolver(fieldOptions.property);
}
if (typeConfig.defaultResolver) {
return typeConfig.defaultResolver;
}
if (this.schemaConfig.defaultResolver) {
return this.schemaConfig.defaultResolver;
}
return defaultFieldResolver;
}
}

View File

@ -1,14 +1,15 @@
import {
GraphQLSchema,
GraphQLNamedType,
isNamedType,
isObjectType,
lexicographicSortSchema,
printSchema,
GraphQLScalarType,
} from "graphql";
import * as Types from "./types";
import { gqliteralBuildTypes } from "./utils";
import * as Gen from "./gen";
import {
GQLiteralType,
GQLiteralScalarType,
GQLiteralNamedType,
GQLiteralObjectType,
GQLiteralInterfaceType,
GQLiteralEnumType,
@ -16,7 +17,8 @@ import {
GQLiteralAbstract,
GQLiteralUnionType,
} from "./objects";
import { GeneratedSchemaTypes } from "../generatedTypes";
import { GQLiteralGen } from "../generatedTypes";
import { enumShorthandMembers, buildTypes } from "./utils";
/**
* Wraps a GQLiteralType object, since all GQLiteral types have a
@ -24,7 +26,7 @@ import { GeneratedSchemaTypes } from "../generatedTypes";
* constructed so we don't want it as a public member, purely for
* intellisense/cosmetic purposes :)
*/
export class GQLiteralTypeWrapper<T extends GQLiteralType> {
export class GQLiteralTypeWrapper<T extends GQLiteralNamedType> {
constructor(readonly name: string, readonly type: T) {}
}
@ -34,11 +36,8 @@ export class GQLiteralTypeWrapper<T extends GQLiteralType> {
* @param {string} name
* @param {object} options
*/
export function GQLiteralScalar(
name: string,
options: Types.GQLiteralScalarOptions
) {
return new GQLiteralTypeWrapper(name, new GQLiteralScalarType(name, options));
export function GQLiteralScalar(name: string, options: Types.ScalarOpts) {
return new GraphQLScalarType({ name, ...options });
}
/**
@ -47,18 +46,10 @@ export function GQLiteralScalar(
* @param {string}
*/
export function GQLiteralObject<
GenTypes = GeneratedSchemaTypes,
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: TypeName,
fn: (
arg: GQLiteralObjectType<GenTypes, TypeDefFor<GenTypes, TypeName>>
) => void
) {
const factory = new GQLiteralObjectType<
GenTypes,
TypeDefFor<GenTypes, TypeName>
>(name);
>(name: TypeName, fn: (arg: GQLiteralObjectType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralObjectType<GenTypes, TypeName>();
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -67,15 +58,13 @@ export function GQLiteralObject<
* Define a GraphQL interface type
*/
export function GQLiteralInterface<
GenTypes = GeneratedSchemaTypes,
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: TypeName,
fn: (
arg: GQLiteralInterfaceType<GenTypes, TypeDefFor<GenTypes, TypeName>>
) => void
fn: (arg: GQLiteralInterfaceType<GenTypes, TypeName>) => void
) {
const factory = new GQLiteralInterfaceType(name);
const factory = new GQLiteralInterfaceType();
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -98,10 +87,10 @@ export function GQLiteralInterface<
* })
*/
export function GQLiteralUnion<
GenTypes = GeneratedSchemaTypes,
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralUnionType) => void) {
const factory = new GQLiteralUnionType(name);
>(name: TypeName, fn: (arg: GQLiteralUnionType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralUnionType();
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -129,48 +118,37 @@ export function GQLiteralUnion<
* t.mix('OneThroughThree')
* t.mix('FourThroughSix')
* t.mix('SevenThroughNine')
* t.member({value: 'OTHER'})
* t.members(['OTHER'])
* t.description('All Movies in the Skywalker saga, or OTHER')
* })
*/
export function GQLiteralEnum<GenTypes = GeneratedSchemaTypes>(
export function GQLiteralEnum<GenTypes = GQLiteralGen>(
name: string,
fn:
| ((arg: GQLiteralEnumType<GenTypes>) => void)
| string[]
| Record<string, any>
| Record<string, string | number | object | boolean>
) {
const toCall = typeof fn === "function" ? fn : addEnumValue(fn);
const factory = new GQLiteralEnumType(name);
toCall(factory);
const factory = new GQLiteralEnumType();
if (typeof fn === "function") {
fn(factory);
} else {
factory.members(enumShorthandMembers(fn));
}
return new GQLiteralTypeWrapper(name, factory);
}
/**
* Handles the shorthand syntax for creating the GraphQLEnumType
*/
const addEnumValue = (arg: string[] | Record<string, any>) => (
f: GQLiteralEnumType
) => {
if (Array.isArray(arg)) {
arg.forEach((value) => {
f.member({ value });
});
} else {
Object.keys(arg).forEach((value) => {
f.member({ value: value, internalValue: arg[value] });
});
}
};
/**
*
*/
export function GQLiteralInputObject(
export function GQLiteralInputObject<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: string,
fn: (arg: GQLiteralInputObjectType) => void
fn: (arg: GQLiteralInputObjectType<GenTypes, TypeName>) => void
) {
const factory = new GQLiteralInputObjectType(name);
const factory = new GQLiteralInputObjectType<GenTypes>();
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -188,11 +166,12 @@ export function GQLiteralInputObject(
*
* @return GQLiteralAbstractType
*/
export function GQLiteralAbstractType(fn: (arg: GQLiteralAbstract) => void) {
export function GQLiteralAbstractType<GenTypes = GQLiteralGen>(
fn: (arg: GQLiteralAbstract<GenTypes>) => void
) {
const factory = new GQLiteralAbstract();
fn(factory);
// This is not wrapped in a type, since it's not actually a concrete type.
// This is not wrapped in a type, since it's not actually a concrete (named) type.
return factory;
}
@ -201,10 +180,16 @@ export function GQLiteralAbstractType(fn: (arg: GQLiteralAbstract) => void) {
* This is also exposed during type definition as shorthand via the various
* `__Arg` methods: `fieldArg`, `stringArg`, `intArg`, etc.
*/
export function GQLiteralArgument(
type: Types.GQLArgTypes,
options?: Types.GQLArgOpts
) {}
export function GQLiteralArg(
type: any, // TODO: make type safe
options?: Types.ArgOpts
): Types.ArgDefinition {
// This also isn't wrapped because it's also not a named type, can be reused in multiple locations
return {
type,
...options,
};
}
/**
* Defines the GraphQL schema, by combining the GraphQL types defined
@ -213,8 +198,8 @@ export function GQLiteralArgument(
* Requires at least one type be named "Query", which will be used as the
* root query type.
*/
export function GQLiteralSchema(options: Types.GQLiteralSchemaConfig) {
const typeMap = gqliteralBuildTypes(options.types);
export function GQLiteralSchema(options: Types.SchemaConfig) {
const typeMap = buildTypes(options.types, options);
if (!isObjectType(typeMap["Query"])) {
throw new Error("Missing a Query type");
@ -233,5 +218,31 @@ export function GQLiteralSchema(options: Types.GQLiteralSchemaConfig) {
),
});
// Only in development do we want to worry about regenerating the
// schema definition and/or generated types.
if (process.env.NODE_ENV === "development") {
if (options.definitionFilePath || options.typeGeneration) {
const sortedSchema = lexicographicSortSchema(schema);
const generatedSchema = printSchema(sortedSchema);
if (options.definitionFilePath) {
const fs = require("fs");
fs.writeFile(
options.definitionFilePath,
generatedSchema,
(err: Error | null) => {
if (err) {
return console.error(err);
}
}
);
}
if (options.typeGeneration) {
options.typeGeneration(generatedSchema).catch((e) => {
console.error(e);
});
}
}
}
return schema;
}

View File

@ -1,7 +0,0 @@
export enum NodeType {
MIX = "MIX",
MIX_ABSTRACT = "MIX_ABSTRACT",
FIELD = "FIELD",
ENUM_MEMBER = "ENUM_MEMBER",
UNION_MEMBER = "UNION_MEMBER",
}

76
src/gen.ts Normal file
View File

@ -0,0 +1,76 @@
/**
* Helpers for handling the generated schema
*/
export interface GenTypeEnumShape {
members: string;
}
export interface GenTypeObjectTypeShape {
members: string;
}
export interface GenTypeInputObjectShape {
members: string;
}
export type GenObjectShape = {
args: Record<string, any>;
root: any;
};
export type GenInputObjectShape = {
args: Record<string, any>;
root: any;
};
export type GenTypesShape = {
scalarTypes: Record<string, string>;
enumTypes: Record<string, string>;
objectTypes: Record<string, GenObjectShape>;
inputObjectTypes: Record<string, GenInputObjectShape>;
interfaceTypes: Record<string, GenObjectShape>;
contextType: any;
};
export type OutputNames<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypesShape["enumTypes"], string>
: string;
export type FieldNames<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypesShape["enumTypes"], string>
: string;
export type InterfaceName<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypesShape["interfaceTypes"], string>
: string;
export type EnumName<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypes["enumTypes"], string>
: string;
export type EnumMembers<
GenTypes,
EnumName extends string
> = GenTypes extends GenTypesShape
? EnumName extends keyof GenTypes["enumTypes"]
? GenTypes["enumTypes"][EnumName]
: never
: string;
export type ObjectTypeDef<GenTypes, TypeName> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["objectTypes"]
? GenTypes["objectTypes"][TypeName]
: never
: string;
export type InputObjectTypeDef<
GenTypes,
TypeName
> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["inputObjectTypes"]
? GenTypes["inputObjectTypes"][TypeName]
: never
: string;
export type RootType<GenTypes, TypeName> = any;

View File

@ -1,5 +1,5 @@
export {
GQLiteralArgument,
GQLiteralArg,
GQLiteralObject,
GQLiteralInputObject,
GQLiteralEnum,

View File

@ -1,70 +1,34 @@
import * as Types from "./types";
import {
isEnumType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLFieldResolver,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLEnumType,
GraphQLUnionType,
GraphQLScalarType,
GraphQLScalarTypeConfig,
GraphQLEnumTypeConfig,
GraphQLInterfaceTypeConfig,
GraphQLInputObjectTypeConfig,
GraphQLObjectTypeConfig,
GraphQLUnionTypeConfig,
GraphQLEnumValueConfigMap,
GraphQLFieldConfig,
GraphQLList,
GraphQLNonNull,
GraphQLField,
isInterfaceType,
GraphQLFieldConfigMap,
isOutputType,
isInputType,
isNamedType,
GraphQLInputFieldConfigMap,
GraphQLEnumType,
} from "graphql";
import { GQLiteralArgument } from "./definitions";
import { GQLiteralGen } from "../generatedTypes";
import * as Gen from "./gen";
import { SchemaBuilder } from "./utils";
export type GQLiteralType =
| GQLiteralScalarType
export type GQLiteralNamedType =
| GQLiteralEnumType
| GQLiteralObjectType<any>
| GQLiteralInterfaceType
| GQLiteralObjectType<any, any>
| GQLiteralInterfaceType<any, any>
| GQLiteralUnionType
| GQLiteralInputObjectType;
export class GQLiteralScalarType {
constructor(
protected readonly name: string,
readonly options: Types.GQLiteralScalarOptions
) {}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
*/
toConfig(): GraphQLScalarTypeConfig<any, any> {
ensureBuilding();
return {
name: this.name,
...this.options,
};
}
}
/**
* Backing type for an enum member.
*/
export class GQLiteralEnumType<GenTypes> {
protected meta: Types.GQLiteralTypeMetadata = {};
protected members: Types.EnumDefType[] = [];
export class GQLiteralEnumType<GenTypes = GQLiteralGen> {
protected typeConfig: Types.EnumTypeConfig = {};
protected enumMembers: Types.EnumDefType[] = [];
constructor(protected readonly name: string) {}
mix<T extends string>(typeName: T, mixOptions?: Types.GQLiteralMixOptions<>) {
this.members.push({
mix<EnumName extends string>(
typeName: Gen.EnumName<GenTypes>,
mixOptions?: Types.MixOpts<Gen.EnumMembers<GenTypes, EnumName>>
) {
this.enumMembers.push({
item: Types.NodeType.MIX,
typeName,
mixOptions: mixOptions || {},
@ -72,24 +36,28 @@ export class GQLiteralEnumType<GenTypes> {
}
/**
* Add an individual value member to the enum
* Sets the members of the enum
*/
member(info: { value: string; internalValue?: any }) {
const memberInfo = {
...info,
internalValue:
typeof info.internalValue === "undefined"
? info.value
: info.internalValue,
};
this.members.push({ item: Types.NodeType.ENUM_MEMBER, info: memberInfo });
members(info: Array<Types.EnumMemberInfo | string>) {
info.forEach((info) => {
if (typeof info === "string") {
return this.enumMembers.push({
item: Types.NodeType.ENUM_MEMBER,
info: { name: info, value: info },
});
}
this.enumMembers.push({
item: Types.NodeType.ENUM_MEMBER,
info,
});
});
}
/**
* Any description about the enum type.
*/
description(description: string) {
this.meta.description = description;
this.typeConfig.description = description;
}
/**
@ -100,78 +68,26 @@ export class GQLiteralEnumType<GenTypes> {
* needs to synchronously return and therefore must check for / break
* circular references when mixing.
*/
toConfig(typeData: TypeDataArg): GraphQLEnumTypeConfig {
ensureBuilding();
let values: GraphQLEnumValueConfigMap = {},
description;
this.members.forEach((member) => {
switch (member.item) {
case Types.NodeType.ENUM_MEMBER:
values[member.info.value] = {
value: member.info.internalValue,
description: member.info.description,
};
break;
case Types.NodeType.MIX:
if (typeData.currentlyBuilding.has(member.typeName)) {
throw new Error(
`Circular dependency mixin detected when building GQLit Enum: ${
this.name
}`
);
}
const toBuildFn = typeData.pendingTypeMap[member.typeName];
let enumToMix;
if (typeof toBuildFn === "function") {
enumToMix = toBuildFn(typeData.currentlyBuilding.add(this.name));
} else if (typeData.finalTypeMap[member.typeName]) {
enumToMix = typeData.finalTypeMap[member.typeName];
} else {
throw new Error(`Missing mixin enum type: ${member.typeName}`);
}
if (!isEnumType(enumToMix)) {
throw new Error(
`Cannot mix non-enum type ${enumToMix.name} with enum values`
);
}
const { pick, omit } = member.mixOptions;
enumToMix.getValues().forEach((val) => {
if (pick && pick.indexOf(val.name) === -1) {
return;
}
if (omit && omit.indexOf(val.name) !== -1) {
return;
}
values[val.name] = {
description: val.description,
deprecationReason: val.deprecationReason,
value: val.value,
astNode: val.astNode,
};
});
break;
}
buildType(typeName: string, builder: SchemaBuilder): GraphQLEnumType {
return builder.enumType({
name: typeName,
members: this.enumMembers,
typeConfig: this.typeConfig,
});
if (Object.keys(values).length === 0) {
throw new Error(
`GQLiteralEnum ${this.name} must have at least one member`
);
}
return {
name: this.name,
values,
description,
};
}
}
export class GQLiteralUnionType {
protected meta: Types.GQLiteralTypeMetadata = {};
export class GQLiteralUnionType<
GenTypes = GQLiteralGen,
TypeName extends string = any
> {
protected typeConfig: Types.UnionTypeConfig = {};
protected unionMembers: Types.UnionTypeDef[] = [];
constructor(protected readonly name: string) {}
mix(type: string) {
mix<UnionTypeName extends string>(
type: UnionTypeName,
options?: Types.MixOpts<any>
) {
this.unionMembers.push({
item: Types.NodeType.MIX,
typeName: type,
@ -185,31 +101,39 @@ export class GQLiteralUnionType {
});
}
/**
* Optionally provide a custom type resolver function. If one is not provided,
* the default implementation will call `isTypeOf` on each implementing
* Object type.
*/
resolveType(typeResolver: Types.ResolveType<GenTypes, TypeName>) {
this.typeConfig.resolveType = typeResolver;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
*/
toConfig(getType: Types.GetTypeFn): GraphQLUnionTypeConfig<any, any> {
ensureBuilding();
return {
name: this.name,
types: () => {
return this.unionMembers.reduce((result: GraphQLObjectType[], item) => {
return result;
}, []);
},
};
buildType(typeName: string, builder: SchemaBuilder): GraphQLUnionType {
return builder.unionType({
name: typeName,
members: this.unionMembers,
typeConfig: this.typeConfig,
});
}
}
abstract class GQLitWithFields {
abstract class GQLitWithFields<
GenTypes = GQLiteralGen,
TypeName extends string = any
> {
protected fields: Types.FieldDefType[] = [];
/**
* Mixes in an existing field definition or object type
* with the current type.
*/
mix(typeName: string, mixOptions?: Types.GQLiteralMixOptions) {
mix(typeName: string, mixOptions?: Types.MixOpts<any>) {
this.fields.push({
item: Types.NodeType.MIX,
typeName,
@ -220,45 +144,60 @@ abstract class GQLitWithFields {
/**
* Add an ID field type to the object schema.
*/
id<O extends Opts>(name: Types.FieldName<Root, O>, options?: O) {
id<FieldName extends string>(
name: FieldName,
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.field(name, "ID", options);
}
/**
* Add an Int field type to the object schema.
*/
int<O extends Opts>(name: Types.FieldName<Root, O>, options?: O) {
int<FieldName extends string>(
name: FieldName,
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.field(name, "Int", options);
}
/**
* Add a Float field type to the object schema.
*/
float<O extends Opts>(name: Types.FieldName<Root, O>, options?: O) {
float<FieldName extends string>(
name: FieldName,
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.field(name, "Float", options);
}
/**
* Add a String field type to the object schema.
*/
string<O extends Opts>(name: Types.FieldName<Root, O>, options?: O) {
string<FieldName extends string>(
name: FieldName,
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.field(name, "String", options);
}
/**
* Add a Boolean field type to the object schema.
*/
boolean<O extends Opts>(name: Types.FieldName<Root, O>, options?: O) {
boolean<FieldName extends string>(
name: FieldName,
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.field(name, "Boolean", options);
}
/**
* Adds a new field to the object type
*/
field(
name: Types.FieldName<Root, Opts>,
field<FieldName extends string>(
name: FieldName,
type: Types.GQLTypes,
options?: Opts
options?: Types.OutputFieldOpts<GenTypes, TypeName, FieldName>
) {
this.fields.push({
item: Types.NodeType.FIELD,
@ -269,164 +208,77 @@ abstract class GQLitWithFields {
}
}
export class GQLiteralOutputObject extends GQLitWithFields {
/**
* Define an id argument for a field.
*/
idArg(options?: Types.GQLiteralArgOptions) {
return this.fieldArg("ID", options);
}
/**
* Define an int argument for a field.
*/
intArg(options?: Types.GQLiteralArgOptions) {
return this.fieldArg("Int", options);
}
/**
* Define a string argument for a field.
*/
stringArg(options?: Types.GQLiteralArgOptions) {
return this.fieldArg("String", options);
}
/**
* Define a float argument for a field.
*/
floatArg(options?: Types.GQLiteralArgOptions) {
return this.fieldArg("Float", options);
}
/**
* Define a boolean argument for a field.
*/
booleanArg(options?: Types.GQLiteralArgOptions) {
return this.fieldArg("Boolean", options);
}
/**
* Define a field argument for a field.
*/
fieldArg(type: Types.GQLArgTypes, options?: Types.GQLArgOpts) {
return GQLiteralArgument(type, options);
}
}
export class GQLiteralObjectType<GenTypes, ObjTypes> extends GQLitWithFields {
export class GQLiteralObjectType<
GenTypes,
TypeName extends string = any
> extends GQLitWithFields<GenTypes, TypeName> {
/**
* Metadata about the object type
*/
protected meta: Types.GQLiteralTypeMetadata = {};
protected typeConfig: Types.ObjectTypeConfig = {};
/**
* All interfaces the object implements.
*/
protected interfaces: Types.InterfaceNames<GenTypes>[] = [];
constructor(protected readonly name: string) {
super();
}
protected interfaces: Gen.InterfaceName<GenTypes>[] = [];
/**
* Declare that an object type implements a particular interface,
* by providing the name of the interface
*/
implements(interfaceName: Types.InterfaceNames<GenTypes>) {
this.interfaces.push(interfaceName);
implements(...interfaceName: Gen.InterfaceName<GenTypes>[]) {
this.interfaces.push(...interfaceName);
}
/**
* Adds a description to the metadata for the object type.
*/
description(description: string) {
this.meta.description = description;
this.typeConfig.description = description;
}
/**
* Adds an "isTypeOf" check to the object type.
*/
isTypeOf(fn: (value: any) => boolean) {
this.meta.isTypeOf = fn;
this.typeConfig.isTypeOf = fn;
}
/**
* Supply the default field resolver for all members of this type
*/
defaultResolver(resolverFn: GraphQLFieldResolver<any, any>) {
this.typeConfig.defaultResolver = resolverFn;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
*/
toConfig(getType: Types.GetTypeFn): GraphQLObjectTypeConfig<any, any> {
ensureBuilding();
const additional: Partial<GraphQLObjectTypeConfig<any, any>> = {};
if (this.meta.description) {
additional.description = withDeprecationComment(this.meta.description);
}
return {
name: this.name,
interfaces: () => {
return this.interfaces.map((i) => {
const interfaceType = getType(i);
if (!isInterfaceType(interfaceType)) {
throw new Error(
`Expected ${
this.name
} - ${i} to be an interface, saw ${interfaceType}`
);
}
return interfaceType;
});
},
fields: () => {
const interfaceFields: Record<
string,
GraphQLFieldConfig<any, any>
> = {};
const typeFields: Record<string, GraphQLFieldConfig<any, any>> = {};
this.fields.forEach((field) => {
switch (field.item) {
case Types.NodeType.FIELD: {
typeFields[field.fieldName] = buildGraphQLField<Root>(
this.name,
field.fieldName,
field.fieldType,
field.fieldOptions,
getType
);
break;
}
case Types.NodeType.MIX: {
//
break;
}
}
// typeFields[field] =
});
return {
...interfaceFields,
...typeFields,
};
},
};
buildType(typeName: string, builder: SchemaBuilder) {
return builder.objectType({
name: typeName,
fields: this.fields,
typeConfig: this.typeConfig,
interfaces: this.interfaces,
});
}
}
export class GQLiteralInterfaceType<
GenTypes,
InterfaceTypes
> extends GQLitWithFields {
TypeName extends string
> extends GQLitWithFields<GenTypes, TypeName> {
/**
* Metadata about the object type
*/
protected meta: Types.GQLiteralInterfaceMetadata = {};
constructor(protected readonly name: string) {
super();
}
protected typeConfig: Types.UnionTypeConfig = {};
/**
* Adds a description to the metadata for the interface type.
*/
description(description: string) {
this.meta.description = description;
this.typeConfig.description = description;
}
/**
@ -434,50 +286,43 @@ export class GQLiteralInterfaceType<
* the default implementation will call `isTypeOf` on each implementing
* Object type.
*/
resolveType(typeResolver: any) {
this.meta.resolveType = typeResolver;
resolveType(typeResolver: Types.ResolveType<GenTypes, TypeName>) {
this.typeConfig.resolveType = typeResolver;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
*/
toConfig(getType: Types.GetTypeFn): GraphQLInterfaceTypeConfig<any, any> {
ensureBuilding();
let description;
return {
name: this.name,
fields: () => buildObjectFields(this.name, this.fields),
resolveType: this.meta.resolveType,
description,
// astNode?: Maybe<InterfaceTypeDefinitionNode>;
// extensionASTNodes?: Maybe<ReadonlyArray<InterfaceTypeExtensionNode>>;
};
buildType(typeName: string, builder: SchemaBuilder): GraphQLInterfaceType {
return builder.interfaceType({
name: typeName,
fields: this.fields,
typeConfig: this.typeConfig,
});
}
}
export class GQLiteralInputObjectType extends GQLitWithFields {
protected meta: Types.GQLiteralTypeMetadata = {};
constructor(protected readonly name: string) {
super();
}
export class GQLiteralInputObjectType<
GenTypes = GQLiteralGen,
TypeName extends string = any
> extends GQLitWithFields<GenTypes, TypeName> {
protected typeConfig: Types.InputTypeConfig = {};
description(description: string) {
this.meta.description = description;
this.typeConfig.description = description;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
*/
toConfig(getType: Types.GetTypeFn): GraphQLInputObjectTypeConfig {
ensureBuilding();
return {
name: this.name,
fields: () => buildInputObjectFields(this.name, this.fields),
description: this.meta.description,
};
buildType(typeName: string, builder: SchemaBuilder): GraphQLInputObjectType {
return builder.inputObjectType({
name: typeName,
fields: this.fields,
typeConfig: this.typeConfig,
});
}
}
@ -487,7 +332,10 @@ export class GQLiteralInputObjectType extends GQLitWithFields {
*
* Use the `.mix` to mixin the abstract type fields.
*/
export class GQLiteralAbstract extends GQLiteralOutputObject {}
export class GQLiteralAbstract<GenTypes> extends GQLitWithFields<
GenTypes,
never
> {}
// Ignoring these, since they're only provided for a better developer experience,
// we don't want these to actually be picked up by intellisense.
@ -499,159 +347,3 @@ GQLiteralAbstract.prototype.implements = function() {
Call .implements() on the concrete type that uses this AbstractType instead.
`);
};
function buildObjectFields(
typeName: string,
fields: Types.FieldDefType<any>[]
): GraphQLFieldConfigMap<any, any> {
const fieldMap: GraphQLFieldConfigMap<any, any> = {};
fields.forEach((field) => {
switch (field.item) {
case Types.NodeType.MIX: {
break;
}
case Types.NodeType.MIX_ABSTRACT: {
break;
}
case Types.NodeType.FIELD: {
break;
}
}
});
return fieldMap;
}
function buildInputObjectFields(
typeName: string,
fields: Types.FieldDefType<any>[]
): GraphQLInputFieldConfigMap {
const fieldMap: GraphQLInputFieldConfigMap = {};
fields.forEach((field) => {
switch (field.item) {
case Types.NodeType.MIX: {
break;
}
case Types.NodeType.MIX_ABSTRACT: {
break;
}
case Types.NodeType.FIELD: {
break;
}
}
});
return fieldMap;
}
function buildGraphQLField(
typeName: string,
fieldName: string,
fieldType: string,
fieldOptions: Types.GQLiteralOutputFieldOptions<any>,
getType: Types.GetTypeFn
): GraphQLFieldConfig<any, any> {
const type = getType(fieldType);
const nullable = Boolean(fieldOptions.nullable);
if (typeof fieldOptions.listItemNullable === "boolean") {
console.warn("listItemNullable should ");
}
const nullItem = Boolean(fieldOptions.listItemNullable);
if (!isNamedType(fieldType)) {
throw new Error("");
}
if (!isOutputType(fieldType)) {
throw new Error(
`Expected ${typeName} - ${fieldName} to be an object type, saw ${fieldType}`
);
}
let args;
return {
type: nullItem ? fieldType : GraphQLNonNull(fieldType),
// args?: GraphQLFieldConfigArgumentMap;
// resolve?: GraphQLFieldResolver<TSource, TContext, TArgs>;
// subscribe?: GraphQLFieldResolver<TSource, TContext, TArgs>;
// deprecationReason?: Maybe<string>;
// description?: Maybe<string>;
// astNode?: Maybe<FieldDefinitionNode>;
};
}
function buildGraphQLArgument() {
const requiredArg = Boolean();
// if (!isNamedType()) {
// }
// if (!isInputType()) {
// }
}
interface TypeDataArg {
finalTypeMap: Record<string, GraphQLNamedType>;
pendingTypeMap: Record<
string,
((building: Set<string>) => GraphQLNamedType) | null
>;
currentlyBuilding: Set<string>;
}
/**
* buildGQLiteralType
*
* For internal use, builds concrete representations of the GQLit types for the Schema.
*
* @param name name of the type being built
* @param type GQLiteralType representation of the GraphQL type being built
* @param typeMap the map of currently resolved types, used to
* @param currentlyBuilding a Set of types currenty being "mixed", used to break circular dependencies
*/
export function buildGQLiteralType(
name: string,
type: GQLiteralType,
typeData: TypeDataArg,
getType?: Types.GetTypeFn
): GraphQLNamedType {
isBuilding += 1;
const getTypeFn: Types.GetTypeFn =
getType ||
((typeName: string) => {
const t = typeData.finalTypeMap[typeName];
if (!t) {
throw new Error(`Missing type ${typeName}`);
}
return t;
});
let returnType: GraphQLNamedType;
if (type instanceof GQLiteralObjectType) {
returnType = new GraphQLObjectType(type.toConfig(getTypeFn));
} else if (type instanceof GQLiteralInputObjectType) {
returnType = new GraphQLInputObjectType(type.toConfig(getTypeFn));
} else if (type instanceof GQLiteralInterfaceType) {
returnType = new GraphQLInterfaceType(type.toConfig(getTypeFn));
} else if (type instanceof GQLiteralUnionType) {
returnType = new GraphQLUnionType(type.toConfig(getTypeFn));
} else if (type instanceof GQLiteralEnumType) {
returnType = new GraphQLEnumType(type.toConfig(typeData));
} else if (type instanceof GQLiteralScalarType) {
returnType = new GraphQLScalarType(type.toConfig());
} else {
throw new Error(`Invalid value, expected GQLit type, saw ${type}`);
}
isBuilding -= 1;
return returnType;
}
/**
* Counter, to determine whether we're building or not. Used to error if `toConfig` is
* called erroneously from userland.
*/
let isBuilding = 0;
function ensureBuilding() {
if (isBuilding === 0) {
throw new Error(
".toConfig should only be called internally, while GQLiteral is constructing the schema types"
);
}
}
function withDeprecationComment(description?: string | null) {
return description;
}

View File

@ -3,16 +3,14 @@ import {
GraphQLScalarTypeConfig,
GraphQLNamedType,
GraphQLTypeResolver,
GraphQLIsTypeOfFn,
} from "graphql";
declare global {
namespace GQLiteral { export interface Context {} }
}
import * as Gen from "./gen";
export enum NodeType {
MIX = "MIX",
MIX_ABSTRACT = "MIX_ABSTRACT",
FIELD = "FIELD",
MIX_ABSTRACT = "MIX_ABSTRACT",
ENUM_MEMBER = "ENUM_MEMBER",
UNION_MEMBER = "UNION_MEMBER",
}
@ -39,58 +37,42 @@ export type FieldName<Root, Opts> = Opts extends {
export type GQLTypes = "ID" | "String" | "Int" | "Float" | string;
export type GQLArgTypes = "ID" | "String" | "Int" | "Float" | string;
export interface GQLArgOpts {
/**
* Whether
*/
required?: boolean;
/**
* Whether the item in the
*/
requiredItem?: boolean;
/**
*
*/
list?: boolean;
}
export type MixDef<Members extends string> = {
export type MixDef = {
item: NodeType.MIX;
typeName: string;
mixOptions: GQLiteralMixOptions<Members>;
mixOptions: MixOpts<any>;
};
export type MixAbstractDef<Members extends string> = {
export type MixAbstractDef = {
item: NodeType.MIX_ABSTRACT;
typeName: string;
mixOptions: GQLiteralMixOptions<Members>;
mixOptions: MixOpts<any>;
};
export type FieldDef<ObjType> = {
export type FieldDef = {
item: NodeType.FIELD;
fieldName: string;
fieldType: GQLTypes;
fieldOptions: GQLiteralOutputFieldOptions<ObjType>;
fieldOptions: OutputFieldOpts;
};
export type FieldDefType<GenTypes, ObjType = any> =
| MixDef<GenTypes>
| MixAbstractDef<any>
| FieldDef<ObjType>;
export type FieldDefType = MixDef | MixAbstractDef | FieldDef;
export type EnumDefType =
| MixDef
| { item: NodeType.ENUM_MEMBER; info: GQLiteralEnumMemberInfo };
| { item: NodeType.ENUM_MEMBER; info: EnumMemberInfo };
export type UnionTypeDef =
| MixDef
| { item: NodeType.UNION_MEMBER; typeName: string };
export interface GQLiteralEnumMemberInfo {
value: string;
internalValue: string;
export interface EnumMemberOpts extends CommonOpts {
value?: any;
}
export interface EnumMemberInfo {
name: string;
value: any;
description?: string;
}
@ -98,12 +80,12 @@ export interface GQLiteralEnumMemberInfo {
* When you're mixing types/partials, you can pick or omit
* fields from the types you're mixing in.
*/
export interface GQLiteralMixOptions<TMembers extends string> {
export interface MixOpts<TMembers> {
pick?: Array<TMembers>;
omit?: Array<TMembers>;
}
export interface GQLiteralDeprecationInfo {
export interface DeprecationInfo {
/**
* Date | YYYY-MM-DD formatted date of when this field
* became deprecated.
@ -119,7 +101,7 @@ export interface GQLiteralDeprecationInfo {
supersededBy?: string;
}
export interface GQLiteralCommonOptions {
export interface CommonOpts {
/**
* The description of the field, as defined in the GraphQL
* object definition
@ -134,47 +116,15 @@ export interface GQLiteralCommonOptions {
* Info about a field deprecation. Formatted as a string and provided with the
* deprecated directive on field/enum types and as a comment on input fields.
*/
deprecation?: GQLiteralDeprecationInfo;
/**
* Default value for the field, if none is returned.
*/
defaultValue?: any;
deprecation?: string | DeprecationInfo;
}
export interface GQLiteralArgument {
/**
* The name of the argument
*/
argName: string;
/**
* The value for a field,
*/
argType: string;
/**
* The "default value" for the arg, if none is passed.
*/
defaultValue?: string;
}
export interface GQLiteralOutputFieldOptions<ObjType>
extends GQLiteralCommonOptions {
export interface FieldOpts extends CommonOpts {
/**
* Whether the field can be returned or input as null
* @default false
*/
nullable?: boolean;
/**
* Any arguments defined
*/
args?: GQLiteralArgument[];
/**
* Property to use to resolve the field. If resolve is specified, this field is ignored.
*/
property?: Extract<keyof Root, string>;
/**
* Resolver for the output field
*/
resolve?: GraphQLFieldResolver<Root, GQLiteral.Context>;
/**
* Whether the field returns a list of values, or just a single value.
*/
@ -187,14 +137,65 @@ export interface GQLiteralOutputFieldOptions<ObjType>
listItemNullable?: boolean;
}
export interface GQLiteralInputFieldOptions extends GQLiteralCommonOptions {
export interface ArgOpts extends FieldOpts {
/**
* Default value for the input field, if one is not provided.
* Whether the field is required
*/
required?: boolean;
/**
* Whether the item in the list is required
*/
requiredListItem?: boolean;
}
export type ArgDefinition = Readonly<
ArgOpts & {
type: any; // TODO: Make type safe
}
>;
export interface ObjTypeDef {
root: any;
context: any;
args: { [argName: string]: any };
}
type FieldResolver<
GenTypes,
TypeName,
FieldName
> = GenTypes extends Gen.GenTypesShape ? any : any;
export type OutputFieldArgs = Record<string, ArgDefinition>;
export interface OutputFieldOpts<
GenTypes = any,
TypeName = any,
FieldName = any
> extends FieldOpts {
/**
* Any arguments defined
*/
args?: OutputFieldArgs;
/**
* Property to use to resolve the field. If resolve is specified, this field is ignored.
*/
property?: Extract<keyof any, string>;
/**
* Resolver for the output field
*/
resolve?: FieldResolver<GenTypes, TypeName, FieldName>;
/**
* Default value for the field, if none is returned.
*/
defaultValue?: any;
}
export interface GQLiteralScalarOptions
export interface InputFieldOpts extends FieldOpts {}
export interface AllFieldOpts extends InputFieldOpts, OutputFieldOpts {}
export interface ScalarOpts
extends Omit<
GraphQLScalarTypeConfig<any, any>,
"name" | "astNode" | "extensionASTNodes"
@ -202,25 +203,38 @@ export interface GQLiteralScalarOptions
/**
* Any deprecation info for this scalar type
*/
deprecation?: GQLiteralDeprecationInfo;
deprecation?: string | DeprecationInfo;
}
export interface GQLiteralTypeMetadata {
interface SharedTypeConfig {
/**
* Description for a GraphQL type
*/
description?: string;
/**
* An (optional) isTypeOf check for the object type
* Info about a field deprecation. Formatted as a string and provided with the
* deprecated directive on field/enum types and as a comment on input fields.
*/
isTypeOf?: ((value: any) => boolean);
deprecation?: string | DeprecationInfo;
}
export interface GQLiteralInterfaceMetadata {
interface DefaultResolver {
/**
* Description for a GraphQL type
* Default field resolver for all members of this type
*/
description?: string;
defaultResolver?: GraphQLFieldResolver<any, any>;
}
export interface Nullability {
/**
* Configures the nullability defaults at the type-level
*/
nullabilityConfig?: NullabilityConfig;
}
export interface EnumTypeConfig extends SharedTypeConfig {}
export interface UnionTypeConfig extends SharedTypeConfig {
/**
* Optionally provide a custom type resolver function. If one is not provided,
* the default implementation will call `isTypeOf` on each implementing
@ -229,44 +243,107 @@ export interface GQLiteralInterfaceMetadata {
resolveType?: GraphQLTypeResolver<any, any>;
}
export interface GQLiteralSchemaConfig {
export interface InputTypeConfig extends SharedTypeConfig, Nullability {}
export interface ObjectTypeConfig
extends SharedTypeConfig,
Nullability,
DefaultResolver {
/**
* An (optional) isTypeOf check for the object type
*/
isTypeOf?: GraphQLIsTypeOfFn<any, any>;
}
export interface InterfaceTypeConfig extends SharedTypeConfig, Nullability {
/**
* Optionally provide a custom type resolver function. If one is not provided,
* the default implementation will call `isTypeOf` on each implementing
* Object type.
*/
resolveType?: GraphQLTypeResolver<any, any>;
}
export interface SchemaConfig extends Nullability, DefaultResolver {
/**
* All of the GraphQL types
*/
types: any[];
}
export interface GQLiteralArgOptions {
/**
* Whether this argument should be required
* Absolute path to where the GraphQL IDL file should be written
*/
required?: boolean;
definitionFilePath?: string;
/**
* Generates the types for intellisense/typescript
*/
typeGeneration?: (printedSchema: string) => Promise<void>;
}
/**
* Helpers for handling the generated schema
*/
export interface GenTypesShape {
interfaces: string;
enums: string;
enumTypes: Record<string, string>;
objectTypes: Record<string, object>;
inputObjectTypes: Record<string, object>;
}
export type NullabilityConfig = {
/**
* Whether non-list output fields can return null by default
*
* type Example {
* field: String!
* }
*
* @default false
*/
output?: boolean;
/**
* Whether outputs that return lists can be null by default
*
* type Example {
* field: [String]!
* }
*
* @default false
*/
outputList?: boolean;
/**
* Whether non-list output fields can return null by default
*
* type Example {
* field: [String!]
* }
*
* @default false
*/
outputListItem?: boolean;
/**
* Whether non-list input fields (field arguments, input type members) are nullable by default
*
* input Example {
* field: String
* }
*
* @default true
*/
input?: boolean;
/**
* Whether input fields that are lists are nullable by default
*
* input Example {
* field: [String]
* }
*
* @default true
*/
inputList?: boolean;
/**
* Whether any members of input list item values can be null by default
*
* input Example {
* field: [String!]
* }
*
* @default false
*/
inputListItem?: boolean;
};
export type GetTypeFn = (t: string) => GraphQLNamedType;
export type FieldTypeNames<S> = S extends any ? string : string;
export type InterfaceNames<S> = S extends { interfaces: string }
? S["interfaces"]
: string;
type EnumMemberFor<Schema, EnumName> = TypeName extends keyof Schema
? Schema[TypeName]
: any;
type TypeDefFor<Schema, TypeName> = TypeName extends keyof Schema
? Schema[TypeName]
: any;
export type ResolveType<GenTypes, TypeName> = (
root: Gen.RootType<GenTypes, TypeName>
) => Gen.InterfaceName<GenTypes>;

View File

@ -1,51 +1,543 @@
import { GraphQLNamedType, isNamedType } from "graphql";
import {
isNamedType,
GraphQLNonNull,
isOutputType,
GraphQLFieldConfig,
GraphQLInputFieldConfigMap,
GraphQLFieldConfigMap,
GraphQLFieldConfigArgumentMap,
GraphQLInputFieldConfig,
defaultFieldResolver,
isInputObjectType,
GraphQLNamedType,
GraphQLUnionType,
GraphQLInterfaceType,
GraphQLInputObjectType,
GraphQLObjectType,
GraphQLEnumType,
GraphQLScalarType,
GraphQLUnionTypeConfig,
isUnionType,
isObjectType,
isInterfaceType,
isEnumType,
GraphQLFieldResolver,
isInputType,
GraphQLInputType,
GraphQLList,
} from "graphql";
import * as Types from "./types";
import { GQLiteralTypeWrapper } from "./definitions";
import { buildGQLiteralType } from "./objects";
import { NodeType } from "./enums";
const NULL_DEFAULTS = {
output: false,
outputList: false,
outputListItem: false,
input: true,
inputList: true,
inputListItem: false,
};
/**
* Builds all of the types, properly accounts for any enums using "mix".
* Since the enum types are resolved synchronously, these need to guard for circular references.
* Builds the types, normalizing the "types" passed into the schema for a
* better developer experience
*/
export function gqliteralBuildTypes(
types: any[]
export function buildTypes(
types: any[],
schemaOptions?: Types.SchemaConfig
): Record<string, GraphQLNamedType> {
const finalTypeMap: Record<string, GraphQLNamedType> = {};
const pendingTypeMap: Record<
string,
((building: Set<string>) => GraphQLNamedType) | null
> = {};
const builder = new SchemaBuilder(schemaOptions || {});
types.forEach((typeDef) => {
if (typeDef instanceof GQLiteralTypeWrapper) {
pendingTypeMap[typeDef.name] = (currentlyBuilding) => {
finalTypeMap[typeDef.name] = buildGQLiteralType(
typeDef.name,
typeDef.type,
{
finalTypeMap,
pendingTypeMap,
currentlyBuilding,
}
);
pendingTypeMap[typeDef.name] = null;
return finalTypeMap[typeDef.name];
};
} else if (isNamedType(typeDef)) {
finalTypeMap[typeDef.name] = typeDef;
}
builder.addType(typeDef);
});
Object.keys(pendingTypeMap).forEach((key) => {
const pending = pendingTypeMap[key];
if (pending !== null) {
pending(new Set());
}
});
return finalTypeMap;
return builder.getFinalTypeMap();
}
/** Copied from graphql-js */
interface BuildConfig {
union: {
name: string;
members: Types.UnionTypeDef[];
typeConfig: Types.UnionTypeConfig;
};
object: {
name: string;
fields: Types.FieldDefType[];
interfaces: string[];
typeConfig: Types.ObjectTypeConfig;
};
input: {
name: string;
fields: Types.FieldDefType[];
typeConfig: Types.InputTypeConfig;
};
interface: {
name: string;
fields: Types.FieldDefType[];
typeConfig: Types.UnionTypeConfig;
};
enum: {
name: string;
members: Types.EnumDefType[];
typeConfig: Types.EnumTypeConfig;
};
}
/**
* Builds all of the types, properly accounts for any using "mix".
* Since the enum types are resolved synchronously, these need to guard for
* circular references at this step, while fields will guard for it during lazy evaluation.
*/
export class SchemaBuilder {
protected buildingTypes: Set<string> = new Set();
protected finalTypeMap: Record<string, GraphQLNamedType> = {};
protected pendingTypeMap: Record<string, GQLiteralTypeWrapper<any>> = {};
constructor(
protected schemaConfig: Types.Omit<Types.SchemaConfig, "types">
) {}
addType(typeDef: GQLiteralTypeWrapper<any> | GraphQLNamedType) {
if (this.finalTypeMap[typeDef.name] || this.pendingTypeMap[typeDef.name]) {
throw new Error(`Named type ${typeDef.name} declared more than once`);
}
if (isNamedType(typeDef)) {
this.finalTypeMap[typeDef.name] = typeDef;
} else {
this.pendingTypeMap[typeDef.name] = typeDef;
}
}
getFinalTypeMap(): Record<string, GraphQLNamedType> {
Object.keys(this.pendingTypeMap).forEach((key) => {
// If we've already constructed the type by this point,
// via circular dependency resolution don't worry about building it.
if (this.finalTypeMap[key]) {
return;
}
this.finalTypeMap[key] = this.getOrBuildType(key);
this.buildingTypes.clear();
});
return {};
}
inputObjectType(config: BuildConfig["input"]): GraphQLInputObjectType {
const { name, fields, typeConfig } = config;
return new GraphQLInputObjectType({
name,
fields: () => this.buildInputObjectFields(name, fields),
description: config.typeConfig.description,
});
}
objectType(config: BuildConfig["object"]) {
const { fields, interfaces, name, typeConfig } = config;
return new GraphQLObjectType({
name,
interfaces: () => interfaces.map((i) => this.getInterface(i)),
fields: () => this.buildObjectFields(name, fields, typeConfig),
});
}
interfaceType(config: BuildConfig["interface"]) {
let description;
const { name, fields, typeConfig } = config;
return new GraphQLInterfaceType({
name,
fields: () => this.buildObjectFields(name, fields, typeConfig),
resolveType: typeConfig.resolveType,
description,
// astNode?: Maybe<InterfaceTypeDefinitionNode>;
// extensionASTNodes?: Maybe<ReadonlyArray<InterfaceTypeExtensionNode>>;
});
}
enumType(config: BuildConfig["enum"]) {
const { name, typeConfig, members } = config;
let values: GraphQLEnumValueConfigMap = {},
description;
config.members.forEach((member) => {
switch (member.item) {
case Types.NodeType.ENUM_MEMBER:
values[member.info.name] = {
value: member.info.value,
description: member.info.description,
};
break;
case Types.NodeType.MIX:
const { pick, omit } = member.mixOptions;
enumToMix.getValues().forEach((val) => {
if (pick && pick.indexOf(val.name) === -1) {
return;
}
if (omit && omit.indexOf(val.name) !== -1) {
return;
}
values[val.name] = {
description: val.description,
deprecationReason: val.deprecationReason,
value: val.value,
astNode: val.astNode,
};
});
break;
}
});
if (Object.keys(values).length === 0) {
throw new Error(
`GQLiteralEnum ${this.name} must have at least one member`
);
}
return new GraphQLEnumType({
name,
values,
description,
});
}
unionType(config: BuildConfig["union"]) {
return new GraphQLUnionType({
name: config.name,
types: () => {
return config.members.reduce((result: GraphQLObjectType[], member) => {
switch (member.item) {
case Types.NodeType.MIX:
break;
case Types.NodeType.UNION_MEMBER:
const type = this.getOrBuildType(member.typeName);
if (!isObjectType(type)) {
throw new Error(
`Expected ${member.typeName} to be an ObjectType, saw ${
type.constructor.name
}`
);
}
return result.concat(type);
}
return result;
}, []);
},
resolveType: config.typeConfig,
});
}
protected missingType(typeName: string): GraphQLNamedType {
const suggestions = suggestionList(
typeName,
Object.keys(this.buildingTypes).concat(Object.keys(this.finalTypeMap))
);
let suggestionsString = "";
if (suggestions.length > 0) {
suggestionsString = ` or mean ${suggestions.join(", ")}`;
}
throw new Error(
`Missing type ${typeName}, did you forget to import a type${suggestionsString}?`
);
}
protected buildObjectFields(
typeName: string,
fields: Types.FieldDefType[],
typeConfig: Types.ObjectTypeConfig
): GraphQLFieldConfigMap<any, any> {
const fieldMap: GraphQLFieldConfigMap<any, any> = {};
fields.forEach((field) => {
switch (field.item) {
case Types.NodeType.MIX:
case Types.NodeType.MIX_ABSTRACT:
throw new Error("TODO");
break;
case Types.NodeType.FIELD:
fieldMap[field.fieldName] = this.buildObjectField(field, typeConfig);
break;
}
});
return fieldMap;
}
protected buildObjectField(
field: Types.FieldDef,
typeConfig: Types.ObjectTypeConfig
): GraphQLFieldConfig<any, any> {
return {
type: this.getOutputType(field.fieldType),
resolve: this.getResolver(field.fieldOptions, typeConfig),
description: typeConfig.description,
args: this.buildArgs(field.fieldOptions.args || {}, typeConfig),
// subscribe?: GraphQLFieldResolver<TSource, TContext, TArgs>;
// deprecationReason?: Maybe<string>;
// description?: Maybe<string>;
// astNode?: Maybe<FieldDefinitionNode>;
};
}
protected buildInputObjectFields(
typeName: string,
fields: Types.FieldDefType[]
): GraphQLInputFieldConfigMap {
return {};
}
protected buildInputObjectField(): GraphQLInputFieldConfig<any, any> {
return {};
}
protected buildArgs(
args: Types.OutputFieldArgs,
typeConfig: Types.InputTypeConfig
): GraphQLFieldConfigArgumentMap {
const allArgs: GraphQLFieldConfigArgumentMap = {};
Object.keys(allArgs).forEach((argName) => {
const argDef = args[argName];
allArgs[argName] = {
type: this.decorateInputType(
this.getInputType(argDef.type),
argDef,
typeConfig
),
description: argDef.description,
};
});
return {};
}
protected decorateOutputType(
type: GraphQLInputType,
typeOpts: Types.FieldOpts,
typeConfig: Types.ObjectTypeConfig
) {
return this.decorateType(type, typeOpts, typeConfig, false);
}
protected decorateInputType(
type: GraphQLInputType,
argOpts: Types.ArgOpts,
typeConfig: Types.InputTypeConfig
) {
const { required: _required, requiredListItem, ...rest } = argOpts;
const newOpts = rest;
if (typeof _required !== "undefined") {
newOpts.nullable = !_required;
}
if (typeof requiredListItem !== "undefined") {
if (rest.list) {
newOpts.listItemNullable = !requiredListItem;
}
}
return this.decorateType(type, newOpts, typeConfig, true);
}
/**
* Adds the null / list configuration to the type.
*/
protected decorateType(
type: GraphQLInputType,
fieldOpts: Types.FieldOpts,
typeConfig: Types.ObjectTypeConfig,
isInput: boolean
): GraphQLInputType {
let finalType = type;
const nullConfig = {
...NULL_DEFAULTS,
...this.schemaConfig.nullabilityConfig,
...typeConfig.nullabilityConfig,
};
const { list, nullable, listItemNullable } = fieldOpts;
const isNullable =
typeof nullable !== "undefined"
? nullable
: list
? isInput
? nullConfig.inputList
: nullConfig.outputList
: isInput
? nullConfig.input
: nullConfig.output;
// TODO: Figure out how lists of lists will be represented.
if (list) {
const nullableItem =
typeof listItemNullable !== "undefined"
? listItemNullable
: isInput
? nullConfig.inputListItem
: nullConfig.outputListItem;
if (nullableItem) {
finalType = GraphQLNonNull(finalType);
}
finalType = GraphQLList(finalType);
} else if (typeof listItemNullable !== "undefined") {
console.log(
"listItemNullable should only be set with list: true, this option is ignored"
);
}
if (isNullable) {
return GraphQLNonNull(finalType);
}
return finalType;
}
protected getInterface(name: string): GraphQLInterfaceType {
const type = this.getOrBuildType(name);
if (!isInterfaceType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLInterfaceType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getEnum(name: string): GraphQLEnumType {
const type = this.getOrBuildType(name);
if (!isEnumType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLEnumType, saw ${type.constructor.name}`
);
}
return type;
}
protected getUnion(name: string): GraphQLUnionType {
const type = this.getOrBuildType(name);
if (!isUnionType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLUnionType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getInputType(name: string) {
const type = this.getOrBuildType(name);
if (!isInputType(type)) {
throw new Error(
`Expected ${name} to be a valid input type, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getOutputType(name: string) {
const type = this.getOrBuildType(name);
if (!isOutputType(type)) {
throw new Error(
`Expected ${name} to be a valid output type, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getObjectType(name: string) {
const type = this.getOrBuildType(name);
if (!isObjectType(type)) {
throw new Error(
`Expected ${name} to be a GraphQLObjectType, saw ${
type.constructor.name
}`
);
}
return type;
}
protected getOrBuildType(name: string): GraphQLNamedType {
if (this.finalTypeMap[name]) {
return this.finalTypeMap[name];
}
if (this.buildingTypes.has(name)) {
throw new Error(
`GQLiteral: Circular dependency detected, while building types ${Array.from(
this.buildingTypes
)}`
);
}
const pendingType = this.pendingTypeMap[name];
if (pendingType) {
this.buildingTypes.add(name);
return pendingType.type.buildType(name, this);
}
return this.missingType(name);
}
protected getResolver(
fieldOptions: Types.OutputFieldOpts,
typeConfig: Types.ObjectTypeConfig
) {
if (fieldOptions.resolve) {
if (typeof fieldOptions.property !== "undefined") {
console.warn(
`Both resolve and property should not be supplied, property will be ignored`
);
}
return fieldOptions.resolve;
}
if (fieldOptions.property) {
return propertyFieldResolver(fieldOptions.property);
}
if (typeConfig.defaultResolver) {
return typeConfig.defaultResolver;
}
if (this.schemaConfig.defaultResolver) {
return this.schemaConfig.defaultResolver;
}
return defaultFieldResolver;
}
}
export function withDeprecationComment(description?: string | null) {
return description;
}
export const enumShorthandMembers = (
arg: string[] | Record<string, string | number | object | boolean>
): Types.EnumMemberInfo[] => {
if (Array.isArray(arg)) {
return arg.map((name) => ({ name, value: name }));
}
return Object.keys(arg).map((name) => {
return {
name,
value: arg[name],
};
});
};
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function while passing along args and context value.
*/
export const propertyFieldResolver = (
key: string
): GraphQLFieldResolver<any, any> =>
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?
const property = source[key];
if (typeof property === "function") {
return source[key](args, contextValue, info);
}
return property;
}
};
// ----------------------------
/**
*
* Copied from graphql-js:
*
*/
/**
* Given an invalid input string and a list of valid options, returns a filtered
@ -86,7 +578,6 @@ export default function suggestionList(
*
* This distance can be useful for detecting typos in input or sorting
*/
function lexicalDistance(aStr: string, bStr: string): number {
if (aStr === bStr) {
return 0;

View File

@ -6,17 +6,17 @@ authorURL: http://twitter.com/tgriesser
## Why GQLiteral?
GQLiteral comes from my experiences building several production grade APIs in GraphQL, ever since the project was initially released. I have been lucky enough to have the opportunity to work in different languages and frameworks while building these tools - namely [graphql-js](https://github.com/graphql/graphql-js), [graphql-tools](https://github.com/graphql/graphql-js) (Apollo), [graphene-python](https://docs.graphene-python.org/en/latest/) and [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
GQLiteral comes from my experiences building several production APIs in GraphQL, and actively contributing to tooling since the project was initially released. I've been lucky enough to have the opportunity to work in different languages and frameworks while building these APIs - namely [graphql-js](https://github.com/graphql/graphql-js), [graphql-tools](https://github.com/graphql/graphql-js) (Apollo), [graphene-python](https://docs.graphene-python.org/en/latest/) and [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
<!--truncate-->
After working with some of these other toolkits, it felt like the JavaScript implementations were lacking a bit. The schema-first development starts out great, by simply expressing your schema in the GraphQL Interface Definition Language (IDL), and providing resolvers matching to the types as needed you are up and running fast! No need for tons of requires just to get your schema working properly, no
After working with the toolkits in other languages, it felt like the JavaScript implementations were lacking a bit. The schema-first development starts out great, by simply expressing your schema in the GraphQL Interface Definition Language (IDL) and providing resolvers matching to the types as needed you are up and running fast! No need for tons of requires or "overhead" to get a GraphQL server running.
As your schema then grows to hundreds or thousands of types, manually curating these IDL fragments becomes tedious. Documentation changes can be tough. Changes to fields on interfaces can require manual changes to many implementing types, a process that can be quite error prone. GQLiteral looks to split the difference between these approaches, keeping the process as simple as possible while leveraging the runtime to introduce powerful ways of [mixing types]().
As your schema then grows to hundreds or thousands of types, manually curating these IDL fragments becomes tedious. Documentation changes can be tough. Modifying fields on interfaces can require manual changes to many implementing types, a process that can be quite error prone. If only there were a way to combine the simplicity of schema-first development, with the maintainability of the traditional approach. GQLiteral aims to fill that voide, keeping the process as simple as possible while leveraging the runtime to introduce powerful ways of [mixing types](), introducing [type]() or [schema]() wide changes, and much more.
The core idea of GQLiteral draws from the simplicity of basing the schema off the IDL - just using the type names to reference types! It's an idea that had been sitting in the back of my head since a PR for resolving interfaces with the [type name](https://github.com/graphql/graphql-js/pull/509) - just haven't taken the time since then to write it all out.
The core idea of GQLiteral draws from basing the schema off the IDL - it uses the type names rather than imports to reference types! It's an idea that had been sitting in the back of my mind since a PR for resolving interfaces with the [type name](https://github.com/graphql/graphql-js/pull/509).
GQLiteral was strongly influenced by [graphene-python](https://docs.graphene-python.org/en/latest/), which was up until now my favorite experience building a GraphQL schema.
GQLiteral was strongly influenced by [graphene-python](https://docs.graphene-python.org/en/latest/), which until this point has been my favorite experience of building a GraphQL schema.
Because some features of Python do not exist or do not translate well in JavaScript, namely multiple-inheritance and class metaprogramming, some parts of the API have been reimagined to work well with what the JavaScript does provide us: excellent support for first class functions!
@ -31,3 +31,6 @@ Opinions on resolvers or context. While I have preferences on how resolvers and
Explicit integration with any ORMs, data-stores, etc.
## Further Reading
- [Getting Started]()
- [Simple Example API]()

View File

@ -73,7 +73,9 @@ class HomeSplash extends React.Component {
<div className="inner">
<ProjectTitle />
<PromoSection>
<Button href="#try">Why GQLiteral?</Button>
<Button href={pageUrl("blog/2018/11/04/introducing-gqliteral")}>
Why GQLiteral?
</Button>
<Button href={docUrl("doc2.html", language)}>API Docs</Button>
<Button href={docUrl("getting-started.html", language)}>
Examples

View File

@ -1,4 +1,4 @@
import React from "react";
const React = require("react");
const Playground = (props) => {
return <div>Hello World</div>;

View File

@ -0,0 +1,5 @@
env:
es6: true
browser: true
parserOptions:
sourceType: module

View File

@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom";
const root = document.getElementById("bottom-bar");
export default function({ left, right }) {
return ReactDOM.createPortal(
<React.Fragment>
<div className="bottom-bar-buttons">{left}</div>
<div className="bottom-bar-buttons bottom-bar-buttons-right">{right}</div>
</React.Fragment>,
root
);
}

View File

@ -0,0 +1,34 @@
import React from "react";
import { stateToggler, shallowEqual } from "./helpers";
import * as storage from "./storage";
export default class extends React.Component {
constructor() {
super();
this.state = Object.assign(
{
showSidebar: false,
showAst: false,
showDoc: false,
showSecondFormat: false,
toggleSidebar: () => this.setState(stateToggler("showSidebar")),
toggleAst: () => this.setState(stateToggler("showAst")),
toggleDoc: () => this.setState(stateToggler("showDoc")),
toggleSecondFormat: () =>
this.setState(stateToggler("showSecondFormat"))
},
storage.get("editor_state")
);
}
componentDidUpdate(_, prevState) {
if (!shallowEqual(this.state, prevState)) {
storage.set("editor_state", this.state);
}
}
render() {
return this.props.children(this.state);
}
}

View File

@ -0,0 +1,278 @@
import React from "react";
import { Button, ClipboardButton, LinkButton } from "./buttons";
import EditorState from "./EditorState";
import { DebugPanel, InputPanel, OutputPanel } from "./panels";
import PrettierFormat from "./PrettierFormat";
import { shallowEqual } from "./helpers";
import * as urlHash from "./urlHash";
import formatMarkdown from "./markdown";
import * as util from "./util";
import { Sidebar, SidebarCategory } from "./sidebar/components";
import SidebarOptions from "./sidebar/SidebarOptions";
import Option from "./sidebar/options";
import { Checkbox } from "./sidebar/inputs";
const CATEGORIES_ORDER = [
"Global",
"Common",
"JavaScript",
"Markdown",
"HTML",
"Special",
];
const ENABLED_OPTIONS = [
"parser",
"printWidth",
"tabWidth",
"useTabs",
"semi",
"singleQuote",
"bracketSpacing",
"jsxBracketSameLine",
"arrowParens",
"trailingComma",
"proseWrap",
"htmlWhitespaceSensitivity",
"insertPragma",
"requirePragma",
];
class Playground extends React.Component {
constructor(props) {
super();
const original = urlHash.read();
const defaultOptions = util.getDefaults(
props.availableOptions,
ENABLED_OPTIONS
);
const options = Object.assign(defaultOptions, original.options);
const content = original.content || getCodeSample(options.parser);
this.state = { content, options };
this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
this.setContent = (content) => this.setState({ content });
this.clearContent = this.setContent.bind(this, "");
this.resetOptions = () => this.setState({ options: defaultOptions });
this.enabledOptions = orderOptions(props.availableOptions, ENABLED_OPTIONS);
this.rangeStartOption = props.availableOptions.find(
(opt) => opt.name === "rangeStart"
);
this.rangeEndOption = props.availableOptions.find(
(opt) => opt.name === "rangeEnd"
);
}
componentDidUpdate(_, prevState) {
const { content, options } = this.state;
if (
!shallowEqual(prevState.options, this.state.options) ||
prevState.content !== content
) {
urlHash.replace({ content, options });
}
}
handleOptionValueChange(option, value) {
this.setState((state) => {
const options = Object.assign({}, state.options);
if (option.type === "int" && isNaN(value)) {
delete options[option.name];
} else {
options[option.name] = value;
}
const content =
state.content === "" ||
state.content === getCodeSample(state.options.parser)
? getCodeSample(options.parser)
: state.content;
return { options, content };
});
}
getMarkdown(formatted, reformatted, full) {
const { content, options } = this.state;
const { availableOptions, version } = this.props;
return formatMarkdown(
content,
formatted,
reformatted || "",
version,
window.location.href,
options,
util.buildCliArgs(availableOptions, options),
full
);
}
render() {
const { worker } = this.props;
const { content, options } = this.state;
return (
<EditorState>
{(editorState) => (
<PrettierFormat
worker={worker}
code={content}
options={options}
debugAst={editorState.showAst}
debugDoc={editorState.showDoc}
reformat={editorState.showSecondFormat}
>
{({ formatted, debug }) => (
<React.Fragment>
<div className="editors-container">
<Sidebar visible={editorState.showSidebar}>
<SidebarOptions
categories={CATEGORIES_ORDER}
availableOptions={this.enabledOptions}
optionValues={options}
onOptionValueChange={this.handleOptionValueChange}
/>
<SidebarCategory title="Range">
<label>
The selected range will be highlighted in yellow in the
input editor
</label>
<Option
option={this.rangeStartOption}
value={options.rangeStart}
onChange={this.handleOptionValueChange}
/>
<Option
option={this.rangeEndOption}
value={options.rangeEnd}
overrideMax={content.length}
onChange={this.handleOptionValueChange}
/>
</SidebarCategory>
<SidebarCategory title="Debug">
<Checkbox
label="show AST"
checked={editorState.showAst}
onChange={editorState.toggleAst}
/>
<Checkbox
label="show doc"
checked={editorState.showDoc}
onChange={editorState.toggleDoc}
/>
<Checkbox
label="show second format"
checked={editorState.showSecondFormat}
onChange={editorState.toggleSecondFormat}
/>
</SidebarCategory>
<div className="sub-options">
<Button onClick={this.resetOptions}>
Reset to defaults
</Button>
</div>
</Sidebar>
<div className="editors">
<InputPanel
mode={util.getCodemirrorMode(options.parser)}
ruler={options.printWidth}
value={content}
codeSample={getCodeSample(options.parser)}
overlayStart={options.rangeStart}
overlayEnd={options.rangeEnd}
onChange={this.setContent}
/>
{editorState.showAst ? (
<DebugPanel value={debug.ast || ""} />
) : null}
{editorState.showDoc ? (
<DebugPanel value={debug.doc || ""} />
) : null}
<OutputPanel
mode={util.getCodemirrorMode(options.parser)}
value={formatted}
ruler={options.printWidth}
/>
{editorState.showSecondFormat ? (
<OutputPanel
mode={util.getCodemirrorMode(options.parser)}
value={getSecondFormat(formatted, debug.reformatted)}
ruler={options.printWidth}
/>
) : null}
</div>
</div>
<div className="bottom-bar">
<div className="bottom-bar-buttons">
<Button onClick={editorState.toggleSidebar}>
{editorState.showSidebar ? "Hide" : "Show"} options
</Button>
<Button onClick={this.clearContent}>Clear</Button>
<ClipboardButton copy={JSON.stringify(options, null, 2)}>
Copy config JSON
</ClipboardButton>
</div>
<div className="bottom-bar-buttons bottom-bar-buttons-right">
<ClipboardButton copy={window.location.href}>
Copy link
</ClipboardButton>
<ClipboardButton
copy={() =>
this.getMarkdown(formatted, debug.reformatted)
}
>
Copy markdown
</ClipboardButton>
<LinkButton
href={getReportLink(
this.getMarkdown(formatted, debug.reformatted, true)
)}
target="_blank"
rel="noopener"
>
Report issue
</LinkButton>
</div>
</div>
</React.Fragment>
)}
</PrettierFormat>
)}
</EditorState>
);
}
}
function orderOptions(availableOptions, order) {
const optionsByName = {};
for (const option of availableOptions) {
optionsByName[option.name] = option;
}
return order.map((name) => optionsByName[name]);
}
function getReportLink(reportBody) {
return `https://github.com/prettier/prettier/issues/new?body=${encodeURIComponent(
reportBody
)}`;
}
function getSecondFormat(formatted, reformatted) {
return formatted === ""
? ""
: formatted === reformatted
? "✓ Second format is unchanged."
: reformatted;
}
export default Playground;

View File

@ -0,0 +1,40 @@
import React from "react";
export default class PrettierFormat extends React.Component {
constructor() {
super();
this.state = { formatted: "", debug: {} };
}
componentDidMount() {
this.format();
}
componentDidUpdate(prevProps) {
for (const key of ["code", "options", "debugAst", "debugDoc", "reformat"]) {
if (prevProps[key] !== this.props[key]) {
this.format();
break;
}
}
}
format() {
const {
worker,
code,
options,
debugAst: ast,
debugDoc: doc,
reformat
} = this.props;
worker
.format(code, options, { ast, doc, reformat })
.then(result => this.setState(result));
}
render() {
return this.props.children(this.state);
}
}

View File

@ -0,0 +1,26 @@
import React from "react";
import ReactDOM from "react-dom";
const root = document.getElementById("version");
export default function({ version }) {
const match = version.match(/^pr-(\d+)$/);
let href;
if (match) {
href = `pull/${match[1]}`;
} else if (version.match(/\.0$/)) {
href = `releases/tag/${version}`;
} else {
href = `blob/master/CHANGELOG.md#${version.replace(/\./g, "")}`;
}
return ReactDOM.createPortal(
<a
href={`https://github.com/prettier/prettier/${href}`}
target="_blank"
rel="noopener"
>
{match ? `PR #${match[1]}` : `v${version}`}
</a>,
root
);
}

View File

@ -0,0 +1,40 @@
export default function(source) {
const worker = new Worker(source);
let counter = 0;
const handlers = {};
worker.addEventListener("message", (event) => {
const { uid, message, error } = event.data;
if (!handlers[uid]) {
return;
}
const [resolve, reject] = handlers[uid];
delete handlers[uid];
if (error) {
reject(error);
} else {
resolve(message);
}
});
function postMessage(message) {
const uid = ++counter;
return new Promise((resolve, reject) => {
handlers[uid] = [resolve, reject];
worker.postMessage({ uid, message });
});
}
return {
getMetadata() {
return postMessage({ type: "meta" });
},
format(code, options, debug) {
return postMessage({ type: "format", code, options, debug });
},
postMessage,
};
}

View File

@ -0,0 +1,57 @@
import React from "react";
import ClipboardJS from "clipboard";
export const Button = React.forwardRef((props, ref) => (
<button type="button" className="btn" ref={ref} {...props} />
));
export function LinkButton(props) {
return <a className="btn" {...props} />;
}
export class ClipboardButton extends React.Component {
constructor() {
super();
this.state = { showTooltip: false, tooltipText: "" };
this.timer = null;
this.ref = React.createRef();
}
componentDidMount() {
this.clipboard = new ClipboardJS(this.ref.current, {
text: () => {
const { copy } = this.props;
return typeof copy === "function" ? copy() : copy;
}
});
this.clipboard.on("success", () => this.showTooltip("Copied!"));
this.clipboard.on("error", () => this.showTooltip("Press ctrl+c to copy"));
}
showTooltip(text) {
this.setState({ showTooltip: true, tooltipText: text }, () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.timer = null;
this.setState({ showTooltip: false });
}, 2000);
});
}
render() {
const rest = Object.assign({}, this.props);
delete rest.children;
delete rest.copy;
return (
<Button ref={this.ref} {...rest}>
{this.state.showTooltip ? (
<span className="tooltip">{this.state.tooltipText}</span>
) : null}
{this.props.children}
</Button>
);
}
}

View File

@ -0,0 +1,49 @@
export function stateToggler(key) {
return state => ({ [key]: !state[key] });
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
function is(x, y) {
// SameValue algorithm
if (x === y) {
// Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
}
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
export function shallowEqual(objA, objB) {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== "object" ||
objA === null ||
typeof objB !== "object" ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,61 @@
import "codemirror-graphql/mode";
import React from "react";
import ReactDOM from "react-dom";
import Playground from "./Playground";
import VersionLink from "./VersionLink";
import WorkerApi from "./WorkerApi";
import { fixPrettierVersion } from "./util";
class App extends React.Component {
constructor() {
super();
this.state = { loaded: false };
this.worker = new WorkerApi("/worker.js");
}
componentDidMount() {
this.worker.getMetadata().then(({ supportInfo, version }) => {
this.setState({
loaded: true,
availableOptions: supportInfo.options.map(augmentOption),
version: fixPrettierVersion(version)
});
});
}
render() {
const { loaded, availableOptions, version } = this.state;
if (!loaded) {
return "Loading...";
}
return (
<React.Fragment>
<VersionLink version={version} />
<Playground
worker={this.worker}
availableOptions={availableOptions}
version={version}
/>
</React.Fragment>
);
}
}
function augmentOption(option) {
if (option.type === "boolean" && option.default === true) {
option.inverted = true;
}
option.cliName =
"--" +
(option.inverted ? "no-" : "") +
option.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
return option;
}
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -0,0 +1,76 @@
function formatMarkdown(
input,
output,
output2,
version,
url,
options,
cliOptions,
full
) {
const syntax = getMarkdownSyntax(options);
const optionsString = formatCLIOptions(cliOptions);
const isIdempotent = output2 === "" || output === output2;
return [
`**Prettier ${version}**`,
`[Playground link](${url})`,
optionsString === "" ? null : codeBlock(optionsString, "sh"),
"",
"**Input:**",
codeBlock(input, syntax),
"",
"**Output:**",
codeBlock(output, syntax)
]
.concat(
isIdempotent ? [] : ["", "**Second Output:**", codeBlock(output2, syntax)]
)
.concat(full ? ["", "**Expected behavior:**", ""] : [])
.filter(part => {
return part != null;
})
.join("\n");
}
function getMarkdownSyntax(options) {
switch (options.parser) {
case "babylon":
case "flow":
return "jsx";
case "typescript":
return "tsx";
case "json":
case "json-stringify":
return "jsonc";
case "glimmer":
return "hbs";
case "angular":
return "html";
default:
return options.parser;
}
}
function formatCLIOptions(cliOptions) {
return cliOptions
.map(option => {
const name = option[0];
const value = option[1];
return value === true ? name : `${name} ${value}`;
})
.join("\n");
}
function codeBlock(content, syntax) {
const backtickSequences = content.match(/`+/g) || [];
const longestBacktickSequenceLength = Math.max.apply(
null,
backtickSequences.map(backticks => backticks.length)
);
const fenceLength = Math.max(3, longestBacktickSequenceLength + 1);
const fence = Array(fenceLength + 1).join("`");
return [fence + (syntax || ""), content, fence].join("\n");
}
module.exports = formatMarkdown;

View File

@ -0,0 +1,185 @@
import CodeMirror from "codemirror";
import React from "react";
class CodeMirrorPanel extends React.Component {
constructor() {
super();
this._textareaRef = React.createRef();
this._codeMirror = null;
this._cached = "";
this._overlay = null;
this.handleChange = this.handleChange.bind(this);
this.handleFocus = this.handleFocus.bind(this);
}
componentDidMount() {
const options = Object.assign({}, this.props);
delete options.ruler;
delete options.rulerColor;
delete options.value;
delete options.onChange;
options.rulers = [makeRuler(this.props)];
this._codeMirror = CodeMirror.fromTextArea(
this._textareaRef.current,
options
);
this._codeMirror.on("change", this.handleChange);
this._codeMirror.on("focus", this.handleFocus);
this.updateValue(this.props.value || "");
this.updateOverlay();
}
componentWillUnmount() {
this._codeMirror && this._codeMirror.toTextArea();
}
componentDidUpdate(prevProps) {
if (this.props.value !== this._cached) {
this.updateValue(this.props.value);
}
if (
this.props.overlayStart !== prevProps.overlayStart ||
this.props.overlayEnd !== prevProps.overlayEnd
) {
this.updateOverlay();
}
if (this.props.mode !== prevProps.mode) {
this._codeMirror.setOption("mode", this.props.mode);
}
if (this.props.ruler !== prevProps.ruler) {
this._codeMirror.setOption("rulers", [makeRuler(this.props)]);
}
}
updateValue(value) {
this._cached = value;
this._codeMirror.setValue(value);
}
updateOverlay() {
if (!this.props.readOnly) {
if (this._overlay) {
this._codeMirror.removeOverlay(this._overlay);
}
const [start, end] = getIndexPosition(this.props.value, [
this.props.overlayStart,
this.props.overlayEnd
]);
this._overlay = createOverlay(start, end);
this._codeMirror.addOverlay(this._overlay);
}
}
handleFocus(/* codeMirror, event */) {
if (this._codeMirror.getValue() === this.props.codeSample) {
this._codeMirror.execCommand("selectAll");
}
}
handleChange(doc, change) {
if (change.origin !== "setValue") {
this._cached = doc.getValue();
this.props.onChange(this._cached);
this.updateOverlay();
}
}
render() {
return (
<div className="editor input">
<textarea ref={this._textareaRef} />
</div>
);
}
}
function getIndexPosition(text, indexes) {
indexes = indexes.slice();
let line = 0;
let count = 0;
let lineStart = 0;
const result = [];
while (indexes.length) {
const index = indexes.shift();
while (count < index && count < text.length) {
if (text[count] === "\n") {
line++;
lineStart = count;
}
count++;
}
result.push({ line, pos: count - lineStart });
}
return result;
}
function createOverlay(start, end) {
return {
token(stream) {
const line = stream.lineOracle.line;
if (line < start.line || line > end.line) {
stream.skipToEnd();
} else if (line === start.line && stream.pos < start.pos) {
stream.pos = start.pos;
} else if (line === end.line) {
if (stream.pos < end.pos) {
stream.pos = end.pos;
return "searching";
}
stream.skipToEnd();
} else {
stream.skipToEnd();
return "searching";
}
}
};
}
function makeRuler(props) {
return { column: props.ruler, color: props.rulerColor };
}
export function InputPanel(props) {
return (
<CodeMirrorPanel
lineNumbers={true}
keyMap="sublime"
autoCloseBrackets={true}
matchBrackets={true}
showCursorWhenSelecting={true}
tabSize={4}
rulerColor="#eeeeee"
{...props}
/>
);
}
export function OutputPanel(props) {
return (
<CodeMirrorPanel
readOnly={true}
lineNumbers={true}
rulerColor="#444444"
{...props}
/>
);
}
export function DebugPanel({ value }) {
return (
<CodeMirrorPanel
readOnly={true}
lineNumbers={false}
mode="jsx"
value={value}
/>
);
}

View File

@ -0,0 +1,29 @@
import React from "react";
import groupBy from "lodash.groupby";
import { SidebarCategory } from "./components";
import Option from "./options";
export default function({
categories,
availableOptions,
optionValues,
onOptionValueChange
}) {
const options = groupBy(availableOptions, "category");
return categories.map(
category =>
options[category] ? (
<SidebarCategory key={category} title={category}>
{options[category].map(option => (
<Option
key={option.name}
option={option}
value={optionValues[option.name]}
onChange={onOptionValueChange}
/>
))}
</SidebarCategory>
) : null
);
}

View File

@ -0,0 +1,18 @@
import React from "react";
export function Sidebar({ visible, children }) {
return (
<div className={`options-container ${visible ? "open" : ""}`}>
<div className="options">{children}</div>
</div>
);
}
export function SidebarCategory({ title, children }) {
return (
<details className="sub-options" open="true">
<summary>{title}</summary>
{children}
</details>
);
}

View File

@ -0,0 +1,53 @@
import React from "react";
export function Checkbox({ label: _label, title, checked, onChange }) {
return (
<label title={title}>
<input
type="checkbox"
checked={checked}
onChange={ev => onChange(ev.target.checked)}
/>{" "}
{_label}
</label>
);
}
export function Select({ label: _label, title, values, selected, onChange }) {
return (
<label title={title}>
{_label}{" "}
<select value={selected} onChange={ev => onChange(ev.target.value)}>
{values.map(val => (
<option key={val} value={val}>
{val}
</option>
))}
</select>
</label>
);
}
export function NumberInput({
label: _label,
title,
value,
min,
max,
step,
onChange
}) {
return (
<label title={title}>
{_label}{" "}
<input
type="number"
min={min}
max={max}
step={step}
value={value}
onChange={ev => onChange(parseInt(ev.target.value, 10))}
/>
</label>
);
}

View File

@ -0,0 +1,63 @@
import React from "react";
import { Checkbox, Select, NumberInput } from "./inputs";
export function BooleanOption({ option, value, onChange }) {
function maybeInvert(value) {
return option.inverted ? !value : value;
}
return (
<Checkbox
label={option.cliName}
title={getDescription(option)}
checked={maybeInvert(value)}
onChange={checked => onChange(option, maybeInvert(checked))}
/>
);
}
export function ChoiceOption({ option, value, onChange }) {
return (
<Select
label={option.cliName}
title={getDescription(option)}
values={option.choices.map(choice => choice.value)}
selected={value}
onChange={val => onChange(option, val)}
/>
);
}
export function NumberOption({ option, value, onChange }) {
return (
<NumberInput
label={option.cliName}
title={getDescription(option)}
min={option.range.start}
max={option.range.end}
step={option.range.step}
value={value}
onChange={val => onChange(option, val)}
/>
);
}
export default function(props) {
switch (props.option.type) {
case "boolean":
return <BooleanOption {...props} />;
case "int":
return <NumberOption {...props} />;
case "choice":
return <ChoiceOption {...props} />;
default:
throw new Error("unsupported type");
}
}
function getDescription(option) {
const description = option.inverted
? option.oppositeDescription
: option.description;
return description && description.replace(/\n/g, " ");
}

View File

@ -0,0 +1,15 @@
export function get(key) {
try {
return JSON.parse(window.localStorage.getItem(key));
} catch (_) {
// noop
}
}
export function set(key, value) {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (_) {
// noop
}
}

View File

@ -0,0 +1,35 @@
import LZString from "lz-string";
export function read() {
const hash = document.location.hash.slice(1);
if (!hash) {
return {};
}
// backwards support for old json encoded URIComponent
const decode =
hash.indexOf("%7B%22") !== -1
? decodeURIComponent
: LZString.decompressFromEncodedURIComponent;
try {
return JSON.parse(decode(hash));
} catch (_) {
return {};
}
}
export function replace(state) {
const hash = LZString.compressToEncodedURIComponent(JSON.stringify(state));
if (
typeof URL === "function" &&
typeof history === "object" &&
typeof history.replaceState === "function"
) {
const url = new URL(location);
url.hash = hash;
history.replaceState(null, null, url);
} else {
location.hash = hash;
}
}

View File

@ -0,0 +1,53 @@
export function fixPrettierVersion(version) {
const match = version.match(/^\d+\.\d+\.\d+-pr.(\d+)$/);
if (match) {
return `pr-${match[1]}`;
}
return version;
}
export function getDefaults(availableOptions, optionNames) {
const defaults = {};
for (const option of availableOptions) {
if (optionNames.includes(option.name)) {
defaults[option.name] =
option.name === "parser" ? "babylon" : option.default;
}
}
return defaults;
}
export function buildCliArgs(availableOptions, options) {
const args = [];
for (const option of availableOptions) {
const value = options[option.name];
if (typeof value === "undefined") {
continue;
}
if (option.type === "boolean") {
if ((value && !option.inverted) || (!value && option.inverted)) {
args.push([option.cliName, true]);
}
} else if (value !== option.default || option.name === "rangeStart") {
args.push([option.cliName, value]);
}
}
return args;
}
export function getCodemirrorMode(parser) {
switch (parser) {
case "css":
case "less":
case "scss":
return "css";
case "graphql":
return "graphql";
case "markdown":
return "markdown";
default:
return "jsx";
}
}

View File

@ -4,7 +4,8 @@
"getting-started",
"best-practices",
"typescript-setup",
"api-reference"
"api-reference",
"faq"
]
}
}

29
website/webpack.config.js Normal file
View File

@ -0,0 +1,29 @@
"use strict";
module.exports = {
entry: {
playground: "./playground/index.js",
},
output: {
filename: "[name].js",
path: __dirname + "/static/",
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["env", "react"],
},
},
],
},
externals: {
clipboard: "ClipboardJS",
codemirror: "CodeMirror",
react: "React",
"react-dom": "ReactDOM",
},
};

View File

@ -70,6 +70,10 @@
version "23.3.7"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.7.tgz#77f9a4332ccf8db680a31818ade3ee454c831a79"
"@types/node@^10.12.2":
version "10.12.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.2.tgz#d77f9faa027cadad9c912cd47f4f8b07b0fb0864"
"@types/prettier@1.13.2":
version "1.13.2"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.13.2.tgz#ffe96278e712a8d4e467e367a338b05e22872646"