{"id":16372456,"url":"https://github.com/mgwidmann/sw-graphql-workshop","last_synced_at":"2026-02-06T05:02:22.747Z","repository":{"id":43986152,"uuid":"243591141","full_name":"mgwidmann/sw-graphql-workshop","owner":"mgwidmann","description":null,"archived":false,"fork":false,"pushed_at":"2023-01-05T08:30:01.000Z","size":1240,"stargazers_count":0,"open_issues_count":13,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-03T04:15:49.307Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mgwidmann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-27T18:44:20.000Z","updated_at":"2020-05-20T20:07:06.000Z","dependencies_parsed_at":"2023-02-03T20:30:52.148Z","dependency_job_id":null,"html_url":"https://github.com/mgwidmann/sw-graphql-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mgwidmann/sw-graphql-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgwidmann%2Fsw-graphql-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgwidmann%2Fsw-graphql-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgwidmann%2Fsw-graphql-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgwidmann%2Fsw-graphql-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mgwidmann","download_url":"https://codeload.github.com/mgwidmann/sw-graphql-workshop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgwidmann%2Fsw-graphql-workshop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29151560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T02:39:25.012Z","status":"ssl_error","status_checked_at":"2026-02-06T02:37:22.784Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T03:11:28.235Z","updated_at":"2026-02-06T05:02:22.732Z","avatar_url":"https://github.com/mgwidmann.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Star Wars GraphQL Workshop\n\n## Setup\n\nInstall dependencies:\n\n1. `brew install nodejs`\n1. `npm install -g yarn`\n1. `yarn install`\n\nTest building:\n\n1. `yarn build`\n\n## Running\n\n1. `yarn server`\n\nOpen http://localhost:3000\n\nExplore the documentation on the right with the hello world for GraphQL.\n\nIn the left panel, paste in the following to execute the query:\n\n```graphql\nquery HelloGraphQL($name: String!){\n  hello(name: $name)\n}\n```\n\nAt the bottom of the page, click `QUERY VARIABLES` and paste in the following:\n\n```json\n{\"name\": \"Matt\"}\n```\n\nHit the ▶️ button to execute your query!\n\nTry running without the name parameter:\n\n```graphql\nquery {\n  hello\n}\n```\n\nNotice the name parameter is required, even if you run it it will not accept the query. Its because the parameter is defined as required by using `!` in the type definition.\n\n## Tasks\n\n### 1. Create the Person GraphQL Model\n\nA simple version of the `Person` model with just a few fields looks like this:\n\n```graphql\n\"\"\"A character in the Star Wars universe\"\"\"\ntype Person {\n  \"\"\"The internal identifier for this object.\"\"\"\n  id: ID!\n  \"\"\"The full name of this character.\"\"\"\n  name: String!\n  \"\"\"How tall this character is (in centimeters?)\"\"\"\n  height: Float!\n  \"\"\"How much this character weighs (in kilograms?)\"\"\"\n  mass: Float!\n}\n```\n\nThe `ID`, `String` and `Float` types are defined by the GraphQL implementation, so we didn't need to create them. The `!` after the type means it is required (without the `!` would mean optional). This can be expressed as well with arrays. For example: `[String]!` means the array will always be present but the string is optional (an empty array would satisfy this condition). However, `[String!]!` says not only will the array be present but that at least one element will be inside.\n\nAdd this to the `src/schema.graphql` file and refresh the browser. You should now be able to find the `Person` model in the right side Documentation Tab if you search for it. But if you look in the `Query` type, theres no function you can call to fetch it.\n\n### 2. Return a Person\n\nIn the `src/schema.graphql` file, add the following to the `Query` type:\n\n```graphql\n  \"\"\"Get a specific person\"\"\"\n  person(id: ID!): Person\n```\n\nNow running the following query in the left panel of the GraphiQL browser you get back a `null` person.\n\n```graphql\nquery {\n  person(id: 1) {\n    id\n    name\n    height\n    mass\n  }\n}\n```\n\nTo fix this, add a resolver which will fetch the person specified. Add the following to `src/resolvers.ts` under the `Query` key.\n\nUncomment the import at the top of the file.\n\n```typescript\nimport { Person } from \"./starWarsApi\";\n```\n\nAdd the resolver under the `Query` key.\n\n```typescript\n    person: (_: undefined, parameters: { id: number }): Person | null =\u003e {\n      return Person.find\u003cPerson\u003e(parameters.id);\n    }\n```\n\nThe first parameter is the parent object, in this its case `undefined` because this is the top level already.\nThe second parameter is the parameters for the field, in this case the `person(id: 1)` part of the GraphQL query.\nThere is a third parameter missing here, which is the context object. Normally, the context can hold things like authentication or authorization info and can allow you to have a \"global\" state for the life of the entire query execution. Since executing a GraphQL query is just a function call, the context is passed in with this informaiton when the query is executed, allowing you to gather that information in any normal way you would do today.\n\nExecute the above query again and you should get data! **CONGRATS ON WRITING YOUR FIRST GRAPHQL QUERY!!**\n\n_NOTE: The console will log out `\"Person.find(1)\"` to show you how access to the backend is happening. More on this later._\n\n### 3. Related Model\n\nEnough hand-holding. Write the `World` schema and add a relationship from the `Person` to a world called `homeworld`. You will fetch it with the following query:\n\n```graphql\nquery {\n  person(id: 1) {\n    homeworld {\n      id\n      name\n      # Feel free to fetch more fields if you've added them to the schema for `World`\n    }\n  }\n}\n```\n\n### 4. Circular Related Models\n\nWe want to be able to return all the starships piloted by a character. And all the characters who've piloted a ship. The reason for this is the user's entry point might be a `Person` or it might be a `Starship`, so the schema naturally creates a **Graph**. We want the following query to work:\n\n```\nquery {\n  person(id: 10) {\n    starships {\n      id\n      name\n      pilots {\n        id\n        name\n        starships {\n          id\n          name\n          # This can continue indefinately... \n        }\n      }\n    }\n  }\n}\n```\n\nThis above query can be resource intensive. If we add the starship schema, we can see how much fetching is going to go on in order to fulfill this query in the console. Add the `starships` relationship to the `Person` schema then add a Starship schema like the following:\n\n```graphql\ntype Starship {\n  id: ID!\n  name: String!\n  model: String!\n  pilots: [Person]!\n}\n```\n\nIn the console you will see the same thing getting fetched over and over again. Additionally, they are fetched one by one instead of all at once.\n\nTo fix this GraphQL recommends using a pattern called [dataloader](https://github.com/graphql/dataloader). You define a new dataloader like so:\n\n```typescript\nconst PersonLoader = new DataLoader(async (keys: readonly number[]) =\u003e {\n  return Person.all\u003cPerson\u003e(...keys as number[]);\n});\n```\n\nThen in your resolver you can call:\n\n```typescript\nPersonLoader.loadMany(starship.pilotIds());\n```\n\nMake this query work. Add a `starships(id: ID!): Starship` type to the `Query` type to allow the user to start at a starship first instead of a person.\n\n### 5. Parameters\n\nAdd a parameter to the name field of `Person` to allow the name to come back as uppercased, lowercase or natural (the default). The query will look like this:\n\n```graphql\nquery {\n  person(id: 1) {\n    name(format: UPPERCASE)\n  }\n}\n```\n\nThe `homeworld` field before did a little bit of magic for you. The default resolver was able to find the `World` object because the property was named exactly the same on the model as it was in the GraphQL schema. If you change the GraphQL schema to `home` for example, it will stop working because the `Person` model has no field named `home`. Try this and see what happens.\n\nTo deal with name changes, you have to put in a resolver and find the data to return. Additionally, to accept parameters you also have to put in a resolver. The resolver will be installed under the `Person.name` key like so:\n\n```typescript\n  Query: {\n    // ...\n  },\n  Person: {\n    name: (person: Person, parameters: { format: StringFormat }): string =\u003e {\n      return ''; // Do logic here, format has already been validated by GraphQL\n    }\n  }\n```\n\nThe above code won't compile because the `StringFormat` enum is missing. So add it:\n\n```typescript\nenum StringFormat {\n  UPPERCASE = 'UPPERCASE',\n  NATURAL = 'NATURAL',\n  LOWERCASE = 'LOWERCASE',\n}\n```\n\n_NOTE: Because the enum definitions are the same and we've told TypeScript to use a string for the runtime implementation, these end up matching because of GraphQL's validation ensuring it matches the enum value. In order to use the more natural `number` based `enum`, you need to add resolvers for this enum to translate each of them to their corresponding runtime value. To do that, you can add something like the following which maps them:_\n\n```typescript\n  // Note: this can be done automatically by reducing the enum to invert the keys and values and then when a new\n  // GraphQL schema change adds or removes to this enum it will cause an error.\n  StringFormat: { // This line should be the name of your enum in your GraphQL schema\n    [StringFormat[StringFormat.UPPERCASE]]: StringFormat.UPPERCASE,\n    [StringFormat[StringFormat.NATURAL]]: StringFormat.NATURAL,\n    [StringFormat[StringFormat.LOWERCASE]]: StringFormat.LOWERCASE,\n  },\n```\n\n### 6. Interfaces\n\nSo far you have a `Person`, `Starship` and `World` schemas in your graphql file. They all have an `id` field of type `ID!` in common. If we wanted to ensure each of our schemas always had that field and that the type was a required `ID` type, we could create an interface to express this.\n\n```graphql\ninterface Base {\n  id: ID!\n}\n```\n\nNow update the `Person`, `Starship` and `World` models to be defined as `type Person implements Base`. Try removing the `id` field from either and see how the app crashes when it reboots.\n\n_NOTE: To get rid of the warning for `Base`'s `__resolveType`, add a `Base.__resolveType` function to your resolvers. In this function you should return the name of the class as a string for the object which is passed in as the first parameter._\n\n### 7. Unions\n\nA union is a joining of multiple types. The user will be required to express which fields from each of the unioned models they want and GraphQL will handle calling the right resolver.\n\nA union can be expressed as:\n\n```graphql\nunion Transportation = Starship | Vehicle\n```\n\nImplement a basic version of the `Vehicle` schema and then implement the following query:\n\n```graphql\ntransportation(type: TransportationType): Transportation\n```\n\n`TransportationType` is simply an enum defined as:\n\n```graphql\nenum TransportationType {\n  STARSHIP\n  VEHICLE\n  ALL\n}\n```\n\nIn the case of `STARSHIP` return `Starship.all\u003cStarship\u003e()`, in the case of `VEHICLE` return `Vehicle.all\u003cVehicle\u003e()`, and in the case of `ALL` return:\n\n```typescript\nlet starships: (Starship | Vehicle)[] = Starship.all\u003cStarship\u003e();\nlet vehicles: (Starship | Vehicle)[] = Vehicle.all\u003cVehicle\u003e();\nreturn starships.map((_, i) =\u003e [starships[i], vehicles[i]]).flat().filter((t) =\u003e t);\n```\n\nThen execute a query to get them using the following syntax. The data should alternate between `Starship`s and `Vehicle` objects.\n\n```graphql\nquery {\n  transportation(type: ALL) {\n    ... on Starship {\n      id\n      name\n    }\n    ... on Vehicle {\n      id\n      name\n    }\n  }\n}\n```\n\n### 8. Scalars\n\nThe data behind the `Person` model has a string value for `birthYear` which is expressed in the format `123BBY` which means [123 years Before Battle of Yavin](https://starwars.fandom.com/wiki/0_BBY). This string with a special format should not be represented as a string, since jibberish like `\"hello\"` is not valid. There are other date formats in the Star Wars universe like LY and C.R.C., but this data set does not have those so we won't handle them.\n\nIn your schema file, to define a new scalar is very easy. Just add the following anywhere:\n\n```graphql\nscalar SWYear\n```\n\nImplement a new `GraphQLScalarType` by filling in the three required functions below. When we return data to the client, or receive it, we want to represent it in a string format as described above. But internally, we don't want to parse the integer field out again if we manipulate or perform logic concerning this data. Using a scalar lets us have an implementation like `{ value: number, calendar: SWCalendar }` instead. But the most powerful part of the scalar features is its ability to validate the input such that the query **will not run** when the data is invalid, such as nonsense like `\"hello\"`.\n\n```typescript\nimport { GraphQLScalarType } from 'graphql';\n\n// ...\n\n  SWYear: new GraphQLScalarType({\n    name: 'SWYear',\n    description: 'Format of years in the Star Wars universe. Before the Battle of Yavin (BBY) only supported format.',\n    // Takes as input the interal representation of the scalar\n    // Should return the value which we want to send back to the client\n    // Used when YOU give GraphQL this scalar and it needs to be translated to the client\n    serialize(value) {\n      return fromYear(value);\n    },\n    // Takes as input the primitive representation of the scalar\n    // Should return the internal representation of the scalar\n    // Used when YOU receive data from GraphQL\n    parseValue(value) {\n      return toYear(value);\n    },\n    // Takes as input the Abstract Syntax Tree (AST) of the query at the node point where\n    // this scalar is being used. The `kind` field indicates what the underlying primitive\n    // type that is used to represent it, in this case a String.\n    // This function is meant as a validation function while GraphQL is walking the graph.\n    // It is typical to try performing the conversion here to ensure everything is valid.\n    parseLiteral(ast) {\n      if (ast.kind == Kind.STRING) {\n        return toYear(ast.value)\n      }\n      return null;\n    }\n  })\n```\n\nWe can then internally represent the years as the interface defined below, making our code easier to write rather than having to handle the string version everywhere. Write the functions used above.\n\n```typescript\nexport enum SWCalendar {\n  BBY,\n  LY,\n  CRC,\n}\n\nexport interface SWYear {\n  value: number\n  calendar: SWCalendar\n}\n\nexport function toYear(s: string): SWYear {\n  // fill this in\n}\n\nexport function fromYear(year: SWYear): string {\n  // fill this in\n}\n```\n\n### 9. Pagination\n\nMake the following query work. You can use the function provided to fetch a page, `Person.list\u003cPerson\u003e(3)` will fetch the 3rd page for example.\n\n```graphql\nquery {\n  people(page: 3) {\n    count\n    next\n    previous\n    results {\n      id\n      name\n    }\n  }\n}\n```\n\n### 10. Simple Mutations\n\nA mutation to update a person's name would look like this:\n\n```\nmutation {\n  personUpdateName(id: 1, name: \"George Lucas\") {\n    id\n    name\n  }\n}\n```\n\nTry to make this query work by adding this to the schema and adding a `Mutations.personUpdateName` resolver (not in the `Query` key). Use `Person.find\u003cPerson\u003e(id)` to get an instance of the person and `person.update({ name: name })` function to update it. _NOTE: The updated values only exist in memory so every change to the source code causing a server reload will reset all changes._\n\nA return value is required, so typically just the primary identifier is returned when you don't need any data back (and you can ignore the response). However, if you do want an update of other fields, here is your chance! You can even get related model data back. For example, after updating the name of the person you can fetch all starships for this person and get all the pilot names that have piloted those ships.\n\n### 11. Input Objects\n\nMutations won't work well if every field needs to be updated like above. GraphQL defines `input_object`s for the purpose of defining types that are used on input. The person update function you need to make work is the following:\n\n```graphql\n# The name of my input_object is `PersonUpdate`\nmutation UpdatePerson($id: ID!, $person: PersonUpdate) {\n  personUpdate(id: $id, person: $person) {\n    id\n  }\n}\n```\n\nWith the query variables set as:\n\n```json\n{\"id\": 1, \"person\": {\"name\": \"George Lucas\"}}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgwidmann%2Fsw-graphql-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmgwidmann%2Fsw-graphql-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgwidmann%2Fsw-graphql-workshop/lists"}