@ndlib/marble-models
v1.0.17
Published
This repo contains the various components of the backend infrastructure for the Marble project.
Downloads
6
Keywords
Readme
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
prisma
subdirectory. These models are specified using the Prisma ORM. - Migrations for our database, located in the
prisma/migrations
subdirectory. 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/scripts
subdirectory - 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:[email protected]:5432/mynetid
You 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 dev
DynamoDB 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 start
This 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
}]
}
}