Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← 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 · TypeScript
import { 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 }
}