From 853d19d9a437926de1235ce4fe78c6db5b84fce7 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 21 Nov 2020 14:50:33 +0800 Subject: [PATCH] Implement inferring of Sequelize models to TypeScript types, delete functionality Use basic infer map of Sequelize models. Implement local and server-side deletion of notes. --- src/pages/notes.tsx | 384 +++++++++++++++++++++++++------------ src/services/Controller.ts | 7 +- src/services/Folder.ts | 16 +- src/services/Note.ts | 14 +- src/services/Storage.ts | 38 +++- src/utilities/Instance.ts | 36 +++- src/utilities/Response.ts | 4 + 7 files changed, 359 insertions(+), 140 deletions(-) diff --git a/src/pages/notes.tsx b/src/pages/notes.tsx index 30262ff..f0f59da 100644 --- a/src/pages/notes.tsx +++ b/src/pages/notes.tsx @@ -7,7 +7,7 @@ import generateId from '../utilities/Id' import Link from 'next/link' import { formatDate } from '../utilities/Date' import { useRouter } from 'next/router' -import { Trash2, FilePlus, FolderPlus } from 'react-feather' +import { Trash2, FilePlus, FolderPlus, FileText, GitBranch, User } from 'react-feather' const Navbar = styled('aside')({ width: 360, @@ -22,6 +22,43 @@ const Navbar = styled('aside')({ }, }) +const PrimaryNavItems = styled('nav')({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'stretch', + '@media (min-width: 1080px)': { + width: '4rem', + }, +}) + +const SecondaryNavItems = styled('nav')({ + height: '100%', + position: 'relative', + '::before': { + content: "''", + display: 'block', + top: 0, + left: 0, + width: '100%', + height: '100%', + position: 'absolute', + zIndex: -1, + backgroundColor: 'white', + opacity: 0.5, + }, + '@media (min-width: 1080px)': { + flex: 'auto', + }, +}) + +const SecondaryNavItemsOverflow = styled('div')({ + overflow: 'auto', + width: '100%', + height: '100%', +}) + const Main = styled('main')({ margin: '2rem 0', '@media (min-width: 1080px)': { @@ -40,11 +77,17 @@ const Container = styled('div')({ }, }) -const NavbarContainer = styled('span')({ +const NavbarItems = styled('div')({ + display: 'flex', + width: '100%', + height: '100%', +}) + +const NavbarContainer = styled('div')({ display: 'block', width: '100%', + height: '100%', margin: '0 0 0 auto', - padding: '0 1rem', boxSizing: 'border-box', maxWidth: 360, }) @@ -59,7 +102,6 @@ const TitleInput = styled('input')({ fontSize: '3rem', fontWeight: 'bold', outline: 0, - marginBottom: '2rem', }) const NoteLink = styled('a')({ @@ -127,6 +169,30 @@ const BinIcon = styled(Trash2)({ marginRight: '0.5rem', }) +const NavbarItemContent = styled('span')({ + padding: '0 1rem', + boxSizing: 'border-box', +}) + +const PrimaryNavItem = styled('a')({ + width: '4rem', + height: '4rem', + display: 'grid', + placeContent: 'center', + color: 'inherit', +}) + +const PostMeta = styled('small')({ + opacity: 0.5, + height: '1.25rem', + display: 'block', + lineHeight: 1.25, +}) + +const PostPrimary = styled('div')({ + marginBottom: '2rem', +}) + type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, } const Notes = ({ id: idProp }) => { @@ -145,7 +211,7 @@ const Notes = ({ id: idProp }) => { timeoutRef.current = window.setTimeout(async () => { const newNote = await Storage.saveNote({ ...stateRef.current, - updatedAt: new Date().toISOString(), + updatedAt: (stateRef.current.updatedAt = new Date().toISOString()), }) if (router.query.id !== id) { await router.push(`/notes/${id}`, undefined, { shallow: true }) @@ -174,6 +240,14 @@ const Notes = ({ id: idProp }) => { autoSave() } + const deleteNote = note => async () => { + setNotes(notes.filter(n => n.id !== note.id)) + const result = await Storage.deleteNote(note) + if (!result) { + setNotes(notes) + } + } + React.useEffect(() => { const loadNotes = async () => { const theNotes = await Storage.loadNotes() @@ -210,121 +284,168 @@ const Notes = ({ id: idProp }) => { - - - - - - - Personal - - - - - - - - - - - - Create Folder - - - - - - - - - - - - Create Note - - - - - - { - Array.isArray(notes!) - && notes.map(n => ( - - { - n.id === id - && ( - - ) - } - - - - - 0 ? 1 : 0.5, }} + + + +
+ + + + + + + + + + +
+
+ + + + + +
+
+ + + + + + + + + Personal + + + + + + + + + + + + Create Folder + + + + + + + + + + + + Create Note + + + + + + { + Array.isArray(notes!) + && notes.map(n => ( + + { + n.id === id + && ( + + ) + } + - {n.title.length > 0 ? n.title : '(untitled)'} -
-
- {' '} - - - -
-
- - - - - - -
- )) - } - - - - - - - View Binned Notes - - - - - + + + + 0 ? 1 : 0.5, }} + > + {n.title.length > 0 ? n.title : '(untitled)'} + + + {' '} + + + + + + + + + + + + + )) + } + + + + + + + View Binned Notes + + + + + + + + +
@@ -332,11 +453,26 @@ const Notes = ({ id: idProp }) => { Array.isArray(notes!) && ( - + + + + { + stateRef.current.updatedAt + && router.query.id + && ( + + ) + } + + async (req, res) => { const methodHandlers = { 'GET': Service.getSingle(repository), 'PUT': Service.save(repository)(req.body), + 'DELETE': Service.remove(repository) } const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers @@ -50,7 +51,11 @@ export const item = (Model, Service) => async (req, res) => { try { const { status, data, } = await handler(id) res.statusCode = status - res.json(data) + if (data) { + res.json(data) + return + } + res.end() } catch (err) { console.log('ERROR', err) const { status, data, } = err diff --git a/src/services/Folder.ts b/src/services/Folder.ts index 4cec27c..1fcd502 100644 --- a/src/services/Folder.ts +++ b/src/services/Folder.ts @@ -1,7 +1,8 @@ -import Model from '../../models/Folder' +import FolderModel from '../../models/Folder' import Instance from '../utilities/Instance' import * as Response from '../utilities/Response' -type ModelInstance = Instance + +type Folder = Instance export const getSingle = repository => async (id: string) => { const instance = await repository.findByPk(id) @@ -20,7 +21,7 @@ export const getMultiple = repository => async (query: Record) }) } -export const save = repository => (body: Partial) => async (id: string, idColumnName = 'id') => { +export const save = repository => (body: Partial) => async (id: string, idColumnName = 'id') => { const [newInstance, created] = await repository.findOrCreate({ where: { [idColumnName]: id }, defaults: { @@ -44,3 +45,12 @@ export const save = repository => (body: Partial) => async (id: s data: updatedInstance.toJSON() }) } + +export const remove = repository => async (id: string) => { + const instanceDAO = repository.findByPk(id) + if (instanceDAO === null) { + throw new Response.NotFound({ message: 'Not found.' }) + } + await instanceDAO.destroy() + return new Response.Destroyed() +} diff --git a/src/services/Note.ts b/src/services/Note.ts index b5d95c0..6c45193 100644 --- a/src/services/Note.ts +++ b/src/services/Note.ts @@ -1,7 +1,8 @@ import Model from '../../models/Note' -import Instance from '../utilities/Instance' +import InferType from '../utilities/Instance' import * as Response from '../utilities/Response' -type ModelInstance = Instance + +type ModelInstance = InferType export const getSingle = repository => async (id: string) => { const instanceDAO = await repository.findByPk(id) @@ -66,3 +67,12 @@ export const save = repository => (body: Partial) => async (id: s } }) } + +export const remove = repository => async (id: string) => { + const instanceDAO = await repository.findByPk(id) + if (instanceDAO === null) { + throw new Response.NotFound({ message: 'Not found.' }) + } + await instanceDAO.destroy() + return new Response.Destroyed() +} diff --git a/src/services/Storage.ts b/src/services/Storage.ts index d64b535..cf928f5 100644 --- a/src/services/Storage.ts +++ b/src/services/Storage.ts @@ -1,13 +1,13 @@ -import {addTime, TimeDivision} from '../utilities/Date' +import { addTime, TimeDivision } from '../utilities/Date' import * as Serialization from '../utilities/Serialization' import LocalStorage from './LocalStorage' -type LoadItemParams = { +type StorageParams = { id: string, url: string, } -type LoadItems = >(params: LoadItemParams) => () => Promise +type LoadItems = >(params: StorageParams) => () => Promise const loadItems: LoadItems = >(params) => async (): Promise => { const { id, url, } = params @@ -39,7 +39,7 @@ const loadItems: LoadItems = >(params) => asyn return localData.items } -type SaveItem = >(p: LoadItemParams) => (item: T) => Promise +type SaveItem = >(p: StorageParams) => (item: T) => Promise const saveItem: SaveItem = >(params) => async (item) => { const { id: storageId, url } = params @@ -50,10 +50,9 @@ const saveItem: SaveItem = >(params) => async ) const localData = storage.getItem(storageId, { expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), - items: [] + items: [], }) - console.log(localData) - const localItems = localData.items + const { items: localItems } = localData const { id: itemId, ...theBody } = item const theItems: T[] = ( localItems.some(i => i.id === itemId) @@ -76,7 +75,32 @@ const saveItem: SaveItem = >(params) => async return responseBody as T } +type DeleteItem = >(p: StorageParams) => (item: T) => Promise + +const deleteItem: DeleteItem = >(params) => async (item) => { + const { id: storageId, url } = params + const storage = new LocalStorage( + window.localStorage, + Serialization.serialize, + Serialization.deserialize + ) + + const localData = storage.getItem(storageId, { + expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), + items: [], + }) + const { items: localItems } = localData + const { id: itemId } = item + const theItems: T[] = localItems.filter(i => i.id !== itemId) + storage.setItem(storageId, { ...localData, items: theItems }) + const response = await window.fetch(`${url}/${itemId}`, { + method: 'delete', + }) + return response.status === 204 +} + export const loadNotes = loadItems({ id: 'notes', url: '/api/notes' }) export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' }) export const saveNote = saveItem({ id: 'notes', url: '/api/notes' }) export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' }) +export const deleteNote = deleteItem({ id: 'notes', url: '/api/notes' }) diff --git a/src/utilities/Instance.ts b/src/utilities/Instance.ts index d2e8848..c3dfeef 100644 --- a/src/utilities/Instance.ts +++ b/src/utilities/Instance.ts @@ -1,5 +1,35 @@ -type Instance = { - [key in keyof T]: unknown +import * as Sequelize from 'sequelize' + +type ModelAttribute = { + allowNull?: boolean, + primaryKey?: boolean, + type: Sequelize.DataType, +} + +type Model = { + tableName?: string, + modelName?: string, + options?: { + timestamps?: boolean, + paranoid?: boolean, + createdAt?: string | boolean, + updatedAt?: string | boolean, + deletedAt?: string | boolean, + }, + rawAttributes: Record, +} + +type InferType = ( + V extends typeof Sequelize.STRING ? string : + V extends typeof Sequelize.TEXT ? string : + V extends typeof Sequelize.DATE ? Date : + V extends typeof Sequelize.DATEONLY ? Date : + V extends typeof Sequelize.UUIDV4 ? string : + unknown +) + +type InferProps = { + [K in keyof M['rawAttributes']]-?: InferType } -export default Instance +export default InferProps diff --git a/src/utilities/Response.ts b/src/utilities/Response.ts index ea8876e..fd48394 100644 --- a/src/utilities/Response.ts +++ b/src/utilities/Response.ts @@ -43,3 +43,7 @@ export class Retrieved> implements Response { this.data = params.data } } + +export class Destroyed implements Response { + public readonly status = 204 +}