Skip to content

Commit

Permalink
Merge pull request #234 from codex-team/migration-add-note-views-table
Browse files Browse the repository at this point in the history
feat: recent note views
  • Loading branch information
e11sy authored Mar 23, 2024
2 parents bc69668 + b3bfbf8 commit 4325330
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 3 deletions.
17 changes: 17 additions & 0 deletions migrations/tenant/0027-note-visits@add-note-visits-table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--
-- Name: note_visits; Type: TABLE; Schema: public; Owner: codex
--

CREATE TABLE IF NOT EXISTS public.note_visits (
id SERIAL PRIMARY KEY,
user_id integer NOT NULL REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE,
note_id integer NOT NULL REFERENCES public.notes(id) ON UPDATE CASCADE ON DELETE CASCADE,
"visited_at" timestamp with time zone NOT NULL
);

--
-- Name note_visits note_visits_user_id_idx; Type: INDEX; Schema: public; Owner: codex
--
CREATE UNIQUE INDEX note_visits_user_id_idx ON public.note_visits (user_id);

ALTER TABLE public.note_visits OWNER TO codex;
27 changes: 27 additions & 0 deletions src/domain/entities/noteVisit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { NoteInternalId } from '@domain/entities/note.ts';
import type User from '@domain/entities/user.ts';

/**
* NoteVisit is used to store data about the last interaction between the user and the note
*/
export default interface NoteVisit {
/**
* Unique property identifier
*/
id: number,

/**
* Internal id of the note
*/
noteId: NoteInternalId,

/**
* Id of the user
*/
userId: User['id'],

/**
* Time when note was visited for the last time (timestamp with timezone)
*/
visitedAt: string,
}
14 changes: 13 additions & 1 deletion src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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';

/**
* Interface for initiated services
Expand Down Expand Up @@ -42,12 +43,21 @@ export interface DomainServices {
* AI service instance
*/
aiService: AIService

/**
* Editor tools service instance
*/
editorToolsService: EditorToolsService,

/**
* File uploader service instance
*/
fileUploaderService: FileUploaderService
fileUploaderService: FileUploaderService,

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

/**
Expand All @@ -59,6 +69,7 @@ export interface DomainServices {
export function init(repositories: Repositories, appConfig: AppConfig): DomainServices {
const noteListService = new NoteListService(repositories.noteRepository);
const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository);
const noteVisitsService = new NoteVisitsService(repositories.noteVisitsRepository);

const authService = new AuthService(
appConfig.auth.accessSecret,
Expand Down Expand Up @@ -92,5 +103,6 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe
authService,
aiService,
editorToolsService,
noteVisitsService,
};
}
2 changes: 1 addition & 1 deletion src/domain/service/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default class NoteService {
}

/**
* @todo: add check, that new parent is not in childNotes to avoid circular references
* @todo add check, that new parent is not in childNotes to avoid circular references
*/
return await this.noteRelationsRepository.updateNoteRelationById(noteId, parentNote.id);
};
Expand Down
34 changes: 34 additions & 0 deletions src/domain/service/noteVisits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { NoteInternalId } from '@domain/entities/note';
import type User from '@domain/entities/user.js';
import type NoteVisit from '@domain/entities/noteVisit.js';
import type NoteVisitsRepository from '@repository/noteVisits.repository';

/**
* Note Visits service, which will store latest note visit
* it is used to display recent notes for each user
*/
export default class NoteVisitsService {
/**
* Note Visits repository
*/
public noteVisitsRepository: NoteVisitsRepository;

/**
* NoteVisits service constructor
*
* @param noteVisitRepository - note Visits repository
*/
constructor(noteVisitRepository: NoteVisitsRepository) {
this.noteVisitsRepository = noteVisitRepository;
}

/**
* Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time
*
* @param noteId - note internal id
* @param userId - id of the user
*/
public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise<NoteVisit> {
return await this.noteVisitsRepository.saveVisit(noteId, userId);
};
}
23 changes: 22 additions & 1 deletion src/repository/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { S3Storage } from './storage/s3/index.js';
import FileStorage from './storage/file.storage.js';
import FileRepository from './file.repository.js';
import ObjectStorageRepository from './object.repository.js';
import NoteVisitsRepository from './noteVisits.repository.js';
import NoteVisitsStorage from './storage/noteVisits.storage.js';

/**
* Interface for initiated repositories
Expand Down Expand Up @@ -55,6 +57,10 @@ export interface Repositories {
* AI repository instance
*/
aiRepository: AIRepository

/**
* Editor tools repository instance
*/
editorToolsRepository: EditorToolsRepository,

/**
Expand All @@ -71,6 +77,11 @@ export interface Repositories {
* Object repository instance
*/
objectStorageRepository: ObjectStorageRepository,

/**
* Note Visits repository instance
*/
noteVisitsRepository: NoteVisitsRepository,
}

/**
Expand Down Expand Up @@ -107,6 +118,8 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
const teamStorage = new TeamStorage(orm);
const fileStorage = new FileStorage(orm);
const s3Storage = new S3Storage(s3Config.accessKeyId, s3Config.secretAccessKey, s3Config.region, s3Config.endpoint);
const editorToolsStorage = new EditorToolsStorage(orm);
const noteVisitsStorage = new NoteVisitsStorage(orm);

/**
* Create associations between note and note settings
Expand All @@ -125,7 +138,12 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
*/
noteRelationshipStorage.createAssociationWithNoteModel(noteStorage.model);

const editorToolsStorage = new EditorToolsStorage(orm);
/**
* Create associations between note and noteVisit, user and noteVisit
*/
noteVisitsStorage.createAssociationWithNoteModel(noteStorage.model);
noteVisitsStorage.createAssociationWithUserModel(userStorage.model);


/**
* Prepare db structure
Expand All @@ -137,6 +155,7 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
await userSessionStorage.model.sync();
await editorToolsStorage.model.sync();
await noteRelationshipStorage.model.sync();
await noteVisitsStorage.model.sync();

/**
* Create transport instances
Expand All @@ -157,6 +176,7 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
const teamRepository = new TeamRepository(teamStorage);
const fileRepository = new FileRepository(fileStorage);
const objectStorageRepository = new ObjectStorageRepository(s3Storage);
const noteVisitsRepository = new NoteVisitsRepository(noteVisitsStorage);

return {
noteRepository,
Expand All @@ -169,5 +189,6 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
teamRepository,
fileRepository,
objectStorageRepository,
noteVisitsRepository,
};
}
30 changes: 30 additions & 0 deletions src/repository/noteVisits.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { NoteInternalId } from '@domain/entities/note.js';
import type NoteVisit from '@domain/entities/noteVisit.js';
import type User from '@domain/entities/user.js';
import type NoteVisitsStorage from '@repository/storage/noteVisits.storage.js';

/**
* Repository allows accessing data from business-logic (domain) level
*/
export default class NoteVisitsRepository {
public storage: NoteVisitsStorage;

/**
* Note Visits repository constructor
*
* @param storage - storage for note Visits
*/
constructor(storage: NoteVisitsStorage) {
this.storage = storage;
}
/**
* Updates existing noteVisit's vizitedAt or creates new record if user opens note for the first time
*
* @param noteId - note internal id
* @param userId - id of the user
* @returns updated or created NoteVisit
*/
public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise<NoteVisit> {
return await this.storage.saveVisit(noteId, userId);
}
}
5 changes: 5 additions & 0 deletions src/repository/storage/noteVisits.storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import NoteVisitsSequelizeStorage from './postgres/orm/sequelize/noteVisits.js';
/**
* Current note visits storage
*/
export default NoteVisitsSequelizeStorage;
132 changes: 132 additions & 0 deletions src/repository/storage/postgres/orm/sequelize/noteVisits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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 { Model, DataTypes } from 'sequelize';
import type Orm from '@repository/storage/postgres/orm/sequelize/index.js';
import { NoteModel } from './note.js';
import { UserModel } from './user.js';
import type { NoteInternalId } from '@domain/entities/note.js';


/**
*
*/
export class NoteVisitsModel extends Model<InferAttributes<NoteVisitsModel>, InferCreationAttributes<NoteVisitsModel>> {
public declare id: CreationOptional<NoteVisit['id']>;

public declare noteId: NoteVisit['noteId'];

public declare userId: NoteVisit['userId'];

public declare visitedAt: NoteVisit['visitedAt'];
}

/**
*
*/
export default class NoteVisitsSequelizeStorage {
public model: typeof NoteVisitsModel;

public userModel: typeof UserModel | null = null;

public noteModel: typeof NoteModel | null = null;

private readonly database: Sequelize;

private readonly tableName = 'note_visits';

/**
*
* @param ormInstance - ORM instance
*/
constructor({ connection }: Orm) {
this.database = connection;

this.model = NoteVisitsModel.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
noteId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: UserModel.tableName,
key: 'id',
},
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: NoteModel.tableName,
key: 'id',
},
},
visitedAt: {
type: DataTypes.DATE,
allowNull: false,
},
}, {
tableName: this.tableName,
sequelize: this.database,
});
};

/**
* Creates association with user model
*
* @param model - initialized note settings model
*/
public createAssociationWithUserModel(model: ModelStatic<UserModel>): void {
this.userModel = model;

this.model.belongsTo(this.userModel, {
foreignKey: 'userId',
as: this.userModel.tableName,
});
}

/**
* Creates association with note model
*
* @param model - initialized note model
*/
public createAssociationWithNoteModel(model: ModelStatic<NoteModel>): void {
this.noteModel = model;

this.model.belongsTo(this.noteModel, {
foreignKey: 'noteId',
as: this.noteModel.tableName,
});
}

/**
* Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time
*
* @param noteId - note internal id
* @param userId - id of the user
* @returns created or updated NoteVisit
*/
public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise<NoteVisit> {
/**
* 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,
visitedAt: 'CURRENT TIME',
}, {
conflictWhere: {
noteId,
userId,
},
returning: true,
});

return recentVisit;
}
}

0 comments on commit 4325330

Please sign in to comment.