Working through the API with examples, type safety working pretty well
This commit is contained in:
parent
5b59ffd21d
commit
de2b00dd0e
|
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
dist/*
|
||||
dist/*
|
||||
examples/*/dist
|
||||
|
|
@ -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!]!
|
||||
}
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
const { GQLiteralSchema } = require("gqliteral");
|
||||
|
||||
const schema = GQLiteralSchema({
|
||||
types: require("./schema"),
|
||||
definitionFilePath: "../output.graphql",
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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}`
|
||||
)
|
||||
);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export * from "./launch";
|
||||
export * from "./mission";
|
||||
export * from "./mutation";
|
||||
export * from "./query";
|
||||
export * from "./rocket";
|
||||
export * from "./user";
|
||||
|
|
@ -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 });
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
|
|
@ -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 });
|
||||
});
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
|
|
@ -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,
|
||||
}) || []
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
)
|
||||
);
|
||||
|
|
@ -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
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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!]!
|
||||
|
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
154
src/typeGen.ts
154
src/typeGen.ts
|
|
@ -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(
|
||||
|
|
|
|||
78
src/types.ts
78
src/types.ts
|
|
@ -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>;
|
||||
|
|
|
|||
52
src/utils.ts
52
src/utils.ts
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue