Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew Gundersen 2026-02-25 15:35:31 -05:00
commit cbea265775
20 changed files with 730 additions and 0 deletions

BIN
apps/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,7 @@
{
"id": "contacts",
"name": "Contacts",
"port": 3001,
"db": true,
"migrate": "npm run migrate"
}

View file

@ -0,0 +1,22 @@
{
"name": "crimata-contacts",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"migrate": "node -r tsx/cjs src/migrate.ts"
},
"dependencies": {
"express": "^4.18.0",
"pg": "^8.11.0"
},
"devDependencies": {
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"@types/pg": "^8.11.0",
"tsx": "^4.7.0",
"typescript": "^5.4.0"
}
}

5
apps/contacts/src/db.ts Normal file
View file

@ -0,0 +1,5 @@
import { Pool } from "pg"
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})

View file

@ -0,0 +1,9 @@
import express from "express"
import { router } from "./routes"
const app = express()
app.use(express.json())
app.use("/contacts", router)
const port = Number(process.env.PORT ?? 3001)
app.listen(port, () => console.log(`contacts app listening on :${port}`))

View file

@ -0,0 +1,21 @@
import { pool } from "./db"
async function migrate() {
await pool.query(`
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
domain TEXT NOT NULL UNIQUE,
avatar_url TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)
console.log("contacts: migration complete")
await pool.end()
}
migrate().catch((err) => {
console.error("contacts: migration failed", err)
process.exit(1)
})

View file

@ -0,0 +1,61 @@
import { Router } from "express"
import { pool } from "./db"
import type { CreateContactBody, UpdateContactBody } from "./types"
export const router = Router()
// List all contacts
router.get("/", async (req, res) => {
const result = await pool.query(
"SELECT * FROM contacts ORDER BY name ASC"
)
res.json(result.rows)
})
// Get a single contact
router.get("/:id", async (req, res) => {
const result = await pool.query(
"SELECT * FROM contacts WHERE id = $1",
[req.params.id]
)
if (result.rowCount === 0) return res.status(404).json({ error: "Not found" })
res.json(result.rows[0])
})
// Create a contact
router.post("/", async (req, res) => {
const { name, domain, avatar_url, notes } = req.body as CreateContactBody
if (!name || !domain) {
return res.status(400).json({ error: "name and domain are required" })
}
const result = await pool.query(
`INSERT INTO contacts (name, domain, avatar_url, notes)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[name, domain, avatar_url ?? null, notes ?? null]
)
res.status(201).json(result.rows[0])
})
// Update a contact
router.patch("/:id", async (req, res) => {
const { name, domain, avatar_url, notes } = req.body as UpdateContactBody
const result = await pool.query(
`UPDATE contacts
SET name = COALESCE($1, name),
domain = COALESCE($2, domain),
avatar_url = COALESCE($3, avatar_url),
notes = COALESCE($4, notes)
WHERE id = $5
RETURNING *`,
[name ?? null, domain ?? null, avatar_url ?? null, notes ?? null, req.params.id]
)
if (result.rowCount === 0) return res.status(404).json({ error: "Not found" })
res.json(result.rows[0])
})
// Delete a contact
router.delete("/:id", async (req, res) => {
await pool.query("DELETE FROM contacts WHERE id = $1", [req.params.id])
res.status(204).send()
})

View file

@ -0,0 +1,22 @@
export interface Contact {
id: number
name: string
domain: string
avatar_url: string | null
notes: string | null
created_at: string
}
export interface CreateContactBody {
name: string
domain: string
avatar_url?: string
notes?: string
}
export interface UpdateContactBody {
name?: string
domain?: string
avatar_url?: string
notes?: string
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}