diff --git a/src/client/lib/models/notebook.ts b/src/client/lib/models/notebook.ts index 6672e78a..4e030aa1 100644 --- a/src/client/lib/models/notebook.ts +++ b/src/client/lib/models/notebook.ts @@ -3,11 +3,6 @@ //---------------------------------------------------------------------------------------------------------------------- // Models -import { NotebookOptions, NotePage } from '../../../common/interfaces/common'; - -//---------------------------------------------------------------------------------------------------------------------- - -export type NotebookPage = NotePage; -export type Notebook = NotebookOptions; +export { Notebook, NotebookPage } from '../../../common/interfaces/common'; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/common/interfaces/common.ts b/src/common/interfaces/common.ts index 4516b2a3..8f4ab527 100644 --- a/src/common/interfaces/common.ts +++ b/src/common/interfaces/common.ts @@ -5,7 +5,7 @@ export { AccountOptions, AccountSettings, Account } from './models/account'; export { CharacterOptions, SystemDetails, Character } from './models/character'; export { DiceRoll } from './models/dice'; -export { NotebookOptions, NotebookPageOptions, Notebook, NotePage } from './models/notebook'; +export { NotebookOptions, NotebookPageOptions, Notebook, NotebookPage } from './models/notebook'; export { RoleOptions, Role } from './models/role'; export { RPGKMessage, RPGKEventMessage, RPGKRemoveMessage, RPGKUpdateMessage } from './models/messages'; export { SupplementOptions, Supplement } from './models/supplement'; diff --git a/src/common/interfaces/models/notebook.ts b/src/common/interfaces/models/notebook.ts index 29c23f80..7b556152 100644 --- a/src/common/interfaces/models/notebook.ts +++ b/src/common/interfaces/models/notebook.ts @@ -18,8 +18,8 @@ export interface NotebookPageOptions { // FIXME: Once Models are removed, `NotebookOptions` should be named 'Notebook'. export type Notebook = NotebookOptions; -// FIXME: Once Models are removed, `NotebookPageOptions` should be named 'NotePage'. -export type NotePage = NotebookPageOptions; +// FIXME: Once Models are removed, `NotebookPageOptions` should be named 'NotebookPage'. +export type NotebookPage = NotebookPageOptions; //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/decoders/notebook.ts b/src/server/decoders/notebook.ts deleted file mode 100644 index d0c12e3f..00000000 --- a/src/server/decoders/notebook.ts +++ /dev/null @@ -1,24 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Notebook Decoders -// --------------------------------------------------------------------------------------------------------------------- - -import { array, either, number, string, object, optional } from 'decoders'; -import { withDefault } from './utils'; - -// --------------------------------------------------------------------------------------------------------------------- - -export const notebookPageDecoder = object({ - id: optional(either(string, number)), - title: string, - content: optional(string), - notebookID: string -}); - -// --------------------------------------------------------------------------------------------------------------------- - -export const notebookDecoder = object({ - id: string, - pages: withDefault(array(notebookPageDecoder), []) -}); - -// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/managers/notebook.ts b/src/server/managers/notebook.ts index 2cb8cdba..3ad1cbba 100644 --- a/src/server/managers/notebook.ts +++ b/src/server/managers/notebook.ts @@ -3,175 +3,51 @@ // --------------------------------------------------------------------------------------------------------------------- // Models -import { Notebook, NotebookPage } from '../models/notebook'; +import { Notebook, NotebookPage } from '../../common/interfaces/models/notebook'; -// Utils -import { getDB } from '../utils/database'; -import { shortID } from '../utils/misc'; -import { MultipleResultsError, NotFoundError } from '../errors'; - -// --------------------------------------------------------------------------------------------------------------------- - -export interface NoteFilters { - id : unknown, - email : unknown, - title : unknown -} +// Resource Access +import * as noteRA from '../resource-access/notebook'; // --------------------------------------------------------------------------------------------------------------------- export async function get(notebookID : string) : Promise { - const db = await getDB(); - const pages = (await db('note_page as np') - .select( - 'page_id as id', - 'n.note_id as notebookID', - 'content', - 'title' - ) - .join('note as n', 'n.note_id', '=', 'np.note_id') - .where({ 'n.note_id': notebookID })) - .map(NotebookPage.fromDB); - - return Notebook.fromDB({ id: notebookID, pages }); + return noteRA.get(notebookID); } -export async function list(filters : NoteFilters) : Promise +export async function list(filters : noteRA.NoteFilters) : Promise { - const db = await getDB(); - const query = db('note as n') - .select('n.note_id as notebookID') - .distinct('n.note_id') - .leftJoin('note_page as np', 'np.note_id', '=', 'n.note_id') - .join('character as c', 'c.note_id', '=', 'n.note_id') - .join('account as a', 'a.account_id', '=', 'c.account_id'); - - if(filters.id) - { - query.where({ 'n.note_id': filters.id }); - } - - if(filters.email) - { - query.where({ 'a.email': filters.email }); - } - - if(filters.title) - { - query.where({ 'np.title': filters.title }); - } - - return Promise.all((await query) - .map(({ notebookID }) => - { - return get(notebookID); - })); + return noteRA.list(filters); } export async function getPage(pageID : string | number) : Promise { - const db = await getDB(); - const pages = await db('note_page as np') - .select( - 'page_id as id', - 'n.note_id as notebookID', - 'content', - 'title' - ) - .join('note as n', 'n.note_id', '=', 'np.note_id') - .where({ id: pageID }); - - if(pages.length > 1) - { - throw new MultipleResultsError('page'); - } - else if(pages.length === 0) - { - throw new NotFoundError(`No page record found for id '${ pageID }'.`); - } - else - { - return NotebookPage.fromDB(pages[0]); - } + return noteRA.getPage(pageID); } -export async function addPage(notebookID : string, page : Record) : Promise +export async function addPage(notebookID : string, page : NotebookPage) : Promise { - const db = await getDB(); - const notePage = NotebookPage.fromJSON({ ...page, notebookID }); - - const [ pageID ] = await db('note_page') - .insert(notePage.toDB()); - - // Return the notebook page - return getPage(pageID); + return noteRA.addPage(notebookID, page); } -export async function add(pages : Record[] = []) : Promise +export async function add(pages : NotebookPage[] = []) : Promise { - const db = await getDB(); - const newNoteID = shortID(); - - await db('note') - .insert({ note_id: newNoteID }); - - // Add any pages that were specified - await Promise.all(pages.map(async(page) => - { - await addPage(newNoteID, page); - })); - - // Return the note - return get(newNoteID); + return noteRA.add(pages); } -export async function updatePage(pageID : string | number, pageUpdate : Record) : Promise +export async function updatePage(pageID : string | number, pageUpdate : Partial) : Promise { - // Get the current page - const page = await getPage(pageID); - - // Mix the current page with the allowed updates. - const allowedUpdate = { - ...page.toJSON(), - title: pageUpdate.title ?? page.title, - content: pageUpdate.content ?? page.content - }; - - // Make a new page object - const newPage = NotebookPage.fromJSON(allowedUpdate); - - // Drop the note_id from the update - const { note_id, ...dbRest } = newPage.toDB(); - - // Update the database - const db = await getDB(); - await db('note_page') - .update(dbRest) - .where({ page_id: pageID }); - - // Return the updated record - return getPage(pageID); + return noteRA.updatePage(pageID, pageUpdate); } export async function removePage(pageID : string) : Promise<{ status : 'ok' }> { - const db = await getDB(); - await db('note_page') - .where({ page_id: pageID }) - .delete(); - - return { status: 'ok' }; + return noteRA.removePage(pageID); } export async function remove(notebookID : string) : Promise<{ status : 'ok' }> { - const db = await getDB(); - await db('note') - .where({ note_id: notebookID }) - .delete(); - - return { status: 'ok' }; + return noteRA.remove(notebookID); } // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/models/notebook.ts b/src/server/models/notebook.ts deleted file mode 100644 index 378876ec..00000000 --- a/src/server/models/notebook.ts +++ /dev/null @@ -1,124 +0,0 @@ -//---------------------------------------------------------------------------------------------------------------------- -// Note -//---------------------------------------------------------------------------------------------------------------------- - -import * as JsonDecoder from 'decoders'; -import { notebookDecoder, notebookPageDecoder } from '../decoders/notebook'; - -// Models -import { NotebookOptions, NotebookPageOptions } from '../../common/interfaces/models/notebook'; - -//---------------------------------------------------------------------------------------------------------------------- - -export class NotebookPage -{ - public readonly id : string; - public readonly notebookID : string; - - public title = ''; - public content = ''; - - constructor(options : NotebookPageOptions) - { - this.id = options.id; - this.notebookID = options.notebookID; - this.title = options.title; - this.content = options.content; - } - - //------------------------------------------------------------------------------------------------------------------ - // Serialization - //------------------------------------------------------------------------------------------------------------------ - - public toJSON() : Record - { - return { - id: this.id, - title: this.title, - content: this.content, - notebookID: this.notebookID - }; - } - - public toDB() : Record - { - const { id, notebookID, ...jsonObj } = this.toJSON(); - return { - ...jsonObj, - page_id: id, - note_id: notebookID - }; - } - - //------------------------------------------------------------------------------------------------------------------ - // Deserialization - //------------------------------------------------------------------------------------------------------------------ - - static fromDB(noteRecord : Record) : NotebookPage - { - const decoder = JsonDecoder.guard(notebookPageDecoder); - return new NotebookPage(decoder(noteRecord) as NotebookPageOptions); - } - - static fromJSON(jsonObj : Record) : NotebookPage - { - const decoder = JsonDecoder.guard(notebookPageDecoder); - return new NotebookPage(decoder(jsonObj) as NotebookPageOptions); - } -} - -//---------------------------------------------------------------------------------------------------------------------- - -export class Notebook -{ - public readonly id : string; - public pages : NotebookPage[] = []; - - constructor(options : NotebookOptions) - { - this.id = options.id; - - if(options.pages) - { - this.pages = options.pages.map((page) => new NotebookPage(page)); - } - } - - //------------------------------------------------------------------------------------------------------------------ - // Serialization - //------------------------------------------------------------------------------------------------------------------ - - public toJSON() : Record - { - return { - id: this.id, - pages: this.pages.map((page) => page.toJSON()) - }; - } - - public toDB() : Record - { - return { - note_id: this.id - }; - } - - //------------------------------------------------------------------------------------------------------------------ - // Deserialization - //------------------------------------------------------------------------------------------------------------------ - - static fromDB(noteRecord : Record) : Notebook - { - const decoder = JsonDecoder.guard(notebookDecoder); - return new Notebook(decoder(noteRecord) as NotebookOptions); - } - - static fromJSON(jsonObj : Record) : Notebook - { - const decoder = JsonDecoder.guard(notebookDecoder); - return new Notebook(decoder(jsonObj) as NotebookOptions); - } -} - -//---------------------------------------------------------------------------------------------------------------------- - diff --git a/src/server/resource-access/notebook.ts b/src/server/resource-access/notebook.ts new file mode 100644 index 00000000..c679273d --- /dev/null +++ b/src/server/resource-access/notebook.ts @@ -0,0 +1,179 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Notebook Resource Access +// --------------------------------------------------------------------------------------------------------------------- + +// Models +import { Notebook, NotebookPage } from '../../common/interfaces/models/notebook'; + +// Transforms +import * as NoteTransforms from './transforms/notebook'; + +// Utils +import { getDB } from '../utils/database'; +import { shortID } from '../utils/misc'; +import { MultipleResultsError, NotFoundError } from '../errors'; + +// --------------------------------------------------------------------------------------------------------------------- + +export interface NoteFilters { + id ?: string | string[], + email ?: string | string[] + title ?: string | string[] +} + +// --------------------------------------------------------------------------------------------------------------------- + +export async function get(notebookID : string) : Promise +{ + const db = await getDB(); + const pages = await db('note_page as np') + .select( + 'page_id as id', + 'n.note_id as notebookID', + 'content', + 'title' + ) + .join('note as n', 'n.note_id', '=', 'np.note_id') + .where({ 'n.note_id': notebookID }); + + return NoteTransforms.fromDB({ notebook_id: notebookID, pages }); +} + +export async function list(filters : NoteFilters) : Promise +{ + const db = await getDB(); + const query = db('note as n') + .select('n.note_id as notebookID') + .distinct('n.note_id') + .leftJoin('note_page as np', 'np.note_id', '=', 'n.note_id') + .join('character as c', 'c.note_id', '=', 'n.note_id') + .join('account as a', 'a.account_id', '=', 'c.account_id'); + + if(filters.id) + { + query.where({ 'n.note_id': filters.id }); + } + + if(filters.email) + { + query.where({ 'a.email': filters.email }); + } + + if(filters.title) + { + query.where({ 'np.title': filters.title }); + } + + return Promise.all((await query) + .map(({ notebookID }) => + { + return get(notebookID); + })); +} + +export async function getPage(pageID : string | number) : Promise +{ + const db = await getDB(); + const pages = await db('note_page as np') + .select( + 'page_id as id', + 'n.note_id as notebookID', + 'content', + 'title' + ) + .join('note as n', 'n.note_id', '=', 'np.note_id') + .where({ id: pageID }); + + if(pages.length > 1) + { + throw new MultipleResultsError('page'); + } + else if(pages.length === 0) + { + throw new NotFoundError(`No page record found for id '${ pageID }'.`); + } + else + { + return NoteTransforms.pageFromDB(pages[0]); + } +} + +export async function addPage(notebookID : string, page : NotebookPage) : Promise +{ + const db = await getDB(); + const notePage = NoteTransforms.pageToDB({ ...page, notebookID }); + + const [ pageID ] = await db('note_page') + .insert(notePage); + + // Return the notebook page + return getPage(pageID); +} + +export async function add(pages : NotebookPage[] = []) : Promise +{ + const db = await getDB(); + const newNoteID = shortID(); + + await db('note') + .insert({ note_id: newNoteID }); + + // Add any pages that were specified + await Promise.all(pages.map(async(page) => + { + await addPage(newNoteID, page); + })); + + // Return the note + return get(newNoteID); +} + +export async function updatePage(pageID : string | number, pageUpdate : Partial) : Promise +{ + // Get the current page + const page = await getPage(pageID); + + // Mix the current page with the allowed updates. + const allowedUpdate = { + ...page, + title: pageUpdate.title ?? page.title, + content: pageUpdate.content ?? page.content + }; + + // Make a new page object + const newPage = NoteTransforms.pageToDB(allowedUpdate); + + // Drop the note_id from the update + const { note_id, ...dbRest } = newPage; + + // Update the database + const db = await getDB(); + await db('note_page') + .update(dbRest) + .where({ page_id: pageID }); + + // Return the updated record + return getPage(pageID); +} + +export async function removePage(pageID : string) : Promise<{ status : 'ok' }> +{ + const db = await getDB(); + await db('note_page') + .where({ page_id: pageID }) + .delete(); + + return { status: 'ok' }; +} + +export async function remove(notebookID : string) : Promise<{ status : 'ok' }> +{ + const db = await getDB(); + await db('note') + .where({ note_id: notebookID }) + .delete(); + + return { status: 'ok' }; +} + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/server/resource-access/transforms/notebook.ts b/src/server/resource-access/transforms/notebook.ts new file mode 100644 index 00000000..faec2d04 --- /dev/null +++ b/src/server/resource-access/transforms/notebook.ts @@ -0,0 +1,61 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Notebook Database Transforms +// --------------------------------------------------------------------------------------------------------------------- + +import { Notebook, NotebookPage } from '../../../common/interfaces/models/notebook'; + +// --------------------------------------------------------------------------------------------------------------------- + +export interface NotePageDBRecord +{ + page_id : string; + note_id : string; + title : string; + content : string; +} + +export interface NotebookDBRecord +{ + notebook_id : string; + pages : NotePageDBRecord[]; +} + +// --------------------------------------------------------------------------------------------------------------------- + +export function pageFromDB(record : NotePageDBRecord) : NotebookPage +{ + return { + id: record.page_id, + notebookID: record.note_id, + title: record.title, + content: record.content + }; +} + +export function fromDB(record : NotebookDBRecord) : Notebook +{ + return { + id: record.notebook_id, + pages: record.pages.map(pageFromDB) + }; +} + +export function pageToDB(page : NotebookPage) : NotePageDBRecord +{ + return { + page_id: page.id, + note_id: page.notebookID, + title: page.title, + content: page.content + }; +} + +export function toDB(notebook : Notebook) : NotebookDBRecord +{ + return { + notebook_id: notebook.id, + pages: notebook.pages.map(pageToDB) + }; +} + +// ---------------------------------------------------------------------------------------------------------------------