Drop the unused project description field entirely
It never got a UI home on the frontend -- removed from ProjectView a few sessions back and never restored anywhere -- so it was a fully live field (validated, stored, returned by the API, covered by tests) with no consumer. - Migration 011: ALTER TABLE projects DROP COLUMN description. - ProjectRepository: description dropped from ProjectRow, SELECT, create() and update() -- update() is now just a rename (string, not a $fields array; there was never more than one editable field once this left). - ProjectController: store()/update() no longer accept or validate it; update() drops the "provide at least one of title, description" branch, since title is unconditionally the only field now. - Frontend Project type, root README's API docs and curl example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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<string, mixed>
|
||||
*/
|
||||
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'],
|
||||
|
||||
@@ -24,7 +24,7 @@ abstract class ProjectScopedController extends Controller
|
||||
* by the authenticated user.
|
||||
*
|
||||
* @param array<string, string> $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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user