feat: add db migrations with typeORM (#28)

* adding typescript packages for typeORM

* add typeORM initial files

* updating package scripts

* updating compose.yaml to have an exposed port for the postgres

* modifying setup for typeORM

* update database stuff and and package.json to help generate migrations

* made models and migrations in typeORM

* delete unneeded database.js

* made database pattern ignored by jest

* remove sequelize

* separate member repo from member helper

* not sure why i made everything numbers in the model but it's fixed now

* edited package.json script

* remove unused index.ts

* adjusted files to reference repository correctly and appdatasource

* made appdatasource export as named

* removed start-db script

* added init to appdatasource in bot.js

* migrations finally!

* new migration matching model names I want

* updating tests

* removing testpathignore patterns since it seems to be unecessary?

* adjusting migrations to match current schema

* removed reference to secrets file

* delete old migration

* Revert "delete old migration"

This reverts commit db1efa39a7a80d8976878856250ccaac6a753ab2.

* Revert "adjusting migrations to match current schema"

This reverts commit ef89a83f6a2ef0643d6ace0a3fcf9c40f4bc6dd6.

* just deleted system creation since it's got nothing in it anyway

* renamed memberRepository to memberRepo for consistency

* added await back to parseMemberCommand call to memberArgumentHandler

* changed call to memberHelper.getMembersByAuthor to memberRepo

* renamed repo updateMemberValue to updateMemberField

* removed throw references in repo docstrings

* remove unneeded subscriber directory ref

* changed createdAt and updatedAt columns to be auto-generated

made member table have timezone

* changed casing of isInitialized in mock for bot.js

* removed % from ILike query so that it doesn't match substrings/wildcard

* renamed some stray updateMemberValue in mocks -> updateMemberField

---------

Co-authored-by: Aster Fialla <asterfialla@gmail.com>
This commit is contained in:
2026-03-01 21:25:49 -05:00
committed by GitHub
parent d14e89e8b2
commit 8fe53563d0
14 changed files with 1831 additions and 637 deletions

7
.gitignore vendored
View File

@@ -1,8 +1,11 @@
node_modules node_modules/
build/
tmp/
temp/
.idea .idea
secrets/ secrets/
config.json
coverage coverage
config.json
log.txt log.txt
.env .env
oya.png oya.png

View File

@@ -10,6 +10,8 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql - pgdata:/var/lib/postgresql
- ./pgBackup:/mnt/pgBackup - ./pgBackup:/mnt/pgBackup
ports:
- "5432:5432"
pgadmin: pgadmin:
image: dpage/pgadmin4:latest image: dpage/pgadmin4:latest
ports: ports:
@@ -19,9 +21,5 @@ services:
- postgres - postgres
volumes: volumes:
- pgadmindata:/var/lib/pgadmin - pgadmindata:/var/lib/pgadmin
#- ./pgBackup:/mnt/host
# uncomment the above line if you plan to restore / backup dump files from PGAdmin UI
volumes: volumes:
pgdata: pgdata:
pgadmindata:

26
database/data-source.ts Normal file
View File

@@ -0,0 +1,26 @@
import "reflect-metadata"
import { DataSource } from "typeorm"
import * as env from 'dotenv';
import * as path from "path";
env.config();
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "postgres",
synchronize: false,
logging: true,
entities: [path.join(__dirname, "./entity/*.{ts,js}")],
migrations: [path.join(__dirname, "./migrations/*.{ts,js}")],
migrationsRun: true,
migrationsTableName: 'migrations',
migrationsTransactionMode: 'all',
invalidWhereValuesBehavior: {
null: "sql-null",
undefined: "throw",
},
});

39
database/entity/Member.ts Normal file
View File

@@ -0,0 +1,39 @@
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from "typeorm"
@Entity({name: "Member", synchronize: true})
export class Member {
@PrimaryGeneratedColumn()
id: number
@Column()
userid: string
@Column({
length: 100
})
name: string
@Column({
type: "varchar",
nullable: true,
length: 100
})
displayname: string
@Column({
nullable: true,
})
proxy: string
@Column({
nullable: true,
})
propic: string
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Update1772417745487 implements MigrationInterface {
name = 'Update1772417745487'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "Member" ("id" SERIAL NOT NULL, "userid" character varying NOT NULL, "name" character varying(100) NOT NULL, "displayname" character varying(100), "proxy" character varying, "propic" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_235428a1d87c5f639ef7b7cf170" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "Member"`);
}
}

1957
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,22 +12,31 @@
"dependencies": { "dependencies": {
"@fluxerjs/core": "^1.2.2", "@fluxerjs/core": "^1.2.2",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"pg": "^8.18.0", "pg": "^8.19.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"pm2": "^6.0.14", "pm2": "^6.0.14",
"sequelize": "^6.37.7", "psql": "^0.0.1",
"tmp": "^0.2.5" "reflect-metadata": "^0.2.2",
"tmp": "^0.2.5",
"typeorm": "^0.3.28"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0", "@babel/core": "^7.29.0",
"@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/preset-env": "^7.29.0", "@babel/preset-env": "^7.29.0",
"@fetch-mock/jest": "^0.2.20", "@fetch-mock/jest": "^0.2.20",
"@types/node": "^25.3.3",
"babel-jest": "^30.2.0", "babel-jest": "^30.2.0",
"fetch-mock": "^12.6.0", "fetch-mock": "^12.6.0",
"jest": "^30.2.0" "jest": "^30.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}, },
"scripts": { "scripts": {
"test": "jest" "test": "jest",
"start": "ts-node src/bot.js",
"build-db": "tsc",
"generate-db": "typeorm-ts-node-commonjs migration:generate -d database/data-source.ts database/migrations/update",
"run-migration": "typeorm-ts-node-commonjs migration:run -d database/data-source.ts"
} }
} }

View File

@@ -5,6 +5,7 @@ const {commands} = require("./commands.js");
const {webhookHelper} = require("./helpers/webhookHelper.js"); const {webhookHelper} = require("./helpers/webhookHelper.js");
const env = require('dotenv'); const env = require('dotenv');
const {utils} = require("./helpers/utils.js"); const {utils} = require("./helpers/utils.js");
const { AppDataSource } = require("../database/data-source");
env.config(); env.config();
@@ -20,7 +21,7 @@ client = new Client({ intents: 0 });
module.exports.client = client; module.exports.client = client;
client.on(Events.MessageCreate, async (message) => { client.on(Events.MessageCreate, async (message) => {
await handleMessageCreate(message); await module.exports.handleMessageCreate(message);
}); });
/** /**
@@ -85,8 +86,10 @@ const debouncePrintGuilds = utils.debounce(printGuilds, 2000);
module.exports.login = async function() { module.exports.login = async function() {
try { try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
await client.login(token); await client.login(token);
// await db.check_connection();
} catch (err) { } catch (err) {
console.error('Login failed:', err); console.error('Login failed:', err);
process.exit(1); process.exit(1);

View File

@@ -1,86 +0,0 @@
const env = require('dotenv')
const {Sequelize, DataTypes} = require('sequelize');
env.config();
const password = process.env.POSTGRES_PASSWORD;
if (!password) {
console.error("Missing POSTGRES_PASSWORD environment variable.");
process.exit(1);
}
const database = {};
const sequelize = new Sequelize('postgres', 'postgres', password, {
host: 'localhost',
logging: false,
dialect: 'postgres'
});
database.sequelize = sequelize;
database.Sequelize = Sequelize;
database.members = sequelize.define('Member', {
userid: {
type: DataTypes.STRING,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
displayname: {
type: DataTypes.STRING,
},
propic: {
type: DataTypes.STRING,
},
proxy: {
type: DataTypes.STRING,
}
});
database.systems = sequelize.define('System', {
userid: {
type: DataTypes.STRING,
},
fronter: {
type: DataTypes.STRING
},
grouptag: {
type: DataTypes.STRING
},
autoproxy: {
type: DataTypes.BOOLEAN,
}
})
/**
* Checks Sequelize database connection.
*/
database.check_connection = async function () {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
await syncModels();
} catch (err) {
console.error('Unable to connect to the database:', err);
process.exit(1);
}
}
/**
* Syncs Sequelize models.
*/
async function syncModels() {
try {
await sequelize.sync()
console.log('Models synced successfully.');
} catch(err) {
console.error('Syncing models did not work', err);
process.exit(1);
}
}
module.exports.database = database;

View File

@@ -1,8 +1,7 @@
const {database} = require('../database.js');
const {enums} = require("../enums.js"); const {enums} = require("../enums.js");
const {Op} = require("sequelize");
const {EmbedBuilder} = require("@fluxerjs/core"); const {EmbedBuilder} = require("@fluxerjs/core");
const {utils} = require("./utils.js"); const {utils} = require("./utils.js");
const {memberRepo} = require("../repositories/memberRepo.js");
const memberHelper = {}; const memberHelper = {};
@@ -52,7 +51,7 @@ memberHelper.parseMemberCommand = async function (authorId, authorFull, args, at
isHelp = true; isHelp = true;
} }
return await memberHelper.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration) return await memberHelper.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration);
} }
/** /**
@@ -71,6 +70,7 @@ memberHelper.parseMemberCommand = async function (authorId, authorFull, args, at
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed. * @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions. * @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors. * @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors.
* @returns {Promise<string>} - A help message
* @throws {Error} When there's no member or a command is not recognized. * @throws {Error} When there's no member or a command is not recognized.
*/ */
memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) { memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) {
@@ -113,7 +113,7 @@ memberHelper.memberArgumentHandler = async function(authorId, authorFull, isHelp
* @throws {Error} When there's no member * @throws {Error} When there's no member
*/ */
memberHelper.sendCurrentValue = async function(authorId, memberName, command= null) { memberHelper.sendCurrentValue = async function(authorId, memberName, command= null) {
const member = await memberHelper.getMemberByName(authorId, memberName); const member = await memberRepo.getMemberByName(authorId, memberName);
if (!member) throw new Error(enums.err.NO_MEMBER); if (!member) throw new Error(enums.err.NO_MEMBER);
if (!command) { if (!command) {
@@ -167,10 +167,7 @@ memberHelper.sendHelpEnum = function(command) {
* @param {string[]} values - The values to be passed in. Only includes the values after member name and command name. * @param {string[]} values - The values to be passed in. Only includes the values after member name and command name.
* @param {string | null} attachmentUrl - The attachment URL, if any * @param {string | null} attachmentUrl - The attachment URL, if any
* @param {string | null} attachmentExpiration - The attachment expiry date, if any * @param {string | null} attachmentExpiration - The attachment expiry date, if any
* @returns {Promise<string>} A success message. * @returns {Promise<string> | Promise <EmbedBuilder> | Promise<{EmbedBuilder, [string], string}>}
* @returns {Promise <EmbedBuilder>} A list of 25 members as an embed.
* @returns {Promise <EmbedBuilder>} A list of member commands and descriptions.
* @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors.
*/ */
memberHelper.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) { memberHelper.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) {
switch (command) { switch (command) {
@@ -297,12 +294,7 @@ memberHelper.updatePropic = async function (authorId, memberName, values, attach
* @throws {Error} When there is no member to remove. * @throws {Error} When there is no member to remove.
*/ */
memberHelper.removeMember = async function (authorId, memberName) { memberHelper.removeMember = async function (authorId, memberName) {
const destroyed = await database.members.destroy({ const destroyed = await memberRepo.removeMember(authorId, memberName);
where: {
name: {[Op.iLike]: memberName},
userid: authorId
}
})
if (destroyed > 0) { if (destroyed > 0) {
return `Member "${memberName}" has been deleted.`; return `Member "${memberName}" has been deleted.`;
} else { } else {
@@ -322,11 +314,11 @@ memberHelper.removeMember = async function (authorId, memberName) {
* @param {string | null} [proxy] - The proxy tag of the member. * @param {string | null} [proxy] - The proxy tag of the member.
* @param {string | null} [propic] - The profile picture URL of the member. * @param {string | null} [propic] - The profile picture URL of the member.
* @param {string | null} [attachmentExpiration] - The expiration date of an uploaded profile picture. * @param {string | null} [attachmentExpiration] - The expiration date of an uploaded profile picture.
* @returns {Promise<{model, string[]}>} A successful addition object, including errors if there are any. * @returns {Promise<{Members, string[]}>} A successful addition object, including errors if there are any.
* @throws {Error} When the member already exists, there are validation errors, or adding a member doesn't work. * @throws {Error} When the member already exists, there are validation errors, or adding a member doesn't work.
*/ */
memberHelper.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) { memberHelper.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) {
const existingMember = await memberHelper.getMemberByName(authorId, memberName); const existingMember = await memberRepo.getMemberByName(authorId, memberName);
if (existingMember) { if (existingMember) {
throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`);
} }
@@ -380,7 +372,7 @@ memberHelper.addFullMember = async function (authorId, memberName, displayName =
} }
} }
const member = await database.members.create({ const member = await memberRepo.createMember({
name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null
}); });
@@ -400,13 +392,8 @@ memberHelper.addFullMember = async function (authorId, memberName, displayName =
* @throws {Error} When no member row was updated. * @throws {Error} When no member row was updated.
*/ */
memberHelper.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) { memberHelper.updateMemberField = async function (authorId, memberName, columnName, value, expirationWarning = null) {
const res = await database.members.update({[columnName]: value}, { const res = await memberRepo.updateMemberField(authorId, memberName, columnName, value);
where: { if (res === 0) {
name: {[Op.iLike]: memberName},
userid: authorId
}
})
if (res[0] === 0) {
throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`); throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`);
} else { } else {
return `Updated ${columnName} for ${memberName} to ${value}${expirationWarning ? `. ${expirationWarning}.` : '.'}`; return `Updated ${columnName} for ${memberName} to ${value}${expirationWarning ? `. ${expirationWarning}.` : '.'}`;
@@ -416,7 +403,7 @@ memberHelper.updateMemberField = async function (authorId, memberName, columnNam
/** /**
* Gets the details for a member. * Gets the details for a member.
* *
* @param {model} member - The member object * @param {{Members, string[]}} member - The member object
* @returns {EmbedBuilder} The member's info. * @returns {EmbedBuilder} The member's info.
*/ */
memberHelper.getMemberInfo = function (member) { memberHelper.getMemberInfo = function (member) {
@@ -441,7 +428,7 @@ memberHelper.getMemberInfo = function (member) {
* @throws {Error} When there are no members for an author. * @throws {Error} When there are no members for an author.
*/ */
memberHelper.getAllMembersInfo = async function (authorId, authorName) { memberHelper.getAllMembersInfo = async function (authorId, authorName) {
const members = await memberHelper.getMembersByAuthor(authorId); const members = await memberRepo.getMembersByAuthor(authorId);
if (members.length === 0) throw Error(enums.err.USER_NO_MEMBERS); if (members.length === 0) throw Error(enums.err.USER_NO_MEMBERS);
const fields = [...members.entries()].map(([index, member]) => ({ const fields = [...members.entries()].map(([index, member]) => ({
name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true, name: member.name, value: `(Proxy: \`${member.proxy ?? "unset"}\`)`, inline: true,
@@ -451,29 +438,6 @@ memberHelper.getAllMembersInfo = async function (authorId, authorName) {
.addFields(...fields); .addFields(...fields);
} }
/**
* Gets a member based on the author and proxy tag.
*
* @async
* @param {string} authorId - The author of the message.
* @param {string} memberName - The member's name.
* @returns {Promise<model>} The member object.
*/
memberHelper.getMemberByName = async function (authorId, memberName) {
return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}});
}
/**
* Gets all members belonging to the author.
*
* @async
* @param {string} authorId - The author of the message
* @returns {Promise<model[] | null>} The member object array.
*/
memberHelper.getMembersByAuthor = async function (authorId) {
return await database.members.findAll({where: {userid: authorId}});
}
/** /**
* Checks if proxy exists for a member. * Checks if proxy exists for a member.
* *
@@ -487,7 +451,7 @@ memberHelper.checkIfProxyExists = async function (authorId, proxy) {
if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY);
if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER);
const memberList = await memberHelper.getMembersByAuthor(authorId); const memberList = await memberRepo.getMembersByAuthor(authorId);
const proxyExists = memberList.some(member => member.proxy === proxy); const proxyExists = memberList.some(member => member.proxy === proxy);
if (proxyExists) { if (proxyExists) {
throw new Error(enums.err.PROXY_EXISTS); throw new Error(enums.err.PROXY_EXISTS);

View File

@@ -0,0 +1,81 @@
const Member = require("../../database/entity/Member");
const { AppDataSource } = require("../../database/data-source");
const {ILike} = require("typeorm");
const members = AppDataSource.getRepository(Member.Member)
const memberRepo = {};
/**
* Gets a member based on the author and proxy tag.
*
* @async
* @param {string} authorId - The author of the message.
* @param {string} memberName - The member's name.
* @returns {Promise<Member | null>} The member object or null if not found.
*/
memberRepo.getMemberByName = async function (authorId, memberName) {
return await members.findOne({where: {userid: authorId, name: ILike(memberName)}});
}
/**
* Gets all members belonging to the author.
*
* @async
* @param {string} authorId - The author of the message
* @returns {Promise<Member[]>} The member object array.
*/
memberRepo.getMembersByAuthor = async function (authorId) {
return await members.findBy({userid: authorId});
}
/**
* Removes a member.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The name of the member to remove
* @returns {Promise<number>} Number of results removed.
*/
memberRepo.removeMember = async function (authorId, memberName) {
const deleted = await members.delete({
where: {
name: ILike(memberName),
userid: authorId
}
})
return deleted.affected;
}
/**
* Adds a member with full details.
*
* @async
* @param {{name: string, userid: string, displayname: (string|null), proxy: (string|null), propic: (string|null)}} createObj - Object with parameters in it
* @returns {Promise<Member>} A successful inserted object.
*/
memberRepo.createMember = async function (createObj) {
return members.insert({
name: createObj.name, userid: createObj.authorId, displayname: createObj.displayName, proxy: createObj.proxy, propic: createObj.propic
});
}
/**
* Updates one fields for a member in the database.
*
* @async
* @param {string} authorId - The author of the message
* @param {string} memberName - The member to update
* @param {string} columnName - The column name to update.
* @param {string} value - The value to update to.
* @returns {Promise<number>} A successful update.
*/
memberRepo.updateMemberField = async function (authorId, memberName, columnName, value) {
const updated = await members.update({[columnName]: value}, {
where: {
name: ILike(memberName),
userid: authorId
}
})
return updated.affected;
}
module.exports.memberRepo = memberRepo;

View File

@@ -56,6 +56,15 @@ jest.mock("../src/commands.js", () => {
} }
}) })
jest.mock('../database/data-source.ts', () => {
return {
AppDataSource: {
isInitialized: false,
initialize: jest.fn().mockResolvedValue()
}
}
})
const {Client, Events} = require('@fluxerjs/core'); const {Client, Events} = require('@fluxerjs/core');
const {messageHelper} = require("../src/helpers/messageHelper.js"); const {messageHelper} = require("../src/helpers/messageHelper.js");

View File

@@ -2,16 +2,14 @@ const {enums} = require('../../src/enums.js');
const {utils} = require("../../src/helpers/utils.js"); const {utils} = require("../../src/helpers/utils.js");
jest.mock('@fluxerjs/core', () => jest.fn()); jest.mock('@fluxerjs/core', () => jest.fn());
jest.mock('../../src/database.js', () => { jest.mock('../../src/repositories/memberRepo.js', () => {
return { return {
database: { memberRepo: {
members: { getMemberByName: jest.fn().mockResolvedValue(),
create: jest.fn().mockResolvedValue(), getMembersByAuthor: jest.fn().mockResolvedValue(),
update: jest.fn().mockResolvedValue(), removeMember: jest.fn().mockResolvedValue(),
destroy: jest.fn().mockResolvedValue(), createMember: jest.fn().mockResolvedValue(),
findOne: jest.fn().mockResolvedValue(), updateMemberField: jest.fn().mockResolvedValue(),
findAll: jest.fn().mockResolvedValue(),
}
} }
} }
}); });
@@ -25,10 +23,8 @@ jest.mock("../../src/helpers/utils.js", () => {
} }
}); });
const {Op} = require('sequelize');
const {memberHelper} = require("../../src/helpers/memberHelper.js"); const {memberHelper} = require("../../src/helpers/memberHelper.js");
const {database} = require("../../src/database"); const {memberRepo} = require("../../src/repositories/memberRepo.js");
describe('MemberHelper', () => { describe('MemberHelper', () => {
const authorId = "0001"; const authorId = "0001";
@@ -270,29 +266,29 @@ describe('MemberHelper', () => {
['propic', `The profile picture for ${mockMember.name} is \"${mockMember.propic}\".`], ['propic', `The profile picture for ${mockMember.name} is \"${mockMember.propic}\".`],
])('%s calls getMemberByName and returns value', async (command, expected) => { ])('%s calls getMemberByName and returns value', async (command, expected) => {
// Arrange // Arrange
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember); memberRepo.getMemberByName.mockResolvedValue(mockMember);
// Act // Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command); const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
// Assert // Assert
expect(result).toEqual(expected); expect(result).toEqual(expected);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name); expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
}) })
test('returns error if no member found', async () => { test('returns error if no member found', async () => {
// Arrange // Arrange
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(null); memberRepo.getMemberByName.mockResolvedValue(null);
// Act // Act
await expect(memberHelper.sendCurrentValue(authorId, mockMember.name, 'name')).rejects.toThrow(enums.err.NO_MEMBER); await expect(memberHelper.sendCurrentValue(authorId, mockMember.name, 'name')).rejects.toThrow(enums.err.NO_MEMBER);
// Assert // Assert
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name); expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
}); });
test('calls getMemberInfo with member if no command present', async () => { test('calls getMemberInfo with member if no command present', async () => {
// Arrange // Arrange
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember); memberRepo.getMemberByName.mockResolvedValue(mockMember);
jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info'); jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info');
// Act // Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, null); const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, null);
@@ -309,13 +305,13 @@ describe('MemberHelper', () => {
])('returns null message if no value found', async (command, expected) => { ])('returns null message if no value found', async (command, expected) => {
// Arrange // Arrange
const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null} const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null}
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(empty); memberRepo.getMemberByName.mockResolvedValue(empty);
// Act // Act
const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command); const result = await memberHelper.sendCurrentValue(authorId, mockMember.name, command);
// Assert // Assert
expect(result).toEqual(expected); expect(result).toEqual(expected);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name); expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
}) })
}) })
@@ -479,39 +475,38 @@ describe('MemberHelper', () => {
}) })
describe('addFullMember', () => { describe('addFullMember', () => {
const {database} = require('../../src/database.js');
beforeEach(() => {
jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue();
})
test('calls getMemberByName', async () => { test('calls getMemberByName', async () => {
// Arrange
memberRepo.getMemberByName.mockResolvedValue();
// Act // Act
await memberHelper.addFullMember(authorId, mockMember.name) await memberHelper.addFullMember(authorId, mockMember.name)
// Assert // Assert
expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name); expect(memberRepo.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name);
expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); expect(memberRepo.getMemberByName).toHaveBeenCalledTimes(1);
}) })
test('if getMemberByName returns member, throw error', async () => { test('if getMemberByName returns member, throw error', async () => {
// Arrange // Arrange
memberHelper.getMemberByName.mockResolvedValue({name: mockMember.name}); memberRepo.getMemberByName.mockResolvedValue({name: mockMember.name});
// Act & Assert // Act & Assert
await expect(memberHelper.addFullMember(authorId, mockMember.name)).rejects.toThrow(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`) await expect(memberHelper.addFullMember(authorId, mockMember.name)).rejects.toThrow(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`)
// Assert // Assert
expect(database.members.create).not.toHaveBeenCalled(); expect(memberRepo.createMember).not.toHaveBeenCalled();
}) })
test('if name is not filled out, throw error', async () => { test('if name is not filled out, throw error', async () => {
// Act & Assert // Arrange
memberRepo.getMemberByName.mockResolvedValue();
// Act
await expect(memberHelper.addFullMember(authorId, " ")).rejects.toThrow(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`); await expect(memberHelper.addFullMember(authorId, " ")).rejects.toThrow(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`);
// Assert // Assert
expect(database.members.create).not.toHaveBeenCalled(); expect(memberRepo.createMember).not.toHaveBeenCalled();
}) })
test('if displayname is over 32 characters, call database.member.create with null value', async () => { test('if displayname is over 32 characters, call memberRepo.createMember with null value', async () => {
// Arrange // Arrange
memberHelper.getMemberByName.mockResolvedValue(); memberRepo.getMemberByName.mockResolvedValue();
const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const expectedMemberArgs = { const expectedMemberArgs = {
name: mockMember.name, name: mockMember.name,
@@ -520,7 +515,7 @@ describe('MemberHelper', () => {
proxy: null, proxy: null,
propic: null propic: null
} }
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = { const expectedReturn = {
member: expectedMemberArgs, member: expectedMemberArgs,
errors: [`Tried to set displayname to \"${tooLongDisplayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`] errors: [`Tried to set displayname to \"${tooLongDisplayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`]
@@ -530,8 +525,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null); const res = await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null);
// Assert // Assert
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
test('if proxy, call checkIfProxyExists', async () => { test('if proxy, call checkIfProxyExists', async () => {
@@ -544,7 +539,7 @@ describe('MemberHelper', () => {
proxy: null, proxy: null,
propic: null propic: null
} }
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {member: expectedMemberArgs, errors: []} const expectedReturn = {member: expectedMemberArgs, errors: []}
// Act // Act
@@ -553,8 +548,8 @@ describe('MemberHelper', () => {
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy); expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy);
expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1); expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
test('if checkProxyExists throws error, call database.member.create with null value', async () => { test('if checkProxyExists throws error, call database.member.create with null value', async () => {
@@ -567,7 +562,7 @@ describe('MemberHelper', () => {
proxy: null, proxy: null,
propic: null propic: null
} }
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = { const expectedReturn = {
member: expectedMemberArgs, member: expectedMemberArgs,
errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`] errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`]
@@ -577,8 +572,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null) const res = await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null)
// Assert // Assert
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
test('if propic, call checkImageFormatValidity', async () => { test('if propic, call checkImageFormatValidity', async () => {
@@ -591,7 +586,7 @@ describe('MemberHelper', () => {
propic: null propic: null
} }
utils.setExpirationWarning = jest.fn().mockReturnValue(); utils.setExpirationWarning = jest.fn().mockReturnValue();
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = {member: expectedMemberArgs, errors: []} const expectedReturn = {member: expectedMemberArgs, errors: []}
// Act // Act
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic); const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
@@ -599,8 +594,8 @@ describe('MemberHelper', () => {
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic); expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic);
expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1); expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
test('if checkImageFormatValidity throws error, call database.member.create with null value', async () => { test('if checkImageFormatValidity throws error, call database.member.create with null value', async () => {
@@ -613,7 +608,7 @@ describe('MemberHelper', () => {
proxy: null, proxy: null,
propic: null propic: null
} }
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
const expectedReturn = { const expectedReturn = {
member: expectedMemberArgs, member: expectedMemberArgs,
errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`] errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`]
@@ -622,8 +617,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic); const res = await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic);
// Assert // Assert
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
test('calls setExpirationWarning if attachmentExpiration exists', async () => { test('calls setExpirationWarning if attachmentExpiration exists', async () => {
@@ -647,7 +642,7 @@ describe('MemberHelper', () => {
proxy: mockMember.proxy, proxy: mockMember.proxy,
propic: mockMember.propic propic: mockMember.propic
} }
database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); memberRepo.createMember = jest.fn().mockResolvedValue(expectedMemberArgs);
utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true); utils.checkImageFormatValidity = jest.fn().mockResolvedValue(true);
utils.setExpirationWarning = jest.fn().mockReturnValue(); utils.setExpirationWarning = jest.fn().mockReturnValue();
const expectedReturn = {member: expectedMemberArgs, errors: []} const expectedReturn = {member: expectedMemberArgs, errors: []}
@@ -655,19 +650,16 @@ describe('MemberHelper', () => {
const res = await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic); const res = await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic);
// Assert // Assert
expect(res).toEqual(expectedReturn); expect(res).toEqual(expectedReturn);
expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(memberRepo.createMember).toHaveBeenCalledWith(expectedMemberArgs);
expect(database.members.create).toHaveBeenCalledTimes(1); expect(memberRepo.createMember).toHaveBeenCalledTimes(1);
}) })
}) })
describe('updateMemberField', () => { describe('updateMemberField', () => {
const {database} = require('../../src/database.js');
beforeEach(() => { beforeEach(() => {
utils.setExpirationWarning = jest.fn().mockReturnValue(`warning`); utils.setExpirationWarning = jest.fn().mockReturnValue(`warning`);
database.members = { memberRepo.updateMemberField = jest.fn().mockResolvedValue([1]);
update: jest.fn().mockResolvedValue([1])
};
}) })
test.each([ test.each([
@@ -682,20 +674,13 @@ describe('MemberHelper', () => {
const res = await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration) const res = await memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration)
// Assert // Assert
expect(res).toEqual(expected); expect(res).toEqual(expected);
expect(database.members.update).toHaveBeenCalledTimes(1); expect(memberRepo.updateMemberField).toHaveBeenCalledTimes(1);
expect(database.members.update).toHaveBeenCalledWith({[columnName]: value}, { expect(memberRepo.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, columnName, value)
where: {
name: {[Op.iLike]: mockMember.name},
userid: authorId
}
})
}) })
test('if database.members.update returns 0 rows changed, throw error', async () => { test('if database.members.update returns 0 rows changed, throw error', async () => {
// Arrange // Arrange
database.members = { memberRepo.updateMemberField = jest.fn().mockResolvedValue(0);
update: jest.fn().mockResolvedValue([0])
};
// Act // Act
await expect(memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname)).rejects.toThrow(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`); await expect(memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname)).rejects.toThrow(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`);
}) })
@@ -704,7 +689,7 @@ describe('MemberHelper', () => {
describe('checkIfProxyExists', () => { describe('checkIfProxyExists', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(memberHelper, "getMembersByAuthor").mockResolvedValue([mockMember]); memberRepo.getMembersByAuthor.mockResolvedValue([mockMember]);
}) })
test.each([ test.each([
@@ -722,8 +707,8 @@ describe('MemberHelper', () => {
const res = await memberHelper.checkIfProxyExists(authorId, proxy) const res = await memberHelper.checkIfProxyExists(authorId, proxy)
// Assert // Assert
expect(res).toEqual(false) expect(res).toEqual(false)
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1); expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId); expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
}) })
test.each([ test.each([
@@ -734,13 +719,13 @@ describe('MemberHelper', () => {
// Act & Assert // Act & Assert
await expect(memberHelper.checkIfProxyExists(authorId, proxy)).rejects.toThrow(error); await expect(memberHelper.checkIfProxyExists(authorId, proxy)).rejects.toThrow(error);
expect(memberHelper.getMembersByAuthor).not.toHaveBeenCalled(); expect(memberRepo.getMembersByAuthor).not.toHaveBeenCalled();
}) })
test('--text returns correct error and calls getMemberByAuthor', async () => { test('--text returns correct error and calls getMemberByAuthor', async () => {
await expect(memberHelper.checkIfProxyExists(authorId, "--text")).rejects.toThrow(enums.err.PROXY_EXISTS); await expect(memberHelper.checkIfProxyExists(authorId, "--text")).rejects.toThrow(enums.err.PROXY_EXISTS);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1); expect(memberRepo.getMembersByAuthor).toHaveBeenCalledTimes(1);
expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId); expect(memberRepo.getMembersByAuthor).toHaveBeenCalledWith(authorId);
}) })
}) })

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": [
"es2021"
],
"target": "es2021",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./database/build",
"rootDir": "./database",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true
}
}