Skip to content

Commit

Permalink
feat(note): create note mutation (#980)
Browse files Browse the repository at this point in the history
* feat(notes): createNote mutation

[POCKET-10860]

* fix(note): make prosemirror doc a unique scalar

Enables custom serialization/deserialization/parsing
etc. in the future.
  • Loading branch information
kschelonka authored Nov 27, 2024
1 parent 5d3d761 commit 9118d8c
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 15 deletions.
2 changes: 1 addition & 1 deletion packages/apollo-utils/src/scalars/ValidUrlScalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const GraphQLValidUrl = /*#__PURE__*/ new GraphQLScalarType({
}
},
extensions: {
codegenScalarType: 'ValidUrl | string',
codegenScalarType: 'URL | string',
jsonSchema: {
type: 'string',
format: 'uri',
Expand Down
7 changes: 7 additions & 0 deletions packages/apollo-utils/src/scalars/isoStringScalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,11 @@ export const isoStringScalar = new GraphQLScalarType({

return this.parseValue(ast.value);
},
extensions: {
codegenScalarType: 'Date | string',
jsonSchema: {
type: 'string',
format: 'date-time',
},
},
});
8 changes: 8 additions & 0 deletions servers/notes-api/codegen.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodegenConfig } from '@graphql-codegen/cli';
import { PocketDefaultScalars } from '@pocket-tools/apollo-utils';

const config: CodegenConfig = {
schema: './schema.graphql',
Expand All @@ -8,6 +9,13 @@ const config: CodegenConfig = {
federation: true,
useIndexSignature: true,
contextType: '../apollo/context#IContext',
scalars: {
ValidUrl: PocketDefaultScalars.ValidUrl.extensions.codegenScalarType,
ISOString:
PocketDefaultScalars.ISOString.extensions.codegenScalarType,
Markdown: 'string',
ProseMirrorJson: 'string',
},
},
plugins: [
//generated types do not conform to ts/lint rules, disable them for these files
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Note" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP(3);
2 changes: 1 addition & 1 deletion servers/notes-api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ model Note {
title String? @db.VarChar(300)
sourceUrl String?
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP(0)")) @db.Timestamptz(0)
updatedAt DateTime @db.Timestamptz(3)
updatedAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP(3)")) @db.Timestamptz(3)
docContent Json?
deleted Boolean @default(false)
archived Boolean @default(false)
Expand Down
38 changes: 37 additions & 1 deletion servers/notes-api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extend schema
scalar ISOString
scalar ValidUrl
scalar Markdown
scalar ProseMirrorJson

"""
A Note is an entity which may contain extracted components
Expand All @@ -24,7 +25,7 @@ type Note @key(fields: "id") {
"""
JSON representation of a ProseMirror document
"""
docContent: String
docContent: ProseMirrorJson
"""
Markdown preview of the note content for summary view.
"""
Expand Down Expand Up @@ -68,3 +69,38 @@ type Query {
"""Retrieve a specific Note"""
note(id: ID!): Note @requiresScopes(scopes: [["ROLE_USER"]])
}


"""
Input to create a new Note
"""
input CreateNoteInput {
"""Optional title for this Note"""
title: String
"""
Client-provided UUID for the new Note.
If not provided, will be generated on the server.
"""
id: ID
"""
Optional URL to link this Note to.
"""
source: ValidUrl
"""
JSON representation of a ProseMirror document
"""
docContent: ProseMirrorJson!
"""
When this note was created. If not provided, defaults to server time upon
receiving request.
"""
createdAt: ISOString
}


type Mutation {
"""
Create a new note, optionally with title and content
"""
createNote(input: CreateNoteInput!): Note! @requiresScopes(scopes: [["ROLE_USER"]])
}
2 changes: 1 addition & 1 deletion servers/notes-api/src/__generated__/db.d.ts

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

54 changes: 47 additions & 7 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.

5 changes: 4 additions & 1 deletion servers/notes-api/src/apollo/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@pocket-tools/apollo-utils';
import { Kysely } from 'kysely';
import { DB } from '../__generated__/db';
import { roDb } from '../datasources/db';
import { roDb, db } from '../datasources/db';
import { NoteModel } from '../models/Note';

/**
Expand All @@ -28,12 +28,14 @@ export async function getContext({

export interface IContext extends PocketContext {
roDb: Kysely<DB>;
db: Kysely<DB>;
userId: string;
NoteModel: NoteModel;
}

export class ContextManager extends PocketContextManager implements IContext {
roDb: Kysely<DB>;
db: Kysely<DB>;
_userId: string;
NoteModel: NoteModel;
constructor(options: { request: Request }) {
Expand All @@ -48,6 +50,7 @@ export class ContextManager extends PocketContextManager implements IContext {
this._userId = super.userId;
}
this.roDb = roDb;
this.db = db;
this.NoteModel = new NoteModel(this);
}
/** Override because userId is guaranteed in this Context */
Expand Down
5 changes: 5 additions & 0 deletions servers/notes-api/src/apollo/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ export const resolvers: Resolvers = {
return context.NoteModel.load(id);
},
},
Mutation: {
createNote(root, { input }, context) {
return context.NoteModel.create(input);
},
},
};
13 changes: 13 additions & 0 deletions servers/notes-api/src/datasources/NoteService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Insertable } from 'kysely';
import { IContext } from '../apollo/context';
import { Note as NoteEntity } from '../__generated__/db';

/**
* Database methods for retrieving and creating Notes
Expand Down Expand Up @@ -36,4 +38,15 @@ export class NotesService {
.execute();
return result;
}
/**
* Create a new Note and return the row.
*/
async create(entity: Insertable<NoteEntity>) {
const result = await this.context.db
.insertInto('Note')
.values(entity)
.returningAll()
.executeTakeFirstOrThrow();
return result;
}
}
46 changes: 43 additions & 3 deletions servers/notes-api/src/models/Note.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import DataLoader from 'dataloader';
import { Note } from '../__generated__/graphql';
import { Note, CreateNoteInput } from '../__generated__/graphql';
import { Note as NoteEntity } from '../__generated__/db';
import { Selectable } from 'kysely';
import { Insertable, Selectable } from 'kysely';
import { orderAndMap } from '../utils/dataloader';
import { IContext } from '../apollo/context';
import { NotesService } from '../datasources/NoteService';
import { ProseMirrorDoc } from './ProseMirrorDoc';
import { UserInputError } from '@pocket-tools/apollo-utils';
import { DatabaseError } from 'pg';

/**
* Model for retrieving and creating Notes
*/
export class NoteModel {
loader: DataLoader<string, Selectable<NoteEntity> | null>;
service: NotesService;
constructor(context: IContext) {
constructor(public readonly context: IContext) {
this.service = new NotesService(context);
this.loader = new DataLoader<string, Selectable<NoteEntity> | null>(
async (keys: readonly string[]) => {
Expand Down Expand Up @@ -77,4 +79,42 @@ export class NoteModel {
const note = await this.loader.load(id);
return note != null ? this.toGraphql(note) : null;
}
/**
* Create a new Note.
*/
async create(input: CreateNoteInput) {
try {
// At some point do more validation
// We can move this to a scalar
const docContent = JSON.parse(input.docContent);
const entity: Insertable<NoteEntity> = {
createdAt: input.createdAt ?? undefined,
docContent,
noteId: input.id ?? undefined,
sourceUrl: input.source?.toString() ?? undefined,
title: input.title ?? undefined,
userId: this.context.userId,
updatedAt: input.createdAt ?? undefined,
};
const note = await this.service.create(entity);
return this.toGraphql(note);
} catch (error) {
if (error instanceof SyntaxError) {
throw new UserInputError(
`Received malformed JSON for docContent: ${error.message}`,
);
} else if (error instanceof DatabaseError) {
if (error.code === '23505' && error.constraint === 'Note_noteId_key') {
throw new UserInputError(
`Received duplicate value for note ID. ` +
`Ensure you are generating v4 UUIDs and try again.`,
);
} else {
throw error;
}
} else {
throw error;
}
}
}
}
Loading

0 comments on commit 9118d8c

Please sign in to comment.