Skip to content

Commit faa633c

Browse files
leo-aa88cursoragent
andcommitted
feat: PUT full replace and PATCH partial update for books
- PUT requires both title and author; PATCH updates only provided fields - Add PatchFields persistence, service PatchBook, handler and route - Tests: unit, auth routes, GORM; E2E patch title-only; isolated SQLite in PATCH test - Regenerate Swagger; document endpoints in README Closes #118 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 44a582d commit faa633c

16 files changed

Lines changed: 579 additions & 38 deletions

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ http://localhost:8001/swagger/index.html
184184
- `GET /api/v1/books`: Get all books.
185185
- `GET /api/v1/books/:id`: Get a single book by ID.
186186
- `POST /api/v1/books`: Create a new book.
187-
- `PUT /api/v1/books/:id`: Update a book.
187+
- `PUT /api/v1/books/:id`: Replace a book's title and author (both fields required in the JSON body).
188+
- `PATCH /api/v1/books/:id`: Partially update a book (send only the fields to change; at least one of `title` or `author` is required).
188189
- `DELETE /api/v1/books/:id`: Delete a book.
189190
- `POST /api/v1/login`: Login.
190191
- `POST /api/v1/register`: Register a new user.
@@ -194,7 +195,7 @@ http://localhost:8001/swagger/index.html
194195

195196
Under **`/api/v1`**, every route **except** `GET /api/v1/` (health) requires the **`X-API-Key`** header matching **`API_SECRET_KEY`** (service-to-service gate).
196197

197-
Book **mutations** (`POST`, `PUT`, and `DELETE` on `/api/v1/books` and `/api/v1/books/:id`) also require a valid user JWT in `Authorization: Bearer <token>` (obtain via `/api/v1/register` and `/api/v1/login`). Book **reads** (`GET` list and `GET` by id) require the API key only.
198+
Book **mutations** (`POST`, `PUT`, `PATCH`, and `DELETE` on `/api/v1/books` and `/api/v1/books/:id`) also require a valid user JWT in `Authorization: Bearer <token>` (obtain via `/api/v1/register` and `/api/v1/login`). Book **reads** (`GET` list and `GET` by id) require the API key only.
198199

199200
```bash
200201
curl -H "X-API-Key: <YOUR_API_KEY>" http://localhost:8001/api/v1/books
@@ -272,7 +273,7 @@ The tests will perform the following actions:
272273
2. Create a new book in the system.
273274
3. Retrieve all books and verify the created book is present.
274275
4. Retrieve a specific book by its ID.
275-
5. Update the book's details.
276+
5. Replace the book's title and author via `PUT`, and patch the title only via `PATCH`.
276277
6. Delete the book and verify it is no longer accessible.
277278

278279
Each test includes assertions to ensure that the API behaves as expected.

docs/docs.go

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ const docTemplate = `{
209209
"JwtAuth": []
210210
}
211211
],
212-
"description": "Update the book details for the given ID",
212+
"description": "Replaces both title and author. Use PATCH for partial updates.",
213213
"consumes": [
214214
"application/json"
215215
],
@@ -219,7 +219,7 @@ const docTemplate = `{
219219
"tags": [
220220
"books"
221221
],
222-
"summary": "Update a book by ID",
222+
"summary": "Replace a book by ID (PUT)",
223223
"parameters": [
224224
{
225225
"type": "string",
@@ -229,12 +229,12 @@ const docTemplate = `{
229229
"required": true
230230
},
231231
{
232-
"description": "Update book object",
232+
"description": "Full book fields",
233233
"name": "input",
234234
"in": "body",
235235
"required": true,
236236
"schema": {
237-
"$ref": "#/definitions/models.UpdateBook"
237+
"$ref": "#/definitions/models.ReplaceBook"
238238
}
239239
}
240240
],
@@ -332,6 +332,83 @@ const docTemplate = `{
332332
}
333333
}
334334
}
335+
},
336+
"patch": {
337+
"security": [
338+
{
339+
"ApiKeyAuth": []
340+
},
341+
{
342+
"JwtAuth": []
343+
}
344+
],
345+
"description": "Updates only fields present in the JSON body (at least one of title or author).",
346+
"consumes": [
347+
"application/json"
348+
],
349+
"produces": [
350+
"application/json"
351+
],
352+
"tags": [
353+
"books"
354+
],
355+
"summary": "Partially update a book by ID (PATCH)",
356+
"parameters": [
357+
{
358+
"type": "string",
359+
"description": "Book ID",
360+
"name": "id",
361+
"in": "path",
362+
"required": true
363+
},
364+
{
365+
"description": "Fields to change",
366+
"name": "input",
367+
"in": "body",
368+
"required": true,
369+
"schema": {
370+
"$ref": "#/definitions/models.PatchBook"
371+
}
372+
}
373+
],
374+
"responses": {
375+
"200": {
376+
"description": "Successfully updated book",
377+
"schema": {
378+
"$ref": "#/definitions/models.Book"
379+
}
380+
},
381+
"400": {
382+
"description": "Bad Request",
383+
"schema": {
384+
"type": "string"
385+
}
386+
},
387+
"401": {
388+
"description": "Unauthorized",
389+
"schema": {
390+
"type": "string"
391+
}
392+
},
393+
"403": {
394+
"description": "Forbidden",
395+
"schema": {
396+
"type": "string"
397+
}
398+
},
399+
"404": {
400+
"description": "book not found",
401+
"schema": {
402+
"type": "string"
403+
}
404+
},
405+
"500": {
406+
"description": "Internal Server Error",
407+
"schema": {
408+
"type": "string"
409+
}
410+
}
411+
}
335412
}
336413
},
337414
"/login": {
@@ -503,7 +580,7 @@ const docTemplate = `{
503580
}
504581
}
505582
},
506-
"models.UpdateBook": {
583+
"models.PatchBook": {
507584
"type": "object",
508585
"properties": {
509586
"author": {
@@ -513,6 +590,21 @@ const docTemplate = `{
513590
"type": "string"
514591
}
515592
}
593+
},
594+
"models.ReplaceBook": {
595+
"type": "object",
596+
"required": [
597+
"author",
598+
"title"
599+
],
600+
"properties": {
601+
"author": {
602+
"type": "string"
603+
},
604+
"title": {
605+
"type": "string"
606+
}
607+
}
516608
}
517609
},
518610
"securityDefinitions": {

docs/swagger.json

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@
203203
"JwtAuth": []
204204
}
205205
],
206-
"description": "Update the book details for the given ID",
206+
"description": "Replaces both title and author. Use PATCH for partial updates.",
207207
"consumes": [
208208
"application/json"
209209
],
@@ -213,7 +213,7 @@
213213
"tags": [
214214
"books"
215215
],
216-
"summary": "Update a book by ID",
216+
"summary": "Replace a book by ID (PUT)",
217217
"parameters": [
218218
{
219219
"type": "string",
@@ -223,12 +223,12 @@
223223
"required": true
224224
},
225225
{
226-
"description": "Update book object",
226+
"description": "Full book fields",
227227
"name": "input",
228228
"in": "body",
229229
"required": true,
230230
"schema": {
231-
"$ref": "#/definitions/models.UpdateBook"
231+
"$ref": "#/definitions/models.ReplaceBook"
232232
}
233233
}
234234
],
@@ -326,6 +326,83 @@
326326
}
327327
}
328328
}
329+
},
330+
"patch": {
331+
"security": [
332+
{
333+
"ApiKeyAuth": []
334+
},
335+
{
336+
"JwtAuth": []
337+
}
338+
],
339+
"description": "Updates only fields present in the JSON body (at least one of title or author).",
340+
"consumes": [
341+
"application/json"
342+
],
343+
"produces": [
344+
"application/json"
345+
],
346+
"tags": [
347+
"books"
348+
],
349+
"summary": "Partially update a book by ID (PATCH)",
350+
"parameters": [
351+
{
352+
"type": "string",
353+
"description": "Book ID",
354+
"name": "id",
355+
"in": "path",
356+
"required": true
357+
},
358+
{
359+
"description": "Fields to change",
360+
"name": "input",
361+
"in": "body",
362+
"required": true,
363+
"schema": {
364+
"$ref": "#/definitions/models.PatchBook"
365+
}
366+
}
367+
],
368+
"responses": {
369+
"200": {
370+
"description": "Successfully updated book",
371+
"schema": {
372+
"$ref": "#/definitions/models.Book"
373+
}
374+
},
375+
"400": {
376+
"description": "Bad Request",
377+
"schema": {
378+
"type": "string"
379+
}
380+
},
381+
"401": {
382+
"description": "Unauthorized",
383+
"schema": {
384+
"type": "string"
385+
}
386+
},
387+
"403": {
388+
"description": "Forbidden",
389+
"schema": {
390+
"type": "string"
391+
}
392+
},
393+
"404": {
394+
"description": "book not found",
395+
"schema": {
396+
"type": "string"
397+
}
398+
},
399+
"500": {
400+
"description": "Internal Server Error",
401+
"schema": {
402+
"type": "string"
403+
}
404+
}
405+
}
329406
}
330407
},
331408
"/login": {
@@ -497,7 +574,7 @@
497574
}
498575
}
499576
},
500-
"models.UpdateBook": {
577+
"models.PatchBook": {
501578
"type": "object",
502579
"properties": {
503580
"author": {
@@ -507,6 +584,21 @@
507584
"type": "string"
508585
}
509586
}
587+
},
588+
"models.ReplaceBook": {
589+
"type": "object",
590+
"required": [
591+
"author",
592+
"title"
593+
],
594+
"properties": {
595+
"author": {
596+
"type": "string"
597+
},
598+
"title": {
599+
"type": "string"
600+
}
601+
}
510602
}
511603
},
512604
"securityDefinitions": {

0 commit comments

Comments
 (0)