Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/postman.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }}
run: |
postman login --with-api-key "$POSTMAN_API_KEY"
postman collection run ./postman/collections/Book-API
postman collection run postman/collections/Book-API

- name: Push to Postman Cloud
if: github.event_name == 'push'
Expand Down
5 changes: 5 additions & 0 deletions .postman/resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
workspace:
id: bc3391dd-f878-43c3-977f-da01f0b63f07
cloudResources:
collections:
../postman/collections/Book API: 21505573-9b973608-fff1-41d9-a176-67845f911fd0
7 changes: 7 additions & 0 deletions postman/collections/Book-API/.resources/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$kind: collection
name: Book API
description: REST API for managing a book collection. Provides endpoints for
CRUD operations on books.
variables:
baseUrl: http://localhost:3000
bookId: ""
4 changes: 4 additions & 0 deletions postman/collections/Book-API/Books/.resources/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$kind: collection
name: Books
description: CRUD operations for managing books in the collection
order: 2000
48 changes: 48 additions & 0 deletions postman/collections/Book-API/Books/Create Book.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
$kind: http-request
name: Create Book
description: Creates a new book in the collection. Returns 201 with the created book.
method: POST
url: "{{baseUrl}}/api/v1/books"
order: 3000
headers:
Content-Type: application/json
body:
type: json
content: |-
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"year": 1925,
"publisher": "Scribner"
}
scripts:
- type: afterResponse
code: |-
pm.test("Status code is 201", function () {
pm.response.to.have.status(201);
});

pm.test("Response has book object", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("book");
});

pm.test("Book has required properties", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.book).to.have.property("id");
pm.expect(jsonData.book).to.have.property("title");
pm.expect(jsonData.book).to.have.property("author");
});

pm.test("Book has publisher property", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.book).to.have.property("publisher");
});

pm.test("Save book id to collection variable", function () {
const jsonData = pm.response.json();
if (jsonData.book && jsonData.book.id) {
pm.collectionVariables.set("bookId", jsonData.book.id);
}
});
language: text/javascript
24 changes: 24 additions & 0 deletions postman/collections/Book-API/Books/Delete Book.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
$kind: http-request
name: Delete Book
description: Deletes a book by ID. Returns success message or 404 if not found.
method: DELETE
url: '{{baseUrl}}/api/v1/books/:id'
order: 5000
pathVariables:
- key: id
value: ''
description: The UUID of the book to delete
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200 or 404", function () {
pm.expect(pm.response.code).to.be.oneOf([200, 404]);
});

pm.test("If 200, response has message property", function () {
if (pm.response.code === 200) {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("message");
}
});
27 changes: 27 additions & 0 deletions postman/collections/Book-API/Books/Get Book.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
$kind: http-request
name: Get Book
description: Returns a single book by ID. Returns 404 if book not found.
method: GET
url: '{{baseUrl}}/api/v1/books/:id'
order: 2000
pathVariables:
- key: id
value: ''
description: The UUID of the book to retrieve
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200 or 404", function () {
pm.expect(pm.response.code).to.be.oneOf([200, 404]);
});

pm.test("If 200, response has book object with required properties", function () {
if (pm.response.code === 200) {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("book");
pm.expect(jsonData.book).to.have.property("id");
pm.expect(jsonData.book).to.have.property("title");
pm.expect(jsonData.book).to.have.property("author");
}
});
28 changes: 28 additions & 0 deletions postman/collections/Book-API/Books/List Books.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
$kind: http-request
name: List Books
description: Returns an array of all books in the collection
method: GET
url: '{{baseUrl}}/api/v1/books'
order: 1000
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});

pm.test("Response has books array", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("books");
pm.expect(jsonData.books).to.be.an("array");
});

pm.test("Each book has required properties", function () {
const jsonData = pm.response.json();
jsonData.books.forEach(function(book) {
pm.expect(book).to.have.property("id");
pm.expect(book).to.have.property("title");
pm.expect(book).to.have.property("author");
});
});
36 changes: 36 additions & 0 deletions postman/collections/Book-API/Books/Update Book.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
$kind: http-request
name: Update Book
description: Updates an existing book by ID. Returns the updated book or 404 if not found.
method: PUT
url: '{{baseUrl}}/api/v1/books/:id'
order: 4000
headers:
- key: Content-Type
value: application/json
pathVariables:
- key: id
value: ''
description: The UUID of the book to update
body:
type: json
content: |-
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"year": 1925,
"publisher": "Scribner"
}
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200 or 404", function () {
pm.expect(pm.response.code).to.be.oneOf([200, 404]);
});

pm.test("If 200, response has book object", function () {
if (pm.response.code === 200) {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("book");
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$kind: collection
name: Health
description: Health and status endpoints for the API
order: 1000
24 changes: 24 additions & 0 deletions postman/collections/Book-API/Health/Health Check.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
$kind: http-request
name: Health Check
description: Returns the health status of the API with current timestamp
method: GET
url: '{{baseUrl}}/health'
order: 2000
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});

pm.test("Response has status property equal to ok", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("status");
pm.expect(jsonData.status).to.eql("ok");
});

pm.test("Response has timestamp property", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("timestamp");
});
18 changes: 18 additions & 0 deletions postman/collections/Book-API/Health/Root.request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$kind: http-request
name: Root
description: Returns API info message with welcome information
method: GET
url: '{{baseUrl}}/'
order: 1000
scripts:
- type: afterResponse
language: text/javascript
code: |-
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});

pm.test("Response has message property", function () {
const jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("message");
});
2 changes: 2 additions & 0 deletions postman/globals/workspace.globals.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: Globals
values: []
7 changes: 4 additions & 3 deletions src/database/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class Database {

_seed() {
const sample = [
new Book('1', 'The Great Gatsby', 'F. Scott Fitzgerald', 1925),
new Book('2', '1984', 'George Orwell', 1949)
new Book('1', 'The Great Gatsby', 'F. Scott Fitzgerald', 1925, 'Scribner'),
new Book('2', '1984', 'George Orwell', 1949, 'Secker & Warburg')
];
sample.forEach(b => this.books.set(b.id, b));
}
Expand All @@ -29,7 +29,7 @@ class Database {

createBook(data) {
const id = uuidv4().slice(0, 8);
const book = new Book(id, data.title.trim(), data.author.trim(), data.year ?? null);
const book = new Book(id, data.title.trim(), data.author.trim(), data.year ?? null, data.publisher ? data.publisher.trim() : null);
this.books.set(id, book);
return book;
}
Expand All @@ -40,6 +40,7 @@ class Database {
if (data.title !== undefined) book.title = data.title.trim();
if (data.author !== undefined) book.author = data.author.trim();
if (data.year !== undefined) book.year = data.year;
if (data.publisher !== undefined) book.publisher = data.publisher ? data.publisher.trim() : null;
return book;
}

Expand Down
11 changes: 8 additions & 3 deletions src/models/Book.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* Book model – title, author, optional year
* Book model – title, author, optional year, optional publisher
*/

class Book {
constructor(id, title, author, year = null) {
constructor(id, title, author, year = null, publisher = null) {
this.id = id;
this.title = title;
this.author = author;
this.year = year;
this.publisher = publisher;
}

static validate(data) {
Expand All @@ -20,6 +21,9 @@ class Book {
if (data.year != null && (typeof data.year !== 'number' || data.year < 0 || !Number.isInteger(data.year))) {
return { isValid: false, error: 'Year must be a non-negative integer' };
}
if (data.publisher != null && (typeof data.publisher !== 'string' || !data.publisher.trim())) {
return { isValid: false, error: 'Publisher must be a non-empty string if provided' };
}
return { isValid: true };
}

Expand All @@ -28,7 +32,8 @@ class Book {
id: this.id,
title: this.title,
author: this.author,
year: this.year
year: this.year,
publisher: this.publisher
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/books.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ router.put('/:id', (req, res) => {
return res.status(404).json({ error: { name: 'notFound', message: 'Book not found' } });
}
const updates = req.body;
const merged = { title: updates.title ?? book.title, author: updates.author ?? book.author, year: updates.year !== undefined ? updates.year : book.year };
const merged = { title: updates.title ?? book.title, author: updates.author ?? book.author, year: updates.year !== undefined ? updates.year : book.year, publisher: updates.publisher !== undefined ? updates.publisher : book.publisher };
const validation = Book.validate(merged);
if (!validation.isValid) {
return res.status(400).json({ error: { name: 'validationError', message: validation.error } });
Expand Down
Loading