Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
cbea265775
20 changed files with 730 additions and 0 deletions
BIN
apps/.DS_Store
vendored
Normal file
BIN
apps/.DS_Store
vendored
Normal file
Binary file not shown.
7
apps/contacts/crimata.json
Normal file
7
apps/contacts/crimata.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "contacts",
|
||||
"name": "Contacts",
|
||||
"port": 3001,
|
||||
"db": true,
|
||||
"migrate": "npm run migrate"
|
||||
}
|
||||
22
apps/contacts/package.json
Normal file
22
apps/contacts/package.json
Normal 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
5
apps/contacts/src/db.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Pool } from "pg"
|
||||
|
||||
export const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
})
|
||||
9
apps/contacts/src/index.ts
Normal file
9
apps/contacts/src/index.ts
Normal 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}`))
|
||||
21
apps/contacts/src/migrate.ts
Normal file
21
apps/contacts/src/migrate.ts
Normal 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)
|
||||
})
|
||||
61
apps/contacts/src/routes.ts
Normal file
61
apps/contacts/src/routes.ts
Normal 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()
|
||||
})
|
||||
22
apps/contacts/src/types.ts
Normal file
22
apps/contacts/src/types.ts
Normal 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
|
||||
}
|
||||
13
apps/contacts/tsconfig.json
Normal file
13
apps/contacts/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Reference in a new issue