Skip to content

Commit

Permalink
feat(notes): resolve notes from a SavedItem (#994)
Browse files Browse the repository at this point in the history
  • Loading branch information
kschelonka authored Dec 11, 2024
1 parent 19dd8f7 commit a4f4737
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 17 deletions.
2 changes: 2 additions & 0 deletions servers/notes-api/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const config: CodegenConfig = {
},
mappers: {
NoteConnection: '../models/Note#NoteConnectionModel',
SavedItem: '../models/SavedItem#SavedItemModel',
Note: '../models/Note#NoteResponse',
},
},
plugins: [
Expand Down
27 changes: 27 additions & 0 deletions servers/notes-api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ type Note @key(fields: "id") {
type SavedItem @key(fields: "url") {
"""The URL of the SavedItem"""
url: String!
"""
The notes associated with this SavedItem, optionally
filtered to retrieve after a 'since' parameter.
"""
notes(
pagination: PaginationInput,
sort: NoteSortInput,
filter: SavedItemNoteFilterInput
): NoteConnection!
}

"""Information about pagination in a connection."""
Expand Down Expand Up @@ -182,6 +191,24 @@ input NoteFilterInput {
excludeDeleted: Boolean
}

"""Filter for retrieving Notes attached to a SavedItem"""
input SavedItemNoteFilterInput {
"""
Filter to retrieve Notes by archived status (true/false).
If not provided, notes will not be filtered by archived status.
"""
archived: Boolean
"""
Filter to retrieve notes after a timestamp, e.g. for syncing.
"""
since: ISOString
"""
Filter to choose whether to include notes marked for server-side
deletion in the response (defaults to false).
"""
excludeDeleted: Boolean
}


type Query {
"""Retrieve a specific Note"""
Expand Down
47 changes: 40 additions & 7 deletions servers/notes-api/src/__generated__/graphql.ts

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

20 changes: 20 additions & 0 deletions servers/notes-api/src/apollo/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PocketDefaultScalars } from '@pocket-tools/apollo-utils';
import { Resolvers } from '../__generated__/graphql';
import { PaginationInput } from '@pocket-tools/apollo-utils';
import { SavedItemModel } from '../models/SavedItem';

export const resolvers: Resolvers = {
...PocketDefaultScalars,
Expand All @@ -20,6 +21,25 @@ export const resolvers: Resolvers = {
}
},
},
SavedItem: {
notes(parent, { pagination, filter, sort }, context) {
// The GraphQL InputMaybe<> type is causing issues with
// strict nulls; so doing some manipulation here to
// make sure things are undefined vs. null
const _pagination: PaginationInput = {
...(pagination?.after != null && { after: pagination.after }),
...(pagination?.first != null && { first: pagination.first }),
...(pagination?.last != null && { last: pagination.last }),
...(pagination?.before != null && { before: pagination.before }),
};
const opts = {
...(pagination != null && { pagination: _pagination }),
...(filter != null && { filter }),
...(sort != null && { sort }),
};
return new SavedItemModel(parent.url, context).notes(opts);
},
},
Query: {
note(root, { id }, context) {
return context.NoteModel.load(id);
Expand Down
9 changes: 8 additions & 1 deletion servers/notes-api/src/datasources/NoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ export class NotesService {
* @param filters filters to apply to query
* @returns SelectQueryBuilder with filters applied in where statement(s)
*/
filterQuery(filters: NoteFilterInput | undefined) {
filterQuery(
filters: (NoteFilterInput & { sourceUrl?: string | undefined }) | undefined,
) {
let qb = this.context.db
.selectFrom('Note')
.selectAll()
Expand Down Expand Up @@ -207,6 +209,11 @@ export class NotesService {
case 'excludeDeleted': {
return value === true ? eb('deleted', '=', false) : undefined;
}
case 'sourceUrl': {
return value != null && typeof value === 'string'
? eb('sourceUrl', '=', value)
: undefined;
}
}
});
return and(conditions.filter((_) => _ != null));
Expand Down
15 changes: 9 additions & 6 deletions servers/notes-api/src/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import { AllSelection } from 'kysely/dist/cjs/parser/select-parser';

type AllNote = AllSelection<DB, 'Note'>;

export type NoteConnectionModel = PaginationResult<Note>;
export type NoteConnectionModel = PaginationResult<NoteResponse>;
export type NoteResponse = Omit<Note, 'savedItem'> & {
savedItem: { url: string } | null;
};

/**
* Model for retrieving and creating Notes
Expand All @@ -64,7 +67,7 @@ export class NoteModel {
* @param note
* @returns
*/
toGraphql(note: Selectable<NoteEntity>): Note {
toGraphql(note: Selectable<NoteEntity>): NoteResponse {
const savedItem = note.sourceUrl != null ? { url: note.sourceUrl } : null;
return {
createdAt: note.createdAt,
Expand Down Expand Up @@ -102,7 +105,7 @@ export class NoteModel {
* Get multiple Notes by IDs. Prefer using `load`
* unless you need to bypass cache behavior.
*/
async getMany(ids: readonly string[]): Promise<Note[]> {
async getMany(ids: readonly string[]): Promise<NoteResponse[]> {
const notes = await this.service.getMany(ids);
return notes != null && notes.length > 0
? notes.map((note) => this.toGraphql(note))
Expand All @@ -114,7 +117,7 @@ export class NoteModel {
* behavior. Will return null if ID does not exist
* or is inaccessible for the user.
*/
async getOne(id: string): Promise<Note | null> {
async getOne(id: string): Promise<NoteResponse | null> {
const note = await this.service.get(id);
return note != null ? this.toGraphql(note) : null;
}
Expand All @@ -123,7 +126,7 @@ export class NoteModel {
* Will return null if ID does not exist or is inaccessible
* for the user.
*/
async load(id: string): Promise<Note | null> {
async load(id: string): Promise<NoteResponse | null> {
const note = await this.loader.load(id);
return note != null ? this.toGraphql(note) : null;
}
Expand Down Expand Up @@ -300,7 +303,7 @@ export class NoteModel {
*/
async paginate(opts: {
sort?: NoteSortInput;
filter?: NoteFilterInput;
filter?: NoteFilterInput & { sourceUrl?: string | undefined };
pagination?: PaginationInput;
}): Promise<NoteConnectionModel> {
const { sort, filter, pagination } = opts;
Expand Down
44 changes: 44 additions & 0 deletions servers/notes-api/src/models/SavedItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { PaginationInput } from '@pocket-tools/apollo-utils';
import {
NoteFilterInput,
NoteSortInput,
SavedItemNoteFilterInput,
} from '../__generated__/graphql';
import { IContext } from '../apollo';
import { NoteConnectionModel, NoteModel } from './Note';

/**
* Model for resolving note-related fields
* on a SavedItem entity.
*/
export class SavedItemModel {
constructor(
public readonly url: string,
public readonly context: IContext,
) {}
/**
* Paginate over a note connection, filtered only to
* notes attached to thie save (plus other optional filters).
* @param opts pagination options
* @returns NoteConnectionModel
*/
async notes(opts: {
sort?: NoteSortInput;
filter?: SavedItemNoteFilterInput;
pagination?: PaginationInput;
}): Promise<NoteConnectionModel> {
const noteModel = new NoteModel(this.context);
const filter: (NoteFilterInput & { sourceUrl: string }) | undefined =
opts.filter != null
? {
...opts.filter,
sourceUrl: this.url,
}
: { sourceUrl: this.url };
return await noteModel.paginate({
sort: opts.sort,
filter: filter,
pagination: opts.pagination,
});
}
}
29 changes: 29 additions & 0 deletions servers/notes-api/src/test/operations/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,32 @@ export const GET_NOTES = print(gql`
}
}
`);

export const NOTES_FROM_SAVE = print(gql`
${NoteFragment}
${PageInfoFragment}
query NotesFromSave(
$sort: NoteSortInput
$filter: SavedItemNoteFilterInput
$pagination: PaginationInput
$url: String
) {
_entities(representations: { url: $url, __typename: "SavedItem" }) {
... on SavedItem {
url
notes(sort: $sort, filter: $filter, pagination: $pagination) {
pageInfo {
...PageInfoFields
}
totalCount
edges {
cursor
node {
...NoteFields
}
}
}
}
}
}
`);
6 changes: 3 additions & 3 deletions servers/notes-api/src/test/queries/notes.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ describe('notes', () => {
describe('filters', () => {
it('returns only archived notes', async () => {
const variables = {
pagination: { first: 10 },
pagination: { first: 30 },
filter: { archived: true },
};
const res = await request(app)
Expand All @@ -257,7 +257,7 @@ describe('notes', () => {
});
it('returns only not-archived notes', async () => {
const variables = {
pagination: { first: 10 },
pagination: { first: 30 },
filter: { archived: false },
};
const res = await request(app)
Expand All @@ -273,7 +273,7 @@ describe('notes', () => {
});
it('returns notes after a timestamp', async () => {
const variables = {
pagination: { first: 10 },
pagination: { first: 30 },
filter: { since: new Date(now - 1) },
};
const res = await request(app)
Expand Down
Loading

0 comments on commit a4f4737

Please sign in to comment.