@ndlib/marble-models v1.0.17
Overview
This repo contains the various components of the backend infrastructure for the Marble project.
In this repo, you will find code for:
- Our database models, located in the
prismasubdirectory. These models are specified using the Prisma ORM. - Migrations for our database, located in the
prisma/migrationssubdirectory. These migrations are SQL migrations needed to set up and modify a database according to our schema. - Scripts for migrating data into a database from the legacy
DynamoDB database, located in the
src/scriptssubdirectory - All the necessary code to run our GraphQL API,
located in the
src/subdirectory.
Getting Started
Postgres Setup
To get started with this project, you will need to have access PostgreSQL database. You can set up a local database using homebrew, following the instructions here.
Reference and access to your postgres db is managed by setting the
PG_DATABASE_URL environment variable. This variable should be
formatted as follows:
postgres://<username>:<password>@<host>:<port>/<database>For example, on your local machine, this might look like:
postgres://mynetid:password@127.0.0.1:5432/mynetidYou can also run postgres using docker if you have docker installed.
Start Postgres in Docker
docker-compose up -d
Stop Postgres in Docker
- find your container id:
docker ps docker stop CONTAINER_ID
Once you have set up your database, you can run the following commands to get started:
npm install
npx prisma migrate devDynamoDB Setup
Since our graphql api also uses the legacy DynamoDB database for some operations, you will also need to set up your environment with permissions to access the database you want to use.
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_SESSION_TOKEN=""These values can be found by following the Access keys link on your AWS IAM login page. Notes these values are regenerated every day, so you will need to copy them into your .env file every day.
You will also need to set the name of the DynamoDB table you want to use in your environment as well.
DYNAMO_TABLE="..."Running the API
To run the API, you can run the following command:
yarn startThis will start the API on port 4000. You can access the GraphQL playground
by visiting http://localhost:4000 in your browser.
Graphql
Graphql is a query language for APIs that allows clients to request only the data they need. This is a powerful tool for optimizing data fetching and reducing the number of requests needed to get the data you need.
In order to implement a graphql API, you need two things:
- A schema that defines the types and queries that are available
- Resolvers that implement the logic used to fetch the data for a given query or type
In this project, our schema is defined in the src/schema.graphql file.
Our resolvers are defined in the src/resolvers directory.
We use the apollo-server library to run our graphql server.
Resolvers
Resolvers are functions that implement the logic for fetching the data.
The fulfillment of any query in graphql is done by cascading sequentially through the structure of the query and calling the appropriate resolver for each field, as defined the parent type. The result of each field in the query is then passed to the resolver for the field's type in order to resolve that field's children.
Example
It is easiest to explain this with a simple example. Consider the following graphql schema, resolvers, and query:
Schema:
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
title: String!
}Resolvers:
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return { id: args.id, name: "Alice" };
}
},
User: {
posts: (parent, args, context, info) => {
return [{ title: "My first post", author: parent.name }];
}
}
}Query:
{
user(id: "1") {
id
name
posts {
title
author
}
}
}Execution
The first level of fields in a graphql query must be defined as field on
the Query type or the Mutation type. In this case, the user field is
defined on the Query type.
When the query is executed, the first operation to run will be the resolver
for the user field on the Query type. This resolver is a function
specified at the Query.user property path in our resolvers object.
Because Query.user maps to the User type, after the Query.user
resolver function runs, its return value will then be passed to the
resolvers for the User type.
Notice that some of the fields on the User type are scalar types
(ID and String in this case).
For these fields, the value of the field will resolve to the value
on the parent object.
However, the posts field on the User type is a list of Post objects,
ie not a scalar.
In this case, the resolver for the User.posts field will be called.
When this function is called, it will receive the value of Query.user
as it's first argument (parent).
This resolver will return an array of Post objects.
The last step of the execution is to resolve the fields on the Post type.
However, since these are all scalar types, the resolvers for these fields
will simply return the value of the parent object.
After the final layer resolves, the values get passed back based on the structure of the query
{
user: { // executes Query.user resolver
id: 1, // resolves to id property of Query.user resolver call
name: "Alice" // resolves to name property of Query.user resolver call
posts: [{ // executes User.posts resolver
title: "My first post" // resolves to title property of User.posts resolver call
author: "Alice" // resolves to author proprety of User.posts resolver call
}]
}
}