import { Inject, Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Query, Req, Res, Headers } from "@nestjs/common"; import type { Request, Response } from "express"; import { DatabaseService } from "./database.service.js"; import { DatabaseQueryService } from "./query.service.js"; import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; import { requireActor } from "../../common/types.js"; import { BulkRecordsDto, type BulkRecordsInput, CreateFormulaDto, type CreateFormulaInput, CreatePropertyDto, type CreatePropertyInput, CreateRecordDto, type CreateRecordInput, CsvImportDto, type CsvImportInput, CreateRelationDto, type CreateRelationInput, CreateViewDto, type CreateViewInput, DatabaseQueryDto, type DatabaseQueryInput, ListRecordsQuery, type ListRecordsInput, UpdateDatabaseDto, type UpdateDatabaseInput, UpdatePropertyDto, type UpdatePropertyInput, UpdateRecordDto, type UpdateRecordInput, UpdateViewDto, type UpdateViewInput, ValidateFormulaDto, type ValidateFormulaInput } from "./dto.js"; @Controller() export class DatabasesController { constructor( @Inject(DatabaseService) private readonly databases: DatabaseService, @Inject(DatabaseQueryService) private readonly query: DatabaseQueryService ) {} // ---------- database ---------- @Get("databases/:id") get(@Param("id") id: string, @Req() req: Request) { return this.databases.get(id, requireActor(req)); } @Patch("databases/:id") update( @Param("id") id: string, @Body(ZodValidate(UpdateDatabaseDto)) body: UpdateDatabaseInput, @Req() req: Request ) { return this.databases.update(id, requireActor(req), body, req); } @Delete("databases/:id") remove(@Param("id") id: string, @Req() req: Request) { return this.databases.remove(id, requireActor(req), req).then(() => ({ deleted: true })); } // ---------- properties ---------- @Get("databases/:id/properties") properties(@Param("id") id: string, @Req() req: Request) { return this.databases.listProperties(id, requireActor(req)); } @Post("databases/:id/properties") createProperty( @Param("id") id: string, @Body(ZodValidate(CreatePropertyDto)) body: CreatePropertyInput, @Req() req: Request ) { return this.databases.createProperty(id, requireActor(req), body, req); } @Patch("databases/:id/properties/:propertyId") updateProperty( @Param("id") id: string, @Param("propertyId") propertyId: string, @Body(ZodValidate(UpdatePropertyDto)) body: UpdatePropertyInput, @Req() req: Request ) { return this.databases.updateProperty(id, propertyId, requireActor(req), body, req); } @Delete("databases/:id/properties/:propertyId") deleteProperty( @Param("id") id: string, @Param("propertyId") propertyId: string, @Req() req: Request ) { return this.databases.deleteProperty(id, propertyId, requireActor(req), req).then(() => ({ deleted: true })); } @Post("databases/:id/properties/:propertyId/reorder") reorderProperty( @Param("id") id: string, @Param("propertyId") propertyId: string, @Body() body: { after?: string; before?: string; index?: number }, @Req() req: Request ) { return this.databases.reorderProperty(id, propertyId, requireActor(req), body, req); } // ---------- views ---------- @Get("databases/:id/views") views(@Param("id") id: string, @Req() req: Request) { return this.databases.listViews(id, requireActor(req)); } @Post("databases/:id/views") createView( @Param("id") id: string, @Body(ZodValidate(CreateViewDto)) body: CreateViewInput, @Req() req: Request ) { return this.databases.createView(id, requireActor(req), body, req); } @Patch("databases/:id/views/:viewId") updateView( @Param("id") id: string, @Param("viewId") viewId: string, @Body(ZodValidate(UpdateViewDto)) body: UpdateViewInput, @Req() req: Request ) { return this.databases.updateView(id, viewId, requireActor(req), body, req); } @Delete("databases/:id/views/:viewId") deleteView( @Param("id") id: string, @Param("viewId") viewId: string, @Req() req: Request ) { return this.databases.deleteView(id, viewId, requireActor(req), req).then(() => ({ deleted: true })); } @Post("databases/:id/views/:viewId/reorder") reorderView( @Param("id") id: string, @Param("viewId") viewId: string, @Body() body: { after?: string; before?: string }, @Req() req: Request ) { return this.databases.reorderView(id, viewId, requireActor(req), body, req); } // ---------- records ---------- @Get("databases/:id/records") records( @Param("id") id: string, @Query(ZodValidate(ListRecordsQuery)) query: ListRecordsInput, @Req() req: Request ) { return this.query.list(id, requireActor(req), query); } /** * TASK-0124 (repair): the documented docs §6 query DSL endpoint * (docs/api/databases.md §6) — the web `useDatabaseRecords` fetches * records through it. It was specified in Phase 6 but never implemented, * so every database table view rendered zero rows (404 on the fetch). * `VIEW` (read-only; no events emitted). */ @Post("databases/:id/query") @HttpCode(200) databaseQuery( @Param("id") id: string, @Body(ZodValidate(DatabaseQueryDto)) body: DatabaseQueryInput, @Req() req: Request ) { return this.query.query(id, requireActor(req), body); } @Post("databases/:id/records") createRecord( @Param("id") id: string, @Body(ZodValidate(CreateRecordDto)) body: CreateRecordInput, @Req() req: Request ) { return this.query.createRecord(id, requireActor(req), body, req); } @Post("databases/:id/records/bulk") bulk( @Param("id") id: string, @Body(ZodValidate(BulkRecordsDto)) body: BulkRecordsInput, @Req() req: Request ) { return this.query.bulk(id, requireActor(req), body, req); } // T-165: CSV import. `dryRun` (default) → 200 mapping only; an actual // import → 201. The 2 MB body ceiling is applied upstream by the // route-scoped parser (common/csv-import-body.ts) before the Zod pipe. @Post("databases/:id/import/csv") async importCsv( @Param("id") id: string, @Body(ZodValidate(CsvImportDto)) body: CsvImportInput, @Req() req: Request, @Res() res: Response ) { const result = await this.query.importCsv(id, requireActor(req), body, req); res.status(result.dryRun ? 200 : 201).json(result); } @Get("databases/:id/export") async exportCsv( @Param("id") id: string, @Query(ZodValidate(ListRecordsQuery)) query: ListRecordsInput, @Req() req: Request, @Res() res: Response ) { const { csv, rows, truncated } = await this.query.exportCsv(id, requireActor(req), query); res.setHeader("Content-Type", "text/csv; charset=utf-8"); res.setHeader("Content-Disposition", `attachment; filename="database-${id}.csv"`); if (truncated) res.setHeader("X-Export-Truncated", "true"); res.send(csv); } @Get("records/:id") record(@Param("id") id: string, @Req() req: Request) { return this.query.getRecord(id, requireActor(req)); } @Patch("records/:id") updateRecord( @Param("id") id: string, @Body(ZodValidate(UpdateRecordDto)) body: UpdateRecordInput, @Req() req: Request ) { return this.query.updateRecord(id, requireActor(req), body, req); } @Delete("records/:id") deleteRecord(@Param("id") id: string, @Req() req: Request) { return this.query.deleteRecord(id, requireActor(req), req); } // ---------- relations ---------- @Get("databases/:id/relations") relations(@Param("id") id: string, @Req() req: Request) { return this.databases.listRelations(id, requireActor(req)); } @Post("databases/:id/relations") createRelation( @Param("id") id: string, @Body(ZodValidate(CreateRelationDto)) body: CreateRelationInput, @Req() req: Request ) { return this.databases.createRelation(id, requireActor(req), body, req); } @Patch("databases/:id/relations/:relationId") updateRelation( @Param("id") id: string, @Param("relationId") relationId: string, @Body() body: { name?: string; targetName?: string }, @Req() req: Request ) { return this.databases.updateRelation(id, relationId, requireActor(req), body, req); } @Delete("databases/:id/relations/:relationId") deleteRelation( @Param("id") id: string, @Param("relationId") relationId: string, @Req() req: Request ) { return this.databases.deleteRelation(id, relationId, requireActor(req), req).then(() => ({ deleted: true })); } @Get("records/:id/relations") recordRelations(@Param("id") id: string, @Req() req: Request) { return this.query.getRecord(id, requireActor(req)).then((r) => ({ relations: (r as { relations: unknown }).relations })); } // ---------- formulas ---------- @Get("databases/:id/formulas") formulas(@Param("id") id: string, @Req() req: Request) { return this.databases.listFormulas(id, requireActor(req)); } @Post("databases/:id/formulas") createFormula( @Param("id") id: string, @Body(ZodValidate(CreateFormulaDto)) body: CreateFormulaInput, @Req() req: Request ) { return this.databases.createFormula(id, requireActor(req), body, req); } @Post("databases/:id/formulas/validate") validateFormula( @Param("id") id: string, @Body(ZodValidate(ValidateFormulaDto)) body: ValidateFormulaInput ) { return this.databases.validateFormula(body); } /** * Evaluate a formula on a record (engine-backed when installed). * TASK-0134: this now runs the safe AST evaluator against the record's * values (100k-op + 50 ms guards) and surfaces a budget breach as the * canonical 422 `FORMULA_TIMEOUT`. VIEW-gated (a record you can't see → * 404, never existence). */ @Get("records/:id/formula/:formulaId") formulaValue( @Param("id") id: string, @Param("formulaId") formulaId: string, @Req() req: Request ) { return this.databases.evaluateRecordFormula(id, formulaId, requireActor(req)); } }