- Languages
- Environnement
- Dependency Installation
- Useful commands
- Api Documentation
- Folder Organisation
- How to create a new Service
- How to create an action
- How to create a reaction
Name | Description | Type | Default Value |
---|---|---|---|
DATABASE_URL | Database url format | string |
No default value |
PORT | Backend Port | string |
8080 |
SECRET | Password cripting secret | string |
No default value |
CLIENT_PORT | Port of the client | string |
8081 |
YOUTUBE_API_TOKEN | Youtube api token | string |
No default value |
GOOGLE_CLIENT_SECRET | Google client secret | string |
No default value |
GOOGLE_CLIENT_ID | Google client id | string |
No default value |
GOOGLE_REDIRECT_URL | Google redirect url | string |
No default value |
SPOTIFY_CLIENT_SECRET | Spotify client secret | string |
No default value |
SPOTIFY_CLIENT_ID | Spotify client id | string |
No default value |
SPOTIFY_REDIRECT_URL | Spotify redirect url | string |
No default value |
GITHUB_CLIENT_ID | Github client id | string |
No default value |
GITHUB_REDIRECT_URL | Github redirect url | string |
No default value |
DISCORD_BOT_TOKEN | Discord bot token | string |
No default value |
DISCORD_CLIENT_ID | Discord client id | string |
No default value |
DISCORD_CLIENT_SECRET | Discord client secret | string |
No default value |
DISCORD_REDIRECT_URL | Discord redirect url | string |
No default value |
npm i
npm run db:migrate # Init database
npm start # Launch the project in dev
npm run start:local # Launch the project in watch mode
npm build && node dist/src/index.js # Build the project and launch it
https://app.swaggerhub.com/apis-docs/ludovic-str/AREA/1.0.0
Project API endpoints
Example :
instance.get("/", async (req: FastifyRequest, res: FastifyReply) => {
const users = await UserService.getAllUsers();
res.status(httpStatus.OK).send(users);
});
Functions that interact with the database mostly called in route files.
Example:
const getAllUsers = async (): Promise<User[]> => {
return await prisma.user.findMany();
};
Constants which don't need to be in .env here service list
Example:
const SERVICES: Service[] = [
{
id: 1,
serviceName: "Youtube",
imageUrl: "https://www.iconsdb.com/icons/preview/white/youtube-6-xxl.png",
backgroundColor: "#FF0000",
actions: [
{
id: 1,
actionName: "NewVideoUploaded",
actionParamName: "Channel Name",
fct: YoutubeActions.checkUploadedVideo,
},
{
id: 2,
actionName: "NewLikeOnAVideo",
actionParamName: "Video Id",
fct: YoutubeActions.checkVideoLike,
},
],
reactions: [],
},
];
Files linked to Action and Reaction: loop.area.ts contain the loop of launching action and reaction if action return true. Other folders are here to store actions and reactions.
Exemple Reaction:
const createGithubIssue = async (reactionParam: string, userId: number) => {
const githubClient = await ServiceHelper.getGithubClient(userId);
if (!githubClient) return;
const issueParam = ServiceHelper.getGithubIssueParams(reactionParam);
if (!issueParam) return;
await githubClient.rest.issues.create({
owner: issueParam.owner,
repo: issueParam.repo,
title: issueParam.title,
});
};
Groups of utility functions for a given part of the program
Example:
const getActionFct = (actionServiceId: number, actionId: number) => {
const actionService = SERVICES.find(
service => service.id === actionServiceId,
);
if (actionService === undefined) return null;
const action = actionService.actions.find(action => action.id === actionId);
return action?.fct || null;
};
Project typescript interfaces and types
interface Action {
id: number;
actionName: string;
actionParamName: string;
fct: (area: Area) => Promise<string | null>;
}
Functions that are executed before launching route function
Usage exemple:
instance.get(
"/me",
{ onRequest: [authentificationMiddleware()] },
async (req: FastifyRequest, res: FastifyReply) => {
const userInfos = SecurityHelper.getUserInfos(req);
const user = await UserService.getOneUser(userInfos.id);
res.status(httpStatus.OK).send(user);
},
);
Body schemas for API endpoints
Example:
const loginBodySchema: JSONSchemaType<LoginBody> = {
type: "object",
properties: {
email: { type: "string" },
password: { type: "string" },
},
required: ["email", "password"],
additionalProperties: false,
};
-
Generate your client id and client secret on the service website in developer part (ex: for google https://cloud.google.com) and the redirect url which the path on which you want to redirect on the front
-
Add your credentials to .env and so in src/env.ts like this:
clientId: process.env.CLIENT_ID as string,
clientSecret: process.env.CLIENT_SECRET as string,
redirectUrl: process.env.REDIRECT_URL as string
- Add token field in database schema in prisma/schema.prisma like this:
serviceToken String?
Then run the following command
npm run db:migrate
- Create a service to save your token in the database like this:
const setServiceNameToken = async (
userId: number,
token: string | undefined | null,
): Promise<TokensTable> => {
if (!token) {
throw new ClientError({
name: "Invalid Token",
message: "Given token is invalid",
level: "warm",
status: httpStatus.BAD_REQUEST,
});
}
const tokensTableExist = await prisma.tokensTable.findUnique({
where: { userId },
});
if (!tokensTableExist) {
// this part has for goal to check if the user exist
throw new ClientError({
name: "Invalid Credential",
message: "UserId does not exist",
level: "warm",
status: httpStatus.BAD_REQUEST,
});
}
// this part set the token in the database
const tokenTable = await prisma.tokensTable.update({
where: {
userId,
},
data: {
serviceToken: token,
},
});
return tokenTable;
};
- Add oauth service name in OauthService type in src/types/areaServices/areaServices.types.ts. Create oauth routes in src/routes/oauth.route.ts, one for sending the oauth link to the front like this:
instance.get("/serviceName/link", (req: FastifyRequest, res: FastifyReply) => {
const rootUrl = "https://serviceName.com/api/oauth2/authorize"; // The base services oauth url are usually in this format but not always
const option = {
client_id: ENV.ClientId, // client id previously set
response_type: "code",
redirect_uri: ENV.redirectUrl, // redirect url previously set
scope: [
// scope you want to access to on the application, specific scope are documented on the service website
"identify",
"email",
"guilds",
"connections",
"bot",
"guilds.join",
].join(" "),
};
const qs = new URLSearchParams(option);
res.status(httpStatus.OK).send(`${rootUrl}?${qs.toString()}`);
});
and one to save the route in your database like this:
instance.post(
"/serviceName",
{ onRequest: [authentificationMiddleware()] },
async (req: BaseOauthBody, res: FastifyReply) => {
if (!googleOauthQueryValidator(req.body)) ErrorHelper.throwBodyError();
const userInfos = SecurityHelper.getUserInfos(req);
const code = req.body.code;
// This part is an example with google API using the google npm library, some service have an oauth npm library and one some other you have to make a request on a specific endpoint
const oauthClient = new google.auth.OAuth2(
ENV.googleClientId,
ENV.googleClientSecret,
ENV.googleRedirectUrl,
);
const tokens = (await oauthClient.getToken(code)).tokens;
const tokenTable = await TokenService.setGoogleToken(
userInfos.id,
tokens.refresh_token,
);
res.status(httpStatus.OK).send(tokenTable);
},
);
- Add service name at ServiceName type in src/types/areaServices/areaServices, the service logo in assets/types.ts and then add the service to src/constants/serviceList.ts with the following format:
id: 1,
serviceName: "serviceName",
backgroundColor: "#DE5145", // background color to be displayed in the front
imageUrl: "assets/serviceName.png", // path to image to be displayed in the front
oauthName: "serviceName", // name of the oauth service
actions: []
reactions: []
- Add service name at ServiceName type in src/types/areaServices/areaServices, the service logo in assets/types.ts and then add the service to src/constants/serviceList.ts with the following format:
id: 1,
serviceName: "serviceName",
backgroundColor: "#DE5145", // background color to be displayed in the front
imageUrl: "assets/serviceName.png", // image to be displayed in the front
oauthName: null, // name of the oauth service
actions: []
reactions: []
-
Create a folder with your serviceName if it doesn't exist
-
Create a file named serviceName.action.ts
-
Create a function in src/helpers/service.helpers.ts to get your oauth client or your service credentials. Here is an example with google:
onst getGoogleOauthClient = async (
userId: number,
): Promise<OAuth2Client | null> => {
const refreshToken = await TokenService.getGoogleToken(userId); // get token from database
if (!refreshToken) return null;
const oAuth2Client = new google.auth.OAuth2(
ENV.googleClientId,
ENV.googleClientSecret,
ENV.googleRedirectUrl,
); // configure google oauth client
oAuth2Client.setCredentials({ refresh_token: refreshToken }); // set token on google oauth client
return oAuth2Client;
};
- Create a fonction prototyped as follow:
(area: Area) => Promise<string | null>;
-
Use the helper function you previously created or the one that already exist to get your credential if your service needs oauth
-
Use your service API endpoint or client to get the information you want. If any information you get is invalid return null
-
If it's the first time you pass in the function you have to set the value you want to compare with in futures action check. This can be done like this:
if (area.lastActionValue === null) {
await AreaService.updateAreaValues(area.id, value);
return null;
}
- You can define a params object which contains data available to be injected in the reaction paramater by a user Here is a params object example:
const params = {
like: statistics.likeCount,
viewCount: statistics.viewCount,
};
- For all the other time you pass into this function you will have to check if the default value you've set has changed. Here is an example with params:
if (value > area.lastActionValue) {
await AreaService.updateAreaValues(area.id, value);
return ServiceHelper.injectParamInReaction<typeof params>(
area.reactionParam,
params,
);
}
Here is an example without params:
if (value > area.lastActionValue) {
await AreaService.updateAreaValues(area.id, value);
return area.reactionParam;
}
- Finally, in the file src/constants/serviceList.ts add your action to actions array in your service in this format:
{
id: 1,
actionName: "New file in liked video", // Name of your action
actionParamName: "", // Param that your action can take
paramFormat: null, // if their is a format that your param have to match
description: "New file in your drive", // A short description of your action
fct: DriveAction.newLikedVideo, // The function of your action
availableInjectParams: ["like"], // params which can be injected in the reaction, if their is not it will be empty
}
-
Create a folder with your serviceName if it doesn't exist
-
Create a file named serviceName.reaction.ts
-
Create a function in src/helpers/service.helpers.ts to get your oauth client or your service credentials. Here is an example with google:
onst getGoogleOauthClient = async (
userId: number,
): Promise<OAuth2Client | null> => {
const refreshToken = await TokenService.getGoogleToken(userId); // get token from database
if (!refreshToken) return null;
const oAuth2Client = new google.auth.OAuth2(
ENV.googleClientId,
ENV.googleClientSecret,
ENV.googleRedirectUrl,
); // configure google oauth client
oAuth2Client.setCredentials({ refresh_token: refreshToken }); // set token on google oauth client
return oAuth2Client;
};
- Create a fonction prototyped as follow:
(reactionParam: string, userId: number) => Promise<void>;
-
Use the helper function you previously created or the one that already exist to get your credential if your service need oaut
-
Use your service API endpoint or execute the reaction you want. If there is any problem with the data you get simply return.
-
Finally, in the file src/constants/serviceList.ts add your reaction to reactions array in your service in this format:
{
id: 1,
reactionName: "Create an issue", // Reaction name
reactionParamName: "Issue infos (format: /owner/repo/issueTitle)", // Reaction param, format can be precised in it
paramFormat: FORMAT.githubIssueFormat, // Reaction format check regex, can be null if there isn't
fct: GithubReaction.createGithubIssue, // Reaction Function
description:
"Create an issue on a public repository or a private repository on which you belong", // A short description of your reaction
},