Skip to content

Commit

Permalink
feat(notes): edit title of a note (#988)
Browse files Browse the repository at this point in the history
  • Loading branch information
kschelonka authored Dec 4, 2024
1 parent 8b46851 commit 7d37623
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 3 deletions.
19 changes: 19 additions & 0 deletions servers/notes-api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ input CreateNoteFromQuoteInput {
createdAt: ISOString
}

input EditNoteTitleInput {
"""The ID of the note to edit"""
id: ID!
"""The new title for the note (can be an empty string)"""
title: String! @constraint(maxLength: 100)
"""
When the update was made. If not provided, defaults to the server
time upon receiving request.
"""
updatedAt: ISOString
}


type Mutation {
"""
Expand All @@ -141,4 +153,11 @@ type Mutation {
selected by a user.
"""
createNoteFromQuote(input: CreateNoteFromQuoteInput!): Note! @requiresScopes(scopes: [["ROLE_USER"]])
"""
Edit the title of a Note.
If the Note does not exist or is inaccessible for the current user,
response will be null and a NOT_FOUND error will be included in the
errors array.
"""
editNoteTitle(input: EditNoteTitleInput!): Note @requiresScopes(scopes: [["ROLE_USER"]])
}
24 changes: 23 additions & 1 deletion 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 @@ -15,5 +15,8 @@ export const resolvers: Resolvers = {
createNoteFromQuote(root, { input }, context) {
return context.NoteModel.fromQuote(input);
},
editNoteTitle(root, { input }, context) {
return context.NoteModel.editTitle(input);
},
},
};
25 changes: 25 additions & 0 deletions servers/notes-api/src/datasources/NoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,29 @@ 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 updateTitle(
noteId: string,
title: string,
updatedAt?: Date | string | null,
) {
const setUpdate =
updatedAt != null
? { title, updatedAt }
: { title, updatedAt: new Date(Date.now()) };
const result = await this.context.db
.updateTable('Note')
.set(setUpdate)
.where('noteId', '=', noteId)
.where('userId', '=', this.context.userId)
.returningAll()
.executeTakeFirstOrThrow();
return result;
}
}
26 changes: 24 additions & 2 deletions servers/notes-api/src/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import {
Note,
CreateNoteInput,
CreateNoteFromQuoteInput,
EditNoteTitleInput,
} from '../__generated__/graphql';
import { Note as NoteEntity } from '../__generated__/db';
import { Insertable, Selectable } from 'kysely';
import { Insertable, NoResultError, Selectable } from 'kysely';
import { orderAndMap } from '../utils/dataloader';
import { IContext } from '../apollo/context';
import { NotesService } from '../datasources/NoteService';
import { ProseMirrorDoc, wrapDocInBlockQuote } from './ProseMirrorDoc';
import { UserInputError } from '@pocket-tools/apollo-utils';
import { NotFoundError, UserInputError } from '@pocket-tools/apollo-utils';
import { DatabaseError } from 'pg';

/**
Expand Down Expand Up @@ -149,4 +150,25 @@ export class NoteModel {
}
}
}
/**
* Edit a note's title
*/
async editTitle(input: EditNoteTitleInput) {
try {
const result = await this.service.updateTitle(
input.id,
input.title,
input.updatedAt,
);
return this.toGraphql(result);
} catch (error) {
if (error instanceof NoResultError) {
throw new NotFoundError(
`Note with id=${input.id} does not exist or is forbidden`,
);
} else {
throw error;
}
}
}
}
83 changes: 83 additions & 0 deletions servers/notes-api/src/test/mutations/editNoteTitle.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { type ApolloServer } from '@apollo/server';
import request from 'supertest';
import { IContext, startServer } from '../../apollo';
import { type Application } from 'express';
import { EDIT_NOTE_TITLE } from '../operations';
import { db } from '../../datasources/db';
import { sql } from 'kysely';
import { Chance } from 'chance';
import { Note as NoteFaker } from '../fakes/Note';

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

beforeAll(async () => {
// 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();
});

describe('note', () => {
it('edits a note title with a timestamp', async () => {
const now = new Date(Date.now());
const { userId, noteId } = notes[0];

const input = {
id: noteId,
title: 'Why I Stopped Worrying and Started Loving My Fungus',
updatedAt: now.toISOString(),
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: EDIT_NOTE_TITLE, variables: { input } });
expect(res.body.errors).toBeUndefined();
expect(res.body.data?.editNoteTitle).toMatchObject({
title: 'Why I Stopped Worrying and Started Loving My Fungus',
updatedAt: now.toISOString(),
});
});
it('edits a note title without a timestamp', async () => {
const now = new Date(Date.now());
const { userId, noteId } = notes[1];
const input = {
id: noteId,
title: `How To Talk To Your Kids About Bloodstone Rituals`,
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: userId })
.send({ query: EDIT_NOTE_TITLE, variables: { input } });
expect(res.body.errors).toBeUndefined();
expect(res.body.data?.editNoteTitle).toMatchObject({
title: `How To Talk To Your Kids About Bloodstone Rituals`,
updatedAt: expect.toBeDateString(),
});
const updatedAt = new Date(res.body.data?.editNoteTitle?.updatedAt);
expect(updatedAt.getTime() - now.getTime()).toBeLessThanOrEqual(10000); // within 10 seconds of when this test started
});
it('includes not found error for nonexistent note', async () => {
const input = {
id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa',
title: `How To Talk To Your Kids About Bloodstone Rituals`,
};
const res = await request(app)
.post(graphQLUrl)
.set({ userid: '1' })
.send({ query: EDIT_NOTE_TITLE, variables: { input } });
expect(res.body.errors).toBeArrayOfSize(1);
expect(res.body.errors[0].extensions.code).toEqual('NOT_FOUND');
});
});
9 changes: 9 additions & 0 deletions servers/notes-api/src/test/operations/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ export const CREATE_NOTE_QUOTE = print(gql`
}
}
`);

export const EDIT_NOTE_TITLE = print(gql`
${NoteFragment}
mutation EditNoteTitle($input: EditNoteTitleInput!) {
editNoteTitle(input: $input) {
...NoteFields
}
}
`);

0 comments on commit 7d37623

Please sign in to comment.