diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..76e0161 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "mongodb@claude-plugins-official": true + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9c73e4a..d8245c1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,8 @@ "name": "MongoDB Library Sample App - Nodejs version", "dockerComposeFile": "docker-compose.yml", "service": "server", // container defined in docker-compose - "updateContentCommand": "bash .devcontainer/install-mongodb-db-tools.sh && bash .devcontainer/import.sh && bash .devcontainer/updateContent.sh", // included in prebuilt + "updateContentCommand": "bash .devcontainer/install-mongodb-db-tools.sh && bash .devcontainer/import.sh && bash .devcontainer/updateContent.sh && bash .devcontainer/track.sh created || true", // included in prebuilt + "postStartCommand": "bash .devcontainer/track.sh started || true", // this will open 3 bash sessions, each one running the specified command "postAttachCommand": { "server": "cd server && npm start", diff --git a/.devcontainer/track.sh b/.devcontainer/track.sh new file mode 100644 index 0000000..86136dd --- /dev/null +++ b/.devcontainer/track.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Best-effort telemetry: log a codespace lifecycle event. Never fails the caller. +EVENT="${1:-unknown}" +GIT_EMAIL="$(git config --get user.email 2>/dev/null || true)" +URL="https://us-central1-project-learning-fuel.cloudfunctions.net/trackCodespace" +curl -fsS -m 5 -X POST "$URL" \ + -H "Content-Type: application/json" \ + -d "{\"event\":\"$EVENT\",\"codespace\":\"${CODESPACE_NAME:-}\",\"user\":\"${GITHUB_USER:-}\",\"gitEmail\":\"${GIT_EMAIL:-}\",\"repository\":\"${GITHUB_REPOSITORY:-mongodb-developer/library-management-system}\"}" \ + >/dev/null 2>&1 || true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd0ba6e..c402f90 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1 @@ -# These owners will be the default owners for everything in -# the repo. Unless a later match takes precedence, -# @global-owner1 and @global-owner2 will be requested for -# review when someone opens a pull request. -* @sis0k0 @dfreniche @mrlynn @nestor-daza-mdb @ricardohsmello \ No newline at end of file +* @mongodb-developer/lab-maintainers \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e34d01b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,53 @@ +name: "Tests" + +on: + pull_request: + branches: + - "main" + push: + branches: + - "main" + +jobs: + server: + runs-on: ubuntu-latest + services: + mongodb: + image: mongo:latest + ports: + - 27017:27017 + env: + DATABASE_URI: mongodb://localhost:27017 + SECRET: secret + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: server/package-lock.json + - name: Install dependencies + working-directory: server + run: npm ci + - name: Lint + working-directory: server + run: npm run lint + - name: Run API tests + working-directory: server + run: npm test + + client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: client/package-lock.json + - name: Install dependencies + working-directory: client + run: npm ci + - name: Build + working-directory: client + run: npm run build diff --git a/.gitignore b/.gitignore index 2a6fb72..6f3c2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ .env credentials.json +# Claude Code local overrides +.claude/settings.local.json + # Compiled output dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..59e1923 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,146 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. + +A MEAN-stack (MongoDB, Express, Angular, Node.js) library app used in MongoDB +Developer Days workshops. The server (`server/`) exposes a REST API backed by +MongoDB Atlas; the client (`client/`) is an Angular SPA that consumes it. + +## Build and test commands + +```bash +npm install # root: installs husky for the pre-commit hook +cd server && npm install # server dependencies +cd client && npm install # client dependencies + +cd server && npm run build:watch # compile TypeScript, watch mode +cd server && npm run serve # run compiled server with nodemon +cd server && npm start # build:watch + serve together (typical dev loop) +cd server && npm run lint # eslint src/ +cd server && npm run api-test # mocha, requires a running server + MongoDB +cd server && npm test # starts the server, waits for it, then runs api-test + +cd client && npm start # ng serve on http://localhost:4200 +cd client && npm run build # ng build +cd client && npm test # ng test (Karma) +``` + +The pre-commit hook (`.husky/pre-commit`) brings up `.devcontainer/docker-compose.yml` +(a local MongoDB Atlas Local container) in the background, then runs `npm run lint` +and `npm test` inside `server/`. Docker must be running locally for `git commit` to +succeed outside of Codespaces/the devcontainer. + +Smoke check after a change: + +1. Run the commands above to start `server` (port 5400) and `client` (port 4200). +2. Open http://localhost:4200 and confirm the book list loads. +3. Log in, borrow or reserve a book, and confirm the change is reflected in the UI. + +## Project structure + +``` +server/src/ + server.ts # Express app entry point, route mounting + database.ts # MongoDB client/connection, collection handles + load-env-vars.ts # dotenv bootstrap, imported first in every entry point + models/ # TypeScript interfaces for each collection's documents + routes/ # Express routers (one per resource) plus *.spec.ts API tests + controllers/ # Request handlers used by routes/ + utils/ # Auth middleware, shared test helpers + embeddings/ # Pluggable embedding providers (OpenAI, Google Vertex, a + # serverless endpoint) selected via EMBEDDINGS_SOURCE + search-indexing/ # Standalone scripts that create/update MongoDB Search and + # Vector Search indexes on the books collection (workshop labs) + indexing/ # Standalone script demonstrating a compound index via .explain() +client/src/app/ # Angular components, services, routing +migrations/ # One-off data migration scripts (run manually with Node) +.devcontainer/ # Codespaces/Dev Containers setup: local Atlas container, + # sample data import, port forwarding, best-effort + # created/started lifecycle telemetry (track.sh) +``` + +Notable files: + +- [server/src/database.ts](server/src/database.ts) — throws a descriptive error if + `DATABASE_URI` is unset or still the placeholder value; read this first when the + server won't start. +- [server/src/models/issue-detail.ts](server/src/models/issue-detail.ts) — a single + collection (`issueDetails`) holds both borrowed-book and reservation records, + distinguished by `recordType`. +- [server/src/search-indexing/*.ts](server/src/search-indexing) — each file is a + runnable workshop step (`node --loader ts-node/esm src/search-indexing/.ts` + after building) that creates one MongoDB Search/MongoDB Vector Search index variant. Several + contain `` placeholders left in on purpose for the workshop + exercise — do not "fix" them as bugs. + +## API overview + +| Method | Path | Purpose | +| --- | --- | --- | +| GET | `/authors/:authorId` | Fetch an author and their books | +| GET | `/books` | List books | +| GET | `/books/search` | Full-text/vector search over books | +| GET | `/books/:bookId` | Fetch a single book | +| POST | `/books` | Create a book (admin) | +| PUT | `/books/:bookId` | Update a book (admin) | +| DELETE | `/books/:bookId` | Delete a book (admin) | +| GET | `/books/:bookId/reviews` | List reviews for a book | +| POST | `/books/:bookId/reviews` | Add a review (authenticated) | +| GET | `/books/:bookId/reviews/:reviewId` | Fetch a single review | +| GET | `/users/login/:username?` | Log in / issue a token | +| GET | `/reservations` | List the current user's reservations | +| GET | `/reservations/page` | Paginated reservations (admin) | +| GET | `/reservations/:reservationId` | Fetch a single reservation | +| POST | `/reservations/:bookId` | Reserve a book | +| DELETE | `/reservations/:bookId` | Cancel a reservation | +| GET | `/reservations/user/:userId` | List a user's reservations (admin) | +| GET | `/borrow` | List the current user's borrowed books | +| GET | `/borrow/history` | Borrow history | +| GET | `/borrow/page` | Paginated borrow records (admin) | +| POST | `/borrow/:bookId/:userId` | Borrow a book (admin) | +| POST | `/borrow/:bookId/:userId/return` | Return a book (admin) | + +## Environment variables and configuration + +| Name | Required | Example | Description | +| --- | --- | --- | --- | +| `PORT` | Yes | `5400` | Port the Express server listens on | +| `DATABASE_URI` | Yes | `mongodb://localhost:27017` | MongoDB connection string | +| `DATABASE_NAME` | Yes | `library` | Database name used by `connectToDatabase` | +| `SECRET` | Yes | `secret` | HS256 signing secret for auth tokens | +| `EMBEDDINGS_SOURCE` | No | `openai`, `googleVertex`, `serverlessEndpoint` | Selects the embedding provider in `server/src/embeddings/index.ts`; defaults to `serverlessEndpoint` | +| `EMBEDDING_KEY` | No | — | API key for the OpenAI or serverless embedding provider | +| `PROJECT_ID` / `PROJECT_LOCATION` | No | — | Google Cloud project/location, used only when `EMBEDDINGS_SOURCE=googleVertex` | +| `GOOGLE_APPLICATION_CREDENTIALS` | No | — | Falls back to `EMBEDDING_KEY` if unset and `EMBEDDING_KEY` is present (`googleVertex.ts`) | + +`server/.env` is committed to this repository with working local-dev defaults (it +targets the devcontainer's local Atlas Local instance, not a real cluster) — this is +intentional for the workshop, not a leaked credential. + +Constraints worth knowing before you debug a failure: + +- `connectToDatabase` throws immediately if `DATABASE_URI` is missing or equals the + literal placeholder `mongodb+srv://user:password@cluster` — check that error message + before assuming the driver itself is failing. +- The MongoDB client is created with `appName: 'devrel-workshop-node-angular-library'` + (`server/src/database.ts`), so Atlas-side connection metrics for this app are + distinguishable from other Developer Days workshop repos. +- `server/test` (via `start-server-and-test`) boots a real server against + `DATABASE_NAME=testLibrary` on port 5200 and runs `api-test` against it — it needs a + reachable MongoDB, same as normal dev. + +## MongoDB Skills + +Use the official MongoDB agent skills from https://github.com/mongodb/agent-skills +whenever the task is MongoDB-specific and a matching skill exists. + +## When To Use EDD.md + +Use [EDD.md](./EDD.md) as the source of truth for the MongoDB data model in this repository. + +Consult [EDD.md](./EDD.md) before making changes that touch: + +- MongoDB collections, document structure, or field names +- Express routes that read or write database records +- Validation, form fields, API payloads, or UI that depend on persisted data +- Schema documentation, Mermaid diagrams, or entity modeling discussions diff --git a/EDD.md b/EDD.md new file mode 100644 index 0000000..2542a16 --- /dev/null +++ b/EDD.md @@ -0,0 +1,187 @@ +# EDD — Entity Document Diagram + +Database name is set by `DATABASE_NAME` (default `library` locally, `testLibrary` in +tests), connected via `server/src/database.ts`. + +Field types below were derived from the seeded documents in +`.devcontainer/data/library/*.bson` (sampled with `bsondump`) and the TypeScript +interfaces in `server/src/models/`, not from a formal schema — the driver applies no +JSON Schema validators. + +## Entity overview + +| Collection | Written by | Read by | Vector index | +| --- | --- | --- | --- | +| `books` | `routes/books.ts`, `routes/reviews.ts` (appends to the embedded `reviews` subset) | `routes/books.ts`, `routes/authors.ts` | `search-indexing/*.ts` create `fulltextsearch` and `vectorsearch*` indexes on this collection (not present by default) | +| `authors` | not written by the app (seed data only) | `routes/authors.ts`, `routes/books.ts` (extended reference) | — | +| `reviews` | `routes/reviews.ts` | `routes/reviews.ts` | — | +| `users` | not written by the app (seed data only) | `routes/users.ts`, extended reference in `issueDetails` | — | +| `issueDetails` | `routes/borrows.ts`, `routes/reservations.ts` | `routes/borrows.ts`, `routes/reservations.ts` | — | + +## books + +One book per ISBN. Combines several documented MongoDB schema patterns (comments in +[server/src/models/book.ts](server/src/models/book.ts) link to each). + +| Field | Type | Notes | +| --- | --- | --- | +| `_id` | string | ISBN, used as the primary key instead of an ObjectId | +| `title` | string | | +| `longTitle` | string | optional | +| `year` | int | | +| `pages` | int | optional | +| `synopsis` | string | optional | +| `publisher` | string | optional | +| `language` | string | optional | +| `binding` | string | optional | +| `cover` | string | optional, cover image URL | +| `genres` | array\ | optional | +| `totalInventory` | int | | +| `available` | int | computed field (Computed Pattern) — not derived from a query, must be kept in sync by writers | +| `authors` | array\<{ _id: ObjectId, name: string }\> | Extended Reference Pattern into `authors` | +| `attributes` | array\<{ key: string, value: string }\> | Attribute Pattern (edition, ISBN-10/13, dimensions, MSRP, …) | +| `reviews` | array\ | Subset Pattern — duplicates a copy of each review (minus `_id`/`bookId`) inline for fast reads | +| `bookOfTheMonth` | boolean | optional, added for the Search Lab | +| `embeddings` | array\, len 1408 | ⚠️ referenced by every script in `search-indexing/` as the vector field, but **absent from the base seed data** — only present on documents processed by the embeddings workshop lab. See *Known inconsistencies*. | + +No indexes beyond `_id` in the base seed; the search-indexing lab scripts add +`fulltextsearch` / `vectorsearch` / `vectorsearch-prefilter` / `vectorsearch-quantized` +MongoDB Search indexes on demand. + +## authors + +| Field | Type | Notes | +| --- | --- | --- | +| `_id` | ObjectId | | +| `name` | string | | +| `sanitizedName` | string | lowercased, no punctuation, used for lookups | +| `aliases` | array\ | | +| `bio` | string | optional | +| `books` | array\ | ISBNs, reference to `books._id` | + +Indexes: `_id`, plus a compound `_id_1_books_1` present in the seed data (unusual — +see *Known inconsistencies*). + +## reviews + +Standalone collection. A duplicate/denormalized copy of each review also lives inline +in `books.reviews` (Subset Pattern) — the two are not kept in sync by a transaction, +only by both being written from the same request handler. + +| Field | Type | Notes | +| --- | --- | --- | +| `_id` | ObjectId | | +| `text` | string | ⚠️ typed as required in `Review`, but ~0.9% of seeded documents have `text: null`. See *Known inconsistencies*. | +| `name` | string | reviewer display name, free text | +| `rating` | int | optional in the model | +| `timestamp` | number \| Date | stored as a BSON date in the standalone collection but as an epoch-millisecond double in the embedded `books.reviews` subset — see *Known inconsistencies* | +| `bookId` | string | reference to `books._id` | + +Indexes: `_id`, plus compound `_id_1_bookId_1`. + +## users + +| Field | Type | Notes | +| --- | --- | --- | +| `_id` | ObjectId | | +| `name` | string | | +| `isAdmin` | boolean | optional, gates `adminRoute` middleware | + +No password/credential field — `routes/users.ts` issues a token from `name` alone +(workshop-simplified auth, not production auth). + +## issueDetails + +Single-Collection Pattern: one collection holds two record shapes, discriminated by +`recordType`. + +| Field | Type | Notes | +| --- | --- | --- | +| `_id` | string | format ``, e.g. `64d4c964f0d056ea6bf0f3d8B0864425813` — lets `find({ _id: /^userId/ })` query by user without a secondary index | +| `recordType` | `'borrowedBook'` \| `'reservation'` | | +| `book` | { _id: string, title: string } | Extended Reference into `books` | +| `user` | { _id: ObjectId, name: string } | Extended Reference into `users` | +| `borrowDate`, `dueDate` | Date | `recordType: 'borrowedBook'` only | +| `returned` | boolean | `recordType: 'borrowedBook'` only | +| `returnedDate` | Date | `recordType: 'borrowedBook'` only, set once returned | +| `expirationDate` | Date | `recordType: 'reservation'` only; intended for a TTL index per the code comment, but no TTL index exists in the seed data — see *Known inconsistencies* | + +Index: `{ 'user._id': 1, returnedDate: 1, borrowDate: 1 }`, created and explained by +[server/src/indexing/borrowed-books-index.ts](server/src/indexing/borrowed-books-index.ts). + +## Relationships + +All relationships are **logical only** — no foreign keys, no schema validators, no +indexes beyond `_id` and the compound indexes noted above. + +```mermaid +erDiagram + BOOKS ||--o{ REVIEWS : "bookId" + BOOKS }o--o{ AUTHORS : "authors[]._id (extended ref)" + ISSUEDETAILS }o--|| BOOKS : "book._id (extended ref)" + ISSUEDETAILS }o--|| USERS : "user._id (extended ref)" + + BOOKS { + string _id + string title + int year + int totalInventory + int available + array embeddings + } + AUTHORS { + ObjectId _id + string name + array books + } + REVIEWS { + ObjectId _id + string text + string bookId + } + USERS { + ObjectId _id + string name + boolean isAdmin + } + ISSUEDETAILS { + string _id + string recordType + object book + object user + } +``` + +## Known inconsistencies + +1. **`books.embeddings` is documented by four scripts but absent from the shipped + seed data.** `search-indexing/vector-search.ts`, `prefilter.ts`, and + `quantization.ts` all assume a `books.embeddings` field with 1408 dimensions, but + none of the 6,777 seeded book documents contain it — it's populated by running the + (separate, unseen-here) embeddings workshop lab against `EMBEDDINGS_SOURCE`. An + agent that seeds data and immediately runs vector search will get zero results, not + an error. Update this entry if a seed/embeddings step is added. + +2. **A `reviews.text` field is nullable in practice (~0.9% of seed documents: + 35/4036) despite `Review.text` being typed as a required `string`.** Code that + renders `review.text` without a null check will render `null`/blank rather than + throw. Update this entry if the model is changed to `text?: string | null`. + +3. **`reviews.timestamp` has two different physical representations**: a BSON `Date` + in the standalone `reviews` collection, but an epoch-millisecond `double` in the + copy embedded under `books.reviews[]` (the Subset Pattern duplicate). Code reading + the embedded copy must not assume it can call `Date` methods on `timestamp` + directly. Update this entry if the two are reconciled. + +4. **An `operations` collection exists in the seed data (`.devcontainer/data/library/operations.bson`, + 2,321 documents) with the exact same shape as `issueDetails`, but no model, route, + controller, or reference to it exists anywhere in `server/src/`.** It appears to be + data left over from an earlier name for the `issueDetails` collection. It is + imported by `.devcontainer/import.sh` on every codespace/devcontainer start but + never read or written by the app. Update this entry (or remove the stale collection + from the seed data) once confirmed safe to drop. + +5. **`authors` has a compound index `{ _id: 1, books: 1 }`** in the seed data even + though `_id` alone is already unique — the `books` component adds no selectivity + for any query in `routes/authors.ts`. Confirm whether this supports a query not + present in the current codebase before removing it. diff --git a/README.md b/README.md index 3b91ec2..6120a66 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,124 @@ # Library Management System -This is a library management system built with the MEAN (MongoDB, Express, Angular, Node.js) stack. - -## Running the project +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/mongodb-developer/library-management-system) + +A full-stack library app — browse a catalog, borrow and reserve books, leave reviews — +built on the MEAN stack (MongoDB, Express, Angular, Node.js). It's the sample app used +in MongoDB Developer Days hands-on labs, so the codebase intentionally shows off +several MongoDB schema design patterns and MongoDB Search / MongoDB Vector Search side by side, +not just CRUD. + +## Capabilities + +- Browse, add, edit, and delete books, each with authors, genres, and free-form + attributes (edition, ISBN, dimensions, MSRP) +- Borrow and return books, with due dates and history, or reserve a book that's + currently checked out +- Leave and read reviews per book +- Role-based access: admin-only routes for managing inventory and borrow records +- Workshop labs for full-text and vector search over the book catalog, including + prefiltering and scalar quantization variants of a Vector Search index + +## Architecture overview + +```mermaid +flowchart LR + Client["Angular client (client/)"] -- REST --> Server["Express API (server/)"] + Server -- MongoDB driver --> Atlas[("MongoDB Atlas")] + Server -- embeds via OpenAI / Vertex AI --> Embeddings["Embedding provider"] +``` -1. Clone the repository. +The client is a standalone Angular SPA; the server is an Express + TypeScript API. The +two talk over HTTP only — there's no server-side rendering or shared process. The +server owns all MongoDB access; embedding generation for search is a pluggable +provider selected at runtime (see [AGENTS.md](./AGENTS.md)). - ``` - git clone git@github.com:mongodb-developer/library-management-system.git library - ``` +## Quick start -1. Install the dependencies in the root level of the project. +### Option A — GitHub Codespaces (recommended) - **library/** - ``` - npm install - ``` +Click the badge above, or **Code → Codespaces → Create codespace on main**. The +devcontainer will: -1. Set your [Atlas URI connection string](https://docs.atlas.mongodb.com/getting-started/), database name and server port in `server/.env`. Make sure you replace the username and password placeholders with your own credentials. +- Start a local MongoDB Atlas container and restore the sample library data +- Install server and client dependencies +- Open three terminal tabs running the server, the client, and port setup - **library/server/.env** - ``` - PORT="5400" - DATABASE_URI="mongodb+srv://:@m0.kwqkoewm.mongodb.net" - DATABASE_NAME="library" - SECRET="secret" - ``` +Once it's done, open the forwarded port for **4200** (opens automatically). +### Option B — Local machine -1. Start the **server** application. +Prerequisites: Node.js 20+, npm, and a MongoDB cluster (an +[Atlas free tier (M0) cluster](https://www.mongodb.com/cloud/atlas/register?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) +works, or a local `mongod`). - **library/server/** - ``` - npm install && npm start - ``` +```bash +git clone git@github.com:mongodb-developer/library-management-system.git library +cd library +npm install +``` -1. Open a new terminal window and start the **client** application. +Set your connection details in `server/.env` (already committed with working defaults +for a local database — edit `DATABASE_URI` if you're pointing at Atlas instead): - **library/client/** - ``` - npm install && npm start - ``` +``` +PORT=5400 +DATABASE_URI=mongodb://localhost:27017 +DATABASE_NAME=library +SECRET=secret +``` -1. When both applications are built and running, open your browser on http://localhost:4200/. +Start the server: -## Executing the tests +```bash +cd server && npm install && npm start +``` -Currently, the project has only API tests implemented with [`supertest`](https://www.npmjs.com/package/supertest) and [`mocha`](https://www.npmjs.com/package/mocha). To execute them, navigate to the `server/` directory and run: +In a second terminal, start the client: -**library/server/** -``` -npm test +```bash +cd client && npm install && npm start ``` -## Pre-commit hook +Open http://localhost:4200 — you should see the book catalog load. + +## MongoDB features demonstrated + +### Why MongoDB? + +- **Flexible schema for a catalog with wildly inconsistent source data.** Books come + with different attributes depending on edition and publisher; the + [Attribute Pattern](https://www.mongodb.com/blog/post/building-with-patterns-the-attribute-pattern?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + stores those as key/value pairs instead of a rigid column set. +- **Reads that don't fan out.** A book page needs its authors and recent reviews in + one round trip; the + [Extended Reference](https://www.mongodb.com/blog/post/building-with-patterns-the-extended-reference-pattern?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + and + [Subset](https://www.mongodb.com/blog/post/building-with-patterns-the-subset-pattern?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + patterns duplicate just enough of that related data inline to avoid a join-like + lookup on every page view. +- **One collection instead of two nearly-identical ones.** Borrowed books and + reservations differ by only a few fields, so the + [Single Collection Pattern](https://www.mongodb.com/blog/post/building-with-patterns-the-single-collection-pattern?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + keeps them together and discriminates with a `recordType` field. +- **Search and recommendations without a second database.** + [MongoDB Search](https://www.mongodb.com/docs/atlas/atlas-search/?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + and + [MongoDB Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/?utm_campaign=devrel&utm_source=github&utm_medium=referral&utm_content=library_management_system&utm_term=learning.fuel) + run against the same `books` collection the app already reads and writes — no + separate search cluster to keep in sync. + +## Contributing + +Merge your own PR once you have at least one approval from a +[code owner](.github/CODEOWNERS). + +## License + +This project is licensed under the [Apache 2.0 License](LICENSE). Use at your own +risk — not a supported MongoDB product. -The project utilizes [Husky](https://typicode.github.io/husky/) to execute actions before every commit. The pre-commit hook, located in [.husky/pre-commit](./.husky/pre-commit), lints the code and runs the API tests. +## Getting support -Use at your own risk; not a supported MongoDB product +If you run into a problem working through this app, +[open a new issue](https://github.com/mongodb-developer/library-management-system/issues/new). diff --git a/server/src/controllers/reviews.ts b/server/src/controllers/reviews.ts index 9753eef..b886968 100644 --- a/server/src/controllers/reviews.ts +++ b/server/src/controllers/reviews.ts @@ -50,6 +50,10 @@ class ReviewsController { } public async getReview(bookId: string, reviewId: string): Promise { + if (!ObjectId.isValid(reviewId)) { + return null; + } + const review = await collections?.reviews?.findOne({ _id: new ObjectId(reviewId), bookId: bookId }); return review; } diff --git a/server/src/database.ts b/server/src/database.ts index b2bf2bc..50c68ad 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -32,7 +32,7 @@ export async function connectToDatabase(uri?: string) { `); } - const client = new mongodb.MongoClient(uri, { appName: 'devrel.workshop.devday' }); + const client = new mongodb.MongoClient(uri, { appName: 'devrel-workshop-node-angular-library' }); await client.connect(); const db = client.db(process.env.DATABASE_NAME); diff --git a/server/src/search-indexing/prefilter.ts b/server/src/search-indexing/prefilter.ts index 26f96d7..5331ee1 100644 --- a/server/src/search-indexing/prefilter.ts +++ b/server/src/search-indexing/prefilter.ts @@ -24,16 +24,16 @@ await collection.createSearchIndex({ }, { type: 'filter', -// ----------------------------------------------------------------------------------------------- -// <----------- REPLACE THE PLACEHOLDER BELOW WITH THE NAME OF THE YEAR FIELD IN YOUR DOCUMENTS -----------------------> -// ----------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + // <----------- REPLACE THE PLACEHOLDER BELOW WITH THE NAME OF THE YEAR FIELD IN YOUR DOCUMENTS -----------------------> + // ----------------------------------------------------------------------------------------------- path: '' }, { type: 'filter', -// ----------------------------------------------------------------------------------------------- -// <----------- REPLACE THE PLACEHOLDER BELOW WITH THE NAME OF THE LANGUAGE FIELD IN YOUR DOCUMENTS ------------------> -// ----------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + // <----------- REPLACE THE PLACEHOLDER BELOW WITH THE NAME OF THE LANGUAGE FIELD IN YOUR DOCUMENTS ------------------> + // ----------------------------------------------------------------------------------------------- path: '' } ] diff --git a/server/src/search-indexing/quantization.ts b/server/src/search-indexing/quantization.ts index a63a6b0..e46fefd 100644 --- a/server/src/search-indexing/quantization.ts +++ b/server/src/search-indexing/quantization.ts @@ -21,9 +21,9 @@ await collection.createSearchIndex({ path: 'embeddings', numDimensions: 1408, similarity: 'cosine', -// ----------------------------------------------------------------------------------------------- -// <----------- REPLACE THE PLACEHOLDER BELOW WITH TYPE OF QUANTIZATION TO USE ------------------> -// ----------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + // <----------- REPLACE THE PLACEHOLDER BELOW WITH TYPE OF QUANTIZATION TO USE ------------------> + // ----------------------------------------------------------------------------------------------- quantization: '' }, { diff --git a/server/src/search-indexing/vector-search.ts b/server/src/search-indexing/vector-search.ts index d010fb9..393426a 100644 --- a/server/src/search-indexing/vector-search.ts +++ b/server/src/search-indexing/vector-search.ts @@ -16,17 +16,17 @@ const collection = collections?.books; */ await collection.createSearchIndex({ name: 'vectorsearch', -// ----------------------------------------------------------------------------------------------- -// <----------- REPLACE THE PLACEHOLDER BELOW WITH THE CORRECT INDEX TYPE -----------------------> -// ----------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + // <----------- REPLACE THE PLACEHOLDER BELOW WITH THE CORRECT INDEX TYPE -----------------------> + // ----------------------------------------------------------------------------------------------- type: '', definition: { fields: [ { type: 'vector', -// ----------------------------------------------------------------------------------------------- -// <----------- REPLACE THE PLACEHOLDER BELOW WITH PATH TO THE VECTOR FIELD ---------------------> -// ----------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + // <----------- REPLACE THE PLACEHOLDER BELOW WITH PATH TO THE VECTOR FIELD ---------------------> + // ----------------------------------------------------------------------------------------------- path: '', numDimensions: 1408, similarity: 'cosine'