Files
temetro/backend/src/services/notes.ts
T
Khalid Abdi 7d6641ef59 Add notes API: clinic + author-scoped CRUD
New `note` table (src/db/schema/notes.ts) referencing organization + user,
with org+author scoping so a doctor only sees their own notes within the active
clinic. Adds:

- src/types/note.ts + src/lib/note-validation.ts (zod)
- src/services/notes.ts (CRUD, treats non-uuid ids as not-found)
- src/routes/notes.ts mounted at /api/notes, gated requireAuth → requireOrg
- schema barrel + generated migration drizzle/0001_*.sql (applied on startup
  by the runtime migrator)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:48:39 +03:00

111 lines
2.5 KiB
TypeScript

import { and, desc, eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { notes } from "../db/schema/notes.js";
import type { NoteInput } from "../lib/note-validation.js";
import type { Note } from "../types/note.js";
type NoteRow = typeof notes.$inferSelect;
// Postgres throws on a malformed uuid; treat non-uuid ids as "not found".
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function toNote(row: NoteRow): Note {
return {
id: row.id,
title: row.title,
content: row.content,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export async function listNotes(
orgId: string,
authorId: string,
): Promise<Note[]> {
const rows = await db
.select()
.from(notes)
.where(and(eq(notes.organizationId, orgId), eq(notes.authorId, authorId)))
.orderBy(desc(notes.updatedAt));
return rows.map(toNote);
}
export async function getNote(
orgId: string,
authorId: string,
id: string,
): Promise<Note | null> {
if (!UUID_RE.test(id)) return null;
const [row] = await db
.select()
.from(notes)
.where(
and(
eq(notes.id, id),
eq(notes.organizationId, orgId),
eq(notes.authorId, authorId),
),
);
return row ? toNote(row) : null;
}
export async function createNote(
orgId: string,
authorId: string,
input: NoteInput,
): Promise<Note> {
const [row] = await db
.insert(notes)
.values({
organizationId: orgId,
authorId,
title: input.title,
content: input.content,
})
.returning();
return toNote(row!);
}
export async function updateNote(
orgId: string,
authorId: string,
id: string,
input: NoteInput,
): Promise<Note | null> {
if (!UUID_RE.test(id)) return null;
const [row] = await db
.update(notes)
.set({ title: input.title, content: input.content })
.where(
and(
eq(notes.id, id),
eq(notes.organizationId, orgId),
eq(notes.authorId, authorId),
),
)
.returning();
return row ? toNote(row) : null;
}
export async function deleteNote(
orgId: string,
authorId: string,
id: string,
): Promise<boolean> {
if (!UUID_RE.test(id)) return false;
const deleted = await db
.delete(notes)
.where(
and(
eq(notes.id, id),
eq(notes.organizationId, orgId),
eq(notes.authorId, authorId),
),
)
.returning({ id: notes.id });
return deleted.length > 0;
}