|
|
@@ -0,0 +1,266 @@ |
|
|
|
import { NextApiHandler } from 'next'; |
|
|
|
import * as crypto from 'crypto'; |
|
|
|
import * as fs from 'fs/promises'; |
|
|
|
|
|
|
|
const ensureDatabaseDirectory = async () => { |
|
|
|
try { |
|
|
|
await fs.mkdir('.db'); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as NodeJS.ErrnoException; |
|
|
|
|
|
|
|
if (err.code !== 'EEXIST') { |
|
|
|
throw new Error('Failed to create .db directory'); |
|
|
|
} |
|
|
|
|
|
|
|
// noop, .db directory already exists |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const getSingleNote: NextApiHandler = async (req, res) => { |
|
|
|
const { noteId } = req.query; |
|
|
|
|
|
|
|
if (typeof noteId !== 'string') { |
|
|
|
res.status(400).send('Bad Request'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const dataRaw = await fs.readFile('.db/notes.jsonl', { |
|
|
|
encoding: 'utf-8', |
|
|
|
}); |
|
|
|
|
|
|
|
const data = dataRaw |
|
|
|
.split('\n') |
|
|
|
.filter((s) => s.trim().length > 0) |
|
|
|
.map((line) => JSON.parse(line)); |
|
|
|
|
|
|
|
const note = data.find((note) => note.id === noteId); |
|
|
|
|
|
|
|
if (typeof note === 'undefined') { |
|
|
|
res.status(404).send('Not Found'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
res.status(200).json(note); |
|
|
|
}; |
|
|
|
|
|
|
|
const patchNote: NextApiHandler = async (req, res) => { |
|
|
|
const { noteId } = req.query; |
|
|
|
|
|
|
|
if (typeof noteId !== 'string') { |
|
|
|
res.status(400).send('Bad Request'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const { title, content } = req.body; |
|
|
|
|
|
|
|
const dataRaw = await fs.readFile('.db/notes.jsonl', { |
|
|
|
encoding: 'utf-8', |
|
|
|
}); |
|
|
|
|
|
|
|
const data = dataRaw |
|
|
|
.split('\n') |
|
|
|
.filter((s) => s.trim().length > 0) |
|
|
|
.map((line) => JSON.parse(line)); |
|
|
|
|
|
|
|
const noteIndex = data.findIndex((note) => note.id === noteId); |
|
|
|
|
|
|
|
if (noteIndex === -1) { |
|
|
|
res.status(404).send('Not Found'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const note = data[noteIndex]; |
|
|
|
|
|
|
|
const updatedNote = { |
|
|
|
...note, |
|
|
|
title: title ?? note.title, |
|
|
|
content: content ?? note.content, |
|
|
|
}; |
|
|
|
|
|
|
|
data[noteIndex] = updatedNote; |
|
|
|
|
|
|
|
try { |
|
|
|
await ensureDatabaseDirectory(); |
|
|
|
await fs.writeFile( |
|
|
|
'.db/notes.jsonl', |
|
|
|
data.map((note) => JSON.stringify(note)).join('\n'), |
|
|
|
); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as Error; |
|
|
|
res.status(500).send(err.message); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
res.status(200).json(updatedNote); |
|
|
|
}; |
|
|
|
|
|
|
|
const emplaceNote: NextApiHandler = async (req, res) => { |
|
|
|
const { noteId } = req.query; |
|
|
|
|
|
|
|
if (typeof noteId !== 'string') { |
|
|
|
res.status(400).send('Bad Request'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const dataRaw = await fs.readFile('.db/notes.jsonl', { |
|
|
|
encoding: 'utf-8', |
|
|
|
}); |
|
|
|
|
|
|
|
const data = dataRaw |
|
|
|
.split('\n') |
|
|
|
.filter((s) => s.trim().length > 0) |
|
|
|
.map((line) => JSON.parse(line)); |
|
|
|
|
|
|
|
const noteIndex = data.findIndex((note) => note.id === noteId); |
|
|
|
|
|
|
|
if (noteIndex === -1) { |
|
|
|
return createNote({ |
|
|
|
basePath: '/api/notes', |
|
|
|
})(req, res); |
|
|
|
} |
|
|
|
|
|
|
|
return patchNote(req, res); |
|
|
|
} |
|
|
|
|
|
|
|
const deleteNote: NextApiHandler = async (req, res) => { |
|
|
|
const { noteId } = req.query; |
|
|
|
|
|
|
|
if (typeof noteId !== 'string') { |
|
|
|
res.status(400).send('Bad Request'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const dataRaw = await fs.readFile('.db/notes.jsonl', { |
|
|
|
encoding: 'utf-8', |
|
|
|
}); |
|
|
|
|
|
|
|
const data = dataRaw |
|
|
|
.split('\n') |
|
|
|
.filter((s) => s.trim().length > 0) |
|
|
|
.map((line) => JSON.parse(line)); |
|
|
|
|
|
|
|
const noteIndex = data.findIndex((note) => note.id === noteId); |
|
|
|
|
|
|
|
if (noteIndex === -1) { |
|
|
|
res.status(404).send('Not Found'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
data.splice(noteIndex, 1); |
|
|
|
|
|
|
|
try { |
|
|
|
await ensureDatabaseDirectory(); |
|
|
|
await fs.writeFile( |
|
|
|
'.db/notes.jsonl', |
|
|
|
data.map((note) => JSON.stringify(note)).join('\n'), |
|
|
|
); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as Error; |
|
|
|
res.status(500).send(err.message); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
res.status(204).end(); |
|
|
|
} |
|
|
|
|
|
|
|
export const noteResource: NextApiHandler = async (req, res) => { |
|
|
|
switch (req.method?.toLowerCase()) { |
|
|
|
case 'get': |
|
|
|
return getSingleNote(req, res); |
|
|
|
case 'patch': |
|
|
|
return patchNote(req, res); |
|
|
|
case 'put': |
|
|
|
return emplaceNote(req, res); |
|
|
|
case 'delete': |
|
|
|
return deleteNote(req, res); |
|
|
|
default: |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
res.status(405).send('Method not allowed'); |
|
|
|
}; |
|
|
|
|
|
|
|
export interface NoteCollectionParams { |
|
|
|
basePath: string; |
|
|
|
} |
|
|
|
|
|
|
|
const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, res) => { |
|
|
|
const { title, content } = req.body; |
|
|
|
|
|
|
|
if (typeof title !== 'string' || typeof content !== 'string') { |
|
|
|
res.status(400).send('Bad Request'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const newId = crypto.randomUUID(); |
|
|
|
|
|
|
|
try { |
|
|
|
await ensureDatabaseDirectory(); |
|
|
|
await fs.writeFile( |
|
|
|
'.db/notes.jsonl', |
|
|
|
`${JSON.stringify({ |
|
|
|
id: newId, |
|
|
|
title, |
|
|
|
content, |
|
|
|
})}\n`, |
|
|
|
{ |
|
|
|
flag: 'a', |
|
|
|
}, |
|
|
|
); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as Error; |
|
|
|
res.status(500).send(err.message); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// how to genericize the location URL? |
|
|
|
res.setHeader('Location', `${params.basePath}/${newId}`); |
|
|
|
|
|
|
|
res.status(201).json({ |
|
|
|
id: newId, |
|
|
|
title, |
|
|
|
content, |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
const getMultipleNotes: NextApiHandler = async (req, res) => { |
|
|
|
const { ids, q } = req.query; |
|
|
|
const dataRaw = await fs.readFile('.db/notes.jsonl', { |
|
|
|
encoding: 'utf-8', |
|
|
|
}); |
|
|
|
|
|
|
|
const data = dataRaw |
|
|
|
.split('\n') |
|
|
|
.filter((s) => s.trim().length > 0) |
|
|
|
.map((line) => JSON.parse(line)); |
|
|
|
|
|
|
|
let filteredData = data; |
|
|
|
|
|
|
|
if (typeof ids !== 'undefined') { |
|
|
|
const idsArray = Array.isArray(ids) ? ids : [ids]; |
|
|
|
filteredData = data.filter((note) => idsArray.includes(note.id)); |
|
|
|
} |
|
|
|
|
|
|
|
if (typeof q === 'string') { |
|
|
|
const qToLowerCase = q.toLowerCase(); |
|
|
|
filteredData = data.filter((note) => ( |
|
|
|
note.title.toLowerCase().includes(qToLowerCase) |
|
|
|
|| note.content.toLowerCase().includes(qToLowerCase) |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
res.status(200).json(filteredData); |
|
|
|
}; |
|
|
|
|
|
|
|
export const noteCollection = (params: NoteCollectionParams): NextApiHandler =>async (req, res) => { |
|
|
|
switch (req.method?.toLowerCase()) { |
|
|
|
case 'post': |
|
|
|
return createNote(params)(req, res); |
|
|
|
case 'get': |
|
|
|
return getMultipleNotes(req, res); |
|
|
|
default: |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
res.status(405).send('Method not allowed'); |
|
|
|
}; |