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](https://github.com/nodejs/node-gyp). 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. # File Structure Overview * `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. The `Dockerfile` file contains the same info. Read more about Dockerfiles [here](https://docs.docker.com/engine/reference/builder/). * `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` and `src/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 for `src/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 the `src/graphql/schema` folder. * `index.js` - this file imports every other file in the folder and exports it in one neat package for `src/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 for `src/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 for `src/graphql/resolvers/index.js` * `login/` and `linkOAuthPlatform/` are sub-folders for functions that are split into multiple files (although usually we'd put these files into the `src/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 called `organization.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 run `npm 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 the `app.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. # Common tasks ## Editing the Database If you want to edit the live database directly, go to [adminer.stuysu.org](https://adminer.stuysu.org) and use the credentials located in the `stuyactivities-credentials` repository or in the environment variables of the `mysql` app on [CapRover](https://captain.stuysu.org). 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. ## Creating and Editing Tables (Migrations) In order to create and edit tables, you need to use [sequelize-cli](https://github.com/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. ### Table Creation 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: ```sh 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/.js`. This includes any **associations** that you want to include. For example, the `updates` model includes the following lines dictating its associations: ```js 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 line `static orgIdLoader = findOneLoader(joinInstructions, 'organizationId');` creates an orgIdLoader such that calling `joinInstructions.orgIdLoader(orgId)` returns one joinInstruction with the `organizationId` attribute equal to `orgId`. * The `findManyLoader` does the same thing but for multiple items. For example, the line `static userIdLoader = findManyLoader(updates, 'submittingUserId');` creates a userIdLoader such that calling `updates.userIdLoader(userId)` returns an array of updates for which the `submittingUserId` attribute is equal to `userId`. ### Editing Tables 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](https://sequelize.org/master/manual/migrations.html) has more information on what things you can do. ### Committing Changes 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. ## Creating Queries and Mutations (Resolvers) Adding resolvers is a very common multi-step task. ### Editing the Schema 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`*. ### Creating a Resolver: Context 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](https://graphql.org/learn/execution/) 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: ```js export default async ( parent, args, { models: { memberships, organizations, charterEdits, charterApprovalMessages, Sequelize: { Op } }, authenticationRequired, user, orgAdminRequired } ) => { let { charter, orgId, force } = args; authenticationRequired(); //... } ``` ### Creating a Resolver: Dealing with the Database 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`: ```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](https://sequelize.org/master/manual/model-querying-basics.html) 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`: ```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//index.js`*. ### Making a New (Complex) Type Let's say that a user makes the following query: ```gql 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`*.