import config from "config"; import Model from "../src/db/Model.js"; import ModelFactory from "../src/db/ModelFactory.js"; import {ManyThroughModelRelation, OneModelRelation} from "../src/db/ModelRelation.js"; import MysqlConnectionManager from "../src/db/MysqlConnectionManager.js"; import {ValidationBag} from "../src/db/Validator.js"; import {logger} from "../src/Logger.js"; import {MIGRATIONS} from "../src/TestApp.js"; class FakeDummyModel extends Model { public id?: number = undefined; public name?: string = undefined; public date?: Date = undefined; public date_default?: Date = undefined; protected init(): void { this.setValidation('name').acceptUndefined().between(3, 256); } } class Post extends Model { public id?: number = undefined; public author_id?: number = undefined; public content?: string = undefined; public readonly author = new OneModelRelation(this, Author, { localKey: 'author_id', foreignKey: 'id', }); protected init(): void { this.setValidation('author_id').defined().exists(Author, 'id'); } } class Author extends Model { public id?: number = undefined; public name?: string = undefined; public readonly roles = new ManyThroughModelRelation(this, Role, { localKey: 'id', foreignKey: 'id', pivotTable: 'author_role', localPivotKey: 'author_id', foreignPivotKey: 'role_id', }); } class Role extends Model { public id?: number = undefined; public name?: string = undefined; public readonly permissions = new ManyThroughModelRelation(this, Permission, { localKey: 'id', foreignKey: 'id', pivotTable: 'role_permission', localPivotKey: 'role_id', foreignPivotKey: 'permission_id', }); } class Permission extends Model { public id?: number = undefined; public name?: string = undefined; } class AuthorRole extends Model { public static get table(): string { return 'author_role'; } public author_id?: number = undefined; public role_id?: number = undefined; protected init(): void { this.setValidation('author_id').defined().exists(Author, 'id'); this.setValidation('role_id').defined().exists(Role, 'id'); } } class RolePermission extends Model { public static get table(): string { return 'role_permission'; } public role_id?: number = undefined; public permission_id?: number = undefined; protected init(): void { this.setValidation('role_id').defined().exists(Role, 'id'); this.setValidation('permission_id').defined().exists(Permission, 'id'); } } let fakeDummyModelModelFactory: ModelFactory; let postFactory: ModelFactory; let authorFactory: ModelFactory; let roleFactory: ModelFactory; let permissionFactory: ModelFactory; beforeAll(async () => { await MysqlConnectionManager.prepare(); await MysqlConnectionManager.query('DROP DATABASE IF EXISTS ' + config.get('mysql.database')); await MysqlConnectionManager.endPool(); logger.setSettings({minLevel: "trace"}); MysqlConnectionManager.registerMigrations(MIGRATIONS); ModelFactory.register(FakeDummyModel); ModelFactory.register(Post); ModelFactory.register(Author); ModelFactory.register(Role); ModelFactory.register(Permission); ModelFactory.register(AuthorRole); ModelFactory.register(RolePermission); await MysqlConnectionManager.prepare(); // Create FakeDummyModel table fakeDummyModelModelFactory = ModelFactory.get(FakeDummyModel); postFactory = ModelFactory.get(Post); authorFactory = ModelFactory.get(Author); roleFactory = ModelFactory.get(Role); permissionFactory = ModelFactory.get(Permission); await MysqlConnectionManager.query(`DROP TABLE IF EXISTS author_role`); await MysqlConnectionManager.query(`DROP TABLE IF EXISTS role_permission`); for (const factory of [ fakeDummyModelModelFactory, postFactory, authorFactory, roleFactory, permissionFactory, ]) { await MysqlConnectionManager.query(`DROP TABLE IF EXISTS ${factory.table}`); } await MysqlConnectionManager.query(`CREATE TABLE ${fakeDummyModelModelFactory.table}( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(256), date DATETIME, date_default DATETIME DEFAULT NOW(), PRIMARY KEY(id) )`); await MysqlConnectionManager.query(`CREATE TABLE ${authorFactory.table}( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64), PRIMARY KEY(id) )`); await MysqlConnectionManager.query(`CREATE TABLE ${postFactory.table}( id INT NOT NULL AUTO_INCREMENT, author_id INT NOT NULL, content VARCHAR(512), PRIMARY KEY(id), FOREIGN KEY post_author_fk (author_id) REFERENCES ${authorFactory.table} (id) )`); await MysqlConnectionManager.query(`CREATE TABLE ${roleFactory.table}( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64), PRIMARY KEY(id) )`); await MysqlConnectionManager.query(`CREATE TABLE ${permissionFactory.table}( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64), PRIMARY KEY(id) )`); await MysqlConnectionManager.query(`CREATE TABLE author_role( id INT NOT NULL AUTO_INCREMENT, author_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(id), FOREIGN KEY author_role_author_fk (author_id) REFERENCES ${authorFactory.table} (id), FOREIGN KEY author_role_role_fk (role_id) REFERENCES ${roleFactory.table} (id) )`); await MysqlConnectionManager.query(`CREATE TABLE role_permission( id INT NOT NULL AUTO_INCREMENT, role_id INT NOT NULL, permission_id INT NOT NULL, PRIMARY KEY(id), FOREIGN KEY role_permission_role_fk (role_id) REFERENCES ${roleFactory.table} (id), FOREIGN KEY role_permission_permission_fk (permission_id) REFERENCES ${permissionFactory.table} (id) )`); /// SEED /// // permissions createPostPermission = Permission.create({name: 'create-post'}); await createPostPermission.save(); moderatePostPermission = Permission.create({name: 'moderate-post'}); await moderatePostPermission.save(); viewLogsPermission = Permission.create({name: 'view-logs'}); await viewLogsPermission.save(); // roles guestRole = Role.create({name: 'guest'}); await guestRole.save(); await RolePermission.create({role_id: guestRole.id, permission_id: createPostPermission.id}).save(); moderatorRole = Role.create({name: 'moderator'}); await moderatorRole.save(); await RolePermission.create({role_id: moderatorRole.id, permission_id: createPostPermission.id}).save(); await RolePermission.create({role_id: moderatorRole.id, permission_id: moderatePostPermission.id}).save(); adminRole = Role.create({name: 'admin'}); await adminRole.save(); await RolePermission.create({role_id: adminRole.id, permission_id: createPostPermission.id}).save(); await RolePermission.create({role_id: adminRole.id, permission_id: moderatePostPermission.id}).save(); await RolePermission.create({role_id: adminRole.id, permission_id: viewLogsPermission.id}).save(); // authors glimmerAuthor = Author.create({name: 'glimmer'}); await glimmerAuthor.save(); await AuthorRole.create({author_id: glimmerAuthor.id, role_id: guestRole.id}).save(); bowAuthor = Author.create({name: 'bow'}); await bowAuthor.save(); await AuthorRole.create({author_id: bowAuthor.id, role_id: moderatorRole.id}).save(); adoraAuthor = Author.create({name: 'adora'}); await adoraAuthor.save(); await AuthorRole.create({author_id: adoraAuthor.id, role_id: adminRole.id}).save(); // posts post1 = Post.create({author_id: glimmerAuthor.id, content: 'I\'m the queen now and you\'ll do as I order.'}); await post1.save(); post2 = Post.create({author_id: adoraAuthor.id, content: 'But you\'re wrong!'}); await post2.save(); post3 = Post.create({author_id: bowAuthor.id, content: 'Come on guys, let\'s talk this through.'}); await post3.save(); }); afterAll(async () => { await MysqlConnectionManager.endPool(); }); describe('Model', () => { it('should construct properly', () => { const date = new Date(888); const model = fakeDummyModelModelFactory.create({ name: 'a_name', date: date, non_existing_property: 'dropped_value', }, true); expect(model.id).toBeUndefined(); expect(model.name).toBe('a_name'); expect(model.date).toBe(date); expect(model.date_default).toBeUndefined(); expect(model.non_existing_property).toBeUndefined(); }); it('should have a proper table name', () => { expect(fakeDummyModelModelFactory.table).toBe('fake_dummy_models'); expect(FakeDummyModel.table).toBe('fake_dummy_models'); expect(FakeDummyModel.create({}).table).toBe('fake_dummy_models'); }); it('should insert properly', async () => { const date = new Date(569985); const insertInstance: FakeDummyModel | null = fakeDummyModelModelFactory.create({ name: 'name1', date: date, }, true); // Insert expect(insertInstance.exists()).toBeFalsy(); await insertInstance.save(); expect(insertInstance.exists()).toBeTruthy(); expect(insertInstance.id).toBe(1); // Auto id from insert expect(insertInstance.name).toBe('name1'); expect(insertInstance.date?.getTime()).toBeCloseTo(date.getTime(), -4); expect(insertInstance.date_default).toBeDefined(); // Check that row exists in DB const retrievedInstance = await FakeDummyModel.getById(1); expect(retrievedInstance).toBeDefined(); expect(retrievedInstance?.id).toBe(1); expect(retrievedInstance?.name).toBe('name1'); expect(retrievedInstance?.date?.getTime()).toBeCloseTo(date.getTime(), -4); expect(retrievedInstance?.date_default).toBeDefined(); const failingInsertModel = fakeDummyModelModelFactory.create({ name: 'a', }, true); await expect(failingInsertModel.save()).rejects.toBeInstanceOf(ValidationBag); }); it('should update properly', async () => { const insertModel = fakeDummyModelModelFactory.create({ name: 'update', }, true); await insertModel.save(); const preUpdatedModel = await FakeDummyModel.getById(insertModel.id); expect(preUpdatedModel).not.toBeNull(); expect(preUpdatedModel?.name).toBe(insertModel.name); // Update model if (preUpdatedModel) { preUpdatedModel.name = 'updated_name'; await preUpdatedModel.save(); } const postUpdatedModel = await FakeDummyModel.getById(insertModel.id); expect(postUpdatedModel).not.toBeNull(); expect(postUpdatedModel?.id).toBe(insertModel.id); expect(postUpdatedModel?.name).not.toBe(insertModel.name); expect(postUpdatedModel?.name).toBe(preUpdatedModel?.name); }); it('should delete properly', async () => { const insertModel = fakeDummyModelModelFactory.create({ name: 'delete', }, true); await insertModel.save(); const preDeleteModel = await FakeDummyModel.getById(insertModel.id); expect(preDeleteModel).not.toBeNull(); await preDeleteModel?.delete(); const postDeleteModel = await FakeDummyModel.getById(insertModel.id); expect(postDeleteModel).toBeNull(); }); }); let createPostPermission: Permission; let moderatePostPermission: Permission; let viewLogsPermission: Permission; let guestRole: Role; let moderatorRole: Role; let adminRole: Role; let glimmerAuthor: Author; let bowAuthor: Author; let adoraAuthor: Author; let post1: Post; let post2: Post; let post3: Post; describe('ModelRelation', () => { test('Query and check relations', async () => { const posts = await Post.select() .with('author.roles.permissions') .sortBy('id', 'ASC') .get(); expect(posts.length).toBe(3); async function testPost( post: Post, originalPost: Post, expectedAuthor: Author, expectedRoles: Role[], expectedPermissions: Permission[], ) { console.log('Testing post', post); expect(post.id).toBe(originalPost.id); expect(post.content).toBe(originalPost.content); const actualAuthor = await post.author.get(); expect(actualAuthor).not.toBeNull(); expect(await post.author.has(expectedAuthor)).toBeTruthy(); expect(actualAuthor?.equals(expectedAuthor)).toBe(true); const authorRoles = await actualAuthor?.roles.get() || []; console.log('Roles:'); expect(authorRoles.map(r => r.id)).toStrictEqual(expectedRoles.map(r => r.id)); const authorPermissions = (await Promise.all(authorRoles.map(async r => await r.permissions.get()))) .flatMap(p => p); console.log('Permissions:'); expect(authorPermissions.map(p => p.id)).toStrictEqual(expectedPermissions.map(p => p.id)); } await testPost(posts[0], post1, glimmerAuthor, [guestRole], [createPostPermission]); await testPost(posts[1], post2, adoraAuthor, [adminRole], [createPostPermission, moderatePostPermission, viewLogsPermission]); await testPost(posts[2], post3, bowAuthor, [moderatorRole], [createPostPermission, moderatePostPermission]); }); });