Skip to content

Commit

Permalink
feat(notes): delete a note (#990)
Browse files Browse the repository at this point in the history
* feat(notes): delete a note

[POCKET-10864]

* fix(note): change deleteNote response to ID
  • Loading branch information
kschelonka authored Dec 4, 2024
1 parent 076aa3e commit ca985aa
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 1 deletion.
16 changes: 16 additions & 0 deletions servers/notes-api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ input EditNoteContentInput {
updatedAt: ISOString
}

input DeleteNoteInput {
"""The ID of the note to delete"""
id: ID!
"""
When the note was deleted was made. If not provided, defaults to
the server time upon receiving request.
"""
deletedAt: ISOString
}

type Mutation {
"""
Create a new note, optionally with title and content
Expand All @@ -183,4 +193,10 @@ type Mutation {
errors array.
"""
editNoteContent(input: EditNoteContentInput!): Note @requiresScopes(scopes: [["ROLE_USER"]])
"""
Delete a note and all attachments. Returns True if the note was successfully
deleted. If the note cannot be deleted or does not exist, returns False.
Errors will be included in the errors array if applicable.
"""
deleteNote(input: DeleteNoteInput!): ID! @requiresScopes(scopes: [["ROLE_USER"]])
}
24 changes: 24 additions & 0 deletions servers/notes-api/src/__generated__/graphql.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions servers/notes-api/src/apollo/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ export const resolvers: Resolvers = {
editNoteContent(root, { input }, context) {
return context.NoteModel.editContent(input);
},
deleteNote(root, { input }, context) {
return context.NoteModel.deleteNote(input);
},
},
};
19 changes: 19 additions & 0 deletions servers/notes-api/src/datasources/NoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class NotesService {
.where('noteId', '=', noteId)
.where('userId', '=', this.context.userId);
}

/**
* Update the title field in a Note
* @param noteId the UUID of the Note entity to update
Expand Down Expand Up @@ -109,4 +110,22 @@ export class NotesService {
.executeTakeFirstOrThrow();
return result;
}
/**
* Update the title field in a Note
* @param noteId the UUID of the Note entity to update
* @param title the new title (can be empty string)
* @param updatedAt when the update was performed
* @returns
*/
async delete(noteId: string, deletedAt?: Date | string | null) {
const setUpdate =
deletedAt != null
? { deleted: true, updatedAt: deletedAt }
: { deleted: true, updatedAt: new Date(Date.now()) };
const result = await this.updateBase(noteId)
.set(setUpdate)
.returning('noteId')
.executeTakeFirstOrThrow();
return result.noteId;
}
}
15 changes: 15 additions & 0 deletions servers/notes-api/src/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CreateNoteFromQuoteInput,
EditNoteTitleInput,
EditNoteContentInput,
DeleteNoteInput,
} from '../__generated__/graphql';
import { Note as NoteEntity } from '../__generated__/db';
import { Insertable, NoResultError, Selectable } from 'kysely';
Expand Down Expand Up @@ -202,4 +203,18 @@ export class NoteModel {
}
}
}
/**
* Delete a Note
*/
async deleteNote(input: DeleteNoteInput) {
try {
return await this.service.delete(input.id, input.deletedAt);
} catch (error) {
if (error instanceof NoResultError) {
return input.id;
} else {
throw error;
}
}
}
}
91 changes: 91 additions & 0 deletions servers/notes-api/src/test/mutations/deleteNote.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { type ApolloServer } from '@apollo/server';
import request from 'supertest';
import { IContext, startServer } from '../../apollo';
import { type Application } from 'express';
import { DELETE_NOTE, GET_NOTE } from '../operations';
import { db, roDb } from '../../datasources/db';
import { sql } from 'kysely';
import { Chance } from 'chance';
import { Note as NoteFaker } from '../fakes/Note';
import { DeleteNoteInput } from '../../__generated__/graphql';

let app: Application;
let server: ApolloServer<IContext>;
let graphQLUrl: string;
const chance = new Chance();
const notes = [...Array(4).keys()].map((_) => NoteFaker(chance));

beforeAll(async () => {
await sql`truncate table ${sql.table('Note')} CASCADE`.execute(db);
// port 0 tells express to dynamically assign an available port
({ app, server, url: graphQLUrl } = await startServer(0));
await db
.insertInto('Note')
.values(notes)
.returning(['noteId', 'userId'])
.execute();
});
afterAll(async () => {
await sql`truncate table ${sql.table('Note')} CASCADE`.execute(db);
await server.stop();
await db.destroy();
await roDb.destroy();
});

describe('note', () => {
it('deletes a note with a timestamp', async () => {
const now = new Date(Date.now());
const { userId, noteId } = notes[0];
const input: DeleteNoteInput = {
id: noteId,
deletedAt: now.toISOString(),
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: DELETE_NOTE, variables: { input } });
expect(res.body.errors).toBeUndefined();
expect(res.body.data?.deleteNote).toEqual(noteId);
// Do a roundtrip and query back the note
const noteRoundtrip = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: GET_NOTE, variables: { id: noteId } });
const note = noteRoundtrip.body.data?.note;
expect(note.deleted).toBeTrue();
expect(note.updatedAt).toEqual(now.toISOString());
});
it('deletes a note without a timestamp', async () => {
const now = new Date(Date.now());
const { userId, noteId } = notes[1];
const input: DeleteNoteInput = {
id: noteId,
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: DELETE_NOTE, variables: { input } });
expect(res.body.errors).toBeUndefined();
expect(res.body.data?.deleteNote).toEqual(noteId);
// Do a roundtrip and query back the Note
const noteRoundtrip = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: GET_NOTE, variables: { id: noteId } });
const note = noteRoundtrip.body.data?.note;
expect(note.deleted).toBeTrue();
const updatedAt = new Date(note.updatedAt);
expect(updatedAt.getTime() - now.getTime()).toBeLessThanOrEqual(10000); // within 10 seconds of when this test started
});
it('Returns ID for nonexistent note (no errors)', async () => {
const input: DeleteNoteInput = {
id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa',
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: '1' })
.send({ query: DELETE_NOTE, variables: { input } });
expect(res.body.errors).toBeUndefined();
expect(res.body.data?.deleteNote).toEqual(input.id);
});
});
6 changes: 6 additions & 0 deletions servers/notes-api/src/test/operations/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ export const EDIT_NOTE_CONTENT = print(gql`
}
}
`);

export const DELETE_NOTE = print(gql`
mutation DeleteNote($input: DeleteNoteInput!) {
deleteNote(input: $input)
}
`);
3 changes: 2 additions & 1 deletion servers/notes-api/src/test/queries/note.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import request from 'supertest';
import { IContext, startServer } from '../../apollo';
import { type Application } from 'express';
import { GET_NOTE } from '../operations';
import { db } from '../../datasources/db';
import { db, roDb } from '../../datasources/db';
import { Note as NoteFaker } from '../fakes/Note';
import { Chance } from 'chance';
import { sql } from 'kysely';
Expand All @@ -26,6 +26,7 @@ afterAll(async () => {
await sql`truncate table ${sql.table('Note')} CASCADE`.execute(db);
await server.stop();
await db.destroy();
await roDb.destroy();
});

describe('note', () => {
Expand Down

0 comments on commit ca985aa

Please sign in to comment.