Skip to content

Commit

Permalink
Merge pull request #241 from codex-team/recent-note-list
Browse files Browse the repository at this point in the history
feat(recent-notes): save and delete note visits on the appropriate routes
  • Loading branch information
e11sy authored Apr 7, 2024
2 parents e59e56d + d6e6e70 commit a75c07e
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 19 deletions.
12 changes: 5 additions & 7 deletions src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UserService from '@domain/service/user.js';
import AIService from './service/ai.js';
import EditorToolsService from '@domain/service/editorTools.js';
import FileUploaderService from './service/fileUploader.service.js';
import NoteVisitsService from '@domain/service/noteVisits.js';
import NoteVisitsService from './service/noteVisits.js';

/**
* Interface for initiated services
Expand Down Expand Up @@ -49,9 +49,9 @@ export interface DomainServices {
fileUploaderService: FileUploaderService,

/**
* Note Visits service instance
* Note visits service instance
*/
noteVisitsService: NoteVisitsService
noteVisitsService: NoteVisitsService;
}

/**
Expand All @@ -61,16 +61,14 @@ export interface DomainServices {
* @param appConfig - app config
*/
export function init(repositories: Repositories, appConfig: AppConfig): DomainServices {
const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository);

const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository, repositories.noteVisitsRepository);
const noteVisitsService = new NoteVisitsService(repositories.noteVisitsRepository);
const authService = new AuthService(
appConfig.auth.accessSecret,
appConfig.auth.accessExpiresIn,
appConfig.auth.refreshExpiresIn,
repositories.userSessionRepository
);
const noteVisitsService = new NoteVisitsService(repositories.noteVisitsRepository);

const editorToolsService = new EditorToolsService(repositories.editorToolsRepository);

const sharedServices = {
Expand Down
10 changes: 9 additions & 1 deletion src/domain/service/note.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Note, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
import type NoteRepository from '@repository/note.repository.js';
import type NoteVisitsRepository from '@repository/noteVisits.repository.js';
import { createPublicId } from '@infrastructure/utils/id.js';
import { DomainError } from '@domain/entities/DomainError.js';
import type NoteRelationsRepository from '@repository/noteRelations.repository.js';
Expand All @@ -20,6 +21,11 @@ export default class NoteService {
*/
public noteRelationsRepository: NoteRelationsRepository;

/**
* Note visits repository
*/
public noteVisitsRepository: NoteVisitsRepository;

/**
* Number of the notes to be displayed on one page
* it is used to calculate offset and limit for getting notes that the user has recently opened
Expand All @@ -31,10 +37,12 @@ export default class NoteService {
*
* @param noteRepository - note repository
* @param noteRelationsRepository - note relationship repository
* @param noteVisitsRepository - note visits repository
*/
constructor(noteRepository: NoteRepository, noteRelationsRepository: NoteRelationsRepository) {
constructor(noteRepository: NoteRepository, noteRelationsRepository: NoteRelationsRepository, noteVisitsRepository: NoteVisitsRepository) {
this.noteRepository = noteRepository;
this.noteRelationsRepository = noteRelationsRepository;
this.noteVisitsRepository = noteVisitsRepository;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/domain/service/noteVisits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ export default class NoteVisitsService {
public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise<NoteVisit> {
return await this.noteVisitsRepository.saveVisit(noteId, userId);
};

/**
* Deletes all visits of the note when a note is deleted
*
* @param noteId - note internal id
*/
public async deleteNoteVisits(noteId: NoteInternalId): Promise<boolean> {
return await this.noteVisitsRepository.deleteNoteVisits(noteId);
}
}
1 change: 1 addition & 0 deletions src/presentation/http/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export default class HttpApi implements Api {
prefix: '/note',
noteService: domainServices.noteService,
noteSettingsService: domainServices.noteSettingsService,
noteVisitsService: domainServices.noteVisitsService,
});

await this.server?.register(NoteListRouter, {
Expand Down
37 changes: 36 additions & 1 deletion src/presentation/http/router/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettings
import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js';
import { MemberRole } from '@domain/entities/team.js';
import { type NotePublic, definePublicNote } from '@domain/entities/notePublic.js';
import type NoteVisitsService from '@domain/service/noteVisits.js';

/**
* Interface for the note router.
Expand All @@ -22,6 +23,11 @@ interface NoteRouterOptions {
* Note Settings service instance
*/
noteSettingsService: NoteSettingsService,

/**
* Note visits service instance
*/
noteVisitsService: NoteVisitsService;
}

/**
Expand All @@ -37,6 +43,7 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
*/
const noteService = opts.noteService;
const noteSettingsService = opts.noteSettingsService;
const noteVisitsService = opts.noteVisitsService;

/**
* Prepare note id resolver middleware
Expand Down Expand Up @@ -111,14 +118,26 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
],
}, async (request, reply) => {
const { note } = request;
const noteId = request.note?.id as number;
const { memberRole } = request;
const { userId } = request;

/**
* Check if note exists
*/
if (note === null) {
return reply.notFound('Note not found');
}

/**
* Check if user is authorized
*
* @todo use event bus to save note visits
*/
if (userId !== null) {
await noteVisitsService.saveVisit(noteId, userId);
}

const parentId = await noteService.getParentNoteIdByNoteId(note.id);

const parentNote = parentId !== null ? definePublicNote(await noteService.getNoteById(parentId)) : undefined;
Expand Down Expand Up @@ -367,6 +386,13 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don

const isDeleted = await noteService.unlinkParent(noteId);

/**
* Delete all visits of the note
*
* @todo use event bus to delete note visits
*/
await noteVisitsService.deleteNoteVisits(noteId);

/**
* Check if parent relation was successfully deleted
*/
Expand Down Expand Up @@ -417,7 +443,7 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
},
}, async (request, reply) => {
const params = request.params;

const { userId } = request;
const note = await noteService.getNoteByHostname(params.hostname);

/**
Expand All @@ -427,6 +453,15 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
return reply.notFound('Note not found');
}

/**
* Save note visit if user is authorized
*
* @todo use event bus to save note visits
*/
if (userId !== null) {
await noteVisitsService.saveVisit(note.id, userId);
}

/**
* By default, unauthorized user can not edit the note
*/
Expand Down
9 changes: 9 additions & 0 deletions src/repository/noteVisits.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ export default class NoteVisitsRepository {
public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise<NoteVisit> {
return await this.storage.saveVisit(noteId, userId);
}

/**
* Deletes all visits of the note when a note is deleted
*
* @param noteId - note internal id
*/
public async deleteNoteVisits(noteId: NoteInternalId): Promise<boolean> {
return await this.storage.deleteNoteVisits(noteId);
}
}
57 changes: 49 additions & 8 deletions src/repository/storage/postgres/orm/sequelize/noteVisits.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type NoteVisit from '@domain/entities/noteVisit.js';
import type User from '@domain/entities/user.js';
import type { Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, ModelStatic } from 'sequelize';
import { literal } from 'sequelize';
import { Model, DataTypes } from 'sequelize';
import type Orm from '@repository/storage/postgres/orm/sequelize/index.js';
import { NoteModel } from './note.js';
Expand Down Expand Up @@ -117,18 +118,58 @@ export default class NoteVisitsSequelizeStorage {
* If user has already visited note, then existing record will be updated
* If user is visiting note for the first time, new record will be created
*/
/* eslint-disable-next-line */
const [recentVisit, _] = await this.model.upsert({
noteId,
userId,
}, {
conflictWhere: {
const existingVisit = await this.model.findOne({
where: {
noteId,
userId,
},
returning: true,
});

return recentVisit;
let updatedVisits: NoteVisit[];
let _;

if (existingVisit === null) {
return await this.model.create({
noteId,
userId,
/**
* we should pass to model datatype respectfully to declared in NoteVisitsModel class
* if we will pass just 'CLOCK_TIMESTAMP()' it will be treated by orm just like a string, that is why we should use literal
* but model wants string, this is why we use this cast
*/
visitedAt: literal('CLOCK_TIMESTAMP()') as unknown as string,
});
} else {
[_, updatedVisits] = await this.model.update({
visitedAt: literal('CLOCK_TIMESTAMP()') as unknown as string,
}, {
where: {
noteId,
userId,
},
returning: true,
});
}

return updatedVisits[0];
}

/**
* Deletes all visits of the note when a note is deleted
*
* @param noteId - note internal id
*/
public async deleteNoteVisits(noteId: NoteInternalId): Promise<boolean> {
const deletedNoteVisits = await this.model.destroy({
where: {
noteId,
},
});

if (deletedNoteVisits) {
return true;
}

return false;
}
}
4 changes: 2 additions & 2 deletions src/tests/utils/database-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@ export default class DatabaseHelpers {
*
* @param visit object which contain all info about noteVisit (visitedAt is optional)
*
* if no visitedAt passed, then visited_at would have CURRENT_DATE value
* if no visitedAt passed, then visited_at would have CLOCK_TIMESTAMP() value
*/
public async insertNoteVisit(visit: NoteVisitCreationAttributes): Promise<NoteVisit> {
const visitedAt = visit.visitedAt ?? 'NOW()';
const visitedAt = visit.visitedAt ?? 'CLOCK_TIMESTAMP()';


const [results, _] = await this.orm.connection.query(`INSERT INTO public.note_visits ("user_id", "note_id", "visited_at")
Expand Down

0 comments on commit a75c07e

Please sign in to comment.