-
Notifications
You must be signed in to change notification settings - Fork 2
Backend
The backend of StuyActivities is written in JavaScript. The main technologies/libraries it uses are Node.JS, GraphQL (Apollo), and Sequelize. The actual database uses MySQL, but you can also use SQLite in development. To work on the API, pull the repository and install the dependencies (usually with npm i
). You might have trouble installing the SQLite dependency, sqlite3
, in which case you may need to install some dependencies as outlined in the Installation section of this page. You can also skip SQLite3 and run a local MySQL server (!). Run the program with npm run dev
to work on it, but make sure you run npm authenticate
first. You can log in with any account, but make sure you don't put other people's emails in your database or you might end up sending them emails from your account.
-
package.json
- outlines the dependencies. -
captain-definition
- The backend runs on CapRover (captain.stuysu.org), so each application is a docker container, including the backend and the MySQL server it connects to. This file contains the Dockerfile lines (as a JSON array) required to create the docker container from the code that CapRover pulls from GitHub. TheDockerfile
file contains the same info. Read more about Dockerfiles here. -
src/
- the code for the API-
app.db
- the SQLite database that gets created if you use SQLite in your development -
index.js
- this is the entryway into the app. In production mode, it spawns several workers, one for each CPU core, so that the program can be multi-threaded. -
app.js
- this is the main app file. It creates an Express.JS app and adds the GraphQL middleware. It also initializes the tasks that are run on intervals (src/googleApis/gmailWatcher
andsrc/utils/createRecurringMeetings
). -
constants.js
- this contains the constants that the app uses. -
graphql/
- this folder contains all of the information and code behind the GraphQL API---this is the meat of the code-
index.js
- this is the main GraphQL file. It creates an Apollo server and defines the functions that get passed to resolvers, as well as error handling code and JWT code. -
schema/
- this folder contains all of the GraphQL code that defines how the API works-
index.js
- this file imports every other file in the folder and exports it in one neat package forsrc/graphql/index.js
-
Query.js
- this file contains all of the query operations you can make. -
Mutation.js
- this file contains all of the mutation operations you can make. - The rest of the files in this folder are GraphQL type definitions.
-
-
resolvers/
- this folder contains all of the JavaScript code that implements the schema in thesrc/graphql/schema
folder.-
index.js
- this file imports every other file in the folder and exports it in one neat package forsrc/graphql/index.js
-
Query/
- this folder contains all of the functions for query operations-
index.js
- this file imports every other file in the folder and exports it forsrc/graphql/resolvers/index.js
-
-
Mutation/
- this folder contains all of the functions for mutations-
index.js
- this file imports every other file in the folder and exports it forsrc/graphql/resolvers/index.js
-
login/
andlinkOAuthPlatform/
are sub-folders for functions that are split into multiple files (although usually we'd put these files into thesrc/utils
folder now)
-
- Every other folder in this directory has files that describe how to get certain properties of the type their folder is named after. For example, the
Meeting/
folder has a file calledorganization.js
which fetches the organization related to the meeting when someone queries for it.
-
-
-
database/
- this folder contains information on how to work with the database.-
models/
- this folder contains the database models. Each model corresponds to a database table, and describes how the table works. Most of these models are partially or completely automatically generated.-
index.js
- this file automatically imports every model in the directory and initializes the Sequelize object.
-
-
migrations/
- this folder contains migrations, which are changes made to the database (such as new tables or changes in table structure). Most of these are also automatically generated, although some are created by hand. These files are only run when you runnpm run migrate
. -
seeders/
- this folder contains files to "seed" the database by adding info to it. These files have not been updated and should not be used. If you need to seed the database, you can download the existing one and create a fake club for yourself to play around with. -
dataloaders/
- this folder contains basic methods for loading specific objects from the databse. They are used in the models.
-
-
middleware/
- this folder contains various middlewares that theapp.js
file uses. -
utils/
- this folder contains utilities that are either too long to be put into a file with the other code or that are used in multiple places. -
googleApis/
- this folder contains the files used for interacting with Google APIs. -
emailTemplates/
- this folder contains the email templates used in certain resolvers.
-
If you want to edit the live database directly, go to adminer.stuysu.org and use the credentials located in the stuyactivities-credentials
repository or in the environment variables of the mysql
app on CapRover. If you don't see the stuyactivities-credentials
repo, you might not have access to it. For adminer, use the url captain.stuysu.org
or srv-captain--mysql-db
, the database stuyactivities
, and the user stuyactivities
.
If you want to edit your local database, and you're using SQLite, run the command sqlite3 app.db
(the app.db
folder is located in src/
). Then, run any SQL commands you'd like. To make it easier to see, you can change the display format by using .mode
(e.g. .mode table
). You can also run commands like .schema
to get the schema of a table.
In order to create and edit tables, you need to use sequelize-cli to create migrations (it should be installed after you run npm i
). Each migration is a change to the database. The first command you need to run is a command that generates the model or migration.
NOTE: When you use sequelize-cli, make sure you do so in the base directory of the API, and not in any sub-directory.
To generate a model, use the command npx sequelize-cli model:generate
with the appropriate options. Just running npx sequelize-cli model:generate
will list the options, and the types required on the right. As the command will tell you, the only required options are name
and attributes
. Here is an example command with these options:
npx sequelize-cli model:generate --name rooms --attributes name:string,floor:integer,approvalRequired:boolean
You don't need to include an id
, createdAt
, or updatedAt
, as sequelize automatically includes those.
Once that command has been sucecssfully run, you can make any changes you need to the database model by editing the file in src/database/models/<table name>.js
. This includes any associations that you want to include. For example, the updates
model includes the following lines dictating its associations:
updates.belongsTo(models.organizations);
updates.belongsTo(models.users, { foreignKey: 'submittingUserId' });
updates.hasMany(models.updateApprovalMessage);
You should also add any data loaders in the model. Data loaders are efficient ways of finding certain rows in a table. There are a few generic data loaders in the src/database/dataloaders
folder that you might want to include.
- The
findOneLoader
is for finding one entry whose attribute matches the given attribute. For example, the linestatic orgIdLoader = findOneLoader(joinInstructions, 'organizationId');
creates an orgIdLoader such that callingjoinInstructions.orgIdLoader(orgId)
returns one joinInstruction with theorganizationId
attribute equal toorgId
. - The
findManyLoader
does the same thing but for multiple items. For example, the linestatic userIdLoader = findManyLoader(updates, 'submittingUserId');
creates a userIdLoader such that callingupdates.userIdLoader(userId)
returns an array of updates for which thesubmittingUserId
attribute is equal touserId
.
Tables are edited through migrations. To generate a migration, run npx sequelize migration:generate
with the name attribute. Again, make sure to run the command in the base directory of the API, so it puts the file in the right location.
Once you've done that, it will tell you that it's created a new file. Edit that file with the changes you want to make. This page has more information on what things you can do.
Once you're done with all of the changes, run npm migrate
to commit the migrations to the database. The backend will automatically migrate when the master branch is updated, so you don't have to worry about that.
Adding resolvers is a very common multi-step task.
The first thing you want to do is add your query/mutation to the schema. The schema is located in src/graphql/schema
. The most important files here are Query.js
and Mutation.js
, which is where your query or mutation is going to go. If you are adding a new GraphQL type, add a file defining that type into this folder as well. You can look at the GraphQL documentation and at other examples in the folder for more info. If you add a new type, make sure to put it in src/graphql/schema/index.js
.
Once you've added to the schema, you need to add a resolver. Your resolver will go in either src/graphql/resolvers/Query
or src/graphql/resolvers/Mutation
, depending on the type of resolver.
Your resolver will be a function that gets exported. The function can have 4 parameters but most (or all) of our resolvers only use 3.
- The first parameter is the object being requested. If you're writing a resolver for a query or mutation, you won't need to use this.
- The second parameter is the arguments to the query/mutation.
- The third parameter is the context, which includes a lot of things that are used in fulfilling the request:
-
models
is the database models that you can make requests out of -
authenticationRequired
is a function that throws an error if the user is not signed in. For all requests that need the user to be signed in, use this function. The error is returned to the caller. -
adminRoleRequired
is a function that takes an admin role and throws an error if the user isn't signed in or if they do not have that admin role. -
orgAdminRequired
is a function that takes an organization ID and throws an error if the user isn't an admin of the organization with that ID. -
isOrgAdmin
is a function that takes an organization ID and returns a boolean for whether or not the user is the admin of the organization with that ID. -
signedIn
is a boolean that represents whether or not the person is signed in -
user
is the user object of the user making the request. -
setCookie
is a function that sets a cookie (ports through express'res.cookie
) -
ipAddress
is the IP address of the request.
-
This is a lot of stuff, so in the resolvers we use deconstruction to get what we want. Here is an example from src/graphql/resolvers/Mutation/alterCharter.js
, where deconstruction is used to grab only what we need from the 3rd argument and later from the 2nd:
export default async (
parent,
args,
{
models: {
memberships,
organizations,
charterEdits,
charterApprovalMessages,
Sequelize: { Op }
},
authenticationRequired,
user,
orgAdminRequired
}
) => {
let { charter, orgId, force } = args;
authenticationRequired();
//...
}
The bulk of what a resolver does boils down to this:
- It grabs what it need from the args and the context
- It validates the input
- It manipulates the database and does other things (sending emails, changing google calendar stuff) that it needs to, assuming it hasn't thrown an error because of invalid input
- It returns the result
If you're editing a query, it can be as simple as grabbing the object you need like in resolvers/Query/charterById.js
:
export default (root, { id }, { models }) => models.charters.idLoader.load(id);
However, that gets more complicated for mutations, where changes have to be made to the database and more complex validation has to be done. For more info, see this page on performing queries. You may need to perform select queries to get related info (or for more thorough validation).
Here's an example insert query to get you started, from src/graphql/resolvers/Mutation/updateQuestion.js
:
return await updateQuestions.create({
updateId: update.id,
userId: user.id,
question,
private: false
});
This is returned because mutations usually return the object they create/mutate, or a boolean for when they remove/don't create objects.
Once you're done writing your resolver, make sure you add it to src/graphql/resolvers/<Query or Mutation>/index.js
.
Let's say that a user makes the following query:
query {
meetingById(id: 1) {
title
description
start
end
organization {
name
url
charter {
picture
}
}
}
}
This query is complex. Not everything is handled by one resolver, but not everything is done automatically either. Sometimes, some object properties require additional resolvers to get their info. In this case, a new folder is created in src/graphql/resolvers/
with the name being the name of the type. In this case, meetingById(id: String!)
returns a Meeting
type---the type is described in src/graphql/schema/Meeting.js
and the associated resolvers are in src/graphql/resolvers/Meeting/
. In this case, that folder includes resolvers for the description
, organization
, and rooms
properties. It also includes an index.js
file that exports an object containing all 3 resolvers. Don't forget to include this one.
The other thing to point out about these resolvers is that they use the first parameter of the function (as mentioned before), because when they are called the first parameter is set to the object whose property they were called to get. For example, following the query above, the organization
part calls src/graphql/resolvers/Meeting/organization.js
, and the first parameter of that call is the Meeting
that was grabbed. Then, the charter
part calls src/graphql/resolvers/Organiation/charter
, and the first parameter of that call is the Organization
that was grabbed.
If you create a new folder for a new type, make sure to add it into the src/graphql/resolvers/index.js
.