-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #234 from codex-team/migration-add-note-views-table
feat: recent note views
- Loading branch information
Showing
9 changed files
with
281 additions
and
3 deletions.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
migrations/tenant/0027-note-visits@add-note-visits-table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
src/repository/storage/postgres/orm/sequelize/noteVisits.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |