Merge pull request #1827 from hedgedoc/enhancement/lazy_load_relations

This commit is contained in:
David Mehren 2022-01-03 19:51:39 +01:00 committed by GitHub
commit 745a1078f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 531 additions and 442 deletions

View file

@ -56,7 +56,7 @@ export class AliasController {
const note = await this.noteService.getNoteByIdOrAlias( const note = await this.noteService.getNoteByIdOrAlias(
newAliasDto.noteIdOrAlias, newAliasDto.noteIdOrAlias,
); );
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
const updatedAlias = await this.aliasService.addAlias( const updatedAlias = await this.aliasService.addAlias(
@ -88,7 +88,7 @@ export class AliasController {
} }
try { try {
const note = await this.noteService.getNoteByIdOrAlias(alias); const note = await this.noteService.getNoteByIdOrAlias(alias);
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
const updatedAlias = await this.aliasService.makeAliasPrimary( const updatedAlias = await this.aliasService.makeAliasPrimary(
@ -115,7 +115,7 @@ export class AliasController {
): Promise<void> { ): Promise<void> {
try { try {
const note = await this.noteService.getNoteByIdOrAlias(alias); const note = await this.noteService.getNoteByIdOrAlias(alias);
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
await this.aliasService.removeAlias(note, alias); await this.aliasService.removeAlias(note, alias);

View file

@ -45,8 +45,10 @@ export class HistoryController {
async getHistory(@RequestUser() user: User): Promise<HistoryEntryDto[]> { async getHistory(@RequestUser() user: User): Promise<HistoryEntryDto[]> {
try { try {
const foundEntries = await this.historyService.getEntriesByUser(user); const foundEntries = await this.historyService.getEntriesByUser(user);
return foundEntries.map((entry) => return await Promise.all(
this.historyService.toHistoryEntryDto(entry), foundEntries.map((entry) =>
this.historyService.toHistoryEntryDto(entry),
),
); );
} catch (e) { } catch (e) {
if (e instanceof NotInDBError) { if (e instanceof NotInDBError) {
@ -96,7 +98,7 @@ export class HistoryController {
user, user,
entryUpdateDto, entryUpdateDto,
); );
return this.historyService.toHistoryEntryDto(newEntry); return await this.historyService.toHistoryEntryDto(newEntry);
} catch (e) { } catch (e) {
if (e instanceof NotInDBError) { if (e instanceof NotInDBError) {
throw new NotFoundException(e.message); throw new NotFoundException(e.message);

View file

@ -40,7 +40,9 @@ export class MeController {
@Get('media') @Get('media')
async getMyMedia(@RequestUser() user: User): Promise<MediaUploadDto[]> { async getMyMedia(@RequestUser() user: User): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByUser(user); const media = await this.mediaService.listUploadsByUser(user);
return media.map((media) => this.mediaService.toMediaUploadDto(media)); return await Promise.all(
media.map((media) => this.mediaService.toMediaUploadDto(media)),
);
} }
@Delete() @Delete()

View file

@ -131,7 +131,7 @@ export class MediaController {
const mediaUpload = await this.mediaService.findUploadByFilename( const mediaUpload = await this.mediaService.findUploadByFilename(
filename, filename,
); );
if (mediaUpload.user.username !== username) { if ((await mediaUpload.user).username !== username) {
this.logger.warn( this.logger.warn(
`${username} tried to delete '${filename}', but is not the owner`, `${username} tried to delete '${filename}', but is not the owner`,
'deleteMedia', 'deleteMedia',

View file

@ -76,7 +76,9 @@ export class NotesController {
@UseGuards(PermissionsGuard) @UseGuards(PermissionsGuard)
async getNotesMedia(@RequestNote() note: Note): Promise<MediaUploadDto[]> { async getNotesMedia(@RequestNote() note: Note): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByNote(note); const media = await this.mediaService.listUploadsByNote(note);
return media.map((media) => this.mediaService.toMediaUploadDto(media)); return await Promise.all(
media.map((media) => this.mediaService.toMediaUploadDto(media)),
);
} }
@Post() @Post()

View file

@ -69,7 +69,7 @@ export class AliasController {
const note = await this.noteService.getNoteByIdOrAlias( const note = await this.noteService.getNoteByIdOrAlias(
newAliasDto.noteIdOrAlias, newAliasDto.noteIdOrAlias,
); );
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
const updatedAlias = await this.aliasService.addAlias( const updatedAlias = await this.aliasService.addAlias(
@ -107,7 +107,7 @@ export class AliasController {
} }
try { try {
const note = await this.noteService.getNoteByIdOrAlias(alias); const note = await this.noteService.getNoteByIdOrAlias(alias);
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
const updatedAlias = await this.aliasService.makeAliasPrimary( const updatedAlias = await this.aliasService.makeAliasPrimary(
@ -139,7 +139,7 @@ export class AliasController {
): Promise<void> { ): Promise<void> {
try { try {
const note = await this.noteService.getNoteByIdOrAlias(alias); const note = await this.noteService.getNoteByIdOrAlias(alias);
if (!this.permissionsService.isOwner(user, note)) { if (!(await this.permissionsService.isOwner(user, note))) {
throw new UnauthorizedException('Reading note denied!'); throw new UnauthorizedException('Reading note denied!');
} }
await this.aliasService.removeAlias(note, alias); await this.aliasService.removeAlias(note, alias);

View file

@ -101,7 +101,7 @@ export class MeController {
): Promise<HistoryEntryDto> { ): Promise<HistoryEntryDto> {
try { try {
const foundEntry = await this.historyService.getEntryByNote(note, user); const foundEntry = await this.historyService.getEntryByNote(note, user);
return this.historyService.toHistoryEntryDto(foundEntry); return await this.historyService.toHistoryEntryDto(foundEntry);
} catch (e) { } catch (e) {
if (e instanceof NotInDBError) { if (e instanceof NotInDBError) {
throw new NotFoundException(e.message); throw new NotFoundException(e.message);
@ -126,7 +126,7 @@ export class MeController {
): Promise<HistoryEntryDto> { ): Promise<HistoryEntryDto> {
// ToDo: Check if user is allowed to pin this history entry // ToDo: Check if user is allowed to pin this history entry
try { try {
return this.historyService.toHistoryEntryDto( return await this.historyService.toHistoryEntryDto(
await this.historyService.updateHistoryEntry( await this.historyService.updateHistoryEntry(
note, note,
user, user,
@ -188,6 +188,8 @@ export class MeController {
@ApiUnauthorizedResponse({ description: unauthorizedDescription }) @ApiUnauthorizedResponse({ description: unauthorizedDescription })
async getMyMedia(@RequestUser() user: User): Promise<MediaUploadDto[]> { async getMyMedia(@RequestUser() user: User): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByUser(user); const media = await this.mediaService.listUploadsByUser(user);
return media.map((media) => this.mediaService.toMediaUploadDto(media)); return await Promise.all(
media.map((media) => this.mediaService.toMediaUploadDto(media)),
);
} }
} }

View file

@ -136,7 +136,7 @@ export class MediaController {
const mediaUpload = await this.mediaService.findUploadByFilename( const mediaUpload = await this.mediaService.findUploadByFilename(
filename, filename,
); );
if (mediaUpload.user.username !== username) { if ((await mediaUpload.user).username !== username) {
this.logger.warn( this.logger.warn(
`${username} tried to delete '${filename}', but is not the owner`, `${username} tried to delete '${filename}', but is not the owner`,
'deleteMedia', 'deleteMedia',

View file

@ -237,7 +237,7 @@ export class NotesController {
@RequestNote() note: Note, @RequestNote() note: Note,
@Body() updateDto: NotePermissionsUpdateDto, @Body() updateDto: NotePermissionsUpdateDto,
): Promise<NotePermissionsDto> { ): Promise<NotePermissionsDto> {
return this.noteService.toNotePermissionsDto( return await this.noteService.toNotePermissionsDto(
await this.noteService.updateNotePermissions(note, updateDto), await this.noteService.updateNotePermissions(note, updateDto),
); );
} }
@ -305,6 +305,8 @@ export class NotesController {
@RequestNote() note: Note, @RequestNote() note: Note,
): Promise<MediaUploadDto[]> { ): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByNote(note); const media = await this.mediaService.listUploadsByNote(note);
return media.map((media) => this.mediaService.toMediaUploadDto(media)); return await Promise.all(
media.map((media) => this.mediaService.toMediaUploadDto(media)),
);
} }
} }

View file

@ -55,11 +55,11 @@ export class PermissionsGuard implements CanActivate {
const note = await getNote(this.noteService, noteIdOrAlias); const note = await getNote(this.noteService, noteIdOrAlias);
switch (permissions[0]) { switch (permissions[0]) {
case Permission.READ: case Permission.READ:
return this.permissionsService.mayRead(user, note); return await this.permissionsService.mayRead(user, note);
case Permission.WRITE: case Permission.WRITE:
return this.permissionsService.mayWrite(user, note); return await this.permissionsService.mayWrite(user, note);
case Permission.OWNER: case Permission.OWNER:
return this.permissionsService.isOwner(user, note); return await this.permissionsService.isOwner(user, note);
} }
return false; return false;
} }

View file

@ -24,7 +24,7 @@ export class AuthToken {
@ManyToOne((_) => User, (user) => user.authTokens, { @ManyToOne((_) => User, (user) => user.authTokens, {
onDelete: 'CASCADE', // This deletes the AuthToken, when the associated User is deleted onDelete: 'CASCADE', // This deletes the AuthToken, when the associated User is deleted
}) })
user: User; user: Promise<User>;
@Column() @Column()
label: string; label: string;
@ -53,7 +53,7 @@ export class AuthToken {
): Omit<AuthToken, 'id' | 'createdAt'> { ): Omit<AuthToken, 'id' | 'createdAt'> {
const newToken = new AuthToken(); const newToken = new AuthToken();
newToken.keyId = keyId; newToken.keyId = keyId;
newToken.user = user; newToken.user = Promise.resolve(user);
newToken.label = label; newToken.label = label;
newToken.accessTokenHash = accessToken; newToken.accessTokenHash = accessToken;
newToken.validUntil = validUntil; newToken.validUntil = validUntil;

View file

@ -93,7 +93,7 @@ describe('AuthService', () => {
.digest('hex'); .digest('hex');
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
accessTokenHash: accessTokenHash, accessTokenHash: accessTokenHash,
}); });
const authTokenFromCall = await service.getAuthTokenAndValidate( const authTokenFromCall = await service.getAuthTokenAndValidate(
@ -102,7 +102,7 @@ describe('AuthService', () => {
); );
expect(authTokenFromCall).toEqual({ expect(authTokenFromCall).toEqual({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
accessTokenHash: accessTokenHash, accessTokenHash: accessTokenHash,
}); });
}); });
@ -116,7 +116,7 @@ describe('AuthService', () => {
it('AuthToken has wrong hash', async () => { it('AuthToken has wrong hash', async () => {
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
accessTokenHash: 'the wrong hash', accessTokenHash: 'the wrong hash',
}); });
await expect( await expect(
@ -127,7 +127,7 @@ describe('AuthService', () => {
const accessTokenHash = await hashPassword(token); const accessTokenHash = await hashPassword(token);
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
accessTokenHash: accessTokenHash, accessTokenHash: accessTokenHash,
validUntil: new Date(1549312452000), validUntil: new Date(1549312452000),
}); });
@ -142,7 +142,7 @@ describe('AuthService', () => {
it('works', async () => { it('works', async () => {
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
lastUsed: new Date(1549312452000), lastUsed: new Date(1549312452000),
}); });
jest jest
@ -174,11 +174,11 @@ describe('AuthService', () => {
.digest('hex'); .digest('hex');
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({ jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({
...user, ...user,
authTokens: [authToken], authTokens: Promise.resolve([authToken]),
}); });
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
accessTokenHash: accessTokenHash, accessTokenHash: accessTokenHash,
}); });
jest jest
@ -191,7 +191,7 @@ describe('AuthService', () => {
); );
expect(userByToken).toEqual({ expect(userByToken).toEqual({
...user, ...user,
authTokens: [authToken], authTokens: Promise.resolve([authToken]),
}); });
}); });
describe('fails:', () => { describe('fails:', () => {
@ -212,14 +212,14 @@ describe('AuthService', () => {
it('works', async () => { it('works', async () => {
jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({ jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
}); });
jest jest
.spyOn(authTokenRepo, 'remove') .spyOn(authTokenRepo, 'remove')
.mockImplementationOnce(async (token, __): Promise<AuthToken> => { .mockImplementationOnce(async (token, __): Promise<AuthToken> => {
expect(token).toEqual({ expect(token).toEqual({
...authToken, ...authToken,
user: user, user: Promise.resolve(user),
}); });
return authToken; return authToken;
}); });

View file

@ -46,7 +46,11 @@ export class AuthService {
} }
const accessToken = await this.getAuthTokenAndValidate(keyId, secret); const accessToken = await this.getAuthTokenAndValidate(keyId, secret);
await this.setLastUsedToken(keyId); await this.setLastUsedToken(keyId);
return await this.usersService.getUserByUsername(accessToken.user.username); return await this.usersService.getUserByUsername(
(
await accessToken.user
).username,
);
} }
async createTokenForUser( async createTokenForUser(
@ -54,9 +58,9 @@ export class AuthService {
identifier: string, identifier: string,
validUntil: TimestampMillis, validUntil: TimestampMillis,
): Promise<AuthTokenWithSecretDto> { ): Promise<AuthTokenWithSecretDto> {
user.authTokens = await this.getTokensByUser(user); user.authTokens = this.getTokensByUser(user);
if (user.authTokens.length >= 200) { if ((await user.authTokens).length >= 200) {
// This is a very high ceiling unlikely to hinder legitimate usage, // This is a very high ceiling unlikely to hinder legitimate usage,
// but should prevent possible attack vectors // but should prevent possible attack vectors
throw new TooManyTokensError( throw new TooManyTokensError(

View file

@ -39,21 +39,21 @@ export class Author {
* Only contains sessions for anonymous users, which don't have a user set * Only contains sessions for anonymous users, which don't have a user set
*/ */
@OneToMany(() => Session, (session) => session.author) @OneToMany(() => Session, (session) => session.author)
sessions: Session[]; sessions: Promise<Session[]>;
/** /**
* User that this author corresponds to * User that this author corresponds to
* Only set when the user was identified (by a browser session) as a registered user at edit-time * Only set when the user was identified (by a browser session) as a registered user at edit-time
*/ */
@ManyToOne(() => User, (user) => user.authors, { nullable: true }) @ManyToOne(() => User, (user) => user.authors, { nullable: true })
user: User | null; user: Promise<User | null>;
/** /**
* List of edits that this author created * List of edits that this author created
* All edits must belong to the same note * All edits must belong to the same note
*/ */
@OneToMany(() => Edit, (edit) => edit.author) @OneToMany(() => Edit, (edit) => edit.author)
edits: Edit[]; edits: Promise<Edit[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -61,9 +61,9 @@ export class Author {
public static create(color: number): Omit<Author, 'id'> { public static create(color: number): Omit<Author, 'id'> {
const newAuthor = new Author(); const newAuthor = new Author();
newAuthor.color = color; newAuthor.color = color;
newAuthor.sessions = []; newAuthor.sessions = Promise.resolve([]);
newAuthor.user = null; newAuthor.user = Promise.resolve(null);
newAuthor.edits = []; newAuthor.edits = Promise.resolve([]);
return newAuthor; return newAuthor;
} }
} }

View file

@ -38,7 +38,7 @@ export class Group {
eager: true, eager: true,
}) })
@JoinTable() @JoinTable()
members: User[]; members: Promise<User[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -52,7 +52,7 @@ export class Group {
newGroup.name = name; newGroup.name = name;
newGroup.displayName = displayName; newGroup.displayName = displayName;
newGroup.special = special; // this attribute should only be true for the two special groups newGroup.special = special; // this attribute should only be true for the two special groups
newGroup.members = []; newGroup.members = Promise.resolve([]);
return newGroup; return newGroup;
} }
} }

View file

@ -10,6 +10,13 @@ import { User } from '../users/user.entity';
@Entity() @Entity()
export class HistoryEntry { 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
*/
@ManyToOne((_) => User, (user) => user.historyEntries, { @ManyToOne((_) => User, (user) => user.historyEntries, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
primary: true, primary: true,

View file

@ -159,9 +159,9 @@ describe('HistoryService', () => {
Note.create(user, alias) as Note, Note.create(user, alias) as Note,
user, user,
); );
expect(createHistoryEntry.note.aliases).toHaveLength(1); expect(await createHistoryEntry.note.aliases).toHaveLength(1);
expect(createHistoryEntry.note.aliases[0].name).toEqual(alias); expect((await createHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(createHistoryEntry.note.owner).toEqual(user); expect(await createHistoryEntry.note.owner).toEqual(user);
expect(createHistoryEntry.user).toEqual(user); expect(createHistoryEntry.user).toEqual(user);
expect(createHistoryEntry.pinStatus).toEqual(false); expect(createHistoryEntry.pinStatus).toEqual(false);
}); });
@ -177,9 +177,9 @@ describe('HistoryService', () => {
Note.create(user, alias) as Note, Note.create(user, alias) as Note,
user, user,
); );
expect(createHistoryEntry.note.aliases).toHaveLength(1); expect(await createHistoryEntry.note.aliases).toHaveLength(1);
expect(createHistoryEntry.note.aliases[0].name).toEqual(alias); expect((await createHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(createHistoryEntry.note.owner).toEqual(user); expect(await createHistoryEntry.note.owner).toEqual(user);
expect(createHistoryEntry.user).toEqual(user); expect(createHistoryEntry.user).toEqual(user);
expect(createHistoryEntry.pinStatus).toEqual(false); expect(createHistoryEntry.pinStatus).toEqual(false);
expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual( expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual(
@ -223,9 +223,9 @@ describe('HistoryService', () => {
pinStatus: true, pinStatus: true,
}, },
); );
expect(updatedHistoryEntry.note.aliases).toHaveLength(1); expect(await updatedHistoryEntry.note.aliases).toHaveLength(1);
expect(updatedHistoryEntry.note.aliases[0].name).toEqual(alias); expect((await updatedHistoryEntry.note.aliases)[0].name).toEqual(alias);
expect(updatedHistoryEntry.note.owner).toEqual(user); expect(await updatedHistoryEntry.note.owner).toEqual(user);
expect(updatedHistoryEntry.user).toEqual(user); expect(updatedHistoryEntry.user).toEqual(user);
expect(updatedHistoryEntry.pinStatus).toEqual(true); expect(updatedHistoryEntry.pinStatus).toEqual(true);
}); });
@ -371,11 +371,13 @@ describe('HistoryService', () => {
const mockedManager = { const mockedManager = {
find: jest.fn().mockResolvedValueOnce([historyEntry]), find: jest.fn().mockResolvedValueOnce([historyEntry]),
createQueryBuilder: () => createQueryBuilder, createQueryBuilder: () => createQueryBuilder,
remove: jest.fn().mockImplementationOnce((entry: HistoryEntry) => { remove: jest
expect(entry.note.aliases).toHaveLength(1); .fn()
expect(entry.note.aliases[0].name).toEqual(alias); .mockImplementationOnce(async (entry: HistoryEntry) => {
expect(entry.pinStatus).toEqual(false); expect(await entry.note.aliases).toHaveLength(1);
}), expect((await entry.note.aliases)[0].name).toEqual(alias);
expect(entry.pinStatus).toEqual(false);
}),
save: jest.fn().mockImplementationOnce((entry: HistoryEntry) => { save: jest.fn().mockImplementationOnce((entry: HistoryEntry) => {
expect(entry.note.aliases).toEqual( expect(entry.note.aliases).toEqual(
newlyCreatedHistoryEntry.note.aliases, newlyCreatedHistoryEntry.note.aliases,
@ -402,11 +404,13 @@ describe('HistoryService', () => {
const tags = ['tag1', 'tag2']; const tags = ['tag1', 'tag2'];
const note = Note.create(user, alias) as Note; const note = Note.create(user, alias) as Note;
note.title = title; note.title = title;
note.tags = tags.map((tag) => { note.tags = Promise.resolve(
const newTag = new Tag(); tags.map((tag) => {
newTag.name = tag; const newTag = new Tag();
return newTag; newTag.name = tag;
}); return newTag;
}),
);
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry; const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
historyEntry.pinStatus = true; historyEntry.pinStatus = true;
const createQueryBuilder = { const createQueryBuilder = {
@ -420,7 +424,7 @@ describe('HistoryService', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
.mockImplementation(() => createQueryBuilder); .mockImplementation(() => createQueryBuilder);
const historyEntryDto = service.toHistoryEntryDto(historyEntry); const historyEntryDto = await service.toHistoryEntryDto(historyEntry);
expect(historyEntryDto.pinStatus).toEqual(true); expect(historyEntryDto.pinStatus).toEqual(true);
expect(historyEntryDto.identifier).toEqual(alias); expect(historyEntryDto.identifier).toEqual(alias);
expect(historyEntryDto.tags).toEqual(tags); expect(historyEntryDto.tags).toEqual(tags);

View file

@ -186,11 +186,11 @@ export class HistoryService {
* @param {HistoryEntry} entry - the history entry to use * @param {HistoryEntry} entry - the history entry to use
* @return {HistoryEntryDto} the built HistoryEntryDto * @return {HistoryEntryDto} the built HistoryEntryDto
*/ */
toHistoryEntryDto(entry: HistoryEntry): HistoryEntryDto { async toHistoryEntryDto(entry: HistoryEntry): Promise<HistoryEntryDto> {
return { return {
identifier: getIdentifier(entry), identifier: await getIdentifier(entry),
lastVisited: entry.updatedAt, lastVisited: entry.updatedAt,
tags: this.notesService.toTagList(entry.note), tags: await this.notesService.toTagList(entry.note),
title: entry.note.title ?? '', title: entry.note.title ?? '',
pinStatus: entry.pinStatus, pinStatus: entry.pinStatus,
}; };

View file

@ -18,19 +18,19 @@ describe('getIdentifier', () => {
note = Note.create(user, alias) as Note; note = Note.create(user, alias) as Note;
entry = HistoryEntry.create(user, note) as HistoryEntry; entry = HistoryEntry.create(user, note) as HistoryEntry;
}); });
it('returns the publicId if there are no aliases', () => { it('returns the publicId if there are no aliases', async () => {
note.aliases = undefined as unknown as Alias[]; note.aliases = Promise.resolve(undefined as unknown as Alias[]);
expect(getIdentifier(entry)).toEqual(note.publicId); expect(await getIdentifier(entry)).toEqual(note.publicId);
}); });
it('returns the publicId, if the alias array is empty', () => { it('returns the publicId, if the alias array is empty', async () => {
note.aliases = []; note.aliases = Promise.resolve([]);
expect(getIdentifier(entry)).toEqual(note.publicId); expect(await getIdentifier(entry)).toEqual(note.publicId);
}); });
it('returns the publicId, if the only alias is not primary', () => { it('returns the publicId, if the only alias is not primary', async () => {
note.aliases[0].primary = false; (await note.aliases)[0].primary = false;
expect(getIdentifier(entry)).toEqual(note.publicId); expect(await getIdentifier(entry)).toEqual(note.publicId);
}); });
it('returns the primary alias, if one exists', () => { it('returns the primary alias, if one exists', async () => {
expect(getIdentifier(entry)).toEqual(note.aliases[0].name); expect(await getIdentifier(entry)).toEqual((await note.aliases)[0].name);
}); });
}); });

View file

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

View file

@ -32,7 +32,7 @@ export class Identity {
@ManyToOne((_) => User, (user) => user.identities, { @ManyToOne((_) => User, (user) => user.identities, {
onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted
}) })
user: User; user: Promise<User>;
/** /**
* The ProviderType of the identity * The ProviderType of the identity
@ -103,7 +103,7 @@ export class Identity {
syncSource: boolean, syncSource: boolean,
): Omit<Identity, 'id' | 'createdAt' | 'updatedAt'> { ): Omit<Identity, 'id' | 'createdAt' | 'updatedAt'> {
const newIdentity = new Identity(); const newIdentity = new Identity();
newIdentity.user = user; newIdentity.user = Promise.resolve(user);
newIdentity.providerType = providerType; newIdentity.providerType = providerType;
newIdentity.providerName = null; newIdentity.providerName = null;
newIdentity.syncSource = syncSource; newIdentity.syncSource = syncSource;

View file

@ -60,7 +60,7 @@ describe('IdentityService', () => {
await checkPassword(password, identity.passwordHash ?? '').then( await checkPassword(password, identity.passwordHash ?? '').then(
(result) => expect(result).toBeTruthy(), (result) => expect(result).toBeTruthy(),
); );
expect(identity.user).toEqual(user); expect(await identity.user).toEqual(user);
}); });
}); });
@ -83,7 +83,7 @@ describe('IdentityService', () => {
await checkPassword(newPassword, identity.passwordHash ?? '').then( await checkPassword(newPassword, identity.passwordHash ?? '').then(
(result) => expect(result).toBeTruthy(), (result) => expect(result).toBeTruthy(),
); );
expect(identity.user).toEqual(user); expect(await identity.user).toEqual(user);
}); });
it('fails, when user has no local identity', async () => { it('fails, when user has no local identity', async () => {
user.identities = Promise.resolve([]); user.identities = Promise.resolve([]);

View file

@ -25,12 +25,12 @@ export class MediaUpload {
@ManyToOne((_) => Note, (note) => note.mediaUploads, { @ManyToOne((_) => Note, (note) => note.mediaUploads, {
nullable: true, nullable: true,
}) })
note: Note | null; note: Promise<Note | null>;
@ManyToOne((_) => User, (user) => user.mediaUploads, { @ManyToOne((_) => User, (user) => user.mediaUploads, {
nullable: false, nullable: false,
}) })
user: User; user: Promise<User>;
@Column({ @Column({
nullable: false, nullable: false,
@ -72,8 +72,8 @@ export class MediaUpload {
): Omit<MediaUpload, 'createdAt'> { ): Omit<MediaUpload, 'createdAt'> {
const upload = new MediaUpload(); const upload = new MediaUpload();
upload.id = id; upload.id = id;
upload.note = note; upload.note = Promise.resolve(note);
upload.user = user; upload.user = Promise.resolve(user);
upload.backendType = backendType; upload.backendType = backendType;
upload.backendData = null; upload.backendData = null;
upload.fileUrl = fileUrl; upload.fileUrl = fileUrl;

View file

@ -167,9 +167,9 @@ describe('MediaService', () => {
const mockMediaUploadEntry = { const mockMediaUploadEntry = {
id: 'testMediaUpload', id: 'testMediaUpload',
backendData: 'testBackendData', backendData: 'testBackendData',
user: { user: Promise.resolve({
username: 'hardcoded', username: 'hardcoded',
} as User, } as User),
} as MediaUpload; } as MediaUpload;
jest jest
.spyOn(service.mediaBackend, 'deleteFile') .spyOn(service.mediaBackend, 'deleteFile')
@ -196,15 +196,15 @@ describe('MediaService', () => {
const mockMediaUploadEntry = { const mockMediaUploadEntry = {
id: 'testMediaUpload', id: 'testMediaUpload',
backendData: backendData, backendData: backendData,
user: { user: Promise.resolve({
username: username, username: username,
} as User, } as User),
} as MediaUpload; } as MediaUpload;
jest jest
.spyOn(mediaRepo, 'findOne') .spyOn(mediaRepo, 'findOne')
.mockResolvedValueOnce(mockMediaUploadEntry); .mockResolvedValueOnce(mockMediaUploadEntry);
const mediaUpload = await service.findUploadByFilename(testFileName); const mediaUpload = await service.findUploadByFilename(testFileName);
expect(mediaUpload.user.username).toEqual(username); expect((await mediaUpload.user).username).toEqual(username);
expect(mediaUpload.backendData).toEqual(backendData); expect(mediaUpload.backendData).toEqual(backendData);
}); });
it("fails: can't find mediaUpload", async () => { it("fails: can't find mediaUpload", async () => {
@ -218,13 +218,14 @@ describe('MediaService', () => {
describe('listUploadsByUser', () => { describe('listUploadsByUser', () => {
describe('works', () => { describe('works', () => {
const username = 'hardcoded';
it('with one upload from user', async () => { it('with one upload from user', async () => {
const mockMediaUploadEntry = { const mockMediaUploadEntry = {
id: 'testMediaUpload', id: 'testMediaUpload',
backendData: 'testBackendData', backendData: 'testBackendData',
user: { user: Promise.resolve({
username: 'hardcoded', username: username,
} as User, } as User),
} as MediaUpload; } as MediaUpload;
jest jest
.spyOn(mediaRepo, 'find') .spyOn(mediaRepo, 'find')
@ -237,14 +238,14 @@ describe('MediaService', () => {
it('without uploads from user', async () => { it('without uploads from user', async () => {
jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce([]); jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce([]);
const mediaList = await service.listUploadsByUser({ const mediaList = await service.listUploadsByUser({
username: 'hardcoded', username: username,
} as User); } as User);
expect(mediaList).toEqual([]); expect(mediaList).toEqual([]);
}); });
it('with error (undefined as return value of find)', async () => { it('with error (undefined as return value of find)', async () => {
jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce(undefined); jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce(undefined);
const mediaList = await service.listUploadsByUser({ const mediaList = await service.listUploadsByUser({
username: 'hardcoded', username: username,
} as User); } as User);
expect(mediaList).toEqual([]); expect(mediaList).toEqual([]);
}); });
@ -257,9 +258,9 @@ describe('MediaService', () => {
const mockMediaUploadEntry = { const mockMediaUploadEntry = {
id: 'testMediaUpload', id: 'testMediaUpload',
backendData: 'testBackendData', backendData: 'testBackendData',
note: { note: Promise.resolve({
id: '123', id: '123',
} as Note, } as Note),
} as MediaUpload; } as MediaUpload;
jest jest
.spyOn(mediaRepo, 'find') .spyOn(mediaRepo, 'find')
@ -290,19 +291,21 @@ describe('MediaService', () => {
describe('removeNoteFromMediaUpload', () => { describe('removeNoteFromMediaUpload', () => {
it('works', async () => { it('works', async () => {
const mockNote = {} as Note; const mockNote = {} as Note;
mockNote.aliases = [Alias.create('test', mockNote, true) as Alias]; mockNote.aliases = Promise.resolve([
Alias.create('test', mockNote, true) as Alias,
]);
const mockMediaUploadEntry = { const mockMediaUploadEntry = {
id: 'testMediaUpload', id: 'testMediaUpload',
backendData: 'testBackendData', backendData: 'testBackendData',
note: mockNote, note: Promise.resolve(mockNote),
user: { user: Promise.resolve({
username: 'hardcoded', username: 'hardcoded',
} as User, } as User),
} as MediaUpload; } as MediaUpload;
jest jest
.spyOn(mediaRepo, 'save') .spyOn(mediaRepo, 'save')
.mockImplementationOnce(async (entry: MediaUpload) => { .mockImplementationOnce(async (entry: MediaUpload) => {
expect(entry.note).toBeNull(); expect(await entry.note).toBeNull();
return entry; return entry;
}); });
await service.removeNoteFromMediaUpload(mockMediaUploadEntry); await service.removeNoteFromMediaUpload(mockMediaUploadEntry);

View file

@ -181,7 +181,7 @@ export class MediaService {
'Setting note to null for mediaUpload: ' + mediaUpload.id, 'Setting note to null for mediaUpload: ' + mediaUpload.id,
'removeNoteFromMediaUpload', 'removeNoteFromMediaUpload',
); );
mediaUpload.note = null; mediaUpload.note = Promise.resolve(null);
await this.mediaUploadRepository.save(mediaUpload); await this.mediaUploadRepository.save(mediaUpload);
} }
@ -219,12 +219,12 @@ export class MediaService {
} }
} }
toMediaUploadDto(mediaUpload: MediaUpload): MediaUploadDto { async toMediaUploadDto(mediaUpload: MediaUpload): Promise<MediaUploadDto> {
return { return {
url: mediaUpload.fileUrl, url: mediaUpload.fileUrl,
noteId: mediaUpload.note?.id ?? null, noteId: (await mediaUpload.note)?.id ?? null,
createdAt: mediaUpload.createdAt, createdAt: mediaUpload.createdAt,
username: mediaUpload.user.username, username: (await mediaUpload.user).username,
}; };
} }

View file

@ -49,7 +49,7 @@ export class Alias {
@ManyToOne((_) => Note, (note) => note.aliases, { @ManyToOne((_) => Note, (note) => note.aliases, {
onDelete: 'CASCADE', // This deletes the Alias, when the associated Note is deleted onDelete: 'CASCADE', // This deletes the Alias, when the associated Note is deleted
}) })
note: Note; note: Promise<Note>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -58,7 +58,7 @@ export class Alias {
const alias = new Alias(); const alias = new Alias();
alias.name = name; alias.name = name;
alias.primary = primary; alias.primary = primary;
alias.note = note; alias.note = Promise.resolve(note);
return alias; return alias;
} }
} }

View file

@ -158,8 +158,11 @@ describe('AliasService', () => {
const alias2 = 'testAlias2'; const alias2 = 'testAlias2';
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
describe('removes one alias correctly', () => { describe('removes one alias correctly', () => {
const note = Note.create(user, alias) as Note; let note: Note;
note.aliases.push(Alias.create(alias2, note, false) as Alias); beforeAll(async () => {
note = Note.create(user, alias) as Note;
(await note.aliases).push(Alias.create(alias2, note, false) as Alias);
});
it('with two aliases', async () => { it('with two aliases', async () => {
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
@ -170,9 +173,10 @@ describe('AliasService', () => {
async (alias: Alias): Promise<Alias> => alias, async (alias: Alias): Promise<Alias> => alias,
); );
const savedNote = await service.removeAlias(note, alias2); const savedNote = await service.removeAlias(note, alias2);
expect(savedNote.aliases).toHaveLength(1); const aliases = await savedNote.aliases;
expect(savedNote.aliases[0].name).toEqual(alias); expect(aliases).toHaveLength(1);
expect(savedNote.aliases[0].primary).toBeTruthy(); expect(aliases[0].name).toEqual(alias);
expect(aliases[0].primary).toBeTruthy();
}); });
it('with one alias, that is primary', async () => { it('with one alias, that is primary', async () => {
jest jest
@ -184,12 +188,15 @@ describe('AliasService', () => {
async (alias: Alias): Promise<Alias> => alias, async (alias: Alias): Promise<Alias> => alias,
); );
const savedNote = await service.removeAlias(note, alias); const savedNote = await service.removeAlias(note, alias);
expect(savedNote.aliases).toHaveLength(0); expect(await savedNote.aliases).toHaveLength(0);
}); });
}); });
describe('does not remove one alias', () => { describe('does not remove one alias', () => {
const note = Note.create(user, alias) as Note; let note: Note;
note.aliases.push(Alias.create(alias2, note, false) as Alias); beforeEach(async () => {
note = Note.create(user, alias) as Note;
(await note.aliases).push(Alias.create(alias2, note, false) as Alias);
});
it('if the alias is unknown', async () => { it('if the alias is unknown', async () => {
await expect(service.removeAlias(note, 'non existent')).rejects.toThrow( await expect(service.removeAlias(note, 'non existent')).rejects.toThrow(
NotInDBError, NotInDBError,
@ -206,10 +213,18 @@ describe('AliasService', () => {
describe('makeAliasPrimary', () => { describe('makeAliasPrimary', () => {
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
const aliasName = 'testAlias'; const aliasName = 'testAlias';
const note = Note.create(user, aliasName) as Note; let note: Note;
const alias = Alias.create(aliasName, note, true) as Alias; let alias: Alias;
const alias2 = Alias.create('testAlias2', note, false) as Alias; let alias2: Alias;
note.aliases.push(alias2); beforeEach(async () => {
note = Note.create(user, aliasName) as Note;
alias = Alias.create(aliasName, note, true) as Alias;
alias2 = Alias.create('testAlias2', note, false) as Alias;
(await note.aliases).push(
Alias.create('testAlias2', note, false) as Alias,
);
});
it('mark the alias as primary', async () => { it('mark the alias as primary', async () => {
jest jest
.spyOn(aliasRepo, 'findOne') .spyOn(aliasRepo, 'findOne')
@ -224,10 +239,10 @@ describe('AliasService', () => {
where: () => createQueryBuilder, where: () => createQueryBuilder,
orWhere: () => createQueryBuilder, orWhere: () => createQueryBuilder,
setParameter: () => createQueryBuilder, setParameter: () => createQueryBuilder,
getOne: () => { getOne: async () => {
return { return {
...note, ...note,
aliases: note.aliases.map((anAlias) => { aliases: (await note.aliases).map((anAlias) => {
if (anAlias.primary) { if (anAlias.primary) {
anAlias.primary = false; anAlias.primary = false;
} }

View file

@ -62,13 +62,13 @@ export class AliasService {
); );
} }
let newAlias; let newAlias;
if (note.aliases.length === 0) { if ((await note.aliases).length === 0) {
// the first alias is automatically made the primary alias // the first alias is automatically made the primary alias
newAlias = Alias.create(alias, note, true); newAlias = Alias.create(alias, note, true);
} else { } else {
newAlias = Alias.create(alias, note, false); newAlias = Alias.create(alias, note, false);
} }
note.aliases.push(newAlias as Alias); (await note.aliases).push(newAlias as Alias);
await this.noteRepository.save(note); await this.noteRepository.save(note);
return newAlias as Alias; return newAlias as Alias;
@ -87,7 +87,7 @@ export class AliasService {
let oldPrimaryId = ''; let oldPrimaryId = '';
let newPrimaryId = ''; let newPrimaryId = '';
for (const anAlias of note.aliases) { for (const anAlias of await note.aliases) {
// found old primary // found old primary
if (anAlias.primary) { if (anAlias.primary) {
oldPrimaryId = anAlias.id; oldPrimaryId = anAlias.id;
@ -134,9 +134,9 @@ export class AliasService {
* @throws {PrimaryAliasDeletionForbiddenError} the primary alias can only be deleted if it's the only alias * @throws {PrimaryAliasDeletionForbiddenError} the primary alias can only be deleted if it's the only alias
*/ */
async removeAlias(note: Note, alias: string): Promise<Note> { async removeAlias(note: Note, alias: string): Promise<Note> {
const primaryAlias = getPrimaryAlias(note); const primaryAlias = await getPrimaryAlias(note);
if (primaryAlias === alias && note.aliases.length !== 1) { if (primaryAlias === alias && (await note.aliases).length !== 1) {
this.logger.debug( this.logger.debug(
`The alias '${alias}' is the primary alias, which can only be removed if it's the only alias.`, `The alias '${alias}' is the primary alias, which can only be removed if it's the only alias.`,
'removeAlias', 'removeAlias',
@ -146,10 +146,10 @@ export class AliasService {
); );
} }
const filteredAliases = note.aliases.filter( const filteredAliases = (await note.aliases).filter(
(anAlias) => anAlias.name !== alias, (anAlias) => anAlias.name !== alias,
); );
if (note.aliases.length === filteredAliases.length) { if ((await note.aliases).length === filteredAliases.length) {
this.logger.debug( this.logger.debug(
`The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`, `The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`,
'removeAlias', 'removeAlias',
@ -158,13 +158,13 @@ export class AliasService {
`The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`, `The alias '${alias}' is not used by this note or is the primary alias, which can't be removed.`,
); );
} }
const aliasToDelete = note.aliases.find( const aliasToDelete = (await note.aliases).find(
(anAlias) => anAlias.name === alias, (anAlias) => anAlias.name === alias,
); );
if (aliasToDelete !== undefined) { if (aliasToDelete !== undefined) {
await this.aliasRepository.remove(aliasToDelete); await this.aliasRepository.remove(aliasToDelete);
} }
note.aliases = filteredAliases; note.aliases = Promise.resolve(filteredAliases);
return await this.noteRepository.save(note); return await this.noteRepository.save(note);
} }

View file

@ -36,21 +36,21 @@ export class Note {
(alias) => alias.note, (alias) => alias.note,
{ cascade: true }, // This ensures that embedded Aliases are automatically saved to the database { cascade: true }, // This ensures that embedded Aliases are automatically saved to the database
) )
aliases: Alias[]; aliases: Promise<Alias[]>;
@OneToMany( @OneToMany(
(_) => NoteGroupPermission, (_) => NoteGroupPermission,
(groupPermission) => groupPermission.note, (groupPermission) => groupPermission.note,
{ cascade: true }, // This ensures that embedded NoteGroupPermissions are automatically saved to the database { cascade: true }, // This ensures that embedded NoteGroupPermissions are automatically saved to the database
) )
groupPermissions: NoteGroupPermission[]; groupPermissions: Promise<NoteGroupPermission[]>;
@OneToMany( @OneToMany(
(_) => NoteUserPermission, (_) => NoteUserPermission,
(userPermission) => userPermission.note, (userPermission) => userPermission.note,
{ cascade: true }, // This ensures that embedded NoteUserPermission are automatically saved to the database { cascade: true }, // This ensures that embedded NoteUserPermission are automatically saved to the database
) )
userPermissions: NoteUserPermission[]; userPermissions: Promise<NoteUserPermission[]>;
@Column({ @Column({
nullable: false, nullable: false,
@ -62,16 +62,16 @@ export class Note {
onDelete: 'CASCADE', // This deletes the Note, when the associated User is deleted onDelete: 'CASCADE', // This deletes the Note, when the associated User is deleted
nullable: true, nullable: true,
}) })
owner: User | null; owner: Promise<User | null>;
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true }) @OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
revisions: Promise<Revision[]>; revisions: Promise<Revision[]>;
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[]; historyEntries: Promise<HistoryEntry[]>;
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note) @OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
mediaUploads: MediaUpload[]; mediaUploads: Promise<MediaUpload[]>;
@Column({ @Column({
nullable: true, nullable: true,
@ -87,7 +87,7 @@ export class Note {
@ManyToMany((_) => Tag, (tag) => tag.notes, { eager: true, cascade: true }) @ManyToMany((_) => Tag, (tag) => tag.notes, { eager: true, cascade: true })
@JoinTable() @JoinTable()
tags: Tag[]; tags: Promise<Tag[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -101,18 +101,18 @@ export class Note {
const newNote = new Note(); const newNote = new Note();
newNote.publicId = generatePublicId(); newNote.publicId = generatePublicId();
newNote.aliases = alias newNote.aliases = alias
? [Alias.create(alias, newNote, true) as Alias] ? Promise.resolve([Alias.create(alias, newNote, true) as Alias])
: []; : Promise.resolve([]);
newNote.userPermissions = []; newNote.userPermissions = Promise.resolve([]);
newNote.groupPermissions = []; newNote.groupPermissions = Promise.resolve([]);
newNote.viewCount = 0; newNote.viewCount = 0;
newNote.owner = owner; newNote.owner = Promise.resolve(owner);
newNote.revisions = Promise.resolve([]); newNote.revisions = Promise.resolve([]);
newNote.historyEntries = []; newNote.historyEntries = Promise.resolve([]);
newNote.mediaUploads = []; newNote.mediaUploads = Promise.resolve([]);
newNote.description = null; newNote.description = null;
newNote.title = null; newNote.title = null;
newNote.tags = []; newNote.tags = Promise.resolve([]);
return newNote; return newNote;
} }
} }

View file

@ -168,51 +168,51 @@ describe('NotesService', () => {
const revisions = await newNote.revisions; const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1); expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content); expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(0); expect(await newNote.historyEntries).toHaveLength(0);
expect(newNote.userPermissions).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0); expect(await newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0); expect(await newNote.tags).toHaveLength(0);
expect(newNote.owner).toBeNull(); expect(await newNote.owner).toBeNull();
expect(newNote.aliases).toHaveLength(0); expect(await newNote.aliases).toHaveLength(0);
}); });
it('without alias, with owner', async () => { it('without alias, with owner', async () => {
const newNote = await service.createNote(content, user); const newNote = await service.createNote(content, user);
const revisions = await newNote.revisions; const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1); expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content); expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(1); expect(await newNote.historyEntries).toHaveLength(1);
expect(newNote.historyEntries[0].user).toEqual(user); expect((await newNote.historyEntries)[0].user).toEqual(user);
expect(newNote.userPermissions).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0); expect(await newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0); expect(await newNote.tags).toHaveLength(0);
expect(newNote.owner).toEqual(user); expect(await newNote.owner).toEqual(user);
expect(newNote.aliases).toHaveLength(0); expect(await newNote.aliases).toHaveLength(0);
}); });
it('with alias, without owner', async () => { it('with alias, without owner', async () => {
const newNote = await service.createNote(content, null, alias); const newNote = await service.createNote(content, null, alias);
const revisions = await newNote.revisions; const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1); expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content); expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(0); expect(await newNote.historyEntries).toHaveLength(0);
expect(newNote.userPermissions).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0); expect(await newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0); expect(await newNote.tags).toHaveLength(0);
expect(newNote.owner).toBeNull(); expect(await newNote.owner).toBeNull();
expect(newNote.aliases).toHaveLength(1); expect(await newNote.aliases).toHaveLength(1);
}); });
it('with alias, with owner', async () => { it('with alias, with owner', async () => {
const newNote = await service.createNote(content, user, alias); const newNote = await service.createNote(content, user, alias);
const revisions = await newNote.revisions; const revisions = await newNote.revisions;
expect(revisions).toHaveLength(1); expect(revisions).toHaveLength(1);
expect(revisions[0].content).toEqual(content); expect(revisions[0].content).toEqual(content);
expect(newNote.historyEntries).toHaveLength(1); expect(await newNote.historyEntries).toHaveLength(1);
expect(newNote.historyEntries[0].user).toEqual(user); expect((await newNote.historyEntries)[0].user).toEqual(user);
expect(newNote.userPermissions).toHaveLength(0); expect(await newNote.userPermissions).toHaveLength(0);
expect(newNote.groupPermissions).toHaveLength(0); expect(await newNote.groupPermissions).toHaveLength(0);
expect(newNote.tags).toHaveLength(0); expect(await newNote.tags).toHaveLength(0);
expect(newNote.owner).toEqual(user); expect(await newNote.owner).toEqual(user);
expect(newNote.aliases).toHaveLength(1); expect(await newNote.aliases).toHaveLength(1);
expect(newNote.aliases[0].name).toEqual(alias); expect((await newNote.aliases)[0].name).toEqual(alias);
}); });
}); });
describe('fails:', () => { describe('fails:', () => {
@ -379,8 +379,8 @@ describe('NotesService', () => {
sharedToUsers: [], sharedToUsers: [],
sharedToGroups: [], sharedToGroups: [],
}); });
expect(savedNote.userPermissions).toHaveLength(0); expect(await savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions).toHaveLength(0); expect(await savedNote.groupPermissions).toHaveLength(0);
}); });
it('with empty GroupPermissions and with new UserPermissions', async () => { it('with empty GroupPermissions and with new UserPermissions', async () => {
jest jest
@ -393,24 +393,24 @@ describe('NotesService', () => {
sharedToUsers: [userPermissionUpdate], sharedToUsers: [userPermissionUpdate],
sharedToGroups: [], sharedToGroups: [],
}); });
expect(savedNote.userPermissions).toHaveLength(1); expect(await savedNote.userPermissions).toHaveLength(1);
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions).toHaveLength(0); expect(await savedNote.groupPermissions).toHaveLength(0);
}); });
it('with empty GroupPermissions and with existing UserPermissions', async () => { it('with empty GroupPermissions and with existing UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note }; const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.userPermissions = [ noteWithPreexistingPermissions.userPermissions = Promise.resolve([
{ {
note: noteWithPreexistingPermissions, note: noteWithPreexistingPermissions,
user: user, user: user,
canEdit: !userPermissionUpdate.canEdit, canEdit: !userPermissionUpdate.canEdit,
}, },
]; ]);
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => { .mockImplementationOnce(async (entry: Note) => {
@ -421,14 +421,14 @@ describe('NotesService', () => {
sharedToUsers: [userPermissionUpdate], sharedToUsers: [userPermissionUpdate],
sharedToGroups: [], sharedToGroups: [],
}); });
expect(savedNote.userPermissions).toHaveLength(1); expect(await savedNote.userPermissions).toHaveLength(1);
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions).toHaveLength(0); expect(await savedNote.groupPermissions).toHaveLength(0);
}); });
it('with new GroupPermissions and with empty UserPermissions', async () => { it('with new GroupPermissions and with empty UserPermissions', async () => {
jest jest
@ -441,11 +441,11 @@ describe('NotesService', () => {
sharedToUsers: [], sharedToUsers: [],
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}); });
expect(savedNote.userPermissions).toHaveLength(0); expect(await savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
@ -461,28 +461,28 @@ describe('NotesService', () => {
sharedToUsers: [userPermissionUpdate], sharedToUsers: [userPermissionUpdate],
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}); });
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
it('with new GroupPermissions and with existing UserPermissions', async () => { it('with new GroupPermissions and with existing UserPermissions', async () => {
const noteWithUserPermission: Note = { ...note }; const noteWithUserPermission: Note = { ...note };
noteWithUserPermission.userPermissions = [ noteWithUserPermission.userPermissions = Promise.resolve([
{ {
note: noteWithUserPermission, note: noteWithUserPermission,
user: user, user: user,
canEdit: !userPermissionUpdate.canEdit, canEdit: !userPermissionUpdate.canEdit,
}, },
]; ]);
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => { .mockImplementationOnce(async (entry: Note) => {
@ -497,28 +497,28 @@ describe('NotesService', () => {
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}, },
); );
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
it('with existing GroupPermissions and with empty UserPermissions', async () => { it('with existing GroupPermissions and with empty UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note }; const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [ noteWithPreexistingPermissions.groupPermissions = Promise.resolve([
{ {
note: noteWithPreexistingPermissions, note: noteWithPreexistingPermissions,
group: group, group: group,
canEdit: !groupPermissionUpate.canEdit, canEdit: !groupPermissionUpate.canEdit,
}, },
]; ]);
jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group); jest.spyOn(groupRepo, 'findOne').mockResolvedValueOnce(group);
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
@ -532,23 +532,23 @@ describe('NotesService', () => {
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}, },
); );
expect(savedNote.userPermissions).toHaveLength(0); expect(await savedNote.userPermissions).toHaveLength(0);
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
it('with existing GroupPermissions and with new UserPermissions', async () => { it('with existing GroupPermissions and with new UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note }; const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [ noteWithPreexistingPermissions.groupPermissions = Promise.resolve([
{ {
note: noteWithPreexistingPermissions, note: noteWithPreexistingPermissions,
group: group, group: group,
canEdit: !groupPermissionUpate.canEdit, canEdit: !groupPermissionUpate.canEdit,
}, },
]; ]);
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => { .mockImplementationOnce(async (entry: Note) => {
@ -563,35 +563,35 @@ describe('NotesService', () => {
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}, },
); );
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
it('with existing GroupPermissions and with existing UserPermissions', async () => { it('with existing GroupPermissions and with existing UserPermissions', async () => {
const noteWithPreexistingPermissions: Note = { ...note }; const noteWithPreexistingPermissions: Note = { ...note };
noteWithPreexistingPermissions.groupPermissions = [ noteWithPreexistingPermissions.groupPermissions = Promise.resolve([
{ {
note: noteWithPreexistingPermissions, note: noteWithPreexistingPermissions,
group: group, group: group,
canEdit: !groupPermissionUpate.canEdit, canEdit: !groupPermissionUpate.canEdit,
}, },
]; ]);
noteWithPreexistingPermissions.userPermissions = [ noteWithPreexistingPermissions.userPermissions = Promise.resolve([
{ {
note: noteWithPreexistingPermissions, note: noteWithPreexistingPermissions,
user: user, user: user,
canEdit: !userPermissionUpdate.canEdit, canEdit: !userPermissionUpdate.canEdit,
}, },
]; ]);
jest jest
.spyOn(noteRepo, 'save') .spyOn(noteRepo, 'save')
.mockImplementationOnce(async (entry: Note) => { .mockImplementationOnce(async (entry: Note) => {
@ -606,16 +606,16 @@ describe('NotesService', () => {
sharedToGroups: [groupPermissionUpate], sharedToGroups: [groupPermissionUpate],
}, },
); );
expect(savedNote.userPermissions[0].user.username).toEqual( expect((await savedNote.userPermissions)[0].user.username).toEqual(
userPermissionUpdate.username, userPermissionUpdate.username,
); );
expect(savedNote.userPermissions[0].canEdit).toEqual( expect((await savedNote.userPermissions)[0].canEdit).toEqual(
userPermissionUpdate.canEdit, userPermissionUpdate.canEdit,
); );
expect(savedNote.groupPermissions[0].group.name).toEqual( expect((await savedNote.groupPermissions)[0].group.name).toEqual(
groupPermissionUpate.groupname, groupPermissionUpate.groupname,
); );
expect(savedNote.groupPermissions[0].canEdit).toEqual( expect((await savedNote.groupPermissions)[0].canEdit).toEqual(
groupPermissionUpate.canEdit, groupPermissionUpate.canEdit,
); );
}); });
@ -653,16 +653,16 @@ describe('NotesService', () => {
describe('toTagList', () => { describe('toTagList', () => {
it('works', async () => { it('works', async () => {
const note = {} as Note; const note = {} as Note;
note.tags = [ note.tags = Promise.resolve([
{ {
id: 1, id: 1,
name: 'testTag', name: 'testTag',
notes: [note], notes: Promise.resolve([note]),
}, },
]; ]);
const tagList = service.toTagList(note); const tagList = await service.toTagList(note);
expect(tagList).toHaveLength(1); expect(tagList).toHaveLength(1);
expect(tagList[0]).toEqual(note.tags[0].name); expect(tagList[0]).toEqual((await note.tags)[0].name);
}); });
}); });
@ -671,21 +671,21 @@ describe('NotesService', () => {
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
const group = Group.create('testGroup', 'testGroup', false) as Group; const group = Group.create('testGroup', 'testGroup', false) as Group;
const note = Note.create(user) as Note; const note = Note.create(user) as Note;
note.userPermissions = [ note.userPermissions = Promise.resolve([
{ {
note: note, note: note,
user: user, user: user,
canEdit: true, canEdit: true,
}, },
]; ]);
note.groupPermissions = [ note.groupPermissions = Promise.resolve([
{ {
note: note, note: note,
group: group, group: group,
canEdit: true, canEdit: true,
}, },
]; ]);
const permissions = service.toNotePermissionsDto(note); const permissions = await service.toNotePermissionsDto(note);
expect(permissions.owner).not.toEqual(null); expect(permissions.owner).not.toEqual(null);
expect(permissions.owner?.username).toEqual(user.username); expect(permissions.owner?.username).toEqual(user.username);
expect(permissions.sharedToUsers).toHaveLength(1); expect(permissions.sharedToUsers).toHaveLength(1);
@ -703,7 +703,7 @@ describe('NotesService', () => {
it('works', async () => { it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
const author = Author.create(1); const author = Author.create(1);
author.user = user; author.user = Promise.resolve(user);
const group = Group.create('testGroup', 'testGroup', false) as Group; const group = Group.create('testGroup', 'testGroup', false) as Group;
const content = 'testContent'; const content = 'testContent';
jest jest
@ -711,22 +711,22 @@ describe('NotesService', () => {
.mockImplementation(async (note: Note): Promise<Note> => note); .mockImplementation(async (note: Note): Promise<Note> => note);
const note = await service.createNote(content, null); const note = await service.createNote(content, null);
const revisions = await note.revisions; const revisions = await note.revisions;
revisions[0].edits = [ revisions[0].edits = Promise.resolve([
{ {
revisions: revisions, revisions: Promise.resolve(revisions),
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452000), updatedAt: new Date(1549312452000),
author: author, author: Promise.resolve(author),
} as Edit, } as Edit,
{ {
revisions: revisions, revisions: Promise.resolve(revisions),
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452001), updatedAt: new Date(1549312452001),
author: author, author: Promise.resolve(author),
} as Edit, } as Edit,
]; ]);
revisions[0].createdAt = new Date(1549312452000); revisions[0].createdAt = new Date(1549312452000);
jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]); jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(revisions[0]);
const createQueryBuilder = { const createQueryBuilder = {
@ -740,36 +740,38 @@ describe('NotesService', () => {
// @ts-ignore // @ts-ignore
.mockImplementation(() => createQueryBuilder); .mockImplementation(() => createQueryBuilder);
note.publicId = 'testId'; note.publicId = 'testId';
note.aliases = [Alias.create('testAlias', note, true) as Alias]; note.aliases = Promise.resolve([
Alias.create('testAlias', note, true) as Alias,
]);
note.title = 'testTitle'; note.title = 'testTitle';
note.description = 'testDescription'; note.description = 'testDescription';
note.owner = user; note.owner = Promise.resolve(user);
note.userPermissions = [ note.userPermissions = Promise.resolve([
{ {
note: note, note: note,
user: user, user: user,
canEdit: true, canEdit: true,
}, },
]; ]);
note.groupPermissions = [ note.groupPermissions = Promise.resolve([
{ {
note: note, note: note,
group: group, group: group,
canEdit: true, canEdit: true,
}, },
]; ]);
note.tags = [ note.tags = Promise.resolve([
{ {
id: 1, id: 1,
name: 'testTag', name: 'testTag',
notes: [note], notes: Promise.resolve([note]),
}, },
]; ]);
note.viewCount = 1337; note.viewCount = 1337;
const metadataDto = await service.toNoteMetadataDto(note); const metadataDto = await service.toNoteMetadataDto(note);
expect(metadataDto.id).toEqual(note.publicId); expect(metadataDto.id).toEqual(note.publicId);
expect(metadataDto.aliases).toHaveLength(1); expect(metadataDto.aliases).toHaveLength(1);
expect(metadataDto.aliases[0]).toEqual(note.aliases[0].name); expect(metadataDto.aliases[0]).toEqual((await note.aliases)[0].name);
expect(metadataDto.title).toEqual(note.title); expect(metadataDto.title).toEqual(note.title);
expect(metadataDto.createTime).toEqual(revisions[0].createdAt); expect(metadataDto.createTime).toEqual(revisions[0].createdAt);
expect(metadataDto.description).toEqual(note.description); expect(metadataDto.description).toEqual(note.description);
@ -787,7 +789,7 @@ describe('NotesService', () => {
).toEqual(group.displayName); ).toEqual(group.displayName);
expect(metadataDto.permissions.sharedToGroups[0].canEdit).toEqual(true); expect(metadataDto.permissions.sharedToGroups[0].canEdit).toEqual(true);
expect(metadataDto.tags).toHaveLength(1); expect(metadataDto.tags).toHaveLength(1);
expect(metadataDto.tags[0]).toEqual(note.tags[0].name); expect(metadataDto.tags[0]).toEqual((await note.tags)[0].name);
expect(metadataDto.updateTime).toEqual(revisions[0].createdAt); expect(metadataDto.updateTime).toEqual(revisions[0].createdAt);
expect(metadataDto.updateUser.username).toEqual(user.username); expect(metadataDto.updateUser.username).toEqual(user.username);
expect(metadataDto.viewCount).toEqual(note.viewCount); expect(metadataDto.viewCount).toEqual(note.viewCount);
@ -798,7 +800,7 @@ describe('NotesService', () => {
it('works', async () => { it('works', async () => {
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
const author = Author.create(1); const author = Author.create(1);
author.user = user; author.user = Promise.resolve(user);
const otherUser = User.create('other hardcoded', 'Testy2') as User; const otherUser = User.create('other hardcoded', 'Testy2') as User;
otherUser.username = 'other hardcoded user'; otherUser.username = 'other hardcoded user';
const group = Group.create('testGroup', 'testGroup', false) as Group; const group = Group.create('testGroup', 'testGroup', false) as Group;
@ -808,22 +810,22 @@ describe('NotesService', () => {
.mockImplementation(async (note: Note): Promise<Note> => note); .mockImplementation(async (note: Note): Promise<Note> => note);
const note = await service.createNote(content, null); const note = await service.createNote(content, null);
const revisions = await note.revisions; const revisions = await note.revisions;
revisions[0].edits = [ revisions[0].edits = Promise.resolve([
{ {
revisions: revisions, revisions: Promise.resolve(revisions),
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452000), updatedAt: new Date(1549312452000),
author: author, author: Promise.resolve(author),
} as Edit, } as Edit,
{ {
revisions: revisions, revisions: Promise.resolve(revisions),
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452001), updatedAt: new Date(1549312452001),
author: author, author: Promise.resolve(author),
} as Edit, } as Edit,
]; ]);
revisions[0].createdAt = new Date(1549312452000); revisions[0].createdAt = new Date(1549312452000);
jest jest
.spyOn(revisionRepo, 'findOne') .spyOn(revisionRepo, 'findOne')
@ -840,36 +842,38 @@ describe('NotesService', () => {
// @ts-ignore // @ts-ignore
.mockImplementation(() => createQueryBuilder); .mockImplementation(() => createQueryBuilder);
note.publicId = 'testId'; note.publicId = 'testId';
note.aliases = [Alias.create('testAlias', note, true) as Alias]; note.aliases = Promise.resolve([
Alias.create('testAlias', note, true) as Alias,
]);
note.title = 'testTitle'; note.title = 'testTitle';
note.description = 'testDescription'; note.description = 'testDescription';
note.owner = user; note.owner = Promise.resolve(user);
note.userPermissions = [ note.userPermissions = Promise.resolve([
{ {
note: note, note: note,
user: user, user: user,
canEdit: true, canEdit: true,
}, },
]; ]);
note.groupPermissions = [ note.groupPermissions = Promise.resolve([
{ {
note: note, note: note,
group: group, group: group,
canEdit: true, canEdit: true,
}, },
]; ]);
note.tags = [ note.tags = Promise.resolve([
{ {
id: 1, id: 1,
name: 'testTag', name: 'testTag',
notes: [note], notes: Promise.resolve([note]),
}, },
]; ]);
note.viewCount = 1337; note.viewCount = 1337;
const noteDto = await service.toNoteDto(note); const noteDto = await service.toNoteDto(note);
expect(noteDto.metadata.id).toEqual(note.publicId); expect(noteDto.metadata.id).toEqual(note.publicId);
expect(noteDto.metadata.aliases).toHaveLength(1); expect(noteDto.metadata.aliases).toHaveLength(1);
expect(noteDto.metadata.aliases[0]).toEqual(note.aliases[0].name); expect(noteDto.metadata.aliases[0]).toEqual((await note.aliases)[0].name);
expect(noteDto.metadata.title).toEqual(note.title); expect(noteDto.metadata.title).toEqual(note.title);
expect(noteDto.metadata.createTime).toEqual(revisions[0].createdAt); expect(noteDto.metadata.createTime).toEqual(revisions[0].createdAt);
expect(noteDto.metadata.description).toEqual(note.description); expect(noteDto.metadata.description).toEqual(note.description);
@ -893,7 +897,7 @@ describe('NotesService', () => {
true, true,
); );
expect(noteDto.metadata.tags).toHaveLength(1); expect(noteDto.metadata.tags).toHaveLength(1);
expect(noteDto.metadata.tags[0]).toEqual(note.tags[0].name); expect(noteDto.metadata.tags[0]).toEqual((await note.tags)[0].name);
expect(noteDto.metadata.updateTime).toEqual(revisions[0].createdAt); expect(noteDto.metadata.updateTime).toEqual(revisions[0].createdAt);
expect(noteDto.metadata.updateUser.username).toEqual(user.username); expect(noteDto.metadata.updateUser.username).toEqual(user.username);
expect(noteDto.metadata.viewCount).toEqual(note.viewCount); expect(noteDto.metadata.viewCount).toEqual(note.viewCount);

View file

@ -100,9 +100,9 @@ export class NotesService {
Revision.create(noteContent, noteContent, newNote as Note) as Revision, Revision.create(noteContent, noteContent, newNote as Note) as Revision,
]); ]);
if (owner) { if (owner) {
newNote.historyEntries = [ newNote.historyEntries = Promise.resolve([
HistoryEntry.create(owner, newNote as Note) as HistoryEntry, HistoryEntry.create(owner, newNote as Note) as HistoryEntry,
]; ]);
} }
try { try {
return await this.noteRepository.save(newNote); return await this.noteRepository.save(newNote);
@ -262,8 +262,8 @@ export class NotesService {
//TODO: Calculate patch //TODO: Calculate patch
revisions.push(Revision.create(noteContent, noteContent, note) as Revision); revisions.push(Revision.create(noteContent, noteContent, note) as Revision);
note.revisions = Promise.resolve(revisions); note.revisions = Promise.resolve(revisions);
note.userPermissions = []; note.userPermissions = Promise.resolve([]);
note.groupPermissions = []; note.groupPermissions = Promise.resolve([]);
return await this.noteRepository.save(note); return await this.noteRepository.save(note);
} }
@ -298,8 +298,8 @@ export class NotesService {
); );
} }
note.userPermissions = []; note.userPermissions = Promise.resolve([]);
note.groupPermissions = []; note.groupPermissions = Promise.resolve([]);
// Create new userPermissions // Create new userPermissions
for (const newUserPermission of newPermissions.sharedToUsers) { for (const newUserPermission of newPermissions.sharedToUsers) {
@ -312,7 +312,7 @@ export class NotesService {
newUserPermission.canEdit, newUserPermission.canEdit,
); );
createdPermission.note = note; createdPermission.note = note;
note.userPermissions.push(createdPermission); (await note.userPermissions).push(createdPermission);
} }
// Create groupPermissions // Create groupPermissions
@ -326,7 +326,7 @@ export class NotesService {
newGroupPermission.canEdit, newGroupPermission.canEdit,
); );
createdPermission.note = note; createdPermission.note = note;
note.groupPermissions.push(createdPermission); (await note.groupPermissions).push(createdPermission);
} }
return await this.noteRepository.save(note); return await this.noteRepository.save(note);
@ -340,15 +340,18 @@ export class NotesService {
*/ */
async calculateUpdateUser(note: Note): Promise<User | null> { async calculateUpdateUser(note: Note): Promise<User | null> {
const lastRevision = await this.getLatestRevision(note); const lastRevision = await this.getLatestRevision(note);
if (lastRevision && lastRevision.edits) { const edits = await lastRevision.edits;
if (edits.length > 0) {
// Sort the last Revisions Edits by their updatedAt Date to get the latest one // Sort the last Revisions Edits by their updatedAt Date to get the latest one
// the user of that Edit is the updateUser // the user of that Edit is the updateUser
return lastRevision.edits.sort( return await (
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(), await edits.sort(
)[0].author.user; (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
)[0].author
).user;
} }
// If there are no Edits, the owner is the updateUser // If there are no Edits, the owner is the updateUser
return note.owner; return await note.owner;
} }
/** /**
@ -356,8 +359,8 @@ export class NotesService {
* @param {Note} note - the note to use * @param {Note} note - the note to use
* @return {string[]} string array of tags names * @return {string[]} string array of tags names
*/ */
toTagList(note: Note): string[] { async toTagList(note: Note): Promise<string[]> {
return note.tags.map((tag) => tag.name); return (await note.tags).map((tag) => tag.name);
} }
/** /**
@ -365,14 +368,17 @@ export class NotesService {
* @param {Note} note - the note to use * @param {Note} note - the note to use
* @return {NotePermissionsDto} the built NotePermissionDto * @return {NotePermissionsDto} the built NotePermissionDto
*/ */
toNotePermissionsDto(note: Note): NotePermissionsDto { async toNotePermissionsDto(note: Note): Promise<NotePermissionsDto> {
const owner = await note.owner;
const userPermissions = await note.userPermissions;
const groupPermissions = await note.groupPermissions;
return { return {
owner: note.owner ? this.usersService.toUserDto(note.owner) : null, owner: owner ? this.usersService.toUserDto(owner) : null,
sharedToUsers: note.userPermissions.map((noteUserPermission) => ({ sharedToUsers: userPermissions.map((noteUserPermission) => ({
user: this.usersService.toUserDto(noteUserPermission.user), user: this.usersService.toUserDto(noteUserPermission.user),
canEdit: noteUserPermission.canEdit, canEdit: noteUserPermission.canEdit,
})), })),
sharedToGroups: note.groupPermissions.map((noteGroupPermission) => ({ sharedToGroups: groupPermissions.map((noteGroupPermission) => ({
group: this.groupsService.toGroupDto(noteGroupPermission.group), group: this.groupsService.toGroupDto(noteGroupPermission.group),
canEdit: noteGroupPermission.canEdit, canEdit: noteGroupPermission.canEdit,
})), })),
@ -389,14 +395,16 @@ export class NotesService {
const updateUser = await this.calculateUpdateUser(note); const updateUser = await this.calculateUpdateUser(note);
return { return {
id: note.publicId, id: note.publicId,
aliases: note.aliases.map((alias) => alias.name), aliases: await Promise.all(
primaryAlias: getPrimaryAlias(note) ?? null, (await note.aliases).map((alias) => alias.name),
),
primaryAlias: (await getPrimaryAlias(note)) ?? null,
title: note.title ?? '', title: note.title ?? '',
createTime: (await this.getFirstRevision(note)).createdAt, createTime: (await this.getFirstRevision(note)).createdAt,
description: note.description ?? '', description: note.description ?? '',
editedBy: (await this.getAuthorUsers(note)).map((user) => user.username), editedBy: (await this.getAuthorUsers(note)).map((user) => user.username),
permissions: this.toNotePermissionsDto(note), permissions: await this.toNotePermissionsDto(note),
tags: this.toTagList(note), tags: await this.toTagList(note),
updateTime: (await this.getLatestRevision(note)).createdAt, updateTime: (await this.getLatestRevision(note)).createdAt,
updateUser: updateUser ? this.usersService.toUserDto(updateUser) : null, updateUser: updateUser ? this.usersService.toUserDto(updateUser) : null,
viewCount: note.viewCount, viewCount: note.viewCount,

View file

@ -18,5 +18,5 @@ export class Tag {
name: string; name: string;
@ManyToMany((_) => Note, (note) => note.tags) @ManyToMany((_) => Note, (note) => note.tags)
notes: Note[]; notes: Promise<Note[]>;
} }

View file

@ -29,12 +29,12 @@ describe('getPrimaryAlias', () => {
const user = User.create('hardcoded', 'Testy') as User; const user = User.create('hardcoded', 'Testy') as User;
note = Note.create(user, alias) as Note; note = Note.create(user, alias) as Note;
}); });
it('finds correct primary alias', () => { it('finds correct primary alias', async () => {
note.aliases.push(Alias.create('annother', note, false) as Alias); (await note.aliases).push(Alias.create('annother', note, false) as Alias);
expect(getPrimaryAlias(note)).toEqual(alias); expect(await getPrimaryAlias(note)).toEqual(alias);
}); });
it('returns undefined if there is no alias', () => { it('returns undefined if there is no alias', async () => {
note.aliases[0].primary = false; (await note.aliases)[0].primary = false;
expect(getPrimaryAlias(note)).toEqual(undefined); expect(await getPrimaryAlias(note)).toEqual(undefined);
}); });
}); });

View file

@ -22,8 +22,8 @@ export function generatePublicId(): string {
* Extract the primary alias from a aliases of a note * Extract the primary alias from a aliases of a note
* @param {Note} note - the note from which the primary alias should be extracted * @param {Note} note - the note from which the primary alias should be extracted
*/ */
export function getPrimaryAlias(note: Note): string | undefined { export async function getPrimaryAlias(note: Note): Promise<string | undefined> {
const listWithPrimaryAlias = note.aliases.filter( const listWithPrimaryAlias = (await note.aliases).filter(
(alias: Alias) => alias.primary, (alias: Alias) => alias.primary,
); );
if (listWithPrimaryAlias.length !== 1) { if (listWithPrimaryAlias.length !== 1) {

View file

@ -10,6 +10,13 @@ import { Note } from '../notes/note.entity';
@Entity() @Entity()
export class NoteGroupPermission { export class NoteGroupPermission {
/**
* The `group` 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
*/
@ManyToOne((_) => Group, { @ManyToOne((_) => Group, {
primary: true, primary: true,
onDelete: 'CASCADE', // This deletes the NoteGroupPermission, when the associated Group is deleted onDelete: 'CASCADE', // This deletes the NoteGroupPermission, when the associated Group is deleted

View file

@ -10,6 +10,13 @@ import { User } from '../users/user.entity';
@Entity() @Entity()
export class NoteUserPermission { export class NoteUserPermission {
/**
* 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
*/
@ManyToOne((_) => User, { @ManyToOne((_) => User, {
primary: true, primary: true,
onDelete: 'CASCADE', // This deletes the NoteUserPermission, when the associated Note is deleted onDelete: 'CASCADE', // This deletes the NoteUserPermission, when the associated Note is deleted

View file

@ -30,6 +30,7 @@ import { GuestPermission, PermissionsService } from './permissions.service';
describe('PermissionsService', () => { describe('PermissionsService', () => {
let permissionsService: PermissionsService; let permissionsService: PermissionsService;
let notes: Note[];
beforeAll(async () => { beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -73,6 +74,7 @@ describe('PermissionsService', () => {
.useValue({}) .useValue({})
.compile(); .compile();
permissionsService = module.get<PermissionsService>(PermissionsService); permissionsService = module.get<PermissionsService>(PermissionsService);
notes = await createNoteUserPermissionNotes();
}); });
// The two users we test with: // The two users we test with:
@ -87,16 +89,16 @@ describe('PermissionsService', () => {
function createNote(owner: User): Note { function createNote(owner: User): Note {
const note = {} as Note; const note = {} as Note;
note.userPermissions = []; note.userPermissions = Promise.resolve([]);
note.groupPermissions = []; note.groupPermissions = Promise.resolve([]);
note.owner = owner; note.owner = Promise.resolve(owner);
return note; return note;
} }
/* /*
* Creates the permission objects for UserPermission for two users with write and with out write permission * Creates the permission objects for UserPermission for two users with write and with out write permission
*/ */
function createNoteUserPermissionNotes(): Note[] { async function createNoteUserPermissionNotes(): Promise<Note[]> {
const note0 = createNote(user1); const note0 = createNote(user1);
const note1 = createNote(user2); const note1 = createNote(user2);
const note2 = createNote(user2); const note2 = createNote(user2);
@ -116,23 +118,23 @@ describe('PermissionsService', () => {
noteUserPermission4.user = user2; noteUserPermission4.user = user2;
noteUserPermission4.canEdit = true; noteUserPermission4.canEdit = true;
note1.userPermissions.push(noteUserPermission1); (await note1.userPermissions).push(noteUserPermission1);
note2.userPermissions.push(noteUserPermission1); (await note2.userPermissions).push(noteUserPermission1);
note2.userPermissions.push(noteUserPermission2); (await note2.userPermissions).push(noteUserPermission2);
note3.userPermissions.push(noteUserPermission2); (await note3.userPermissions).push(noteUserPermission2);
note3.userPermissions.push(noteUserPermission1); (await note3.userPermissions).push(noteUserPermission1);
note4.userPermissions.push(noteUserPermission3); (await note4.userPermissions).push(noteUserPermission3);
note5.userPermissions.push(noteUserPermission3); (await note5.userPermissions).push(noteUserPermission3);
note5.userPermissions.push(noteUserPermission4); (await note5.userPermissions).push(noteUserPermission4);
note6.userPermissions.push(noteUserPermission4); (await note6.userPermissions).push(noteUserPermission4);
note6.userPermissions.push(noteUserPermission3); (await note6.userPermissions).push(noteUserPermission3);
note7.userPermissions.push(noteUserPermission2); (await note7.userPermissions).push(noteUserPermission2);
const everybody = {} as Group; const everybody = {} as Group;
everybody.name = SpecialGroup.EVERYONE; everybody.name = SpecialGroup.EVERYONE;
@ -143,7 +145,9 @@ describe('PermissionsService', () => {
noteGroupPermissionRead.group = everybody; noteGroupPermissionRead.group = everybody;
noteGroupPermissionRead.canEdit = false; noteGroupPermissionRead.canEdit = false;
noteGroupPermissionRead.note = noteEverybodyRead; noteGroupPermissionRead.note = noteEverybodyRead;
noteEverybodyRead.groupPermissions = [noteGroupPermissionRead]; noteEverybodyRead.groupPermissions = Promise.resolve([
noteGroupPermissionRead,
]);
const noteEverybodyWrite = createNote(user1); const noteEverybodyWrite = createNote(user1);
@ -151,7 +155,9 @@ describe('PermissionsService', () => {
noteGroupPermissionWrite.group = everybody; noteGroupPermissionWrite.group = everybody;
noteGroupPermissionWrite.canEdit = true; noteGroupPermissionWrite.canEdit = true;
noteGroupPermissionWrite.note = noteEverybodyWrite; noteGroupPermissionWrite.note = noteEverybodyWrite;
noteEverybodyWrite.groupPermissions = [noteGroupPermissionWrite]; noteEverybodyWrite.groupPermissions = Promise.resolve([
noteGroupPermissionWrite,
]);
return [ return [
note0, note0,
@ -167,82 +173,80 @@ describe('PermissionsService', () => {
]; ];
} }
const notes = createNoteUserPermissionNotes();
describe('mayRead works with', () => { describe('mayRead works with', () => {
it('Owner', () => { it('Owner', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayRead(user1, notes[0])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[0])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[7])).toBeFalsy(); expect(await permissionsService.mayRead(user1, notes[7])).toBeFalsy();
}); });
it('userPermission read', () => { it('userPermission read', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayRead(user1, notes[1])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[1])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[2])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[2])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[3])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[3])).toBeTruthy();
}); });
it('userPermission write', () => { it('userPermission write', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayRead(user1, notes[4])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[4])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[5])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[5])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[6])).toBeTruthy(); expect(await permissionsService.mayRead(user1, notes[6])).toBeTruthy();
expect(permissionsService.mayRead(user1, notes[7])).toBeFalsy(); expect(await permissionsService.mayRead(user1, notes[7])).toBeFalsy();
}); });
describe('guest permission', () => { describe('guest permission', () => {
it('CREATE_ALIAS', () => { it('CREATE_ALIAS', async () => {
permissionsService.guestPermission = GuestPermission.CREATE_ALIAS; permissionsService.guestPermission = GuestPermission.CREATE_ALIAS;
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy(); expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy();
}); });
it('CREATE', () => { it('CREATE', async () => {
permissionsService.guestPermission = GuestPermission.CREATE; permissionsService.guestPermission = GuestPermission.CREATE;
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy(); expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy();
}); });
it('WRITE', () => { it('WRITE', async () => {
permissionsService.guestPermission = GuestPermission.WRITE; permissionsService.guestPermission = GuestPermission.WRITE;
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy(); expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy();
}); });
it('READ', () => { it('READ', async () => {
permissionsService.guestPermission = GuestPermission.READ; permissionsService.guestPermission = GuestPermission.READ;
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy(); expect(await permissionsService.mayRead(null, notes[8])).toBeTruthy();
}); });
}); });
}); });
describe('mayWrite works with', () => { describe('mayWrite works with', () => {
it('Owner', () => { it('Owner', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayWrite(user1, notes[0])).toBeTruthy(); expect(await permissionsService.mayWrite(user1, notes[0])).toBeTruthy();
expect(permissionsService.mayWrite(user1, notes[7])).toBeFalsy(); expect(await permissionsService.mayWrite(user1, notes[7])).toBeFalsy();
}); });
it('userPermission read', () => { it('userPermission read', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayWrite(user1, notes[1])).toBeFalsy(); expect(await permissionsService.mayWrite(user1, notes[1])).toBeFalsy();
expect(permissionsService.mayWrite(user1, notes[2])).toBeFalsy(); expect(await permissionsService.mayWrite(user1, notes[2])).toBeFalsy();
expect(permissionsService.mayWrite(user1, notes[3])).toBeFalsy(); expect(await permissionsService.mayWrite(user1, notes[3])).toBeFalsy();
}); });
it('userPermission write', () => { it('userPermission write', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.mayWrite(user1, notes[4])).toBeTruthy(); expect(await permissionsService.mayWrite(user1, notes[4])).toBeTruthy();
expect(permissionsService.mayWrite(user1, notes[5])).toBeTruthy(); expect(await permissionsService.mayWrite(user1, notes[5])).toBeTruthy();
expect(permissionsService.mayWrite(user1, notes[6])).toBeTruthy(); expect(await permissionsService.mayWrite(user1, notes[6])).toBeTruthy();
expect(permissionsService.mayWrite(user1, notes[7])).toBeFalsy(); expect(await permissionsService.mayWrite(user1, notes[7])).toBeFalsy();
}); });
describe('guest permission', () => { describe('guest permission', () => {
it('CREATE_ALIAS', () => { it('CREATE_ALIAS', async () => {
permissionsService.guestPermission = GuestPermission.CREATE_ALIAS; permissionsService.guestPermission = GuestPermission.CREATE_ALIAS;
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy(); expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy();
}); });
it('CREATE', () => { it('CREATE', async () => {
permissionsService.guestPermission = GuestPermission.CREATE; permissionsService.guestPermission = GuestPermission.CREATE;
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy(); expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy();
}); });
it('WRITE', () => { it('WRITE', async () => {
permissionsService.guestPermission = GuestPermission.WRITE; permissionsService.guestPermission = GuestPermission.WRITE;
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy(); expect(await permissionsService.mayWrite(null, notes[9])).toBeTruthy();
}); });
it('READ', () => { it('READ', async () => {
permissionsService.guestPermission = GuestPermission.READ; permissionsService.guestPermission = GuestPermission.READ;
expect(permissionsService.mayWrite(null, notes[9])).toBeFalsy(); expect(await permissionsService.mayWrite(null, notes[9])).toBeFalsy();
}); });
}); });
}); });
@ -277,11 +281,11 @@ describe('PermissionsService', () => {
result[SpecialGroup.LOGGED_IN] = loggedIn; result[SpecialGroup.LOGGED_IN] = loggedIn;
const user1group = Group.create('user1group', 'user1group', false) as Group; const user1group = Group.create('user1group', 'user1group', false) as Group;
user1group.members = [user1]; user1group.members = Promise.resolve([user1]);
result['user1group'] = user1group; result['user1group'] = user1group;
const user2group = Group.create('user2group', 'user2group', false) as Group; const user2group = Group.create('user2group', 'user2group', false) as Group;
user2group.members = [user2]; user2group.members = Promise.resolve([user2]);
result['user2group'] = user2group; result['user2group'] = user2group;
const user1and2group = Group.create( const user1and2group = Group.create(
@ -289,7 +293,7 @@ describe('PermissionsService', () => {
'user1and2group', 'user1and2group',
false, false,
) as Group; ) as Group;
user1and2group.members = [user1, user2]; user1and2group.members = Promise.resolve([user1, user2]);
result['user1and2group'] = user1and2group; result['user1and2group'] = user1and2group;
const user2and1group = Group.create( const user2and1group = Group.create(
@ -297,7 +301,7 @@ describe('PermissionsService', () => {
'user2and1group', 'user2and1group',
false, false,
) as Group; ) as Group;
user2and1group.members = [user2, user1]; user2and1group.members = Promise.resolve([user2, user1]);
result['user2and1group'] = user2and1group; result['user2and1group'] = user2and1group;
return result; return result;
@ -501,20 +505,20 @@ describe('PermissionsService', () => {
let i = 0; let i = 0;
for (const permission of permissions) { for (const permission of permissions) {
const note = createNote(user2); const note = createNote(user2);
note.groupPermissions = permission.permissions; note.groupPermissions = Promise.resolve(permission.permissions);
let permissionString = ''; let permissionString = '';
for (const perm of permission.permissions) { for (const perm of permission.permissions) {
permissionString += ` ${perm.group.name}:${String(perm.canEdit)}`; permissionString += ` ${perm.group.name}:${String(perm.canEdit)}`;
} }
it(`mayWrite - test #${i}:${permissionString}`, () => { it(`mayWrite - test #${i}:${permissionString}`, async () => {
permissionsService.guestPermission = guestPermission; permissionsService.guestPermission = guestPermission;
expect(permissionsService.mayWrite(user1, note)).toEqual( expect(await permissionsService.mayWrite(user1, note)).toEqual(
permission.allowsWrite, permission.allowsWrite,
); );
}); });
it(`mayRead - test #${i}:${permissionString}`, () => { it(`mayRead - test #${i}:${permissionString}`, async () => {
permissionsService.guestPermission = guestPermission; permissionsService.guestPermission = guestPermission;
expect(permissionsService.mayRead(user1, note)).toEqual( expect(await permissionsService.mayRead(user1, note)).toEqual(
permission.allowsRead, permission.allowsRead,
); );
}); });
@ -550,13 +554,13 @@ describe('PermissionsService', () => {
}); });
describe('isOwner works', () => { describe('isOwner works', () => {
it('for positive case', () => { it('for positive case', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.isOwner(user1, notes[0])).toBeTruthy(); expect(await permissionsService.isOwner(user1, notes[0])).toBeTruthy();
}); });
it('for negative case', () => { it('for negative case', async () => {
permissionsService.guestPermission = GuestPermission.DENY; permissionsService.guestPermission = GuestPermission.DENY;
expect(permissionsService.isOwner(user1, notes[1])).toBeFalsy(); expect(await permissionsService.isOwner(user1, notes[1])).toBeFalsy();
}); });
}); });
}); });

View file

@ -21,24 +21,24 @@ export enum GuestPermission {
@Injectable() @Injectable()
export class PermissionsService { export class PermissionsService {
public guestPermission: GuestPermission; // TODO change to configOption public guestPermission: GuestPermission; // TODO change to configOption
mayRead(user: User | null, note: Note): boolean { async mayRead(user: User | null, note: Note): Promise<boolean> {
if (this.isOwner(user, note)) return true; if (await this.isOwner(user, note)) return true;
if (this.hasPermissionUser(user, note, false)) return true; if (await this.hasPermissionUser(user, note, false)) return true;
// noinspection RedundantIfStatementJS // noinspection RedundantIfStatementJS
if (this.hasPermissionGroup(user, note, false)) return true; if (await this.hasPermissionGroup(user, note, false)) return true;
return false; return false;
} }
mayWrite(user: User | null, note: Note): boolean { async mayWrite(user: User | null, note: Note): Promise<boolean> {
if (this.isOwner(user, note)) return true; if (await this.isOwner(user, note)) return true;
if (this.hasPermissionUser(user, note, true)) return true; if (await this.hasPermissionUser(user, note, true)) return true;
// noinspection RedundantIfStatementJS // noinspection RedundantIfStatementJS
if (this.hasPermissionGroup(user, note, true)) return true; if (await this.hasPermissionGroup(user, note, true)) return true;
return false; return false;
} }
@ -58,21 +58,22 @@ export class PermissionsService {
return false; return false;
} }
isOwner(user: User | null, note: Note): boolean { async isOwner(user: User | null, note: Note): Promise<boolean> {
if (!user) return false; if (!user) return false;
if (!note.owner) return false; const owner = await note.owner;
return note.owner.id === user.id; if (!owner) return false;
return owner.id === user.id;
} }
private hasPermissionUser( private async hasPermissionUser(
user: User | null, user: User | null,
note: Note, note: Note,
wantEdit: boolean, wantEdit: boolean,
): boolean { ): Promise<boolean> {
if (!user) { if (!user) {
return false; return false;
} }
for (const userPermission of note.userPermissions) { for (const userPermission of await note.userPermissions) {
if ( if (
userPermission.user.id === user.id && userPermission.user.id === user.id &&
(userPermission.canEdit || !wantEdit) (userPermission.canEdit || !wantEdit)
@ -83,11 +84,11 @@ export class PermissionsService {
return false; return false;
} }
private hasPermissionGroup( private async hasPermissionGroup(
user: User | null, user: User | null,
note: Note, note: Note,
wantEdit: boolean, wantEdit: boolean,
): boolean { ): Promise<boolean> {
// TODO: Get real config value // TODO: Get real config value
let guestsAllowed = false; let guestsAllowed = false;
switch (this.guestPermission) { switch (this.guestPermission) {
@ -99,7 +100,7 @@ export class PermissionsService {
case GuestPermission.READ: case GuestPermission.READ:
guestsAllowed = !wantEdit; guestsAllowed = !wantEdit;
} }
for (const groupPermission of note.groupPermissions) { for (const groupPermission of await note.groupPermissions) {
if (groupPermission.canEdit || !wantEdit) { if (groupPermission.canEdit || !wantEdit) {
// Handle special groups // Handle special groups
if (groupPermission.group.special) { if (groupPermission.group.special) {
@ -116,7 +117,7 @@ export class PermissionsService {
} else { } else {
// Handle normal groups // Handle normal groups
if (user) { if (user) {
for (const member of groupPermission.group.members) { for (const member of await groupPermission.group.members) {
if (member.id === user.id) return true; if (member.id === user.id) return true;
} }
} }

View file

@ -28,13 +28,13 @@ export class Edit {
* Revisions this edit appears in * Revisions this edit appears in
*/ */
@ManyToMany((_) => Revision, (revision) => revision.edits) @ManyToMany((_) => Revision, (revision) => revision.edits)
revisions: Revision[]; revisions: Promise<Revision[]>;
/** /**
* Author that created the change * Author that created the change
*/ */
@ManyToOne(() => Author, (author) => author.edits) @ManyToOne(() => Author, (author) => author.edits)
author: Author; author: Promise<Author>;
@Column() @Column()
startPos: number; startPos: number;
@ -57,8 +57,8 @@ export class Edit {
endPos: number, endPos: number,
): Omit<Edit, 'id' | 'createdAt' | 'updatedAt'> { ): Omit<Edit, 'id' | 'createdAt' | 'updatedAt'> {
const newEdit = new Edit(); const newEdit = new Edit();
newEdit.revisions = []; newEdit.revisions = Promise.resolve([]);
newEdit.author = author; newEdit.author = Promise.resolve(author);
newEdit.startPos = startPos; newEdit.startPos = startPos;
newEdit.endPos = endPos; newEdit.endPos = endPos;
return newEdit; return newEdit;

View file

@ -58,14 +58,14 @@ export class Revision {
* Note this revision belongs to. * Note this revision belongs to.
*/ */
@ManyToOne((_) => Note, (note) => note.revisions, { onDelete: 'CASCADE' }) @ManyToOne((_) => Note, (note) => note.revisions, { onDelete: 'CASCADE' })
note: Note; note: Promise<Note>;
/** /**
* All edit objects which are used in the revision. * All edit objects which are used in the revision.
*/ */
@ManyToMany((_) => Edit, (edit) => edit.revisions) @ManyToMany((_) => Edit, (edit) => edit.revisions)
@JoinTable() @JoinTable()
edits: Edit[]; edits: Promise<Edit[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -79,8 +79,8 @@ export class Revision {
newRevision.patch = patch; newRevision.patch = patch;
newRevision.content = content; newRevision.content = content;
newRevision.length = content.length; newRevision.length = content.length;
newRevision.note = note; newRevision.note = Promise.resolve(note);
newRevision.edits = []; newRevision.edits = Promise.resolve([]);
return newRevision; return newRevision;
} }
} }

View file

@ -67,18 +67,18 @@ createConnection({
const identity = Identity.create(user, ProviderType.LOCAL, false); const identity = Identity.create(user, ProviderType.LOCAL, false);
identity.passwordHash = await hashPassword(password); identity.passwordHash = await hashPassword(password);
connection.manager.create(Identity, identity); connection.manager.create(Identity, identity);
author.user = user; author.user = Promise.resolve(user);
const revision = Revision.create( const revision = Revision.create(
'This is a test note', 'This is a test note',
'This is a test note', 'This is a test note',
notes[i], notes[i],
) as Revision; ) as Revision;
const edit = Edit.create(author, 1, 42) as Edit; const edit = Edit.create(author, 1, 42) as Edit;
revision.edits = [edit]; revision.edits = Promise.resolve([edit]);
notes[i].revisions = Promise.all([revision]); notes[i].revisions = Promise.all([revision]);
notes[i].userPermissions = []; notes[i].userPermissions = Promise.resolve([]);
notes[i].groupPermissions = []; notes[i].groupPermissions = Promise.resolve([]);
user.ownedNotes = [notes[i]]; user.ownedNotes = Promise.resolve([notes[i]]);
await connection.manager.save([ await connection.manager.save([
notes[i], notes[i],
user, user,
@ -99,7 +99,7 @@ createConnection({
throw new Error('Could not find freshly seeded notes. Aborting.'); throw new Error('Could not find freshly seeded notes. Aborting.');
} }
for (const note of foundNotes) { for (const note of foundNotes) {
if (!note.aliases[0]) { if (!(await note.aliases)[0]) {
throw new Error( throw new Error(
'Could not find alias of freshly seeded notes. Aborting.', 'Could not find alias of freshly seeded notes. Aborting.',
); );
@ -111,7 +111,7 @@ createConnection({
); );
} }
for (const note of foundNotes) { for (const note of foundNotes) {
console.log(`Created Note '${note.aliases[0].name ?? ''}'`); console.log(`Created Note '${(await note.aliases)[0].name ?? ''}'`);
} }
for (const user of foundUsers) { for (const user of foundUsers) {
for (const note of foundNotes) { for (const note of foundNotes) {
@ -119,7 +119,7 @@ createConnection({
await connection.manager.save(historyEntry); await connection.manager.save(historyEntry);
console.log( console.log(
`Created HistoryEntry for user '${user.username}' and note '${ `Created HistoryEntry for user '${user.username}' and note '${
note.aliases[0].name ?? '' (await note.aliases)[0].name ?? ''
}'`, }'`,
); );
} }

View file

@ -21,5 +21,5 @@ export class Session implements ISession {
public json = ''; public json = '';
@ManyToOne(() => Author, (author) => author.sessions) @ManyToOne(() => Author, (author) => author.sessions)
author: Author; author: Promise<Author>;
} }

View file

@ -53,25 +53,25 @@ export class User {
email: string | null; email: string | null;
@OneToMany((_) => Note, (note) => note.owner) @OneToMany((_) => Note, (note) => note.owner)
ownedNotes: Note[]; ownedNotes: Promise<Note[]>;
@OneToMany((_) => AuthToken, (authToken) => authToken.user) @OneToMany((_) => AuthToken, (authToken) => authToken.user)
authTokens: AuthToken[]; authTokens: Promise<AuthToken[]>;
@OneToMany((_) => Identity, (identity) => identity.user) @OneToMany((_) => Identity, (identity) => identity.user)
identities: Promise<Identity[]>; identities: Promise<Identity[]>;
@ManyToMany((_) => Group, (group) => group.members) @ManyToMany((_) => Group, (group) => group.members)
groups: Group[]; groups: Promise<Group[]>;
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[]; historyEntries: Promise<HistoryEntry[]>;
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user) @OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user)
mediaUploads: MediaUpload[]; mediaUploads: Promise<MediaUpload[]>;
@OneToMany(() => Author, (author) => author.user) @OneToMany(() => Author, (author) => author.user)
authors: Author[]; authors: Promise<Author[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -85,13 +85,13 @@ export class User {
newUser.displayName = displayName; newUser.displayName = displayName;
newUser.photo = null; newUser.photo = null;
newUser.email = null; newUser.email = null;
newUser.ownedNotes = []; newUser.ownedNotes = Promise.resolve([]);
newUser.authTokens = []; newUser.authTokens = Promise.resolve([]);
newUser.identities = Promise.resolve([]); newUser.identities = Promise.resolve([]);
newUser.groups = []; newUser.groups = Promise.resolve([]);
newUser.historyEntries = []; newUser.historyEntries = Promise.resolve([]);
newUser.mediaUploads = []; newUser.mediaUploads = Promise.resolve([]);
newUser.authors = []; newUser.authors = Promise.resolve([]);
return newUser; return newUser;
} }
} }

View file

@ -69,7 +69,7 @@ describe('History', () => {
note, note,
user, user,
); );
const entryDto = testSetup.historyService.toHistoryEntryDto(entry); const entryDto = await testSetup.historyService.toHistoryEntryDto(entry);
const response = await agent const response = await agent
.get('/api/private/me/history') .get('/api/private/me/history')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -92,7 +92,7 @@ describe('History', () => {
const pinStatus = true; const pinStatus = true;
const lastVisited = new Date('2020-12-01 12:23:34'); const lastVisited = new Date('2020-12-01 12:23:34');
const postEntryDto = new HistoryEntryImportDto(); const postEntryDto = new HistoryEntryImportDto();
postEntryDto.note = note2.aliases.filter( postEntryDto.note = (await note2.aliases).filter(
(alias) => alias.primary, (alias) => alias.primary,
)[0].name; )[0].name;
postEntryDto.pinStatus = pinStatus; postEntryDto.pinStatus = pinStatus;
@ -104,13 +104,15 @@ describe('History', () => {
.expect(201); .expect(201);
const userEntries = await testSetup.historyService.getEntriesByUser(user); const userEntries = await testSetup.historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1); expect(userEntries.length).toEqual(1);
expect(userEntries[0].note.aliases[0].name).toEqual( expect((await userEntries[0].note.aliases)[0].name).toEqual(
note2.aliases[0].name, (await note2.aliases)[0].name,
); );
expect(userEntries[0].note.aliases[0].primary).toEqual( expect((await userEntries[0].note.aliases)[0].primary).toEqual(
note2.aliases[0].primary, (await note2.aliases)[0].primary,
);
expect((await userEntries[0].note.aliases)[0].id).toEqual(
(await note2.aliases)[0].id,
); );
expect(userEntries[0].note.aliases[0].id).toEqual(note2.aliases[0].id);
expect(userEntries[0].user.username).toEqual(user.username); expect(userEntries[0].user.username).toEqual(user.username);
expect(userEntries[0].pinStatus).toEqual(pinStatus); expect(userEntries[0].pinStatus).toEqual(pinStatus);
expect(userEntries[0].updatedAt).toEqual(lastVisited); expect(userEntries[0].updatedAt).toEqual(lastVisited);
@ -129,7 +131,7 @@ describe('History', () => {
pinStatus = !previousHistory[0].pinStatus; pinStatus = !previousHistory[0].pinStatus;
lastVisited = new Date('2020-12-01 23:34:45'); lastVisited = new Date('2020-12-01 23:34:45');
postEntryDto = new HistoryEntryImportDto(); postEntryDto = new HistoryEntryImportDto();
postEntryDto.note = note2.aliases.filter( postEntryDto.note = (await note2.aliases).filter(
(alias) => alias.primary, (alias) => alias.primary,
)[0].name; )[0].name;
postEntryDto.pinStatus = pinStatus; postEntryDto.pinStatus = pinStatus;
@ -188,7 +190,8 @@ describe('History', () => {
user, user,
); );
expect(entry.pinStatus).toBeFalsy(); expect(entry.pinStatus).toBeFalsy();
const alias = entry.note.aliases.filter((alias) => alias.primary)[0].name; const alias = (await entry.note.aliases).filter((alias) => alias.primary)[0]
.name;
await agent await agent
.put(`/api/private/me/history/${alias || 'undefined'}`) .put(`/api/private/me/history/${alias || 'undefined'}`)
.send({ pinStatus: true }) .send({ pinStatus: true })
@ -201,15 +204,16 @@ describe('History', () => {
it('DELETE /me/history/:note', async () => { it('DELETE /me/history/:note', async () => {
const entry = await historyService.updateHistoryEntryTimestamp(note2, user); const entry = await historyService.updateHistoryEntryTimestamp(note2, user);
const alias = entry.note.aliases.filter((alias) => alias.primary)[0].name; const alias = (await entry.note.aliases).filter((alias) => alias.primary)[0]
.name;
const entry2 = await historyService.updateHistoryEntryTimestamp(note, user); const entry2 = await historyService.updateHistoryEntryTimestamp(note, user);
const entryDto = historyService.toHistoryEntryDto(entry2); const entryDto = await historyService.toHistoryEntryDto(entry2);
await agent await agent
.delete(`/api/private/me/history/${alias || 'undefined'}`) .delete(`/api/private/me/history/${alias || 'undefined'}`)
.expect(200); .expect(200);
const userEntries = await historyService.getEntriesByUser(user); const userEntries = await historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1); expect(userEntries.length).toEqual(1);
const userEntryDto = historyService.toHistoryEntryDto(userEntries[0]); const userEntryDto = await historyService.toHistoryEntryDto(userEntries[0]);
expect(userEntryDto.identifier).toEqual(entryDto.identifier); expect(userEntryDto.identifier).toEqual(entryDto.identifier);
expect(userEntryDto.title).toEqual(entryDto.title); expect(userEntryDto.title).toEqual(entryDto.title);
expect(userEntryDto.tags).toEqual(entryDto.tags); expect(userEntryDto.tags).toEqual(entryDto.tags);

View file

@ -51,8 +51,9 @@ describe('Me', () => {
.expect(200); .expect(200);
const history: HistoryEntryDto[] = response.body; const history: HistoryEntryDto[] = response.body;
expect(history.length).toEqual(1); expect(history.length).toEqual(1);
const historyDto = const historyDto = await testSetup.historyService.toHistoryEntryDto(
testSetup.historyService.toHistoryEntryDto(createdHistoryEntry); createdHistoryEntry,
);
for (const historyEntry of history) { for (const historyEntry of history) {
expect(historyEntry.identifier).toEqual(historyDto.identifier); expect(historyEntry.identifier).toEqual(historyDto.identifier);
expect(historyEntry.title).toEqual(historyDto.title); expect(historyEntry.title).toEqual(historyDto.title);
@ -75,8 +76,9 @@ describe('Me', () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
const historyEntry: HistoryEntryDto = response.body; const historyEntry: HistoryEntryDto = response.body;
const historyEntryDto = const historyEntryDto = await testSetup.historyService.toHistoryEntryDto(
testSetup.historyService.toHistoryEntryDto(createdHistoryEntry); createdHistoryEntry,
);
expect(historyEntry.identifier).toEqual(historyEntryDto.identifier); expect(historyEntry.identifier).toEqual(historyEntryDto.identifier);
expect(historyEntry.title).toEqual(historyEntryDto.title); expect(historyEntry.title).toEqual(historyEntryDto.title);
expect(historyEntry.tags).toEqual(historyEntryDto.tags); expect(historyEntry.tags).toEqual(historyEntryDto.tags);
@ -109,8 +111,12 @@ describe('Me', () => {
expect(historyEntry.pinStatus).toEqual(true); expect(historyEntry.pinStatus).toEqual(true);
let theEntry: HistoryEntryDto; let theEntry: HistoryEntryDto;
for (const entry of history) { for (const entry of history) {
if (entry.note.aliases.find((element) => element.name === noteName)) { if (
theEntry = testSetup.historyService.toHistoryEntryDto(entry); (await entry.note.aliases).find(
(element) => element.name === noteName,
)
) {
theEntry = await testSetup.historyService.toHistoryEntryDto(entry);
} }
} }
expect(theEntry.pinStatus).toEqual(true); expect(theEntry.pinStatus).toEqual(true);
@ -134,7 +140,11 @@ describe('Me', () => {
expect(response.body).toEqual({}); expect(response.body).toEqual({});
const history = await testSetup.historyService.getEntriesByUser(user); const history = await testSetup.historyService.getEntriesByUser(user);
for (const entry of history) { for (const entry of history) {
if (entry.note.aliases.find((element) => element.name === noteName)) { if (
(await entry.note.aliases).find(
(element) => element.name === noteName,
)
) {
throw new Error('Deleted history entry still in history'); throw new Error('Deleted history entry still in history');
} }
} }

View file

@ -200,16 +200,16 @@ describe('Notes', () => {
updateNotePermission, updateNotePermission,
); );
const updatedNote = await testSetup.notesService.getNoteByIdOrAlias( const updatedNote = await testSetup.notesService.getNoteByIdOrAlias(
note.aliases.filter((alias) => alias.primary)[0].name, (await note.aliases).filter((alias) => alias.primary)[0].name,
); );
expect(updatedNote.userPermissions).toHaveLength(1); expect(await updatedNote.userPermissions).toHaveLength(1);
expect(updatedNote.userPermissions[0].canEdit).toEqual( expect((await updatedNote.userPermissions)[0].canEdit).toEqual(
updateNotePermission.sharedToUsers[0].canEdit, updateNotePermission.sharedToUsers[0].canEdit,
); );
expect(updatedNote.userPermissions[0].user.username).toEqual( expect((await updatedNote.userPermissions)[0].user.username).toEqual(
user.username, user.username,
); );
expect(updatedNote.groupPermissions).toHaveLength(0); expect(await updatedNote.groupPermissions).toHaveLength(0);
await request(testSetup.app.getHttpServer()) await request(testSetup.app.getHttpServer())
.delete('/api/v2/notes/test3') .delete('/api/v2/notes/test3')
.expect(204); .expect(204);