diff --git a/README.md b/README.md index 5040c02..c5c103e 100644 --- a/README.md +++ b/README.md @@ -284,15 +284,14 @@ owner (the creator); another user's project — or a missing one — always resp | `GET` | `/api/projects` | the caller's projects, sorted A→Z by title | | `POST` | `/api/projects` | create a project | | `GET` | `/api/projects/{id}` | one project | -| `PATCH` | `/api/projects/{id}` | update `title` and/or `description` | +| `PATCH` | `/api/projects/{id}` | rename the project (`title`) | | `DELETE` | `/api/projects/{id}` | delete the project and its cards (`204`) | `GET /api/projects` is always ordered alphabetically (case-insensitive) by title; there is no other sort option. A user may own at most **100 projects** — creating one beyond that responds `409`. -Create/update body: `title` (required on create, 1–255 chars), `description` -(optional, ≤ 2000 chars, defaults to `""`). `PATCH` needs at least one field. +Create/update body: `title` (required, 1–255 chars). Project representation: @@ -301,7 +300,6 @@ Project representation: "project": { "id": 1, "title": "Website relaunch", - "description": "Q3", "owner_id": 1, "card_count": 3, "completed_count": 1, @@ -452,7 +450,7 @@ curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN" PROJECT=$(curl -s -X POST $BASE/api/projects \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ - -d '{"title":"Website relaunch","description":"Q3"}' \ + -d '{"title":"Website relaunch"}' \ | tr -d ' \n' | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) curl -s $BASE/api/projects/$PROJECT/statuses -H "Authorization: Bearer $TOKEN" diff --git a/migrations/011_drop_project_description.sql b/migrations/011_drop_project_description.sql new file mode 100644 index 0000000..fdfdf21 --- /dev/null +++ b/migrations/011_drop_project_description.sql @@ -0,0 +1,3 @@ +-- The description field never got a UI home on the frontend and isn't used +-- anywhere; drop it rather than carry an unused column. +ALTER TABLE projects DROP COLUMN description; diff --git a/src/Http/Controllers/ProjectController.php b/src/Http/Controllers/ProjectController.php index 7049f4f..0587c21 100644 --- a/src/Http/Controllers/ProjectController.php +++ b/src/Http/Controllers/ProjectController.php @@ -18,7 +18,6 @@ use Psr\Http\Message\ServerRequestInterface as Request; final class ProjectController extends ProjectScopedController { private const TITLE_MAX = 255; - private const DESCRIPTION_MAX = 2000; private const MAX_PROJECTS_PER_OWNER = 100; public function __construct( @@ -47,7 +46,6 @@ final class ProjectController extends ProjectScopedController $validator = new Validator($this->body($request)); $title = $validator->requiredString('title', self::TITLE_MAX); - $description = $validator->optionalString('description', self::DESCRIPTION_MAX) ?? ''; $validator->assert(); if ($this->projects->countForOwner($ownerId) >= self::MAX_PROJECTS_PER_OWNER) { @@ -57,7 +55,7 @@ final class ProjectController extends ProjectScopedController ); } - $project = $this->projects->create($ownerId, $title, $description); + $project = $this->projects->create($ownerId, $title); $this->statuses->seedDefaults($project['id']); return $this->json($response, ['project' => $this->present($project)], 201); @@ -79,19 +77,10 @@ final class ProjectController extends ProjectScopedController $project = $this->requireOwnedProject($request, $args); $validator = new Validator($this->body($request)); - $fields = []; - if ($validator->has('title')) { - $fields['title'] = $validator->requiredString('title', self::TITLE_MAX); - } - if ($validator->has('description')) { - $fields['description'] = $validator->optionalString('description', self::DESCRIPTION_MAX) ?? ''; - } - if ($fields === [] && !$validator->failed()) { - $validator->add('title', 'Provide at least one of: title, description.'); - } + $title = $validator->requiredString('title', self::TITLE_MAX); $validator->assert(); - $updated = $this->projects->update($project['id'], $project['owner_id'], $fields); + $updated = $this->projects->update($project['id'], $project['owner_id'], $title); return $this->json($response, ['project' => $this->present($updated)]); } @@ -107,7 +96,7 @@ final class ProjectController extends ProjectScopedController } /** - * @param array{id: int, owner_id: int, title: string, description: string, card_count: int, completed_count: int, created_at: string, updated_at: string} $project + * @param array{id: int, owner_id: int, title: string, card_count: int, completed_count: int, created_at: string, updated_at: string} $project * @return array */ private function present(array $project): array @@ -115,7 +104,6 @@ final class ProjectController extends ProjectScopedController return [ 'id' => $project['id'], 'title' => $project['title'], - 'description' => $project['description'], 'owner_id' => $project['owner_id'], 'card_count' => $project['card_count'], 'completed_count' => $project['completed_count'], diff --git a/src/Http/Controllers/ProjectScopedController.php b/src/Http/Controllers/ProjectScopedController.php index 9ba0aac..9c945f6 100644 --- a/src/Http/Controllers/ProjectScopedController.php +++ b/src/Http/Controllers/ProjectScopedController.php @@ -24,7 +24,7 @@ abstract class ProjectScopedController extends Controller * by the authenticated user. * * @param array $args - * @return array{id: int, owner_id: int, title: string, description: string, card_count: int, completed_count: int, created_at: string, updated_at: string} + * @return array{id: int, owner_id: int, title: string, card_count: int, completed_count: int, created_at: string, updated_at: string} */ protected function requireOwnedProject(Request $request, array $args): array { diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index ffce5a7..a7fe659 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -11,14 +11,14 @@ use PDO; * Data access for the `projects` table. * * @phpstan-type ProjectRow array{ - * id: int, owner_id: int, title: string, description: string, + * id: int, owner_id: int, title: string, * card_count: int, completed_count: int, created_at: string, updated_at: string * } */ final class ProjectRepository { private const SELECT = <<<'SQL' - SELECT p.id, p.owner_id, p.title, p.description, p.created_at, p.updated_at, + SELECT p.id, p.owner_id, p.title, p.created_at, p.updated_at, (SELECT COUNT(*) FROM cards c WHERE c.project_id = p.id) AS card_count, (SELECT COUNT(*) FROM cards c WHERE c.project_id = p.id AND c.complete = 1) AS completed_count FROM projects p @@ -68,15 +68,12 @@ final class ProjectRepository /** * @return ProjectRow */ - public function create(int $ownerId, string $title, string $description): array + public function create(int $ownerId, string $title): array { - $stmt = $this->pdo->prepare( - 'INSERT INTO projects (owner_id, title, description) VALUES (:owner, :title, :description)' - ); + $stmt = $this->pdo->prepare('INSERT INTO projects (owner_id, title) VALUES (:owner, :title)'); $stmt->execute([ 'owner' => $ownerId, 'title' => $title, - 'description' => $description, ]); /** @var ProjectRow $project */ @@ -86,23 +83,14 @@ final class ProjectRepository } /** - * @param array{title?: string, description?: string} $fields * @return ProjectRow */ - public function update(int $id, int $ownerId, array $fields): array + public function update(int $id, int $ownerId, string $title): array { - $sets = ['updated_at = ' . Database::nowExpr()]; - $params = ['id' => $id]; - - foreach (['title', 'description'] as $column) { - if (array_key_exists($column, $fields)) { - $sets[] = "{$column} = :{$column}"; - $params[$column] = $fields[$column]; - } - } - - $stmt = $this->pdo->prepare('UPDATE projects SET ' . implode(', ', $sets) . ' WHERE id = :id'); - $stmt->execute($params); + $stmt = $this->pdo->prepare( + 'UPDATE projects SET title = :title, updated_at = ' . Database::nowExpr() . ' WHERE id = :id' + ); + $stmt->execute(['id' => $id, 'title' => $title]); /** @var ProjectRow $project */ $project = $this->findOwnedBy($id, $ownerId); diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index 7e41a36..8107434 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -15,15 +15,11 @@ final class ProjectTest extends ApiTestCase { $auth = $this->authHeader(); - $created = $this->request('POST', '/api/projects', [ - 'title' => ' Groceries ', - 'description' => 'Weekly shop', - ], $auth); + $created = $this->request('POST', '/api/projects', ['title' => ' Groceries '], $auth); self::assertSame(201, $created->getStatusCode()); $project = $this->decode($created)['project']; self::assertSame('Groceries', $project['title']); - self::assertSame('Weekly shop', $project['description']); self::assertSame(0, $project['card_count']); $index = $this->decode($this->request('GET', '/api/projects', null, $auth)); @@ -63,7 +59,7 @@ final class ProjectTest extends ApiTestCase public function test_project_creation_validates_title(): void { - $response = $this->request('POST', '/api/projects', ['description' => 'no title'], $this->authHeader()); + $response = $this->request('POST', '/api/projects', [], $this->authHeader()); self::assertSame(422, $response->getStatusCode()); self::assertArrayHasKey('title', $this->decode($response)['error']['details']); diff --git a/web/src/types.ts b/web/src/types.ts index 6ec9448..efd4229 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -26,7 +26,6 @@ export interface Passkey { export interface Project { id: number title: string - description: string owner_id: number card_count: number completed_count: number