feat(projects): list + create API and creatable ProjectAutocomplete (T4)
- GET /api/projects returns the shared global list; POST /api/projects creates a project, deduped case-insensitively (F3). - Repository find-or-create trims and matches on lower(name); a unique index on lower(name) enforces the same rule at the DB (replacing the case- sensitive column unique constraint) — migration 0001. - ProjectAutocomplete: creatable autocomplete that lists projects, offers to create an unknown name inline, and selects it without reload. - Integration test (dedupe/list against Postgres) + component test.
This commit is contained in:
44
server/db/repositories/projects.ts
Normal file
44
server/db/repositories/projects.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { asc, sql } from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type * as schema from '../schema'
|
||||
import { projects } from '../schema'
|
||||
|
||||
type Db = NodePgDatabase<typeof schema>
|
||||
|
||||
export type Project = typeof projects.$inferSelect
|
||||
|
||||
/** The shared, global project list, alphabetical by name (F3). */
|
||||
export async function listProjects(db: Db): Promise<Project[]> {
|
||||
return db.select().from(projects).orderBy(asc(projects.name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find-or-create a project by name, deduping case-insensitively so "Acme" and
|
||||
* "acme" resolve to one project (F3). The DB enforces the same rule via a
|
||||
* unique index on `lower(name)`; the catch handles a concurrent insert race.
|
||||
*/
|
||||
export async function createProject(db: Db, rawName: string): Promise<Project> {
|
||||
const name = rawName.trim()
|
||||
if (!name) throw new Error('Project name is required')
|
||||
|
||||
const existing = await findByName(db, name)
|
||||
if (existing) return existing
|
||||
|
||||
try {
|
||||
const [row] = await db.insert(projects).values({ name }).returning()
|
||||
return row!
|
||||
} catch (err) {
|
||||
const row = await findByName(db, name)
|
||||
if (row) return row
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function findByName(db: Db, name: string): Promise<Project | undefined> {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(sql`lower(${projects.name}) = lower(${name})`)
|
||||
.limit(1)
|
||||
return row
|
||||
}
|
||||
Reference in New Issue
Block a user