diff --git a/packages/server/src/application/Application.ts b/packages/server/src/application/Application.ts index 0d8f483..3d4acda 100644 --- a/packages/server/src/application/Application.ts +++ b/packages/server/src/application/Application.ts @@ -1,9 +1,6 @@ -import * as comment from '../comment' import * as middleware from '../middleware' import * as routes from '../routes' import * as services from '../services' -import * as site from '../site' -import * as story from '../story' import * as team from '../team' import * as user from '../user' import express from 'express' @@ -23,9 +20,6 @@ export class Application implements IApplication { readonly userService: services.IUserService readonly teamService: team.ITeamService - readonly siteService: site.ISiteService - readonly storyService: story.IStoryService - readonly commentService: comment.ICommentService readonly userPermissions: user.IUserPermissions readonly authenticator: middleware.Authenticator @@ -37,10 +31,6 @@ export class Application implements IApplication { this.userService = new services.UserService(this.transactionManager) this.teamService = new team.TeamService(this.transactionManager) - this.siteService = new site.SiteService(this.transactionManager) - this.storyService = new story.StoryService( - this.transactionManager, this.siteService) - this.commentService = new comment.CommentService(this.transactionManager) this.userPermissions = new user.UserPermissions(this.transactionManager) this.authenticator = new middleware.Authenticator(this.userService) @@ -61,9 +51,10 @@ export class Application implements IApplication { this.configureMiddleware(router) this.configureRouter(router) + this.configureApiErrorHandling(router) server.use(this.config.app.context, router) - this.configureErrorHandling(server) + this.configureGlobalErrorHandling(server) return server } @@ -87,8 +78,6 @@ export class Application implements IApplication { } protected configureRouter(router: express.Router) { - const apiLogger = this.getApiLogger() - router.use('/app', new routes.LoginRoutes( this.userService, this.authenticator, @@ -108,27 +97,14 @@ export class Application implements IApplication { this.userPermissions, this.createTransactionalRouter(), ).handle) + } - router.use('/api', new site.SiteRoutes( - this.siteService, - this.userPermissions, - this.createTransactionalRouter(), - ).handle) - - router.use('/api', new story.StoryRoutes( - this.storyService, - this.createTransactionalRouter(), - ).handle) - - router.use('/api', new comment.CommentRoutes( - this.commentService, - this.createTransactionalRouter(), - ).handle) - + protected configureApiErrorHandling(router: express.Router) { + const apiLogger = this.getApiLogger() router.use('/api', new middleware.ErrorApiHandler(apiLogger).handle) } - protected configureErrorHandling(server: express.Application) { + protected configureGlobalErrorHandling(server: express.Application) { const apiLogger = this.getApiLogger() server.use(new middleware.ErrorPageHandler(apiLogger).handle) } diff --git a/packages/server/src/bootstrap.ts b/packages/server/src/bootstrap.ts index 3a98d21..0d145db 100644 --- a/packages/server/src/bootstrap.ts +++ b/packages/server/src/bootstrap.ts @@ -6,6 +6,7 @@ import {config} from './config' import {Bootstrap} from './application/Bootstrap' export const bootstrap = new Bootstrap(config) +// FIXME determine a port by parsing app url from config const port: string | number = process.env.PORT || 3000 bootstrap.listen(port) diff --git a/packages/server/src/comment/CommentRoutes.test.ts b/packages/server/src/comment/CommentRoutes.test.ts deleted file mode 100644 index 7721241..0000000 --- a/packages/server/src/comment/CommentRoutes.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -import * as CommentTestUtils from './CommentTestUtils' -import {IStory} from '@rondo/common' -import {createSite} from '../site/SiteTestUtils' -import {getStory} from '../story/StoryTestUtils' -import {test} from '../test' - -describe('comment', () => { - - test.withDatabase() - const t = test.request('/api') - - let cookie!: string - let token!: string - let story!: IStory - - const storyUrl = 'https://test.example.com/my/story' - - beforeEach(async () => { - const session = await test.registerAccount() - cookie = session.cookie - token = session.token - t.setHeaders({cookie, 'x-csrf-token': token}) - - await createSite(t, 'test.example.com') - story = await getStory(t, storyUrl) - }) - - async function createComment() { - return CommentTestUtils.createRootComment(t, { - storyId: story.id, - message: 'test', - }) - } - - async function createChildComment() { - const parent = await CommentTestUtils.createRootComment(t, { - storyId: story.id, - message: 'this is a parent comment', - }) - const child = await CommentTestUtils.createComment(t, { - storyId: story.id, - parentId: parent.id, - message: 'this is a child comment', - }) - expect(child.id).toBeGreaterThan(0) - return child - } - - describe('GET /stories/:storyId/comments', () => { - it('retrieves comments by story id', async () => { - const comments = await CommentTestUtils.getComments(t, story.id) - expect(comments.rootIds).toEqual(jasmine.any(Array)) - }) - }) - - describe('POST /stories/:storyId/comments', () => { - it('adds a new root comment', async () => { - const message = 'this is a comment' - const comment = await CommentTestUtils.createRootComment(t, { - storyId: story.id, - message, - }) - expect(comment.id).toBeGreaterThan(0) - expect(comment.message).toEqual(message) - }) - }) - - describe('POST /stories/:storyId/comments/:parentId', () => { - it('adds a new child comment', async () => { - await createChildComment() - const comments = await CommentTestUtils.getComments(t, story.id) - expect(comments.rootIds).toEqual([ - jasmine.any(Number), - ]) - const id = comments.rootIds[0] - const parent = comments.commentsById[id] - expect(parent).toBeTruthy() - expect(parent.message).toMatch(/parent/) - expect(parent.childrenIds).toEqual([jasmine.any(Number)]) - const child = comments.commentsById[parent.childrenIds![0]] - expect(child).toBeTruthy() - expect(child.message).toMatch(/child/) - expect(child.childrenIds).toBe(undefined) - }) - }) - - describe('PUT /comments/:commentId', () => { - - it('updates a comment', async () => { - const comment = await createComment() - await t.put('/comments/:commentId', { - params: { - commentId: comment.id, - }, - }) - .send({ - message: 'test2', - }) - .expect(200) - - const c = await CommentTestUtils.getCommentById(t, comment.id) - expect(c.message).toEqual('test2') - - // TODO save edit history - }) - - // it('fails to update a comment if user is not the owner') - - // it('updates a comment if user is site moderator') // TODO later - }) - - describe('DELETE /comments/:commentId', () => { - it('soft deletes a comment', async () => { - const comment = await createComment() - await t.delete('/comments/:commentId', { - params: { - commentId: comment.id, - }, - }) - .expect(200) - const comment2 = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment2.message) - .toEqual('(this message has been removed)') - }) - }) - - describe('POST /comments/:commentId/vote', () => { - it('adds a new comment vote', async () => { - const comment = await createComment() - await CommentTestUtils.upVote(t, comment.id) - const c = await CommentTestUtils.getCommentById(t, comment.id) - expect(c.votes).toEqual(1) - }) - it('can only upvote once', async () => { - const comment = await createComment() - async function upVote() { - return t.post('/comments/:commentId/vote', { - params: { - commentId: comment.id, - }, - }) - } - - const responses = (await Promise.all([ - upVote(), - upVote(), - ])).map(r => r.status) - - expect(responses).toContain(200) - expect(responses).toContain(400) - - const c = await CommentTestUtils.getCommentById(t, comment.id) - expect(c.votes).toEqual(1) - }) - }) - - describe('DELETE /comments/:commentId/vote', () => { - it('removes a comment vote', async () => { - let comment = await createComment() - await CommentTestUtils.upVote(t, comment.id) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.votes).toEqual(1) - await CommentTestUtils.downVote(t, comment.id) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.votes).toEqual(0) - }) - it('can only downvote once', async () => { - let comment = await createComment() - await CommentTestUtils.upVote(t, comment.id) - await Promise.all([ - CommentTestUtils.downVote(t, comment.id), - CommentTestUtils.downVote(t, comment.id), - ]) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.votes).toEqual(0) - }) - }) - - describe('POST /comments/:commentId/spam', () => { - it('adds a new spam report', async () => { - const comment = await createComment() - await CommentTestUtils.markAsSpam(t, comment.id) - const c = await CommentTestUtils.getCommentById(t, comment.id) - expect(c.spams).toEqual(1) - }) - it('can only report a spam once', async () => { - const comment = await createComment() - async function markAsSpam() { - return t.post('/comments/:commentId/spam', { - params: { - commentId: comment.id, - }, - }) - } - - const responses = (await Promise.all([ - markAsSpam(), - markAsSpam(), - ])).map(r => r.status) - - expect(responses).toContain(200) - expect(responses).toContain(400) - - const c = await CommentTestUtils.getCommentById(t, comment.id) - expect(c.spams).toEqual(1) - }) - }) - - describe('DELETE /comments/:commentId/spam', () => { - it('removes a spam report', async () => { - let comment = await createComment() - await CommentTestUtils.markAsSpam(t, comment.id) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.spams).toEqual(1) - await CommentTestUtils.unmarkAsSpam(t, comment.id) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.spams).toEqual(0) - }) - it('can only remove a spam report once', async () => { - let comment = await createComment() - await CommentTestUtils.markAsSpam(t, comment.id) - await Promise.all([ - CommentTestUtils.unmarkAsSpam(t, comment.id), - CommentTestUtils.unmarkAsSpam(t, comment.id), - ]) - comment = await CommentTestUtils.getCommentById(t, comment.id) - expect(comment.spams).toEqual(0) - }) - }) - -}) diff --git a/packages/server/src/comment/CommentRoutes.ts b/packages/server/src/comment/CommentRoutes.ts deleted file mode 100644 index a8a5bb4..0000000 --- a/packages/server/src/comment/CommentRoutes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {AsyncRouter} from '../router' -import {BaseRoute} from '../routes/BaseRoute' -import {IAPIDef} from '@rondo/common' -import {ICommentService} from './ICommentService' -import {ensureLoggedInApi} from '../middleware' - -export class CommentRoutes extends BaseRoute { - constructor( - protected readonly commentService: ICommentService, - protected readonly t: AsyncRouter, - ) { - super(t) - } - - setup(t: AsyncRouter) { - - t.get('/stories/:storyId/comments', async req => { - const {storyId} = req.params - return this.commentService.find(storyId) - }) - - t.use(ensureLoggedInApi) - - t.post('/stories/:storyId/comments', async req => { - const userId = req.user!.id - const storyId = Number(req.params.storyId) - const {message} = req.body - return this.commentService.saveRoot({ - message, - storyId, - userId, - }) - }) - - t.post('/stories/:storyId/comments/:parentId', async req => { - const userId = req.user!.id - const parentId = Number(req.params.parentId) - const storyId = Number(req.params.storyId) - const {message} = req.body - return this.commentService.save({ - message, - userId, - parentId, - storyId, - }) - }) - - t.get('/comments/:commentId', async req => { - const commentId = req.params.commentId - const comment = await this.commentService.findOne(commentId) - // TODO return status code 404 when not found - return comment! - }) - - t.put('/comments/:commentId', async req => { - const commentId = Number(req.params.commentId) - const {message} = req.body - return this.commentService.edit({ - id: commentId, - message, - userId: req.user!.id, - }) - }) - - t.delete('/comments/:commentId', async req => { - const {commentId} = req.params - return this.commentService.delete(commentId, req.user!.id) - }) - - t.post('/comments/:commentId/vote', async req => { - const {commentId} = req.params - return this.commentService.upVote(commentId, req.user!.id) - }) - - t.delete('/comments/:commentId/vote', async req => { - const {commentId} = req.params - return this.commentService.deleteVote(commentId, req.user!.id) - }) - - t.post('/comments/:commentId/spam', async req => { - const {commentId} = req.params - return this.commentService.markAsSpam(commentId, req.user!.id) - }) - - t.delete('/comments/:commentId/spam', async req => { - const {commentId} = req.params - return this.commentService.unmarkAsSpam(commentId, req.user!.id) - }) - - } -} diff --git a/packages/server/src/comment/CommentService.ts b/packages/server/src/comment/CommentService.ts deleted file mode 100644 index 628f870..0000000 --- a/packages/server/src/comment/CommentService.ts +++ /dev/null @@ -1,214 +0,0 @@ -import {UniqueTransformer} from '../error/ErrorTransformer' -import {BaseService} from '../services/BaseService' -import {Comment} from '../entities/Comment' -import {ICommentService} from './ICommentService' -import {ICommentTree} from '@rondo/common' -import {IEditCommentParams} from './IEditCommentParams' -import {INewCommentParams} from './INewCommentParams' -import {INewRootCommentParams} from './INewRootCommentParams' -import {Spam} from '../entities/Spam' -import {Validator} from '../validator' -import {Vote} from '../entities/Vote' - -export class CommentService extends BaseService implements ICommentService { - - async find(storyId: number) { - const comments = await this.getRepository(Comment).find({ - where: { - storyId, - }, - }) - - const commentTree: ICommentTree = { - rootIds: [], - commentsById: {}, - } - comments.reduce((obj, comment) => { - obj[comment.id] = comment - const {parentId} = comment - if (!parentId) { - commentTree.rootIds.push(comment.id) - return obj - } - const children = - obj[parentId].childrenIds = - obj[parentId].childrenIds || [] - children.push(comment.id) - return obj - }, commentTree.commentsById) - - return commentTree - } - - async findOne(commentId: number) { - return this.getRepository(Comment).findOne(commentId) - } - - async saveRoot(comment: INewRootCommentParams) { - new Validator(comment) - .ensure('message') - .ensure('storyId') - .ensure('userId') - .throw() - - const { - message, - userId, - storyId, - } = comment - - return this.getRepository(Comment).save({ - message: message.trim(), - userId, - storyId, - parentId: undefined, - - votes: 0, - spams: 0, - }) - } - - async save(comment: INewCommentParams) { - new Validator(comment) - .ensure('message') - .ensure('userId') - .ensure('storyId') - .ensure('parentId') - .throw() - - const { - message, - userId, - storyId, - parentId, - } = comment - - return this.getRepository(Comment).save({ - message: message.trim(), - userId, - storyId, - parentId, - - votes: 0, - spams: 0, - }) - } - - async edit(comment: IEditCommentParams) { - new Validator(comment) - .ensure('id') - .ensure('message') - .ensure('userId') - .throw() - - const {id, message, userId} = comment - - await this.getRepository(Comment) - .update({ - id, - userId, - }, { - message, - }) - const editedComment = await this.findOne(comment.id) - - if (!editedComment) { - // TODO 400 or 404 - throw new Error('Comment not found') - } - return editedComment - } - - async delete(commentId: number, userId: number) { - await this.getRepository(Comment) - .update({ - id: commentId, - userId, - }, { - message: '(this message has been removed)', - }) - - return this.findOne(commentId) - } - - async upVote(commentId: number, userId: number) { - try { - await this.getRepository(Vote) - .save({ - commentId, - userId, - }) - } catch (err) { - UniqueTransformer.transform(err, 'Already upvoted!') - } - - await this.getRepository(Comment) - .createQueryBuilder() - .update() - .where({ id: commentId }) - .set({ - votes: () => 'votes + 1', - }) - .execute() - } - - async deleteVote(commentId: number, userId: number) { - await this.getRepository(Vote) - .delete({ - commentId, - userId, - }) - - // TODO rows.affected returns undefined or SQLite driver. This is an - // alternative query that does not depend on it. - await this.getRepository(Comment) - .createQueryBuilder() - .update() - .where({ id: commentId }) - .set({ - votes: () => '(select count(*) from vote where commentId = comment.id)', - }) - .execute() - } - - async markAsSpam(commentId: number, userId: number) { - try { - await this.getRepository(Spam) - .save({ - commentId, - userId, - }) - } catch (err) { - UniqueTransformer.transform(err, 'Already marked as spam!') - } - - await this.getRepository(Comment) - .createQueryBuilder() - .update() - .where({ id: commentId }) - .set({ - spams: () => 'spams + 1', - }) - .execute() - } - - async unmarkAsSpam(commentId: number, userId: number) { - await this.getRepository(Spam) - .delete({ - commentId, - userId, - }) - - // TODO rows.affected returns undefined or SQLite driver. This is an - // alternative query that does not depend on it. - await this.getRepository(Comment) - .createQueryBuilder() - .update() - .where({ id: commentId }) - .set({ - spams: () => '(select count(*) from spam where commentId = comment.id)', - }) - .execute() - } - -} diff --git a/packages/server/src/comment/CommentTestUtils.ts b/packages/server/src/comment/CommentTestUtils.ts deleted file mode 100644 index 7fe02b0..0000000 --- a/packages/server/src/comment/CommentTestUtils.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {RequestTester} from '../test-utils' -import {IAPIDef} from '@rondo/common' - -export async function createRootComment( - t: RequestTester, - {storyId, message}: { - storyId: number, - message: string, - }, -) { - const response = await t - .post('/stories/:storyId/comments', { - params: {storyId}, - }) - .send({ - message, - }) - .expect(200) - - return response.body! -} - -export async function createComment( - t: RequestTester, - {storyId, parentId, message}: { - storyId: number, - parentId: number, - message: string, - }, -) { - const response = await t - .post('/stories/:storyId/comments/:parentId', { - params: {storyId, parentId}, - }) - .send({ - message, - }) - .expect(200) - - return response.body! -} - -export async function getComments( - t: RequestTester, - storyId: number, -) { - const response = await t - .get('/stories/:storyId/comments', { - params: {storyId}, - }) - .expect(200) - - return response.body! -} - -export async function getCommentById( - t: RequestTester, - commentId: number, -) { - const response = await t - .get('/comments/:commentId', { - params: {commentId}, - }) - .expect(200) - - return response.body! -} - -export async function upVote( - t: RequestTester, - commentId: number, -) { - await t.post('/comments/:commentId/vote', { - params: { - commentId, - }, - }) - .expect(200) -} - -export async function downVote( - t: RequestTester, - commentId: number, -) { - await t.delete('/comments/:commentId/vote', { - params: { - commentId, - }, - }) - .expect(200) -} - -export async function markAsSpam( - t: RequestTester, - commentId: number, -) { - await t.post('/comments/:commentId/spam', { - params: { - commentId, - }, - }) - .expect(200) -} - -export async function unmarkAsSpam( - t: RequestTester, - commentId: number, -) { - await t.delete('/comments/:commentId/spam', { - params: { - commentId, - }, - }) - .expect(200) -} diff --git a/packages/server/src/comment/ICommentService.ts b/packages/server/src/comment/ICommentService.ts deleted file mode 100644 index 03e902e..0000000 --- a/packages/server/src/comment/ICommentService.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {IComment, ICommentTree} from '@rondo/common' -import {IEditCommentParams} from './IEditCommentParams' -import {INewCommentParams} from './INewCommentParams' -import {INewRootCommentParams} from './INewRootCommentParams' - -export interface ICommentService { - find(storyId: number): Promise - - findOne(commentId: number): Promise - - saveRoot(comment: INewRootCommentParams): Promise - - save(comment: INewCommentParams): Promise - - edit(comment: IEditCommentParams): Promise - - delete(commentId: number, userId: number): Promise - - upVote(commentId: number, userId: number): Promise - - deleteVote(commentId: number, userId: number): Promise - - markAsSpam(commentId: number, userId: number): Promise - - unmarkAsSpam(commentId: number, userId: number): Promise -} diff --git a/packages/server/src/comment/IEditCommentParams.ts b/packages/server/src/comment/IEditCommentParams.ts deleted file mode 100644 index 44675f8..0000000 --- a/packages/server/src/comment/IEditCommentParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IEditCommentParams { - id: number - message: string - userId: number -} diff --git a/packages/server/src/comment/INewCommentParams.ts b/packages/server/src/comment/INewCommentParams.ts deleted file mode 100644 index c03511e..0000000 --- a/packages/server/src/comment/INewCommentParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface INewCommentParams { - message: string - userId: number - parentId: number - storyId: number -} diff --git a/packages/server/src/comment/INewRootCommentParams.ts b/packages/server/src/comment/INewRootCommentParams.ts deleted file mode 100644 index 1ad65f7..0000000 --- a/packages/server/src/comment/INewRootCommentParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface INewRootCommentParams { - message: string - userId: number - storyId: number -} diff --git a/packages/server/src/comment/index.ts b/packages/server/src/comment/index.ts deleted file mode 100644 index 3857ded..0000000 --- a/packages/server/src/comment/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './CommentRoutes' -export * from './CommentService' -export * from './ICommentService' diff --git a/packages/server/src/entities/Comment.ts b/packages/server/src/entities/Comment.ts deleted file mode 100644 index 9ed25c4..0000000 --- a/packages/server/src/entities/Comment.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Column, Entity, Index, ManyToOne} from 'typeorm' -import {User} from './User' -import {Story} from './Story' -import {BaseEntity} from './BaseEntity' - -@Entity() -export class Comment extends BaseEntity { - @Column({type: 'text'}) - message!: string - - @ManyToOne(type => Story, story => story.comments) - story?: Story - - @Column() - @Index() - storyId!: number - - @ManyToOne(type => User, user => user.comments) - user?: User - - @Column() - userId!: number - - @Column({nullable: true}) - parentId!: number - - @Column() - spams!: number - - @Column() - votes!: number -} diff --git a/packages/server/src/entities/Site.ts b/packages/server/src/entities/Site.ts deleted file mode 100644 index f8f1ad9..0000000 --- a/packages/server/src/entities/Site.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {BaseEntity} from './BaseEntity' -import {Column, Entity, Index, ManyToOne, OneToMany} from 'typeorm' -import {User} from './User' -import {Story} from './Story' -import {Team} from './Team' - -@Entity() -export class Site extends BaseEntity { - @Column() - name!: string - - @Column({ unique: true }) - domain!: string - - @Index() - @Column() - userId!: number - - @ManyToOne(type => User, user => user.sites) - user?: User - - @Index() - @Column() - teamId!: number - - @ManyToOne(type => Team, team => team.sites) - team?: Team - - @OneToMany(type => Story, story => story.site) - stories!: Story[] -} diff --git a/packages/server/src/entities/Spam.ts b/packages/server/src/entities/Spam.ts deleted file mode 100644 index 735a238..0000000 --- a/packages/server/src/entities/Spam.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {BaseEntity} from './BaseEntity' -import {Column, Entity, ManyToOne, Unique} from 'typeorm' -import {User} from './User' -import {Comment} from './Comment' - -@Entity() -@Unique('spam_userid_commentid', ['userId', 'commentId']) -export class Spam extends BaseEntity { - @ManyToOne(type => User) - user?: User - - @Column() - userId!: number - - @ManyToOne(type => Comment) - comment?: Comment - - @Column() - commentId!: number -} diff --git a/packages/server/src/entities/Story.ts b/packages/server/src/entities/Story.ts deleted file mode 100644 index 3711f7b..0000000 --- a/packages/server/src/entities/Story.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {BaseEntity} from './BaseEntity' -import {Column, Entity, ManyToOne} from 'typeorm' -import {Comment} from './Comment' -import {Site} from './Site' - -@Entity() -export class Story extends BaseEntity { - @Column({ unique: true }) - url!: string - - @Column() - siteId!: number - - @ManyToOne(type => Site, site => site.stories) - site?: Site - - @ManyToOne(type => Comment, comment => comment.story) - comments!: Comment[] -} diff --git a/packages/server/src/entities/Team.ts b/packages/server/src/entities/Team.ts index af9d508..66b6e65 100644 --- a/packages/server/src/entities/Team.ts +++ b/packages/server/src/entities/Team.ts @@ -1,6 +1,5 @@ import {BaseEntity} from './BaseEntity' import {Column, Entity, OneToMany, ManyToOne, Index} from 'typeorm' -import {Site} from './Site' import {UserTeam} from './UserTeam' import {User} from './User' @@ -16,9 +15,6 @@ export class Team extends BaseEntity { @ManyToOne(type => User) user?: User - @OneToMany(type => Site, site => site.team) - sites!: Site[] - @OneToMany(type => UserTeam, userTeam => userTeam.team) userTeams!: UserTeam[] } diff --git a/packages/server/src/entities/User.ts b/packages/server/src/entities/User.ts index 8dbbd91..be8294c 100644 --- a/packages/server/src/entities/User.ts +++ b/packages/server/src/entities/User.ts @@ -1,8 +1,6 @@ import {BaseEntity} from './BaseEntity' import {Column, Entity, OneToMany} from 'typeorm' -import {Comment} from './Comment' import {Session} from './Session' -import {Site} from './Site' import {UserTeam} from './UserTeam' import {UserEmail} from './UserEmail' @@ -18,12 +16,6 @@ export class User extends BaseEntity { @OneToMany(type => Session, session => session.user) sessions!: Session[] - @OneToMany(type => Site, site => site.user) - sites!: Site[] - - @OneToMany(type => Comment, comment => comment.user) - comments!: Comment[] - @OneToMany(type => UserTeam, userTeam => userTeam.user) userTeams!: UserTeam[] } diff --git a/packages/server/src/entities/Vote.ts b/packages/server/src/entities/Vote.ts deleted file mode 100644 index 1891c65..0000000 --- a/packages/server/src/entities/Vote.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {BaseEntity} from './BaseEntity' -import {Column, Entity, ManyToOne, Unique} from 'typeorm' -import {Comment} from './Comment' -import {User} from './User' - -@Entity() -@Unique('vote_userid_commentid', ['userId', 'commentId']) -export class Vote extends BaseEntity { - @ManyToOne(type => User) - user?: User - - @Column() - userId!: number - - @ManyToOne(type => Comment) - comment?: Comment - - @Column() - commentId!: number -} diff --git a/packages/server/src/entities/index.ts b/packages/server/src/entities/index.ts index 23221c3..4bfdff5 100644 --- a/packages/server/src/entities/index.ts +++ b/packages/server/src/entities/index.ts @@ -1,3 +1,7 @@ +export * from './BaseEntity' +export * from './Role' export * from './Session' +export * from './Team' export * from './User' export * from './UserEmail' +export * from './UserTeam' diff --git a/packages/server/src/error/index.ts b/packages/server/src/error/index.ts new file mode 100644 index 0000000..9f496dd --- /dev/null +++ b/packages/server/src/error/index.ts @@ -0,0 +1,2 @@ +export * from './ErrorTransformer' +export * from './TransformedError' diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 3294a98..88d5fe3 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,9 +1,13 @@ export * from './application' +export * from './config/index' export * from './database' export * from './entities' +export * from './error' export * from './logger' export * from './middleware' export * from './router' export * from './routes' export * from './services' export * from './session' +export * from './user' +export * from './validator' diff --git a/packages/server/src/services/index.ts b/packages/server/src/services/index.ts index b41f3f1..c0b0049 100644 --- a/packages/server/src/services/index.ts +++ b/packages/server/src/services/index.ts @@ -1,2 +1,3 @@ +export * from './BaseService' export * from './IUserService' export * from './UserService' diff --git a/packages/server/src/site/ISiteCreateParams.ts b/packages/server/src/site/ISiteCreateParams.ts deleted file mode 100644 index b3189a3..0000000 --- a/packages/server/src/site/ISiteCreateParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ISiteCreateParams { - name: string - domain: string - teamId: number - userId: number -} diff --git a/packages/server/src/site/ISiteService.ts b/packages/server/src/site/ISiteService.ts deleted file mode 100644 index 0aa191b..0000000 --- a/packages/server/src/site/ISiteService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {ISite} from '@rondo/common' -import {ISiteCreateParams} from './ISiteCreateParams' -import {ISiteUpdateParams} from './ISiteUpdateParams' - -export interface ISiteService { - create(params: ISiteCreateParams): Promise - - update(params: ISiteUpdateParams): Promise - - remove(params: {id: number, teamId: number}): Promise - - findOne(id: number, teamId: number): Promise - - findByUser(userId: number): Promise - - findByTeam(teamId: number): Promise - - findByDomain(domain: string): Promise - - // TODO add other methods -} diff --git a/packages/server/src/site/ISiteUpdateParams.ts b/packages/server/src/site/ISiteUpdateParams.ts deleted file mode 100644 index bbb476a..0000000 --- a/packages/server/src/site/ISiteUpdateParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ISiteUpdateParams { - id: number - name?: string - domain?: string - teamId: number -} diff --git a/packages/server/src/site/SiteRoutes.test.ts b/packages/server/src/site/SiteRoutes.test.ts deleted file mode 100644 index 88e0340..0000000 --- a/packages/server/src/site/SiteRoutes.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {ITeam} from '@rondo/common' -import {UserTeam} from '../entities/UserTeam' -import {createTeam} from '../team/TeamTestUtils' -import {createSite} from './SiteTestUtils' -import {test} from '../test' - -describe('team', () => { - - test.withDatabase() - const t = test.request('/api') - - let cookie!: string - let token!: string - let team!: ITeam - let userId: number - beforeEach(async () => { - const session = await test.registerAccount() - cookie = session.cookie - token = session.token - userId = session.userId - t.setHeaders({ cookie, 'x-csrf-token': token }) - - team = await createTeam(t, 'test') - }) - - describe('POST /teams/:teamId/sites', () => { - it('creates a new site', async () => { - await createSite(t, 'test.example.com') - }) - - describe('no team access', () => { - beforeEach(async () => { - await test.transactionManager - .getRepository(UserTeam) - .delete({ - userId, - teamId: team.id, - }) - }) - - it('results with 403 when user does not have team access ', async () => { - await t - .post('/teams/:teamId/sites', { - params: { - teamId: team.id, - }, - }) - .send({ - domain: 'test.example.com', - name: 'test', - }) - .expect(403) - }) - }) - - }) - - describe('GET /teams/:teamId/sites/:id', () => { - it('fetches a site belonging to a team', async () => { - const site = await createSite(t, 'test.example.com') - const response = await t.get('/teams/:teamId/sites/:id', { - params: { - teamId: site.teamId, - id: site.id, - }, - }) - .expect(200) - expect(response.body!.id).toEqual(site.id) - }) - }) - - describe('GET /teams/:teamId/sites', () => { - it('fetches all sites belonging to a team', async () => { - const site = await createSite(t, 'test.example.com') - const response = await t.get('/teams/:teamId/sites', { - params: { - teamId: site.teamId, - }, - }) - expect(response.body.map(s => s.id)).toContain(site.id) - }) - }) - - describe('PUT /teams/:teamId/sites/:id', () => { - it('updates site belonging to a team', async () => { - const site = await createSite(t, 'test.example.com') - const response = await t.put('/teams/:teamId/sites/:id', { - params: { - id: site.id, - teamId: site.teamId, - }, - }) - .send({ - name: site.name, - domain: 'test2.example.com', - }) - .expect(200) - expect(response.body.domain).toEqual('test2.example.com') - }) - }) - - describe('DELETE /teams/:teamId/sites/:id', () => { - it('deletes a site', async () => { - const site = await createSite(t, 'test.example.com') - await t.delete('/teams/:teamId/sites/:id', { - params: { - id: site.id, - teamId: site.teamId, - }, - }) - .expect(200) - }) - }) - -}) diff --git a/packages/server/src/site/SiteRoutes.ts b/packages/server/src/site/SiteRoutes.ts deleted file mode 100644 index 0d9212f..0000000 --- a/packages/server/src/site/SiteRoutes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {AsyncRouter} from '../router' -import {BaseRoute} from '../routes/BaseRoute' -import {IAPIDef} from '@rondo/common' -import {ISiteService} from './ISiteService' -import {ensureLoggedInApi} from '../middleware' -import {IUserPermissions} from '../user/IUserPermissions' - -export class SiteRoutes extends BaseRoute { - constructor( - protected readonly siteService: ISiteService, - protected readonly permissions: IUserPermissions, - t: AsyncRouter, - ) { - super(t) - } - - setup(t: AsyncRouter) { - - t.get('/sites/:domain', async req => { - const {domain} = req.params - return this.siteService.findByDomain(domain) - }) - - t.get('/teams/:teamId/sites/:id', async req => { - const {id, teamId} = req.params - return this.siteService.findOne(id, teamId) - }) - - t.get('/teams/:teamId/sites', async req => { - return this.siteService.findByTeam(req.params.teamId) - }) - - t.use(ensureLoggedInApi) - - // TODO do not use this one without teamId - t.get('/my/sites', async req => { - return this.siteService.findByUser(req.user!.id) - }) - - t.post('/teams/:teamId/sites', async req => { - const {name, domain} = req.body - const {teamId} = req.params - - await this.permissions.belongsToTeam({ - teamId, - userId: req.user!.id, - }) - return this.siteService.create({ - name, - domain, - teamId, - userId: req.user!.id, - }) - }) - - t.put('/teams/:teamId/sites/:id', async req => { - const {name, domain} = req.body - const id = Number(req.params.id) - const teamId = Number(req.params.teamId) - - await this.permissions.belongsToTeam({ - teamId, - userId: req.user!.id, - }) - - return this.siteService.update({ - id, - teamId, - name, - domain, - }) - }) - - t.delete('/teams/:teamId/sites/:id', async req => { - const id = Number(req.params.id) - const teamId = Number(req.params.teamId) - - await this.permissions.belongsToTeam({ - teamId, - userId: req.user!.id, - }) - - return this.siteService.remove({ - id, - teamId, - }) - }) - - } - -} diff --git a/packages/server/src/site/SiteService.ts b/packages/server/src/site/SiteService.ts deleted file mode 100644 index bda0df1..0000000 --- a/packages/server/src/site/SiteService.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {BaseService} from '../services/BaseService' -import {ISiteCreateParams} from './ISiteCreateParams' -import {ISiteUpdateParams} from './ISiteUpdateParams' -import {ISiteService} from './ISiteService' -import {Site} from '../entities/Site' - -export class SiteService extends BaseService implements ISiteService { - async create(params: ISiteCreateParams) { - // TODO validate params.domain - - // TODO check site limit per user - return this.getRepository(Site).save(params) - } - - async update({teamId, id, name, domain}: ISiteUpdateParams) { - // TODO validate params.domain - - await this.getRepository(Site) - .update({ - id, - teamId, - }, { - name, - domain, - }) - - return (await this.findOne(id, teamId))! - } - - async remove({id, teamId}: {id: number, teamId: number}) { - await this.getRepository(Site) - .delete({ - id, - teamId, - }) - } - - async findOne(id: number, teamId: number) { - return this.getRepository(Site).findOne({ - where: { - id, - teamId, - }, - }) - } - - async findByDomain(domain: string) { - return this.getRepository(Site).findOne({ - where: { - domain, - }, - }) - } - - async findByUser(userId: number) { - return this.getRepository(Site).find({ - where: { userId }, - }) - } - - async findByTeam(teamId: number) { - return this.getRepository(Site).find({ - where: { teamId }, - }) - } -} diff --git a/packages/server/src/site/SiteTestUtils.ts b/packages/server/src/site/SiteTestUtils.ts deleted file mode 100644 index 401337e..0000000 --- a/packages/server/src/site/SiteTestUtils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {RequestTester} from '../test-utils' -import {IAPIDef} from '@rondo/common' -import {createTeam} from '../team/TeamTestUtils' - -export async function createSite(t: RequestTester, domain: string) { - const team = await createTeam(t, 'test') - const response = await t - .post('/teams/:teamId/sites', { - params: { - teamId: team.id, - }, - }) - .send({ - domain, - name: 'test-site', - }) - .expect(200) - expect(response.body.id).toBeTruthy() - return response.body -} diff --git a/packages/server/src/site/index.ts b/packages/server/src/site/index.ts deleted file mode 100644 index bc5de51..0000000 --- a/packages/server/src/site/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './ISiteService' -export * from './SiteRoutes' -export * from './SiteService' diff --git a/packages/server/src/story/IStoryService.ts b/packages/server/src/story/IStoryService.ts deleted file mode 100644 index fe74028..0000000 --- a/packages/server/src/story/IStoryService.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Story} from '../entities/Story' - -export interface IStoryService { - findOne(url: string): Promise - - // TODO add other methods -} diff --git a/packages/server/src/story/StoryRoutes.test.ts b/packages/server/src/story/StoryRoutes.test.ts deleted file mode 100644 index fdcbe93..0000000 --- a/packages/server/src/story/StoryRoutes.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {ISite} from '@rondo/common' -import {createSite} from '../site/SiteTestUtils' -import {getStory} from './StoryTestUtils' -import {test} from '../test' - -describe('story', () => { - - test.withDatabase() - const t = test.request('/api') - - let cookie!: string - let token!: string - let site!: ISite - beforeEach(async () => { - const session = await test.registerAccount() - cookie = session.cookie - token = session.token - t.setHeaders({ cookie, 'x-csrf-token': token }) - - site = await createSite(t, 'test.example.com') - }) - - const invalidUrl = 'https://invalid.example.com/test' - const validUrl = 'https://test.example.com/test' - - describe('/stories/by-url', () => { - it('returns undefined when a site is not configured', async () => { - const response = await t - .get('/stories/by-url', { - query: { url: invalidUrl }, - }) - .expect(200) - expect(response.body).toEqual('') - }) - - it('creates a story when it does not exist', async () => { - const story = await getStory(t, validUrl) - expect(story.id).toBeTruthy() - expect(story.siteId).toEqual(site.id) - }) - - it('retrieves existing story after it is created', async () => { - const story1 = await getStory(t, validUrl) - const story2 = await getStory(t, validUrl) - expect(story1.id).toBeTruthy() - expect(story1).toEqual(story2) - }) - - it('prevents unique exceptions', async () => { - const p1 = getStory(t, validUrl) - const p2 = getStory(t, validUrl) - const [story1, story2] = await Promise.all([p1, p2]) - expect(story1.id).toBeTruthy() - expect(story1).toEqual(story2) - }) - }) - -}) diff --git a/packages/server/src/story/StoryRoutes.ts b/packages/server/src/story/StoryRoutes.ts deleted file mode 100644 index 6e58c42..0000000 --- a/packages/server/src/story/StoryRoutes.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {AsyncRouter} from '../router' -import {BaseRoute} from '../routes/BaseRoute' -import {IAPIDef} from '@rondo/common' -import {IStoryService} from './IStoryService' - -export class StoryRoutes extends BaseRoute { - constructor( - protected readonly storyService: IStoryService, - protected readonly t: AsyncRouter, - ) { - super(t) - } - - setup(t: AsyncRouter) { - - t.get('/stories/by-url', async req => { - const {url} = req.query - return this.storyService.findOne(url) - }) - - } - -} diff --git a/packages/server/src/story/StoryService.ts b/packages/server/src/story/StoryService.ts deleted file mode 100644 index 2db7d15..0000000 --- a/packages/server/src/story/StoryService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import URL from 'url' -import {BaseService} from '../services/BaseService' -import {ISiteService} from '../site/ISiteService' -import {IStoryService} from './IStoryService' -import {ITransactionManager} from '../database/ITransactionManager' -import {Story} from '../entities/Story' -import {UniqueTransformer} from '../error/ErrorTransformer' - -export class StoryService extends BaseService implements IStoryService { - constructor( - transactionManager: ITransactionManager, - protected readonly siteService: ISiteService, - ) { - super(transactionManager) - } - - async findOne(url: string) { - const storyRepo = this.getRepository(Story) - const story = await storyRepo.findOne({ - where: {url}, - }) - if (story) { - return story - } - - const domain = URL.parse(url).hostname - const site = await this.siteService.findByDomain(domain!) - - if (!site) { - return undefined - } - - try { - return await storyRepo.save({ - url, - siteId: site.id, - }) - } catch (err) { - // throw if not a unique constraint error - UniqueTransformer.throwIfNotMatch(err) - - // This could happen if there are two concurrent requests coming in at - // the same time, and they both cannot find the story, then decide to - // create a record. - return await storyRepo.findOne({ - where: {url}, - }) - } - } -} diff --git a/packages/server/src/story/StoryTestUtils.ts b/packages/server/src/story/StoryTestUtils.ts deleted file mode 100644 index 64a9366..0000000 --- a/packages/server/src/story/StoryTestUtils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {IAPIDef} from '@rondo/common' -import {RequestTester} from '../test-utils' - -export async function getStory(t: RequestTester, url: string) { - const response = await t - .get('/stories/by-url', { - query: {url}, - }) - .expect(200) - - return response.body! -} diff --git a/packages/server/src/story/index.ts b/packages/server/src/story/index.ts deleted file mode 100644 index 0760363..0000000 --- a/packages/server/src/story/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './StoryRoutes' -export * from './IStoryService' -export * from './StoryService'