← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
app/data/notes.ts
94 lines · 2.8 KB · TypeScriptimport { KodyRuntime } from 'kody:runtime'
import type { RequestContext } from 'remix/router'
import { createMigrationRunner } from '../../src/storage-migrations.ts'
export type Note = {
id: string
text: string
createdAt: string
}
/** Document shape after schema v1. */
type NotesDocument = {
items: Array<Note>
}
const notesKey = 'package-app-kit-demo-notes'
const legacyNotesKey = 'package-app-kit-demo-notes-v1'
const schemaVersionKey = 'package-app-kit-demo-schema-version'
function getStorage(context: RequestContext) {
return context.get(KodyRuntime).packageStorage()
}
/**
* Demo schema bump: legacy flat array key → `{ items: Note[] }` document.
* Runner is isolate-memoized; safe to await on every notes read/write.
*/
const ensureNotesSchema = (() => {
let ensure: ReturnType<typeof createMigrationRunner> | null = null
return (context: RequestContext) => {
if (!ensure) {
const storage = getStorage(context)
ensure = createMigrationRunner({
storage,
versionKey: schemaVersionKey,
migrations: [
{
version: 1,
name: 'notes-array-to-document',
async up(s) {
const legacy = await s.get(legacyNotesKey)
const existing = await s.get(notesKey)
if (Array.isArray(legacy)) {
await s.set(notesKey, { items: legacy } satisfies NotesDocument)
await s.delete?.(legacyNotesKey)
} else if (Array.isArray(existing)) {
// Older builds may have stored a bare array at the new key.
await s.set(notesKey, { items: existing } satisfies NotesDocument)
} else if (existing == null) {
await s.set(notesKey, { items: [] } satisfies NotesDocument)
}
},
},
],
})
}
return ensure()
}
})()
async function readDocument(context: RequestContext): Promise<NotesDocument> {
await ensureNotesSchema(context)
const stored = await getStorage(context).get(notesKey)
if (stored && typeof stored === 'object' && Array.isArray((stored as NotesDocument).items)) {
return stored as NotesDocument
}
return { items: [] }
}
export async function listNotes(context: RequestContext): Promise<Array<Note>> {
const doc = await readDocument(context)
return doc.items
}
export async function addNote(context: RequestContext, text: string) {
const storage = getStorage(context)
const doc = await readDocument(context)
const note: Note = {
id: crypto.randomUUID(),
text,
createdAt: new Date().toISOString(),
}
const next: NotesDocument = { items: [note, ...doc.items] }
await storage.set(notesKey, next)
return note
}
export async function deleteNote(context: RequestContext, id: string) {
const storage = getStorage(context)
const doc = await readDocument(context)
const items = doc.items.filter((note) => note.id !== id)
await storage.set(notesKey, { items } satisfies NotesDocument)
return { ok: items.length !== doc.items.length }
}