Working through the API with examples, type safety working pretty well

This commit is contained in:
Tim Griesser 2018-11-11 16:20:30 -05:00
parent 5b59ffd21d
commit de2b00dd0e
37 changed files with 9629 additions and 439 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules
dist/*
dist/*
examples/*/dist

View File

@ -0,0 +1,66 @@
type Launch {
id: ID!
isBooked: Boolean!
mission: Mission!
rocket: Rocket!
site: String
}
"""
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
"""
type LaunchConnection {
cursor: String!
hasMore: Boolean!
launches: [Launch]!
}
type Mission {
missionPatch(size: PatchSize): String!
name: String
}
type Mutation {
bookTrips(launchIds: [ID!]!): TripUpdateResponse!
cancelTrip(launchId: ID!): TripUpdateResponse!
login(email: String): String
}
enum PatchSize {
LARGE
SMALL
}
type Query {
launch(count: Int, id: ID!): Launch!
launches(
"""
If you add a cursor here, it will only return results _after_ this cursor
"""
after: String
"""The number of results to show. Must be >= 1. Default = 20"""
pageSize: Int
): LaunchConnection!
me: User
}
type Rocket {
id: ID!
name: String
type: String
}
type TripUpdateResponse {
launches: Launch
message: String
success: Boolean!
}
type User {
email: String!
id: ID!
trips: [Launch!]!
}

View File

@ -2,25 +2,24 @@
"name": "gqliteral-fullstack-tutorial-server",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"main": "dist/index.js",
"scripts": {
"test": "jest",
"start": "nodemon src/index.js",
"start:ci": "node src/index.js"
"start": "nodemon --ignore fullstackTypes.js dist/index.js",
"start:ci": "node dist/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"apollo-datasource": "^0.1.3",
"apollo-datasource-rest": "^0.1.5",
"apollo-datasource": "^0.2.0",
"apollo-datasource-rest": "^0.2.0",
"apollo-server": "2.2.0-alpha.2",
"apollo-server-testing": "2.2.0-alpha.2",
"dotenv": "^6.1.0",
"fullstack-tutorial": "apollographql/fullstack-tutorial.git",
"graphql": "^14.0.2",
"isemail": "^3.1.3",
"nodemon": "^1.18.4",
"sequelize": "^4.39.0",
"sqlite3": "^4.0.3"
"isemail": "^3.2.0",
"sequelize": "^4.41.1",
"sqlite3": "^4.0.4"
},
"devDependencies": {
"apollo": "^2.0.0-beta.89",
@ -28,7 +27,8 @@
"apollo-link-http": "^1.5.5",
"jest": "^23.6.0",
"nock": "^10.0.2",
"node-fetch": "^2.2.1"
"node-fetch": "^2.2.1",
"typescript": "^3.1.6"
},
"jest": {
"testPathIgnorePatterns": [

View File

@ -1,6 +0,0 @@
const { GQLiteralSchema } = require("gqliteral");
const schema = GQLiteralSchema({
types: require("./schema"),
definitionFilePath: "../output.graphql",
});

View File

@ -1,88 +0,0 @@
// @ts-check
const { GQLiteralObject, GQLiteralEnum, GQLiteralArg } = require("gqliteral");
exports.Query = GQLiteralObject("Query", (t) => {
t.field("launches", "LaunchConnection", {
list: true,
args: {
pageSize: GQLiteralArg("Int", {
description:
"The number of results to show. Must be >= 1. Default = 20",
}),
after: GQLiteralArg("String", {
description:
"If you add a cursor here, it will only return results _after_ this cursor",
}),
},
});
t.field("launch", "Launch", {
args: {
id: GQLiteralArg("ID", { required: true }),
},
});
t.field("me", "User", { nullable: true });
});
exports.Mutation = GQLiteralObject("Mutation", (t) => {
t.field("bookTrips", TripUpdateResponse, {
args: {
launchIds: GQLiteralArg("ID", { list: true, required: true }),
},
});
t.field("cancelTrip", "TripUpdateResponse", {
args: {
launchId: GQLiteralArg("ID", { required: true }),
},
});
t.string("login", {
args: {
email: GQLiteralArg("String"),
},
});
});
exports.TripUpdateResponse = GQLiteralObject("TripUpdateResponse", (t) => {
t.boolean("success");
t.string("message", { nullable: true });
t.field("launches", "Launch", { nullable: true });
});
exports.LaunchConnection = GQLiteralObject("LaunchConnection", (t) => {
t.description(`
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
`);
t.string("cursor");
t.boolean("hasMore");
t.field("launches", "Launch", { list: true, listItemNullable: true });
});
exports.Launch = GQLiteralObject("Launch", (t) => {
t.id("id");
t.string("site", { nullable: true });
t.field("mission", "Mission");
t.field("rocket", "Rocket");
t.boolean("isBooked");
});
exports.Rocket = GQLiteralObject("Rocket", (t) => {
t.id("id");
t.string("name", { nullable: true });
t.string("type", { nullable: true });
});
exports.User = GQLiteralObject("User", (t) => {
t.id("id");
t.string("email");
t.field("trips", "Launch", { list: true });
});
exports.Mission = GQLiteralObject("Mission", (t) => {
t.string("name", { nullable: true });
t.string("missionPatch", {
args: {
size: "PatchSize",
},
});
});

View File

@ -0,0 +1,322 @@
/**
* This file is autogenerated. Do not edit directly.
*/
import * as t from "./typeDefs";
export type BaseScalarNames = "String" | "Int" | "Float" | "ID" | "Boolean";
export interface Generated_Type_Query_Field_launch {
returnType:
| Generated_Type_Launch["backingType"]
| PromiseLike<Generated_Type_Launch["backingType"]>;
args: Generated_Type_Query_Field_launch_Args;
}
export interface Generated_Type_Query_Field_launch_Args {
count?: number | null | undefined;
id: string;
}
export interface Generated_Type_Query_Field_launches {
returnType:
| Generated_Type_LaunchConnection["backingType"]
| PromiseLike<Generated_Type_LaunchConnection["backingType"]>;
args: Generated_Type_Query_Field_launches_Args;
}
export interface Generated_Type_Query_Field_launches_Args {
after?: string | null | undefined;
pageSize?: number | null | undefined;
}
export interface Generated_Type_Query_Field_me {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
args: {};
}
export interface Generated_Type_Query_Fields {
launch: Generated_Type_Query_Field_launch;
launches: Generated_Type_Query_Field_launches;
me: Generated_Type_Query_Field_me;
}
export interface Generated_Type_Query {
backingType: unknown;
fields: Generated_Type_Query_Fields;
}
export interface Generated_Type_Launch_Field_id {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Launch_Field_isBooked {
returnType: boolean | PromiseLike<boolean>;
args: {};
}
export interface Generated_Type_Launch_Field_mission {
returnType:
| Generated_Type_Mission["backingType"]
| PromiseLike<Generated_Type_Mission["backingType"]>;
args: {};
}
export interface Generated_Type_Launch_Field_rocket {
returnType:
| Generated_Type_Rocket["backingType"]
| PromiseLike<Generated_Type_Rocket["backingType"]>;
args: {};
}
export interface Generated_Type_Launch_Field_site {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Launch_Fields {
id: Generated_Type_Launch_Field_id;
isBooked: Generated_Type_Launch_Field_isBooked;
mission: Generated_Type_Launch_Field_mission;
rocket: Generated_Type_Launch_Field_rocket;
site: Generated_Type_Launch_Field_site;
}
export interface Generated_Type_Launch {
backingType: t.Launch;
fields: Generated_Type_Launch_Fields;
}
export interface Generated_Type_Mission_Field_missionPatch {
returnType: string | PromiseLike<string>;
args: Generated_Type_Mission_Field_missionPatch_Args;
}
export interface Generated_Type_Mission_Field_missionPatch_Args {
size?: GeneratedEnums["PatchSize"] | null | undefined;
}
export interface Generated_Type_Mission_Field_name {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Mission_Fields {
missionPatch: Generated_Type_Mission_Field_missionPatch;
name: Generated_Type_Mission_Field_name;
}
export interface Generated_Type_Mission {
backingType: t.Mission;
fields: Generated_Type_Mission_Fields;
}
export interface Generated_Type_Rocket_Field_id {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Rocket_Field_name {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Rocket_Field_type {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Rocket_Fields {
id: Generated_Type_Rocket_Field_id;
name: Generated_Type_Rocket_Field_name;
type: Generated_Type_Rocket_Field_type;
}
export interface Generated_Type_Rocket {
backingType: unknown;
fields: Generated_Type_Rocket_Fields;
}
export interface Generated_Type_LaunchConnection_Field_cursor {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_LaunchConnection_Field_hasMore {
returnType: boolean | PromiseLike<boolean>;
args: {};
}
export interface Generated_Type_LaunchConnection_Field_launches {
returnType:
| Generated_Type_Launch["backingType"][]
| PromiseLike<Generated_Type_Launch["backingType"][]>
| PromiseLike<Generated_Type_Launch["backingType"]>[];
args: {};
}
export interface Generated_Type_LaunchConnection_Fields {
cursor: Generated_Type_LaunchConnection_Field_cursor;
hasMore: Generated_Type_LaunchConnection_Field_hasMore;
launches: Generated_Type_LaunchConnection_Field_launches;
}
export interface Generated_Type_LaunchConnection {
backingType: unknown;
fields: Generated_Type_LaunchConnection_Fields;
}
export interface Generated_Type_User_Field_email {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_User_Field_id {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_User_Field_trips {
returnType:
| Generated_Type_Launch["backingType"][]
| PromiseLike<Generated_Type_Launch["backingType"][]>
| PromiseLike<Generated_Type_Launch["backingType"]>[];
args: {};
}
export interface Generated_Type_User_Fields {
email: Generated_Type_User_Field_email;
id: Generated_Type_User_Field_id;
trips: Generated_Type_User_Field_trips;
}
export interface Generated_Type_User {
backingType: unknown;
fields: Generated_Type_User_Fields;
}
export interface Generated_Type_Mutation_Field_bookTrips {
returnType:
| Generated_Type_TripUpdateResponse["backingType"]
| PromiseLike<Generated_Type_TripUpdateResponse["backingType"]>;
args: Generated_Type_Mutation_Field_bookTrips_Args;
}
export interface Generated_Type_Mutation_Field_bookTrips_Args {
launchIds: string[];
}
export interface Generated_Type_Mutation_Field_cancelTrip {
returnType:
| Generated_Type_TripUpdateResponse["backingType"]
| PromiseLike<Generated_Type_TripUpdateResponse["backingType"]>;
args: Generated_Type_Mutation_Field_cancelTrip_Args;
}
export interface Generated_Type_Mutation_Field_cancelTrip_Args {
launchId: string;
}
export interface Generated_Type_Mutation_Field_login {
returnType: null | string | PromiseLike<null | string>;
args: Generated_Type_Mutation_Field_login_Args;
}
export interface Generated_Type_Mutation_Field_login_Args {
email?: string | null | undefined;
}
export interface Generated_Type_Mutation_Fields {
bookTrips: Generated_Type_Mutation_Field_bookTrips;
cancelTrip: Generated_Type_Mutation_Field_cancelTrip;
login: Generated_Type_Mutation_Field_login;
}
export interface Generated_Type_Mutation {
backingType: unknown;
fields: Generated_Type_Mutation_Fields;
}
export interface Generated_Type_TripUpdateResponse_Field_launches {
returnType:
| null
| Generated_Type_Launch["backingType"]
| PromiseLike<null | Generated_Type_Launch["backingType"]>;
args: {};
}
export interface Generated_Type_TripUpdateResponse_Field_message {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_TripUpdateResponse_Field_success {
returnType: boolean | PromiseLike<boolean>;
args: {};
}
export interface Generated_Type_TripUpdateResponse_Fields {
launches: Generated_Type_TripUpdateResponse_Field_launches;
message: Generated_Type_TripUpdateResponse_Field_message;
success: Generated_Type_TripUpdateResponse_Field_success;
}
export interface Generated_Type_TripUpdateResponse {
backingType: unknown;
fields: Generated_Type_TripUpdateResponse_Fields;
}
export interface GeneratedScalars {}
export interface GeneratedInterfaces {}
export interface GeneratedUnions {}
export interface GeneratedEnums {
PatchSize: "LARGE" | "SMALL";
}
export interface GeneratedInputObjects {}
export interface GeneratedObjects {
Query: Generated_Type_Query;
Launch: Generated_Type_Launch;
Mission: Generated_Type_Mission;
Rocket: Generated_Type_Rocket;
LaunchConnection: Generated_Type_LaunchConnection;
User: Generated_Type_User;
Mutation: Generated_Type_Mutation;
TripUpdateResponse: Generated_Type_TripUpdateResponse;
}
export interface GeneratedSchema {
context: t.Context;
enums: GeneratedEnums;
objects: GeneratedObjects;
inputObjects: GeneratedInputObjects;
unions: GeneratedUnions;
scalars: GeneratedScalars;
interfaces: GeneratedInterfaces;
// For simplicity in autocomplete:
availableInputTypes:
| BaseScalarNames
| Extract<keyof GeneratedInputObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedScalars, string>;
availableOutputTypes:
| BaseScalarNames
| Extract<keyof GeneratedObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedUnions, string>
| Extract<keyof GeneratedInterfaces, string>
| Extract<keyof GeneratedScalars, string>;
}
export type Gen = GeneratedSchema;
declare global {
interface GQLiteralGen extends GeneratedSchema {}
}

View File

@ -0,0 +1,68 @@
// @ts-check
import { ApolloServer } from "apollo-server";
import path from "path";
import { GQLiteralSchema } from "gqliteral";
import isEmail from "isemail";
import * as types from "./schema";
import { Request } from "express";
const { createStore } = require("fullstack-tutorial/final/server/src/utils.js");
const internalEngineDemo = require("fullstack-tutorial/final/server/src/engine-demo");
const LaunchApi = require("fullstack-tutorial/final/server/src/datasources/launch.js");
const UserApi = require("fullstack-tutorial/final/server/src/datasources/user.js");
const schema = GQLiteralSchema({
types,
definitionFilePath: path.join(__dirname, "../fullstack-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../src/fullstackTypes.ts"),
imports: {
t: path.join(__dirname, "../src/typeDefs.ts"),
},
contextType: "t.Context",
backingTypes: {
Launch: "t.Launch",
Mission: "t.Mission",
},
},
});
const store = createStore();
const dataSources = () => ({
launchAPI: new LaunchApi(),
userAPI: new UserApi({ store }),
});
// the function that sets up the global context for each resolver, using the req
const context = async ({ req }: { req: Request }) => {
// simple auth check on every request
const auth = (req.headers && req.headers.authorization) || "";
const email = new Buffer(auth, "base64").toString("ascii");
// if the email isn't formatted validly, return null for user
if (!isEmail.validate(email)) return { user: null };
// find a user by their email
const users = await store.users.findOrCreate({ where: { email } });
const user = users && users[0] ? users[0] : null;
return { user: { ...user.dataValues } };
};
const server = new ApolloServer({
schema,
dataSources,
context,
engine: {
apiKey: process.env.ENGINE_API_KEY,
...internalEngineDemo,
},
});
const port = 4000;
server.listen({ port }, () =>
console.log(
`🚀 Server ready at http://localhost:${port}${server.graphqlPath}`
)
);

View File

@ -0,0 +1,6 @@
export * from "./launch";
export * from "./mission";
export * from "./mutation";
export * from "./query";
export * from "./rocket";
export * from "./user";

View File

@ -0,0 +1,24 @@
import { GQLiteralObject } from "gqliteral";
export const Launch = GQLiteralObject("Launch", (t) => {
t.id("id");
t.string("site", { nullable: true });
t.field("mission", "Mission");
t.field("rocket", "Rocket");
t.boolean("isBooked", {
async resolve(launch, _, { dataSources }) {
return dataSources.userAPI.isBookedOnLaunch({ launchId: launch.id });
},
});
});
export const LaunchConnection = GQLiteralObject("LaunchConnection", (t) => {
t.description(`
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
`);
t.string("cursor");
t.boolean("hasMore");
t.field("launches", "Launch", { list: true, listItemNullable: true });
});

View File

@ -0,0 +1,17 @@
import { GQLiteralObject, GQLiteralEnum } from "gqliteral";
export const Mission = GQLiteralObject("Mission", (t) => {
t.string("name", { nullable: true });
t.string("missionPatch", {
args: {
size: t.fieldArg("PatchSize"),
},
resolve(mission, { size }) {
return size === "SMALL"
? mission.missionPatchSmall
: mission.missionPatchLarge;
},
});
});
export const PatchSize = GQLiteralEnum("PatchSize", ["SMALL", "LARGE"]);

View File

@ -0,0 +1,60 @@
import { GQLiteralObject } from "gqliteral";
export const Mutation = GQLiteralObject("Mutation", (t) => {
t.field("bookTrips", "TripUpdateResponse", {
args: { launchIds: t.idArg({ list: true, required: true }) },
async resolve(_, { launchIds }, { dataSources }) {
const results = await dataSources.userAPI.bookTrips({ launchIds });
const launches = await dataSources.launchAPI.getLaunchesByIds({
launchIds,
});
return {
success: results && results.length === launchIds.length,
message:
results.length === launchIds.length
? "trips booked successfully"
: `the following launches couldn't be booked: ${launchIds.filter(
// @ts-ignore
(id) => !results.includes(id)
)}`,
launches,
};
},
});
t.field("cancelTrip", "TripUpdateResponse", {
args: { launchId: t.idArg({ required: true }) },
async resolve(_, { launchId }, { dataSources }) {
const result = dataSources.userAPI.cancelTrip({ launchId });
if (!result)
return {
success: false,
message: "failed to cancel trip",
};
const launch = await dataSources.launchAPI.getLaunchById({ launchId });
return {
success: true,
message: "trip cancelled",
launches: [launch],
};
},
});
t.string("login", {
nullable: true,
args: { email: t.stringArg() },
async resolve(_, { email }, { dataSources }) {
const user = await dataSources.userAPI.findOrCreateUser({ email });
if (user && email) {
return new Buffer(email).toString("base64");
}
return null;
},
});
});
export const TripUpdateResponse = GQLiteralObject("TripUpdateResponse", (t) => {
t.boolean("success");
t.string("message", { nullable: true });
t.field("launches", "Launch", { nullable: true });
});

View File

@ -0,0 +1,58 @@
/// <reference path="../fullstackTypes.ts" />
import { GQLiteralObject } from "gqliteral";
import { Utils } from "../typeDefs";
const utils: Utils = require("fullstack-tutorial/final/server/src/utils.js");
const dataSources: any = {};
export const Query = GQLiteralObject("Query", (t) => {
t.field("launches", "LaunchConnection", {
args: {
pageSize: t.intArg({
description:
"The number of results to show. Must be >= 1. Default = 20",
}),
after: t.stringArg({
description:
"If you add a cursor here, it will only return results _after_ this cursor",
}),
},
resolve: async (root, { pageSize = 20, after }, { dataSources }) => {
const allLaunches = await dataSources.launchAPI.getAllLaunches();
// we want these in reverse chronological order
allLaunches.reverse();
const launches = utils.paginateResults({
after,
pageSize,
results: allLaunches,
});
return {
launches,
cursor: launches.length ? launches[launches.length - 1].cursor : null,
// if the cursor of the end of the paginated results is the same as the
// last item in _all_ results, then there are no more results after this
hasMore: launches.length
? launches[launches.length - 1].cursor !==
allLaunches[allLaunches.length - 1].cursor
: false,
};
},
});
t.field("launch", "Launch", {
args: {
id: t.idArg({ required: true }),
count: t.intArg(),
},
resolve: (_, args) => {
return dataSources.launchAPI.getLaunchById({ launchId: args.id });
},
});
t.field("me", "User", {
nullable: true,
resolve: (root, args, ctx) => {
return ctx.dataSources.userAPI.findOrCreateUser();
},
});
});

View File

@ -0,0 +1,7 @@
import { GQLiteralObject } from "gqliteral";
export const Rocket = GQLiteralObject("Rocket", (t) => {
t.id("id");
t.string("name", { nullable: true });
t.string("type", { nullable: true });
});

View File

@ -0,0 +1,21 @@
import { GQLiteralObject } from "gqliteral";
export const User = GQLiteralObject("User", (t) => {
t.id("id");
t.string("email");
t.field("trips", "Launch", {
list: true,
async resolve(_, __, { dataSources }) {
// get ids of launches by user
const launchIds = await dataSources.userAPI.getLaunchIdsByUser();
if (!launchIds.length) return [];
// look up those launches by their ids
return (
dataSources.launchAPI.getLaunchesByIds({
launchIds,
}) || []
);
},
});
});

View File

@ -0,0 +1,75 @@
import { RESTDataSource } from "apollo-datasource-rest";
import { DataSource } from "apollo-datasource";
export interface Mission {
name: string;
missionPatchSmall: string;
missionPatchLarge: string;
}
export interface Rocket {
id: number;
name: string;
type: string;
}
export interface Launch {
id: number;
cursor: string;
site: string;
mission: Mission;
rocket: Rocket;
}
export interface DBUser {
id: number;
createdAt: Date;
updatedAt: Date;
email: string;
token: string;
}
export interface DBTrip {
id: number;
createdAt: Date;
updatedAt: Date;
launchId: number;
userId: number;
}
export interface LaunchApi extends RESTDataSource {
getAllLaunches(): Promise<Launch[]>;
getLaunchById(opts: { launchId: string }): Promise<Launch>;
getLaunchesByIds(opts: { launchIds: string[] }): Promise<Launch[]>;
}
export interface UserApi extends DataSource {
/**
* User can be called with an argument that includes email, but it doesn't
* have to be. If the user is already on the context, it will use that user
* instead
*/
findOrCreateUser(obj?: { email?: string | null }): Promise<DBUser | null>;
bookTrips(obj: { launchIds: string[] }): Promise<DBTrip[]>;
bookTrip(obj: { launchId: string }): Promise<DBTrip | false>;
cancelTrip(obj: { launchId: string }): Promise<void>;
getLaunchIdsByUser(): Promise<string[]>;
isBookedOnLaunch(obj: { launchId: string }): boolean;
}
export interface Utils {
paginateResults<T>(opts: {
after?: string | null;
pageSize?: number | null;
results: T[];
getCursor?: Function;
}): T[];
createStore(): {};
}
export interface Context {
dataSources: {
userAPI: UserApi;
launchAPI: LaunchApi;
};
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["es2015"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"allowJs": true,
"strictNullChecks": true,
"skipLibCheck": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
"""A comment about an entry, submitted by a user"""
type Comment {
"""The text of the comment"""
content: String!
"""A timestamp of when the comment was posted"""
createdAt: Float!
"""The SQL ID of this entry"""
id: Int!
"""The GitHub user who posted the comment"""
postedBy: User
"""The repository which this comment is about"""
repoName: String!
}
type Entry {
"""The number of comments posted about this repository"""
commentCount: Int!
"""Comments posted about this repository"""
comments(limit: Int, offset: Int): [Comment!]!
"""A timestamp of when the entry was submitted"""
createdAt: Float!
"""The hot score of this repository"""
hotScore: Float!
"""The SQL ID of this entry"""
id: Int!
"""The GitHub user who posted the comment"""
postedBy: User
"""Information about the repository from GitHub"""
repository: Repository!
"""The score of this repository, upvotes - downvotes"""
score: Int!
"""XXX to be changed"""
vote: Vote!
}
enum FeedType {
"""
Sort by a combination of freshness and score, using Reddit's algorithm
"""
HOT
"""Newest entries first"""
NEW
"""Highest score entries first"""
TOP
}
type Mutation {
submitComment(
"""The text content for the new comment"""
commentContent: String!
"""
The full repository name from GitHub, e.g. "apollostack/GitHunt-API"
"""
repoFullName: String!
): Comment!
"""Submit a new repository, returns the new submission"""
submitRepository(
"""
The full repository name from GitHub, e.g. "apollostack/GitHunt-API"
"""
repoFullName: String!
): Entry!
"""
Vote on a repository submission, returns the submission that was voted on
"""
vote(
"""
The full repository name from GitHub, e.g. "apollostack/GitHunt-API"
"""
repoFullName: String!
"""The type of vote - UP, DOWN, or CANCEL"""
type: VoteType!
): Entry!
}
type Query {
"""Return the currently logged in user, or null if nobody is logged in"""
currentUser: User!
"""A single entry"""
entry(
"""
The full repository name from GitHub, e.g. "apollostack/GitHunt-API"
"""
repoFullName: String!
): Entry
feed(
"""The number of items to fetch starting from the offset, for pagination"""
limit: Int
"""The number of items to skip, for pagination"""
offset: Int
"""The sort order for the feed"""
type: FeedType!
): [Entry!]!
}
"""
A repository object from the GitHub API. This uses the exact field names returned by the
GitHub API for simplicity, even though the convention for GraphQL is usually to camel case.
"""
type Repository {
"""The description of the repository"""
description: String
"""
The full name of the repository with the username, e.g. apollostack/GitHunt-API
"""
full_name: String!
"""The link to the repository on GitHub"""
html_url: String!
"""Just the name of the repository, e.g. GitHunt-API"""
name: String!
"""The number of open issues on this repository on GitHub"""
open_issues_count: Int
"""The owner of this repository on GitHub, e.g. apollostack"""
owner: User
"""The number of people who have starred this repository on GitHub"""
stargazers_count: Int!
}
"""
A user object from the GitHub API. This uses the exact field names returned from the GitHub API.
"""
type User {
"""The URL to a directly embeddable image for this user's avatar"""
avatar_url: String!
"""The URL of this user's GitHub page"""
html_url: String!
"""The name of the user, e.g. apollostack"""
login: String!
}
type Vote {
vote_value: Int!
}
enum VoteType {
CANCEL
DOWN
UP
}

View File

@ -0,0 +1,357 @@
/**
* This file is autogenerated. Do not edit directly.
*/
export type BaseScalarNames = "String" | "Int" | "Float" | "ID" | "Boolean";
export interface Generated_Type_Query_Field_currentUser {
returnType:
| Generated_Type_User["backingType"]
| PromiseLike<Generated_Type_User["backingType"]>;
args: {};
}
export interface Generated_Type_Query_Field_entry {
returnType:
| null
| Generated_Type_Entry["backingType"]
| PromiseLike<null | Generated_Type_Entry["backingType"]>;
args: Generated_Type_Query_Field_entry_Args;
}
export interface Generated_Type_Query_Field_entry_Args {
repoFullName: string;
}
export interface Generated_Type_Query_Field_feed {
returnType:
| Generated_Type_Entry["backingType"][]
| PromiseLike<Generated_Type_Entry["backingType"][]>
| PromiseLike<Generated_Type_Entry["backingType"]>[];
args: Generated_Type_Query_Field_feed_Args;
}
export interface Generated_Type_Query_Field_feed_Args {
limit?: number | null | undefined;
offset?: number | null | undefined;
type: GeneratedEnums["FeedType"];
}
export interface Generated_Type_Query_Fields {
currentUser: Generated_Type_Query_Field_currentUser;
entry: Generated_Type_Query_Field_entry;
feed: Generated_Type_Query_Field_feed;
}
export interface Generated_Type_Query {
backingType: unknown;
fields: Generated_Type_Query_Fields;
}
export interface Generated_Type_User_Field_avatar_url {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_User_Field_html_url {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_User_Field_login {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_User_Fields {
avatar_url: Generated_Type_User_Field_avatar_url;
html_url: Generated_Type_User_Field_html_url;
login: Generated_Type_User_Field_login;
}
export interface Generated_Type_User {
backingType: unknown;
fields: Generated_Type_User_Fields;
}
export interface Generated_Type_Entry_Field_commentCount {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Entry_Field_comments {
returnType:
| Generated_Type_Comment["backingType"][]
| PromiseLike<Generated_Type_Comment["backingType"][]>
| PromiseLike<Generated_Type_Comment["backingType"]>[];
args: Generated_Type_Entry_Field_comments_Args;
}
export interface Generated_Type_Entry_Field_comments_Args {
limit?: number | null | undefined;
offset?: number | null | undefined;
}
export interface Generated_Type_Entry_Field_createdAt {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Entry_Field_hotScore {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Entry_Field_id {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Entry_Field_postedBy {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
args: {};
}
export interface Generated_Type_Entry_Field_repository {
returnType:
| Generated_Type_Repository["backingType"]
| PromiseLike<Generated_Type_Repository["backingType"]>;
args: {};
}
export interface Generated_Type_Entry_Field_score {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Entry_Field_vote {
returnType:
| Generated_Type_Vote["backingType"]
| PromiseLike<Generated_Type_Vote["backingType"]>;
args: {};
}
export interface Generated_Type_Entry_Fields {
commentCount: Generated_Type_Entry_Field_commentCount;
comments: Generated_Type_Entry_Field_comments;
createdAt: Generated_Type_Entry_Field_createdAt;
hotScore: Generated_Type_Entry_Field_hotScore;
id: Generated_Type_Entry_Field_id;
postedBy: Generated_Type_Entry_Field_postedBy;
repository: Generated_Type_Entry_Field_repository;
score: Generated_Type_Entry_Field_score;
vote: Generated_Type_Entry_Field_vote;
}
export interface Generated_Type_Entry {
backingType: unknown;
fields: Generated_Type_Entry_Fields;
}
export interface Generated_Type_Comment_Field_content {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Comment_Field_createdAt {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Comment_Field_id {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Comment_Field_postedBy {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
args: {};
}
export interface Generated_Type_Comment_Field_repoName {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Comment_Fields {
content: Generated_Type_Comment_Field_content;
createdAt: Generated_Type_Comment_Field_createdAt;
id: Generated_Type_Comment_Field_id;
postedBy: Generated_Type_Comment_Field_postedBy;
repoName: Generated_Type_Comment_Field_repoName;
}
export interface Generated_Type_Comment {
backingType: unknown;
fields: Generated_Type_Comment_Fields;
}
export interface Generated_Type_Repository_Field_description {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Repository_Field_full_name {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Repository_Field_html_url {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Repository_Field_name {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Repository_Field_open_issues_count {
returnType: null | number | PromiseLike<null | number>;
args: {};
}
export interface Generated_Type_Repository_Field_owner {
returnType:
| null
| Generated_Type_User["backingType"]
| PromiseLike<null | Generated_Type_User["backingType"]>;
args: {};
}
export interface Generated_Type_Repository_Field_stargazers_count {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Repository_Fields {
description: Generated_Type_Repository_Field_description;
full_name: Generated_Type_Repository_Field_full_name;
html_url: Generated_Type_Repository_Field_html_url;
name: Generated_Type_Repository_Field_name;
open_issues_count: Generated_Type_Repository_Field_open_issues_count;
owner: Generated_Type_Repository_Field_owner;
stargazers_count: Generated_Type_Repository_Field_stargazers_count;
}
export interface Generated_Type_Repository {
backingType: unknown;
fields: Generated_Type_Repository_Fields;
}
export interface Generated_Type_Vote_Field_vote_value {
returnType: number | PromiseLike<number>;
args: {};
}
export interface Generated_Type_Vote_Fields {
vote_value: Generated_Type_Vote_Field_vote_value;
}
export interface Generated_Type_Vote {
backingType: unknown;
fields: Generated_Type_Vote_Fields;
}
export interface Generated_Type_Mutation_Field_submitComment {
returnType:
| Generated_Type_Comment["backingType"]
| PromiseLike<Generated_Type_Comment["backingType"]>;
args: Generated_Type_Mutation_Field_submitComment_Args;
}
export interface Generated_Type_Mutation_Field_submitComment_Args {
commentContent: string;
repoFullName: string;
}
export interface Generated_Type_Mutation_Field_submitRepository {
returnType:
| Generated_Type_Entry["backingType"]
| PromiseLike<Generated_Type_Entry["backingType"]>;
args: Generated_Type_Mutation_Field_submitRepository_Args;
}
export interface Generated_Type_Mutation_Field_submitRepository_Args {
repoFullName: string;
}
export interface Generated_Type_Mutation_Field_vote {
returnType:
| Generated_Type_Entry["backingType"]
| PromiseLike<Generated_Type_Entry["backingType"]>;
args: Generated_Type_Mutation_Field_vote_Args;
}
export interface Generated_Type_Mutation_Field_vote_Args {
repoFullName: string;
type: GeneratedEnums["VoteType"];
}
export interface Generated_Type_Mutation_Fields {
submitComment: Generated_Type_Mutation_Field_submitComment;
submitRepository: Generated_Type_Mutation_Field_submitRepository;
vote: Generated_Type_Mutation_Field_vote;
}
export interface Generated_Type_Mutation {
backingType: unknown;
fields: Generated_Type_Mutation_Fields;
}
export interface GeneratedScalars {}
export interface GeneratedInterfaces {}
export interface GeneratedUnions {}
export interface GeneratedEnums {
FeedType: "HOT" | "NEW" | "TOP";
VoteType: "CANCEL" | "DOWN" | "UP";
}
export interface GeneratedInputObjects {}
export interface GeneratedObjects {
Query: Generated_Type_Query;
User: Generated_Type_User;
Entry: Generated_Type_Entry;
Comment: Generated_Type_Comment;
Repository: Generated_Type_Repository;
Vote: Generated_Type_Vote;
Mutation: Generated_Type_Mutation;
}
export interface GeneratedSchema {
context: {};
enums: GeneratedEnums;
objects: GeneratedObjects;
inputObjects: GeneratedInputObjects;
unions: GeneratedUnions;
scalars: GeneratedScalars;
interfaces: GeneratedInterfaces;
// For simplicity in autocomplete:
availableInputTypes:
| BaseScalarNames
| Extract<keyof GeneratedInputObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedScalars, string>;
availableOutputTypes:
| BaseScalarNames
| Extract<keyof GeneratedObjects, string>
| Extract<keyof GeneratedEnums, string>
| Extract<keyof GeneratedUnions, string>
| Extract<keyof GeneratedInterfaces, string>
| Extract<keyof GeneratedScalars, string>;
}
export type Gen = GeneratedSchema;
declare global {
interface GQLiteralGen extends GeneratedSchema {}
}

View File

@ -1,3 +1,13 @@
{
"name": "gqliteral-githunt-api"
}
"name": "gqliteral-githunt-api",
"dependencies": {
"apollo-server": "2.2.0-alpha.2",
"githunt-api": "apollographql/GitHunt-API.git"
},
"devDependencies": {
"nodemon": "^1.18.6"
},
"scripts": {
"dev": "nodemon src/index.js"
}
}

View File

@ -0,0 +1,25 @@
// @ts-check
const { ApolloServer } = require("apollo-server");
const path = require("path");
const { GQLiteralSchema } = require("gqliteral");
const types = require("./schema");
const schema = GQLiteralSchema({
types,
definitionFilePath: path.join(__dirname, "../githunt-api-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../githuntTypes.ts"),
},
});
const server = new ApolloServer({
schema,
});
const port = 4000;
server.listen({ port }, () =>
console.log(
`🚀 Server ready at http://localhost:${port}${server.graphqlPath}`
)
);

View File

@ -1,5 +1,11 @@
// @ts-check
const { GQLiteralEnum, GQLiteralObject, GQLiteralArg } = require("gqliteral");
/// <reference path="../githuntTypes.ts" />
const {
GQLiteralEnum,
GQLiteralObject,
GQLiteralArg,
GQLiteralAbstractType,
} = require("gqliteral");
exports.FeedType = GQLiteralEnum("FeedType", (t) => {
t.description("A list of options for the sort order of the feed");
@ -75,62 +81,114 @@ exports.Mutation = GQLiteralObject("Mutation", (t) => {
}),
},
});
t.field("submitComment", "Comment", {});
t.field("submitComment", "Comment", {
args: {
repoFullName: RepoNameArg,
commentContent: t.stringArg({
required: true,
description: "The text content for the new comment",
}),
},
});
});
// type Mutation {
// # Comment on a repository, returns the new comment
// submitComment(
// # The full repository name from GitHub, e.g. "apollostack/GitHunt-API"
// repoFullName: String!
// # The text content for the new comment
// commentContent: String!
// ): Comment
// }
const CommonFields = GQLiteralAbstractType((t) => {
t.int("id", { description: "The SQL ID of this entry" });
t.field("postedBy", "User", {
nullable: true,
description: "The GitHub user who posted the comment",
});
});
// # A comment about an entry, submitted by a user
// type Comment @cacheControl(maxAge: 240) {
// # The SQL ID of this entry
// id: Int!
// # The GitHub user who posted the comment
// postedBy: User
// # A timestamp of when the comment was posted
// createdAt: Float! # Actually a date
// # The text of the comment
// content: String!
// # The repository which this comment is about
// repoName: String!
// }
exports.Comment = GQLiteralObject("Comment", (t) => {
t.description("A comment about an entry, submitted by a user");
// t.directive("@cacheControl(maxAge: 240)");
t.mix(CommonFields);
t.float("createdAt", {
description: "A timestamp of when the comment was posted",
});
t.string("content", {
description: "The text of the comment",
});
t.string("repoName", {
description: "The repository which this comment is about",
});
});
// # XXX to be removed
// type Vote {
// vote_value: Int!
// }
// 'XXX to be removed'
exports.Vote = GQLiteralObject("Vote", (t) => {
t.int("vote_value");
});
// # Information about a GitHub repository submitted to GitHunt
// type Entry @cacheControl(maxAge: 240) {
// # Information about the repository from GitHub
// repository: Repository
// # The GitHub user who submitted this entry
// postedBy: User
// # A timestamp of when the entry was submitted
// createdAt: Float! # Actually a date
// # The score of this repository, upvotes - downvotes
// score: Int!
// # The hot score of this repository
// hotScore: Float!
// # Comments posted about this repository
// comments(limit: Int, offset: Int): [Comment]!
// # The number of comments posted about this repository
// commentCount: Int!
// # The SQL ID of this entry
// id: Int!
// # XXX to be changed
// vote: Vote!
// }
exports.Entry = GQLiteralObject("Entry", (t) => {
t.mix(CommonFields);
// t.directive(@cacheControl(maxAge: 240))
t.field("repository", "Repository", {
description: "Information about the repository from GitHub",
});
t.float("createdAt", {
description: "A timestamp of when the entry was submitted",
});
t.int("score", {
description: "The score of this repository, upvotes - downvotes",
});
t.float("hotScore", { description: "The hot score of this repository" });
t.field("comments", "Comment", {
list: true,
args: {
limit: t.intArg(),
offset: t.intArg(),
},
description: "Comments posted about this repository",
});
t.int("commentCount", {
description: "The number of comments posted about this repository",
});
t.field("vote", "Vote", { description: "XXX to be changed" });
});
// schema {
// query: Query
// mutation: Mutation
// subscription: Subscription
// }
exports.Repository = GQLiteralObject("Repository", (t) => {
t.description(`
A repository object from the GitHub API. This uses the exact field names returned by the
GitHub API for simplicity, even though the convention for GraphQL is usually to camel case.
`);
// t.directive @cacheControl(maxAge:240)
t.string("name", {
description: "Just the name of the repository, e.g. GitHunt-API",
});
t.string("full_name", {
description:
"The full name of the repository with the username, e.g. apollostack/GitHunt-API",
});
t.string("description", {
nullable: true,
description: "The description of the repository",
});
t.string("html_url", { description: "The link to the repository on GitHub" });
t.int("stargazers_count", {
description:
"The number of people who have starred this repository on GitHub",
});
t.int("open_issues_count", {
nullable: true,
description: "The number of open issues on this repository on GitHub",
});
t.field("owner", "User", {
nullable: true,
description: "The owner of this repository on GitHub, e.g. apollostack",
});
});
exports.User = GQLiteralObject("User", (t) => {
t.description(
"A user object from the GitHub API. This uses the exact field names returned from the GitHub API."
);
// t.directive @cacheControl(maxAge:240)
t.string("login", { description: "The name of the user, e.g. apollostack" });
t.string("avatar_url", {
description:
"The URL to a directly embeddable image for this user's avatar",
});
t.string("html_url", { description: "The URL of this user's GitHub page" });
});

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +1,137 @@
/**
* This file is autogenerated. Do not edit directly.
*/
import * as swapi from "./gqliteral/backingTypes";
export type BaseScalarNames = "String" | "Int" | "Float" | "ID" | "Boolean";
export type Generated_Interface_Character = {
export interface Generated_Interface_Character_Fields {
appearsIn: {
returnType:
| GeneratedEnums["Episode"][]
| PromiseLike<GeneratedEnums["Episode"][]>
| PromiseLike<GeneratedEnums["Episode"]>[];
args: {};
};
friends: {
returnType:
| Generated_Interface_Character["backingType"][]
| PromiseLike<Generated_Interface_Character["backingType"][]>
| PromiseLike<Generated_Interface_Character["backingType"]>[];
args: {};
};
id: {
returnType: string | PromiseLike<string>;
args: {};
};
name: {
returnType: string | PromiseLike<string>;
args: {};
};
}
export interface Generated_Interface_Character {
implementingTypes: "Droid" | "Human";
backingType: swapi.Droid | swapi.Human;
fields: {
appearsIn: {
returnType:
| null
| GeneratedEnums["Episode"][]
| PromiseLike<GeneratedEnums["Episode"][]>
| PromiseLike<GeneratedEnums["Episode"]>[];
args: {};
};
friends: {
returnType:
| null
| Generated_Interface_Character["backingType"][]
| PromiseLike<Generated_Interface_Character["backingType"][]>
| PromiseLike<Generated_Interface_Character["backingType"]>[];
args: {};
};
id: {
returnType: null | string | PromiseLike<string>;
args: {};
};
name: {
returnType: null | string | PromiseLike<string>;
args: {};
};
};
};
fields: Generated_Interface_Character_Fields;
}
export type Generated_Type_Query = {
backingType: any;
fields: {
droid: {
returnType:
| null
| Generated_Type_Droid["backingType"]
| PromiseLike<Generated_Type_Droid["backingType"]>;
args: {
id: string;
};
};
hero: {
returnType:
| null
| Generated_Interface_Character["backingType"]
| PromiseLike<Generated_Interface_Character["backingType"]>;
args: {
episode: GeneratedEnums["Episode"];
};
};
human: {
returnType:
| null
| Generated_Type_Human["backingType"]
| PromiseLike<Generated_Type_Human["backingType"]>;
args: {
id: string;
};
};
};
};
export interface Generated_Type_Query_Field_droid {
returnType:
| Generated_Type_Droid["backingType"]
| PromiseLike<Generated_Type_Droid["backingType"]>;
args: Generated_Type_Query_Field_droid_Args;
}
export type Generated_Type_Droid = {
export interface Generated_Type_Query_Field_droid_Args {
id: string;
}
export interface Generated_Type_Query_Field_hero {
returnType:
| Generated_Interface_Character["backingType"]
| PromiseLike<Generated_Interface_Character["backingType"]>;
args: Generated_Type_Query_Field_hero_Args;
}
export interface Generated_Type_Query_Field_hero_Args {
episode?: GeneratedEnums["Episode"] | null | undefined;
}
export interface Generated_Type_Query_Field_human {
returnType:
| Generated_Type_Human["backingType"]
| PromiseLike<Generated_Type_Human["backingType"]>;
args: Generated_Type_Query_Field_human_Args;
}
export interface Generated_Type_Query_Field_human_Args {
id: string;
}
export interface Generated_Type_Query_Fields {
droid: Generated_Type_Query_Field_droid;
hero: Generated_Type_Query_Field_hero;
human: Generated_Type_Query_Field_human;
}
export interface Generated_Type_Query {
backingType: unknown;
fields: Generated_Type_Query_Fields;
}
export interface Generated_Type_Droid_Field_primaryFunction {
returnType: string | PromiseLike<string>;
args: {};
}
export interface Generated_Type_Droid_Fields
extends Generated_Interface_Character_Fields {
primaryFunction: Generated_Type_Droid_Field_primaryFunction;
}
export interface Generated_Type_Droid {
backingType: swapi.Droid;
fields: Generated_Interface_Character["fields"] & {
primaryFunction: {
returnType: null | string | PromiseLike<string>;
args: {};
};
};
};
fields: Generated_Type_Droid_Fields;
}
export type Generated_Type_Human = {
export interface Generated_Type_Human_Field_homePlanet {
returnType: null | string | PromiseLike<null | string>;
args: {};
}
export interface Generated_Type_Human_Fields
extends Generated_Interface_Character_Fields {
homePlanet: Generated_Type_Human_Field_homePlanet;
}
export interface Generated_Type_Human {
backingType: swapi.Human;
fields: Generated_Interface_Character["fields"] & {
homePlanet: {
returnType: string | PromiseLike<string>;
args: {};
};
};
};
fields: Generated_Type_Human_Fields;
}
export type GeneratedScalars = {};
export type GeneratedInterfaces = {
export interface GeneratedScalars {}
export interface GeneratedInterfaces {
Character: Generated_Interface_Character;
};
export type GeneratedUnions = {};
export type GeneratedEnums = {
}
export interface GeneratedUnions {}
export interface GeneratedEnums {
Episode: 5 | 6 | 4;
MoreEpisodes: 5 | 6 | 4 | "OTHER";
};
export type GeneratedInputObjects = {};
export type GeneratedObjects = {
}
export interface GeneratedInputObjects {}
export interface GeneratedObjects {
Query: Generated_Type_Query;
Droid: Generated_Type_Droid;
Human: Generated_Type_Human;
};
}
export interface GeneratedSchema {
context: any;
context: {};
enums: GeneratedEnums;
objects: GeneratedObjects;
inputObjects: GeneratedInputObjects;
@ -128,4 +153,9 @@ export interface GeneratedSchema {
| Extract<keyof GeneratedInterfaces, string>
| Extract<keyof GeneratedScalars, string>;
}
export type Gen = GeneratedSchema;
declare global {
interface GQLiteralGen extends GeneratedSchema {}
}

View File

@ -112,7 +112,7 @@ export function getFriends(
/**
* Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2.
*/
export function getHero(episode: number): Character {
export function getHero(episode?: number | null): Character {
if (episode === 5) {
// Luke is the hero of Episode V.
return luke as Human;

View File

@ -8,7 +8,7 @@ import { GQLiteralSchema } from "gqliteral";
*/
export const schema = GQLiteralSchema({
types: allTypes,
definitionFilePath: path.join(__dirname, "../schema.graphql"),
definitionFilePath: path.join(__dirname, "../../star-wars-schema.graphql"),
typeGeneration: {
typesFilePath: path.join(__dirname, "../generatedTypes.ts"),
imports: {

View File

@ -1,24 +1,20 @@
import { GQLiteralInterface } from "gqliteral";
import { getFriends } from "../data";
import { Gen } from "../../generatedTypes";
export const Character = GQLiteralInterface<Gen, "Character">(
"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.",
property: "appears_in",
});
t.resolveType((character) => character.type);
}
);
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.",
property: "appears_in",
});
t.resolveType((character) => character.type);
});

View File

@ -1,5 +1,4 @@
import { GQLiteralEnum } from "gqliteral";
import { Gen } from "../../generatedTypes";
/**
* Note: this could also be:
@ -12,14 +11,14 @@ import { Gen } from "../../generatedTypes";
*
* if we chose to omit the descriptions
*/
export const Episode = GQLiteralEnum<Gen>("Episode", (t) => {
export const Episode = GQLiteralEnum("Episode", (t) => {
t.description("One of the films in the Star Wars Trilogy");
t.member("NEWHOPE", { value: 4, description: "Released in 1977." });
t.member("EMPIRE", { value: 5, description: "Released in 1980." });
t.member("JEDI", { value: 6, description: "Released in 1983" });
});
export const MoreEpisodes = GQLiteralEnum<Gen>("MoreEpisodes", (t) => {
export const MoreEpisodes = GQLiteralEnum("MoreEpisodes", (t) => {
t.mix("Episode");
t.members(["OTHER"]);
});

View File

@ -1,7 +1,6 @@
import { GQLiteralObject } from "gqliteral";
import { Gen } from "../../generatedTypes";
export const Human = GQLiteralObject<Gen, "Human">("Human", (t) => {
export const Human = GQLiteralObject("Human", (t) => {
t.description("A humanoid creature in the Star Wars universe.");
t.implements("Character");
t.string("homePlanet", {

View File

@ -1,6 +1,5 @@
import { GQLiteralObject, GQLiteralArg } from "gqliteral";
import { getHero, getHuman, getDroid } from "../data";
import { Gen } from "../../generatedTypes";
const characterArgs = {
id: GQLiteralArg("String", {
@ -16,7 +15,7 @@ const heroArgs = {
}),
};
export const Query = GQLiteralObject<Gen, "Query">("Query", (t) => {
export const Query = GQLiteralObject("Query", (t) => {
t.field("hero", "Character", {
args: heroArgs,
resolve: (_, { episode }) => getHero(episode),

View File

@ -1,3 +1,4 @@
"""A character in the Star Wars Trilogy"""
interface Character {
"""Which movies they appear in."""
appearsIn: [Episode!]!
@ -12,6 +13,7 @@ interface Character {
name: String!
}
"""A mechanical creature in the Star Wars universe."""
type Droid implements Character {
"""Which movies they appear in."""
appearsIn: [Episode!]!
@ -40,6 +42,7 @@ enum Episode {
NEWHOPE
}
"""A humanoid creature in the Star Wars universe."""
type Human implements Character {
"""Which movies they appear in."""
appearsIn: [Episode!]!

View File

@ -34,6 +34,7 @@ import {
import { GQLiteralTypeWrapper } from "./definitions";
import * as Types from "./types";
import suggestionList, { propertyFieldResolver } from "./utils";
import { GQLiteralAbstract } from "./objects";
const isPromise = (val: any): val is Promise<any> =>
Boolean(val && typeof val.then === "function");
@ -61,7 +62,7 @@ const SCALARS: Record<string, GraphQLScalarType> = {
* circular references at this step, while fields will guard for it during lazy evaluation.
*/
export class SchemaBuilder {
protected buildingTypes: Set<string> = new Set();
protected buildingTypes = new Set();
protected finalTypeMap: Record<string, GraphQLNamedType> = {};
protected pendingTypeMap: Record<string, GQLiteralTypeWrapper> = {};
@ -105,6 +106,7 @@ export class SchemaBuilder {
return new GraphQLObjectType({
name: config.name,
interfaces: () => config.interfaces.map((i) => this.getInterface(i)),
description: config.description,
fields: () => {
const interfaceFields: GraphQLFieldConfigMap<any, any> = {};
const allInterfaces = config.interfaces.map((i) =>
@ -136,8 +138,7 @@ export class SchemaBuilder {
}
interfaceType(config: Types.InterfaceTypeConfig) {
let description;
const { name, resolveType } = config;
const { name, resolveType, description } = config;
return new GraphQLInterfaceType({
name,
fields: () => this.buildObjectFields(config),
@ -249,7 +250,13 @@ export class SchemaBuilder {
case Types.NodeType.MIX:
throw new Error("TODO");
case Types.NodeType.MIX_ABSTRACT:
throw new Error("TODO");
this.mixAbstractOuput(
typeConfig,
fieldMap,
field.type,
field.mixOptions
);
break;
case Types.NodeType.FIELD:
fieldMap[field.config.name] = this.buildObjectField(
field.config,
@ -270,7 +277,13 @@ export class SchemaBuilder {
case Types.NodeType.MIX:
throw new Error("TODO");
case Types.NodeType.MIX_ABSTRACT:
throw new Error("TODO");
this.mixAbstractInput(
typeConfig,
fieldMap,
field.type,
field.mixOptions
);
break;
case Types.NodeType.FIELD:
fieldMap[field.config.name] = this.buildInputObjectField(
field.config,
@ -282,6 +295,42 @@ export class SchemaBuilder {
return fieldMap;
}
protected mixAbstractOuput(
typeConfig: Types.InputTypeConfig,
fieldMap: GraphQLFieldConfigMap<any, any>,
type: GQLiteralAbstract<any>,
{ pick, omit }: Types.MixOpts<any>
) {
const { fields } = type.buildType();
fields.forEach((field) => {
if (pick && pick.indexOf(field.name) === -1) {
return;
}
if (omit && omit.indexOf(field.name) !== -1) {
return;
}
fieldMap[field.name] = this.buildObjectField(field, typeConfig);
});
}
protected mixAbstractInput(
typeConfig: Types.InputTypeConfig,
fieldMap: GraphQLInputFieldConfigMap,
type: GQLiteralAbstract<any>,
{ pick, omit }: Types.MixOpts<any>
) {
const { fields } = type.buildType();
fields.forEach((field) => {
if (pick && pick.indexOf(field.name) === -1) {
return;
}
if (omit && omit.indexOf(field.name) !== -1) {
return;
}
fieldMap[field.name] = this.buildInputObjectField(field, typeConfig);
});
}
protected buildObjectField(
fieldConfig: Types.FieldConfig,
typeConfig: Types.ObjectTypeConfig | Types.InterfaceTypeConfig
@ -298,7 +347,6 @@ export class SchemaBuilder {
args: this.buildArgs(fieldConfig.args || {}, typeConfig),
// subscribe?: GraphQLFieldResolver<TSource, TContext, TArgs>;
// deprecationReason?: Maybe<string>;
// description?: Maybe<string>;
// astNode?: Maybe<FieldDefinitionNode>;
};
}

View File

@ -46,10 +46,10 @@ export function GQLiteralScalar(name: string, options: Types.ScalarOpts) {
*
* @param {string}
*/
export function GQLiteralObject<GenTypes, TypeName extends string>(
name: TypeName,
fn: (arg: GQLiteralObjectType<GenTypes, TypeName>) => void
) {
export function GQLiteralObject<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralObjectType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralObjectType<GenTypes, TypeName>(name);
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
@ -58,7 +58,10 @@ export function GQLiteralObject<GenTypes, TypeName extends string>(
/**
* Define a GraphQL interface type
*/
export function GQLiteralInterface<GenTypes, TypeName extends string>(
export function GQLiteralInterface<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: TypeName,
fn: (arg: GQLiteralInterfaceType<GenTypes, TypeName>) => void
) {
@ -84,11 +87,11 @@ export function GQLiteralInterface<GenTypes, TypeName extends string>(
* t.members('OtherType', 'AnotherType')
* })
*/
export function GQLiteralUnion<GenTypes, TypeName extends string>(
name: TypeName,
fn: (arg: GQLiteralUnionType<GenTypes, TypeName>) => void
) {
const factory = new GQLiteralUnionType(name);
export function GQLiteralUnion<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralUnionType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralUnionType<GenTypes>(name);
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -120,14 +123,17 @@ export function GQLiteralUnion<GenTypes, TypeName extends string>(
* t.description('All Movies in the Skywalker saga, or OTHER')
* })
*/
export function GQLiteralEnum<GenTypes>(
name: string,
export function GQLiteralEnum<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: TypeName,
fn:
| ((arg: GQLiteralEnumType<GenTypes>) => void)
| string[]
| Record<string, string | number | object | boolean>
) {
const factory = new GQLiteralEnumType(name);
const factory = new GQLiteralEnumType<GenTypes>(name);
if (typeof fn === "function") {
fn(factory);
} else {
@ -139,8 +145,11 @@ export function GQLiteralEnum<GenTypes>(
/**
*
*/
export function GQLiteralInputObject<GenTypes, TypeName extends string>(
name: string,
export function GQLiteralInputObject<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(
name: TypeName,
fn: (arg: GQLiteralInputObjectType<GenTypes, TypeName>) => void
) {
const factory = new GQLiteralInputObjectType<GenTypes>(name);
@ -161,7 +170,7 @@ export function GQLiteralInputObject<GenTypes, TypeName extends string>(
*
* @return GQLiteralAbstractType
*/
export function GQLiteralAbstractType<GenTypes>(
export function GQLiteralAbstractType<GenTypes = GQLiteralGen>(
fn: (arg: GQLiteralAbstract<GenTypes>) => void
) {
const factory = new GQLiteralAbstract<GenTypes>();
@ -175,12 +184,12 @@ export function GQLiteralAbstractType<GenTypes>(
* This is also exposed during type definition as shorthand via the various
* `__Arg` methods: `fieldArg`, `stringArg`, `intArg`, etc.
*/
export function GQLiteralArg(
type: any, // TODO: make type safe
export function GQLiteralArg<GenTypes = GQLiteralGen>(
type: Types.AllInputTypes<GenTypes>,
options?: Types.ArgOpts
): Types.ArgDefinition {
// This isn't wrapped for now because it's not a named type, it's really just an
// object that can be reused in multiple locations.
): Readonly<Types.ArgDefinition> {
// This isn't wrapped for now because it's not a named type, it's really
// just an object that can be reused in multiple locations.
return {
type,
...options,
@ -216,7 +225,7 @@ export function GQLiteralSchema(options: Types.SchemaConfig) {
// Only in development do we want to worry about regenerating the
// schema definition and/or generated types.
if (process.env.NODE_ENV === "development") {
if (process.env.NODE_ENV !== "production") {
const sortedSchema = lexicographicSortSchema(schema);
const generatedSchema = printSchema(sortedSchema);
const fs = require("fs");

View File

@ -6,33 +6,40 @@ import {
GraphQLUnionType,
GraphQLEnumType,
GraphQLIsTypeOfFn,
GraphQLInputFieldConfigMap,
GraphQLFieldConfigMap,
GraphQLResolveInfo,
} from "graphql";
import { addMix } from "./utils";
import { addMix, dedent } from "./utils";
import { SchemaBuilder } from "./builder";
import { GQLiteralArg } from "./definitions";
declare global {
interface GQLiteralGen {}
}
export type GQLiteralNamedType =
| GQLiteralEnumType
| GQLiteralEnumType<any>
| GQLiteralObjectType<any, any>
| GQLiteralInterfaceType<any, any>
| GQLiteralUnionType
| GQLiteralInputObjectType;
| GQLiteralUnionType<any, any>
| GQLiteralInputObjectType<any, any>;
/**
* Backing type for an enum member.
*/
export class GQLiteralEnumType<GenTypes = any> {
export class GQLiteralEnumType<GenTypes = GQLiteralGen> {
protected typeConfig: Types.EnumTypeConfig;
constructor(protected name: string) {
constructor(name: string) {
this.typeConfig = {
name,
members: [],
};
}
mix<EnumName extends string>(
typeName: Types.EnumName<GenTypes>,
mix<EnumName extends Types.EnumName<GenTypes>>(
typeName: EnumName,
mixOptions?: Types.MixOpts<Types.EnumMembers<GenTypes, EnumName>>
) {
this.typeConfig.members.push({
@ -75,7 +82,7 @@ export class GQLiteralEnumType<GenTypes = any> {
* Any description about the enum type.
*/
description(description: string) {
this.typeConfig.description = description;
this.typeConfig.description = dedent(description);
}
/**
@ -91,10 +98,13 @@ export class GQLiteralEnumType<GenTypes = any> {
}
}
export class GQLiteralUnionType<GenTypes = any, TypeName extends string = any> {
export class GQLiteralUnionType<
GenTypes = GQLiteralGen,
TypeName extends string = any
> {
protected typeConfig: Types.UnionTypeConfig;
constructor(protected name: string) {
constructor(name: string) {
this.typeConfig = {
name,
members: [],
@ -140,32 +150,41 @@ export class GQLiteralUnionType<GenTypes = any, TypeName extends string = any> {
}
}
abstract class FieldsArgs {
abstract class FieldsArgs<GenTypes = GQLiteralGen> {
idArg(options?: Types.ArgOpts) {
return GQLiteralArg("ID", options);
// @ts-ignore
return GQLiteralArg<GenTypes>("ID", options);
}
intArg(options?: Types.ArgOpts) {
return GQLiteralArg("Int", options);
// @ts-ignore
return GQLiteralArg<GenTypes>("Int", options);
}
floatArg(options?: Types.ArgOpts) {
return GQLiteralArg("Float", options);
// @ts-ignore
return GQLiteralArg<GenTypes>("Float", options);
}
boolArg(options?: Types.ArgOpts) {
return GQLiteralArg("Bool", options);
// @ts-ignore
return GQLiteralArg<GenTypes>("Bool", options);
}
stringArg(options?: Types.ArgOpts) {
return GQLiteralArg("String", options);
// @ts-ignore
return GQLiteralArg<GenTypes>("String", options);
}
fieldArg(type: Types.AllInputTypes<GenTypes>, options?: Types.ArgOpts) {
return GQLiteralArg<GenTypes>(type, options);
}
}
export class GQLiteralObjectType<
GenTypes = any,
GenTypes = GQLiteralGen,
TypeName extends string = any
> extends FieldsArgs {
> extends FieldsArgs<GenTypes> {
/**
* All metadata about the object type
*/
@ -262,6 +281,16 @@ export class GQLiteralObjectType<
...options,
},
});
return {
resolver(
fn: (
root: Types.RootValue<GenTypes, TypeName>,
args: Types.ArgsValue<GenTypes, TypeName, FieldName>,
context: Types.ContextValue<GenTypes>,
info: GraphQLResolveInfo
) => Types.ResultValue<GenTypes, TypeName, FieldName>
): void {},
};
}
/**
@ -276,7 +305,7 @@ export class GQLiteralObjectType<
* Adds a description to the metadata for the object type.
*/
description(description: string) {
this.typeConfig.description = description;
this.typeConfig.description = dedent(description);
}
/**
@ -287,7 +316,9 @@ export class GQLiteralObjectType<
}
/**
* Used to modify a field already defined on an interface.
* Used to modify a field already defined on an interface or
* abstract type.
*
* At this point the type will not change, but the resolver,
* defaultValue, property, or description fields can.
*/
@ -318,7 +349,7 @@ export class GQLiteralObjectType<
}
export class GQLiteralInterfaceType<
GenTypes = any,
GenTypes = GQLiteralGen,
TypeName extends string = any
> {
/**
@ -326,7 +357,7 @@ export class GQLiteralInterfaceType<
*/
protected typeConfig: Types.InterfaceTypeConfig;
constructor(protected name: string) {
constructor(name: string) {
this.typeConfig = {
name,
fields: [],
@ -421,7 +452,7 @@ export class GQLiteralInterfaceType<
* Adds a description to the metadata for the interface type.
*/
description(description: string) {
this.typeConfig.description = description;
this.typeConfig.description = dedent(description);
}
/**
@ -444,12 +475,12 @@ export class GQLiteralInterfaceType<
}
export class GQLiteralInputObjectType<
GenTypes = any,
GenTypes = GQLiteralGen,
TypeName extends string = any
> {
protected typeConfig: Types.InputTypeConfig;
constructor(protected name: string) {
constructor(name: string) {
this.typeConfig = {
name,
fields: [],
@ -530,7 +561,7 @@ export class GQLiteralInputObjectType<
}
description(description: string) {
this.typeConfig.description = description;
this.typeConfig.description = dedent(description);
}
/**
@ -622,12 +653,13 @@ export class GQLiteralAbstract<GenTypes> extends FieldsArgs {
options?: Types.AbstractFieldOpts<GenTypes, FieldName>
) {
this.typeConfig.fields.push({
item: Types.NodeType.FIELD,
config: {
name,
type,
...options,
},
name,
type,
...options,
});
}
buildType(): Types.AbstractTypeConfig {
return this.typeConfig;
}
}

View File

@ -14,6 +14,7 @@ import path from "path";
const writeFile = util.promisify(fs.writeFile);
const SCALAR_TYPES = {
Int: "number",
String: "string",
ID: "string",
Float: "number",
@ -22,17 +23,6 @@ const SCALAR_TYPES = {
Boolean: "boolean",
};
export interface GQLiteralTypegenOptions {
typesFilePath: string;
backingTypes?: object;
typeGenPrefix?: string;
/**
* Files to import as namespaces, for backing types
*/
imports?: Record<string, string>;
contextType?: any;
}
export async function typegen(
options: GQLiteralTypegenOptions,
schema: GraphQLSchema
@ -48,10 +38,30 @@ export async function typegen(
}
export interface GQLiteralTypegenOptions {
/**
* File path where generated types should be saved
*/
typesFilePath: string;
/**
* A map of all types and what fields they can resolve to
*/
backingTypes?: object;
/**
* Optional prefix for all fields generated, defaults to "Generated"
*/
typeGenPrefix?: string;
/**
* A map of an import namespace and
*/
imports?: Record<string, string>;
/**
* An array of additional strings to prefix on the header of the TS file
*/
prefix?: string[];
/**
* The type of the context for the resolvers
*/
contextType?: string;
}
export const makeTypes = (
@ -62,17 +72,18 @@ export const makeTypes = (
backingTypes: backing = {},
imports = {},
typeGenPrefix: prefix = "Generated",
prefix: headerPrefix = [],
} = options;
const backingTypes: Record<string, string> = {
...SCALAR_TYPES,
...backing,
};
const typeMap = context.rawSchema.getTypeMap();
const tmpl = `
/**
* This file is autogenerated. Do not edit directly.
*/
${headerPrefix.join("\n")}
${map(Object.keys(imports || {}), (key) => {
return `import * as ${key} from "${path
.relative(typesFilePath, imports[key])
@ -82,42 +93,75 @@ export const makeTypes = (
export type BaseScalarNames = "String" | "Int" | "Float" | "ID" | "Boolean";
${map(
context.interfaces,
(i) => `
export type ${prefix}_Interface_${i.name} = {
${map(context.interfaces, (i) => {
const typeName = `${prefix}_Interface_${i.name}`;
return `
export interface ${typeName}_Fields {
${map(i.fields, (f) => fieldDef(typeName, f))}
}
export interface ${typeName} {
implementingTypes: ${map(i.implementingTypes, (t) => `"${t}"`, "|")};
backingType: ${map(i.implementingTypes, backingType, "|")};
fields: {
${map(i.fields, fieldDef)}
}
fields: ${typeName}_Fields
};
`
)}
`;
})}
${map(
context.types,
(t) => `
export type ${prefix}_Type_${t.name} = {
${map(context.types, (t) => {
const typeName = `${prefix}_Type_${t.name}`;
return `
${map(nonInterfaceFields(context, t), (f) => {
const args = f.hasArguments ? `${typeName}_Field_${f.name}_Args` : "{}";
return `
export interface ${typeName}_Field_${f.name} {
returnType: ${getReturnType(f)};
args: ${args};
}
${
f.hasArguments
? `
export interface ${typeName}_Field_${f.name}_Args {
${map(f.arguments, (arg) => {
const name = `${arg.name}${arg.isRequired ? ":" : "?:"}`;
return `${name} ${getArgType(arg)}`;
})}
}
`
: ""
}
`;
})}
export interface ${typeName}_Fields ${interfaceFields(prefix, t)} {
${map(
nonInterfaceFields(context, t),
(f) => `${f.name}: ${typeName}_Field_${f.name}`
)}
}
export interface ${typeName} {
backingType: ${backingType(t.name)};
fields: ${interfaceFields(prefix, t)} & {
${map(nonInterfaceFields(context, t), fieldDef)}
}
fields: ${typeName}_Fields
};
`
)}
`;
})}
export type ${prefix}Scalars = {
export interface ${prefix}Scalars {
${map(context.scalars, (scalar) => `${scalar.name}: any;`)}
}
export type ${prefix}Interfaces = {
export interface ${prefix}Interfaces {
${map(
context.interfaces,
(i) => `${i.name}: ${prefix}_Interface_${i.name}`
)}
}
export type ${prefix}Unions = {}
export type ${prefix}Enums = {
export interface ${prefix}Unions {}
export interface ${prefix}Enums {
${map(context.enums, (e) => {
return `${e.name}: ${map(
e.values,
@ -126,17 +170,17 @@ export const makeTypes = (
)};`;
})}
}
export type ${prefix}InputObjects = {
export interface ${prefix}InputObjects {
${map(context.inputTypes, (i) => {
return `${i.name}: any;`;
})}
}
export type ${prefix}Objects = {
export interface ${prefix}Objects {
${map(context.types, (t) => `${t.name}: ${prefix}_Type_${t.name}`)}
}
export interface ${prefix}Schema {
context: any;
context: ${options.contextType || "{}"};
enums: ${prefix}Enums;
objects: ${prefix}Objects;
inputObjects: ${prefix}InputObjects;
@ -156,7 +200,12 @@ export const makeTypes = (
| Extract<keyof ${prefix}Interfaces, string>
| Extract<keyof ${prefix}Scalars, string>;
}
export type Gen = ${prefix}Schema;
declare global {
interface GQLiteralGen extends ${prefix}Schema {}
}
`;
const prettier = require("prettier");
@ -164,22 +213,23 @@ export const makeTypes = (
parser: "typescript",
});
function fieldDef(field: Field) {
function fieldDef(typeName: string, field: Field) {
return `${field.name}: {
returnType: ${getReturnType(field)};
args: {
${map(field.arguments, (arg) => {
return `${arg.name}: ${getType(arg)}`;
const name = `${arg.name}${arg.isRequired ? ":" : "?:"}`;
return `${name} ${getArgType(arg)}`;
})}
}
};`;
}
function backingType(name: string) {
return backingTypes[name] || "any";
return backingTypes[name] || "unknown";
}
function getType(field: Field | Argument) {
function getArgType(field: Field | Argument) {
let type = "any";
if (field.isInterface) {
type = `${prefix}_Interface_${field.type}['backingType']`;
@ -190,10 +240,11 @@ export const makeTypes = (
} else if (field.isEnum) {
type = `${prefix}Enums["${field.type}"]`;
}
const nullSuffix = field.isRequired ? "" : "| null | undefined";
if (field.isArray) {
return `${type}${"[]".repeat(field.dimensionOfArray)}`;
return `${type}${"[]".repeat(field.dimensionOfArray)}${nullSuffix}`;
}
return type;
return `${type}${nullSuffix}`;
}
function getReturnType(field: Field | Argument) {
@ -207,12 +258,12 @@ export const makeTypes = (
} else if (field.isEnum) {
type = `${prefix}Enums["${field.type}"]`;
}
const nullPrefix = field.isRequired ? "null | " : "";
const nullPrefix = field.isRequired ? "" : "null | ";
if (field.isArray) {
const arraySuffix = "[]".repeat(field.dimensionOfArray);
return `${nullPrefix}${type}${arraySuffix} | PromiseLike<${type}${arraySuffix}> | PromiseLike<${type}>${arraySuffix}`;
return `${nullPrefix}${type}${arraySuffix} | PromiseLike<${nullPrefix}${type}${arraySuffix}> | PromiseLike<${type}>${arraySuffix}`;
}
return `${nullPrefix}${type} | PromiseLike<${type}>`;
return `${nullPrefix}${type} | PromiseLike<${nullPrefix}${type}>`;
}
return [{ filename: options.typesFilePath, content }];
@ -228,7 +279,14 @@ function map<T>(arr: T[], mapper: Mapper<T>, join = "\n") {
}
function interfaceFields(prefix: string, t: SchemaTemplateContext["types"][0]) {
return map(t.interfaces, (i) => `${prefix}_Interface_${i}['fields']`);
if (t.interfaces.length) {
return `extends ${map(
t.interfaces,
(i) => `${prefix}_Interface_${i}_Fields`,
", "
)}`;
}
return "";
}
function nonInterfaceFields(

View File

@ -19,8 +19,6 @@ export enum NodeType {
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type GQLTypes = "ID" | "String" | "Int" | "Float" | string;
export type MixDef = {
item: NodeType.MIX;
typeName: string;
@ -146,12 +144,6 @@ export type ArgDefinition = Readonly<
}
>;
export interface ObjTypeDef {
root: any;
context: any;
args: { [argName: string]: any };
}
export type OutputFieldArgs = Record<string, ArgDefinition>;
export interface OutputFieldOpts<
@ -170,7 +162,12 @@ export interface OutputFieldOpts<
/**
* Resolver for the output field
*/
resolve?: GQLitFieldResolver<GenTypes, TypeName, FieldName>;
resolve?: (
root: RootValue<GenTypes, TypeName>,
args: ArgsValue<GenTypes, TypeName, FieldName>,
context: ContextValue<GenTypes>,
info: GraphQLResolveInfo
) => ResultValue<GenTypes, TypeName, FieldName>;
/**
* Default value for the field, if none is returned.
*/
@ -272,7 +269,9 @@ export interface ObjectTypeConfig
isTypeOf?: GraphQLIsTypeOfFn<any, any>;
}
export interface AbstractTypeConfig extends HasFields {}
export interface AbstractTypeConfig {
fields: FieldConfig[];
}
export interface InterfaceTypeConfig
extends Named,
@ -378,43 +377,25 @@ export type ResolveType<GenTypes, TypeName> = (
root: RootValue<GenTypes, TypeName>
) => InterfaceName<GenTypes, TypeName>;
type GenTypesFieldsShape = Record<
string,
{
returnType: any;
args: any;
}
>;
/**
* Helpers for handling the generated schema
*/
export type GenTypesShape = {
context: any;
enums: Record<string, any>;
objects: Record<
string,
{
backingType: any;
fields: Record<
string,
{
returnType: any;
args: any;
}
>;
}
>;
interfaces: Record<string, any>;
objects: Record<string, any>;
inputObjects: Record<string, any>;
unions: Record<string, any>;
scalars: Record<string, any>;
interfaces: Record<
string,
{
implementingTypes: string;
backingType: any;
fields: Record<
string,
{
returnType: any;
args: any;
}
>;
}
>;
availableInputTypes: string;
availableOutputTypes: string;
};
@ -465,15 +446,13 @@ export type AllInterfaces<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypes["interfaces"], string>
: never;
export type AllInputTypes<
GenTypes,
K = "availableInputTypes"
> = K extends keyof GenTypes ? GenTypes[K] : never;
export type AllInputTypes<GenTypes> = GenTypes extends GenTypesShape
? GenTypes["availableInputTypes"]
: never;
export type AllOutputTypes<
GenTypes,
K = "availableOutputTypes"
> = K extends keyof GenTypes ? GenTypes[K] : never;
export type AllOutputTypes<GenTypes> = GenTypes extends GenTypesShape
? GenTypes["availableOutputTypes"]
: never;
export type RootValue<GenTypes, TypeName> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["objects"]
@ -518,10 +497,3 @@ export type ResultValue<
: never
: never
: never;
export type GQLitFieldResolver<GenTypes, TypeName, FieldName> = (
source: RootValue<GenTypes, TypeName>,
args: ArgsValue<GenTypes, TypeName, FieldName>,
context: ContextValue<GenTypes>,
info: GraphQLResolveInfo
) => ResultValue<GenTypes, TypeName, FieldName>;

View File

@ -179,3 +179,55 @@ function lexicalDistance(aStr: string, bStr: string): number {
return d[aLength][bLength];
}
// Borrowed from https://github.com/dmnd/dedent
export function dedent(
strings: string | TemplateStringsArray,
...values: Array<string>
) {
const raw = typeof strings === "string" ? [strings] : strings.raw;
// first, perform interpolation
let result = "";
for (let i = 0; i < raw.length; i++) {
result += raw[i]
// join lines when there is a suppressed newline
.replace(/\\\n[ \t]*/g, "")
// handle escaped backticks
.replace(/\\`/g, "`");
if (i < values.length) {
result += values[i];
}
}
// now strip indentation
const lines = result.split("\n");
let mindent: number | null = null;
lines.forEach((l) => {
let m = l.match(/^(\s+)\S+/);
if (m) {
let indent = m[1].length;
if (!mindent) {
// this is the first indented line
mindent = indent;
} else {
mindent = Math.min(mindent, indent);
}
}
});
if (mindent !== null) {
const m = mindent; // appease Flow
result = lines.map((l) => (l[0] === " " ? l.slice(m) : l)).join("\n");
}
return (
result
// dedent eats leading and trailing whitespace too
.trim()
// handle escaped newlines at the end to ensure they don't get stripped too
.replace(/\\n/g, "\n")
);
}