fix(history-entry): remove composite primary keys

TypeORM promises to support composite primary keys,
but that does not work in reality.
This replaces the composite key used in the permission entities with
a single generated primary key and
a unique index on the relation columns.

See https://github.com/typeorm/typeorm/issues/8513

Signed-off-by: David Mehren <git@herrmehren.de>
This commit is contained in:
David Mehren 2022-09-18 18:59:10 +02:00
parent d1c3058655
commit a626ace4b9
7 changed files with 60 additions and 55 deletions

View file

@ -6,8 +6,9 @@
import {
Column,
Entity,
Index,
ManyToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@ -15,28 +16,22 @@ import { Note } from '../notes/note.entity';
import { User } from '../users/user.entity';
@Entity()
@Index(['note', 'user'], { unique: true })
export class HistoryEntry {
/**
* The `user` and `note` properties cannot be converted to promises
* (to lazy-load them), as TypeORM gets confused about lazy composite
* primary keys.
* See https://github.com/typeorm/typeorm/issues/6908
*/
@PrimaryColumn()
userId: string;
@PrimaryGeneratedColumn()
id: number;
@ManyToOne((_) => User, (user) => user.historyEntries, {
onDelete: 'CASCADE',
orphanedRowAction: 'delete', // This ensures the whole row is deleted when the Entry stops being referenced
})
user: User;
@PrimaryColumn()
noteId: string;
user: Promise<User>;
@ManyToOne((_) => Note, (note) => note.historyEntries, {
onDelete: 'CASCADE',
orphanedRowAction: 'delete', // This ensures the whole row is deleted when the Entry stops being referenced
})
note: Note;
note: Promise<Note>;
@Column()
pinStatus: boolean;
@ -56,8 +51,8 @@ export class HistoryEntry {
pinStatus = false,
): Omit<HistoryEntry, 'updatedAt'> {
const newHistoryEntry = new HistoryEntry();
newHistoryEntry.user = user;
newHistoryEntry.note = note;
newHistoryEntry.user = Promise.resolve(user);
newHistoryEntry.note = Promise.resolve(note);
newHistoryEntry.pinStatus = pinStatus;
return newHistoryEntry;
}

View file

@ -178,10 +178,12 @@ describe('HistoryService', () => {
Note.create(user, alias) as Note,
user,
);
expect(await createHistoryEntry.note.aliases).toHaveLength(1);
expect((await createHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(await createHistoryEntry.note.owner).toEqual(user);
expect(createHistoryEntry.user).toEqual(user);
expect(await (await createHistoryEntry.note).aliases).toHaveLength(1);
expect((await (await createHistoryEntry.note).aliases)[0].name).toEqual(
alias,
);
expect(await (await createHistoryEntry.note).owner).toEqual(user);
expect(await createHistoryEntry.user).toEqual(user);
expect(createHistoryEntry.pinStatus).toEqual(false);
});
@ -196,10 +198,12 @@ describe('HistoryService', () => {
Note.create(user, alias) as Note,
user,
);
expect(await createHistoryEntry.note.aliases).toHaveLength(1);
expect((await createHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(await createHistoryEntry.note.owner).toEqual(user);
expect(createHistoryEntry.user).toEqual(user);
expect(await (await createHistoryEntry.note).aliases).toHaveLength(1);
expect((await (await createHistoryEntry.note).aliases)[0].name).toEqual(
alias,
);
expect(await (await createHistoryEntry.note).owner).toEqual(user);
expect(await createHistoryEntry.user).toEqual(user);
expect(createHistoryEntry.pinStatus).toEqual(false);
expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual(
historyEntry.updatedAt.getTime(),
@ -231,10 +235,12 @@ describe('HistoryService', () => {
pinStatus: true,
},
);
expect(await updatedHistoryEntry.note.aliases).toHaveLength(1);
expect((await updatedHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(await updatedHistoryEntry.note.owner).toEqual(user);
expect(updatedHistoryEntry.user).toEqual(user);
expect(await (await updatedHistoryEntry.note).aliases).toHaveLength(1);
expect(
(await (await updatedHistoryEntry.note).aliases)[0].name,
).toEqual(alias);
expect(await (await updatedHistoryEntry.note).owner).toEqual(user);
expect(await updatedHistoryEntry.user).toEqual(user);
expect(updatedHistoryEntry.pinStatus).toEqual(true);
});
@ -357,13 +363,13 @@ describe('HistoryService', () => {
remove: jest
.fn()
.mockImplementationOnce(async (entry: HistoryEntry) => {
expect(await entry.note.aliases).toHaveLength(1);
expect((await entry.note.aliases)[0].name).toEqual(alias);
expect(await (await entry.note).aliases).toHaveLength(1);
expect((await (await entry.note).aliases)[0].name).toEqual(alias);
expect(entry.pinStatus).toEqual(false);
}),
save: jest.fn().mockImplementationOnce((entry: HistoryEntry) => {
expect(entry.note.aliases).toEqual(
newlyCreatedHistoryEntry.note.aliases,
save: jest.fn().mockImplementationOnce(async (entry: HistoryEntry) => {
expect((await entry.note).aliases).toEqual(
(await newlyCreatedHistoryEntry.note).aliases,
);
expect(entry.pinStatus).toEqual(newlyCreatedHistoryEntry.pinStatus);
expect(entry.updatedAt).toEqual(newlyCreatedHistoryEntry.updatedAt);

View file

@ -177,8 +177,8 @@ export class HistoryService {
return {
identifier: await getIdentifier(entry),
lastVisitedAt: entry.updatedAt,
tags: await this.notesService.toTagList(entry.note),
title: entry.note.title ?? '',
tags: await this.notesService.toTagList(await entry.note),
title: (await entry.note).title ?? '',
pinStatus: entry.pinStatus,
};
}

View file

@ -7,13 +7,13 @@ import { getPrimaryAlias } from '../notes/utils';
import { HistoryEntry } from './history-entry.entity';
export async function getIdentifier(entry: HistoryEntry): Promise<string> {
const aliases = await entry.note.aliases;
const aliases = await (await entry.note).aliases;
if (!aliases || aliases.length === 0) {
return entry.note.publicId;
return (await entry.note).publicId;
}
const primaryAlias = await getPrimaryAlias(entry.note);
const primaryAlias = await getPrimaryAlias(await entry.note);
if (primaryAlias === undefined) {
return entry.note.publicId;
return (await entry.note).publicId;
}
return primaryAlias;
}

View file

@ -313,7 +313,7 @@ describe('NotesService', () => {
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(await newNote.historyEntries).toHaveLength(1);
expect((await newNote.historyEntries)[0].user).toEqual(user);
expect(await (await newNote.historyEntries)[0].user).toEqual(user);
expect(await newNote.userPermissions).toHaveLength(0);
expect(await newNote.groupPermissions).toHaveLength(0);
expect(await newNote.tags).toHaveLength(0);
@ -338,7 +338,7 @@ describe('NotesService', () => {
expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content);
expect(await newNote.historyEntries).toHaveLength(1);
expect((await newNote.historyEntries)[0].user).toEqual(user);
expect(await (await newNote.historyEntries)[0].user).toEqual(user);
expect(await newNote.userPermissions).toHaveLength(0);
expect(await newNote.groupPermissions).toHaveLength(0);
expect(await newNote.tags).toHaveLength(0);

View file

@ -101,16 +101,16 @@ describe('History', () => {
.expect(201);
const userEntries = await testSetup.historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1);
expect((await userEntries[0].note.aliases)[0].name).toEqual(
expect((await (await userEntries[0].note).aliases)[0].name).toEqual(
(await note2.aliases)[0].name,
);
expect((await userEntries[0].note.aliases)[0].primary).toEqual(
expect((await (await userEntries[0].note).aliases)[0].primary).toEqual(
(await note2.aliases)[0].primary,
);
expect((await userEntries[0].note.aliases)[0].id).toEqual(
expect((await (await userEntries[0].note).aliases)[0].id).toEqual(
(await note2.aliases)[0].id,
);
expect(userEntries[0].user.username).toEqual(user.username);
expect((await userEntries[0].user).username).toEqual(user.username);
expect(userEntries[0].pinStatus).toEqual(pinStatus);
expect(userEntries[0].updatedAt).toEqual(lastVisited);
});
@ -161,11 +161,13 @@ describe('History', () => {
user,
);
expect(historyEntries).toHaveLength(1);
expect(await historyEntries[0].note.aliases).toEqual(
await prevEntry.note.aliases,
expect(await (await historyEntries[0].note).aliases).toEqual(
await (
await prevEntry.note
).aliases,
);
expect(historyEntries[0].user.username).toEqual(
prevEntry.user.username,
expect((await historyEntries[0].user).username).toEqual(
(await prevEntry.user).username,
);
expect(historyEntries[0].pinStatus).toEqual(prevEntry.pinStatus);
expect(historyEntries[0].updatedAt).toEqual(prevEntry.updatedAt);
@ -189,8 +191,9 @@ describe('History', () => {
user,
);
expect(entry.pinStatus).toBeFalsy();
const alias = (await entry.note.aliases).filter((alias) => alias.primary)[0]
.name;
const alias = (await (await entry.note).aliases).filter(
(alias) => alias.primary,
)[0].name;
await agent
.put(`/api/private/me/history/${alias || 'null'}`)
.send({ pinStatus: true })
@ -203,8 +206,9 @@ describe('History', () => {
it('DELETE /me/history/:note', async () => {
const entry = await historyService.updateHistoryEntryTimestamp(note2, user);
const alias = (await entry.note.aliases).filter((alias) => alias.primary)[0]
.name;
const alias = (await (await entry.note).aliases).filter(
(alias) => alias.primary,
)[0].name;
const entry2 = await historyService.updateHistoryEntryTimestamp(note, user);
const entryDto = await historyService.toHistoryEntryDto(entry2);
await agent

View file

@ -118,7 +118,7 @@ describe('Me', () => {
let theEntry: HistoryEntryDto;
for (const entry of history) {
if (
(await entry.note.aliases).find(
(await (await entry.note).aliases).find(
(element) => element.name === noteName,
)
) {
@ -147,7 +147,7 @@ describe('Me', () => {
const history = await testSetup.historyService.getEntriesByUser(user);
for (const entry of history) {
if (
(await entry.note.aliases).find(
(await (await entry.note).aliases).find(
(element) => element.name === noteName,
)
) {