Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike a graphql simulator #83

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- schema development
- infinite generators
- app development
- known state, scenarios
3,711 changes: 451 additions & 3,260 deletions package-lock.json

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions packages/graphql-starwars/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@simulacrum/graphql-starwars",
"version": "0.0.0",
"description": "Star Wars GraphQL Schema",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf *.tsbuildinfo dist",
"prepack": "tsc --build tsconfig.dist.json",
"build": "npm run prepack",
"lint": "eslint src",
"test": "echo skip",
"start": "node dist/start.js",
"watch": "PORT=5000 ts-node -P ./tsconfig.watch.json ./watch.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/thefrontside/simulacrum.git"
},
"keywords": [
"simulation",
"emulation",
"graphql",
"starwars"
],
"author": "Frontside Engineering <engineering@frontside.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/thefrontside/simulacrum/issues"
},
"homepage": "https://github.com/thefrontside/simulacrum#readme",
"dependencies": {
"@effection/atom": "2.0.0-beta.3",
"@simulacrum/server": "0.2.0",
"express": "^4.17.1",
"express-graphql": "^0.12.0"
},
"devDependencies": {
"@frontside/eslint-config": "^2.0.0",
"@frontside/tsconfig": "^1.2.0",
"@frontside/typescript": "^1.1.1",
"graphql": "15.5.0",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"peerDependencies": {
"graphql": "*"
},
"volta": {
"extends": "../../package.json"
}
}
2 changes: 2 additions & 0 deletions packages/graphql-starwars/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './schema';
export * from './simulation';
107 changes: 107 additions & 0 deletions packages/graphql-starwars/src/schema/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Context, Droid, Episode, Human, StarWarsStore } from './types';

/**
* This is an example of a real store to accompany the schema.
* We can create a simulacrum store that matches the _shape_ of this one,
* and place it in context to run the schema as a simulation
*/

const luke: Human = {
__typename: 'Human',
id: '1000',
name: 'Luke Skywalker',
friends: ['1002', '1003', '2000', '2001'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
homePlanet: 'Tatooine',
};

const vader: Human = {
__typename: 'Human',
id: '1001',
name: 'Darth Vader',
friends: ['1004'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
homePlanet: 'Tatooine',
};

const han: Human = {
__typename: 'Human',
id: '1002',
name: 'Han Solo',
friends: ['1000', '1003', '2001'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
};

const leia: Human = {
__typename: 'Human',
id: '1003',
name: 'Leia Organa',
friends: ['1000', '1002', '2000', '2001'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
homePlanet: 'Alderaan',
};

const tarkin: Human = {
__typename: 'Human',
id: '1004',
name: 'Wilhuff Tarkin',
friends: ['1001'],
appearsIn: [Episode.EMPIRE],
};

const humanData: { [id: string]: Human } = {
[luke.id]: luke,
[vader.id]: vader,
[han.id]: han,
[leia.id]: leia,
[tarkin.id]: tarkin,
};

const threepio: Droid = {
__typename: 'Droid',
id: '2000',
name: 'C-3PO',
friends: ['1000', '1002', '1003', '2001'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
primaryFunction: 'Protocol',
};

const artoo: Droid = {
__typename: 'Droid',
id: '2001',
name: 'R2-D2',
friends: ['1000', '1002', '1003'],
appearsIn: [Episode.NEW_HOPE, Episode.EMPIRE, Episode.JEDI],
primaryFunction: 'Astromech',
};

const droidData: { [id: string]: Droid } = {
[threepio.id]: threepio,
[artoo.id]: artoo,
};

export const store: StarWarsStore = {
getCharacters() {
return Promise.resolve([
...Object.values(humanData),
...Object.values(droidData)
]);
},
getCharacter(id) {
return Promise.resolve(humanData[id] ?? droidData[id]);
},
async getFriends(character) {
let friends = await Promise.all(character.friends.map((id) => this.getCharacter(id)));
return friends.flatMap(x => x !== undefined ? [x] : []);
},
getHuman(id) {
return Promise.resolve(humanData[id]);
},
getDroid(id) {
return Promise.resolve(droidData[id]);
},
};

export const createContext = (): Context => ({
store,
});
3 changes: 3 additions & 0 deletions packages/graphql-starwars/src/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { createContext } from './context';
export { schema } from './schema';
export * from './types';
172 changes: 172 additions & 0 deletions packages/graphql-starwars/src/schema/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {
GraphQLSchema, GraphQLString,
GraphQLList,
GraphQLNonNull,
GraphQLEnumType,
GraphQLInterfaceType,
GraphQLObjectType } from 'graphql';

import { Context, Droid, Episode, Human } from './types';

/**
* A simple schema for testing and documentation.
*
* Resolvers should be pure; that is they should only rely on what is in 'Context'.
* This allows us to swap the real context for one that is backed by simulation.
*/

const episodeEnum = new GraphQLEnumType({
name: 'Episode',
description: 'One of the films in the Star Wars Trilogy',
values: {
NEW_HOPE: {
value: Episode.NEW_HOPE,
description: 'Released in 1977.',
},
EMPIRE: {
value: Episode.EMPIRE,
description: 'Released in 1980.',
},
JEDI: {
value: Episode.JEDI,
description: 'Released in 1983.',
},
},
});

const characterInterface: GraphQLInterfaceType = new GraphQLInterfaceType({
name: 'Character',
description: 'A character in the Star Wars Trilogy',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLString),
description: 'The id of the character.',
},
name: {
type: GraphQLString,
description: 'The name of the character.',
},
friends: {
type: new GraphQLList(characterInterface),
description:
'The friends of the character, or an empty list if they have none.',
},
appearsIn: {
type: new GraphQLList(episodeEnum),
description: 'Which movies they appear in.',
},
secretBackstory: {
type: GraphQLString,
description: 'All secrets about their past.',
},
}),
});

const humanType = new GraphQLObjectType<Human, Context>({
name: 'Human',
description: 'A humanoid creature in the Star Wars universe.',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLString),
description: 'The id of the human.',
},
name: {
type: GraphQLString,
description: 'The name of the human.',
},
friends: {
type: new GraphQLList(characterInterface),
description:
'The friends of the human, or an empty list if they have none.',
resolve: (human, _args, { store }) => store.getFriends(human),
},
appearsIn: {
type: new GraphQLList(episodeEnum),
description: 'Which movies they appear in.',
},
homePlanet: {
type: GraphQLString,
description: 'The home planet of the human, or null if unknown.',
},
secretBackstory: {
type: GraphQLString,
description: 'Where are they from and how they came to be who they are.',
resolve() {
throw new Error('secretBackstory is secret.');
},
},
}),
interfaces: [characterInterface],
});

const droidType = new GraphQLObjectType<Droid, Context>({
name: 'Droid',
description: 'A mechanical creature in the Star Wars universe.',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLString),
description: 'The id of the droid.',
},
name: {
type: GraphQLString,
description: 'The name of the droid.',
},
friends: {
type: new GraphQLList(characterInterface),
description:
'The friends of the droid, or an empty list if they have none.',
resolve: (droid, _args, { store }) => store.getFriends(droid),
},
appearsIn: {
type: new GraphQLList(episodeEnum),
description: 'Which movies they appear in.',
},
secretBackstory: {
type: GraphQLString,
description: 'Construction date and the name of the designer.',
resolve() {
throw new Error('secretBackstory is secret.');
},
},
primaryFunction: {
type: GraphQLString,
description: 'The primary function of the droid.',
},
}),
interfaces: [characterInterface],
});

const queryType = new GraphQLObjectType<undefined, Context>({
name: 'Query',
fields: () => ({
human: {
type: humanType,
args: {
id: {
description: 'id of the human',
type: new GraphQLNonNull(GraphQLString),
},
},
resolve: (_source, { id }, { store }) => store.getHuman(id),
},
droid: {
type: droidType,
args: {
id: {
description: 'id of the droid',
type: new GraphQLNonNull(GraphQLString),
},
},
resolve: (_source, { id }, { store }) => store.getDroid(id),
},
characters: {
type: new GraphQLList(characterInterface),
resolve: (_source, _args, { store }) => store.getCharacters(),
}
}),
});

export const schema: GraphQLSchema = new GraphQLSchema({
query: queryType,
types: [humanType, droidType],
});
37 changes: 37 additions & 0 deletions packages/graphql-starwars/src/schema/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export enum Episode {
NEW_HOPE = "4",
EMPIRE = "5",
JEDI = "6",
}

export interface Human {
__typename: 'Human';
id: string;
name: string;
friends: ReadonlyArray<string>;
appearsIn: Episode[];
homePlanet?: string;
}

export interface Droid {
__typename: 'Droid';
id: string;
name: string;
friends: ReadonlyArray<string>;
appearsIn: Episode[];
primaryFunction: string;
}

export type Character = Human | Droid;

export interface StarWarsStore {
getCharacters(): Promise<Array<Character>>;
getCharacter(id: string): Promise<Character | undefined>;
getFriends(character: Character): Promise<Array<Character>>;
getHuman(id: string): Promise<Human | undefined>;
getDroid(id: string): Promise<Droid | undefined>;
}

export interface Context {
store: StarWarsStore;
}
Loading