Skip to content
Closed
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
10 changes: 9 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ NODE_ENV=test
# Default methods: GET, POST, PUT, DELETE
ALLOWED_ORIGINS=
ALLOWED_METHODS=
ALLOWED_HEADERS=
ALLOWED_HEADERS=

# === JWT Configuration ===
# JWT (JSON Web Token) secret key for signing tokens
# A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs).
# This is required for authentication to work correctly.
# 🔐 Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js:
# $ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
.github
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid"
}
10 changes: 5 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"accessibility.signals.chatRequestSent": {
"sound": "off",
"announcement": "off"
}
}
"accessibility.signals.chatRequestSent": {
"sound": "off",
"announcement": "off"
}
}
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
# Node.js and Express Backend

[![Backend API - CI Tests](https://github.com/pakeku/backend-api/actions/workflows/tests.yml/badge.svg)](https://github.com/pakeku/backend-api/actions/workflows/tests.yml)
[![Known Vulnerabilities](https://snyk.io/test/github/pakeku/backend-api/badge.svg)](https://snyk.io/test/github/pakeku/backend-api)

## Requirements

Identify your MongoDB URL. Visit MongoDB to sign up and get started.

Environmental Variables:

1. MONGO_URL
2. PORT (optional)
3. ALLOWED_ORIGINS (optional)
4. ALLOWED_METHODS (optional)
5. ALLOWED_HEADERS (optional)
6. NODE_ENV=test --- When set to ***"test"***, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database.
6. NODE\*ENV=test --- When set to \*\*\*"test"\_\*\*, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database.
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line contains malformed Markdown and an incorrect environment variable name (NODE*ENV). It should read NODE_ENV=test without backslashes and with proper emphasis syntax.

Suggested change
6. NODE\*ENV=test --- When set to \*\*\*"test"\_\*\*, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database.
6. NODE_ENV=test --- When set to **"test"**, a `mongodb-memory-server` test URI is used, and no `MONGO_URL` is required. This allows for out-of-the-box testing without a live database.

Copilot uses AI. Check for mistakes.
7. JWT_SECRET --- A cryptographically secure secret used to sign and verify JSON Web Tokens (JWTs). This is required for authentication to work correctly.
Use a long, random string—at least 32 characters, ideally generated using a password manager or Node.js: `bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"`
8.

## Getting Started

1. Copy this file to .env and fill in the actual values
```bash

```bash
cp .env.sample .env
```

1. Run a script:
```json

```json
"scripts": {
"prebuild":"rm -rf dist",
"build":"tsc",
"start": "node ./src/index.js",
"dev": "env-cmd nodemon ./src/index.js",
"test": "jest"
"dev": "env-cmd nodemon ./src/index.ts",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"lint:check": "eslint . --ext .ts --no-ignore",
"format": "prettier --write ."
}
```
```
23 changes: 23 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import perfectionist from 'eslint-plugin-perfectionist';

export default tseslint.config(
{
ignores: ['**/*.js'],
},
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
perfectionist.configs['recommended-natural']
);
6 changes: 3 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
testEnvironment: 'node',
transform: {
"^.+\.tsx?$": ["ts-jest",{}],
'^.+\.tsx?$': ['ts-jest', {}],
},
};
};
25 changes: 22 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@
"description": "",
"main": "./src/index.ts",
"type": "module",
"private": true,
"scripts": {
"prebuild": "rm -rf dist",
"build": "tsc",
"start": "node dist/index.js",
"dev": "env-cmd ts-node --esm src/index.ts",
"test": "jest"
"dev": "env-cmd ts-node src/index.ts",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"lint:check": "eslint . --ext .ts --no-ignore",
"format": "prettier --write ."
},
"imports": {
"#*": "./src/*"
},
"keywords": [
"mongodb",
Expand All @@ -33,6 +43,7 @@
"nodemon": "^3.1.10"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@types/bcrypt": "^5.0.2",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.18",
Expand All @@ -43,11 +54,19 @@
"@types/morgan": "^1.9.9",
"@types/node": "^22.15.17",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-perfectionist": "^4.13.0",
"eslint-plugin-prettier": "^5.4.0",
"jest": "^29.7.0",
"mongodb-memory-server": "^10.1.4",
"prettier": "^3.5.3",
"supertest": "^7.1.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1"
}
}
15 changes: 7 additions & 8 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import express, { Application } from 'express';

import errorHandler from './midleware/errorHandler';
import rateLimiter from './midleware/rateLimiter';
import compression from './midleware/compression';
import cors from './midleware/cors';
import errorHandler from './midleware/errorHandler';
import helmet from './midleware/helmet';
import json from './midleware/json';
import cors from './midleware/cors';
import morgan from './midleware/morgan';

import notFoundRouter from './routes/notFoundRoute';
import rateLimiter from './midleware/rateLimiter';
Comment on lines 3 to +9
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The folder name midleware is misspelled; it should be middleware to match standard conventions and avoid confusion.

Copilot uses AI. Check for mistakes.
import authRouter from './routes/authRoute';
import healthRouter from './routes/healthRoute';
import storesRouter from './routes/storesRoute';
import notFoundRouter from './routes/notFoundRoute';
import rootRouter from './routes/rootRoute';
import authRouter from './routes/authRoute';
import storesRouter from './routes/storesRoute';

const app: Application = express();

Expand All @@ -35,4 +34,4 @@ app.use('/stores', storesRouter);
app.use('/auth', authRouter);
app.use('*', notFoundRouter);

export default app;
export default app;
22 changes: 11 additions & 11 deletions src/database/mongo-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Documentation: https://mongodb.github.io/node-mongodb-native/6.16/classes/MongoClient.html
*/

import { MongoClient, Db } from 'mongodb';
import { Db, MongoClient } from 'mongodb';
import { MongoMemoryServer } from 'mongodb-memory-server';

const mongoDBURL = process.env.MONGO_URL;
Expand All @@ -17,38 +17,38 @@ let database: Db | null = null;
let mongoServer: MongoMemoryServer | null = null; // store reference to in-memory server for shutdown

const getRightMongoDBURL = async (): Promise<string> => {
const env = process.env.NODE_ENV;
const env = process.env.NODE_ENV ?? 'development';

if (env === 'test') {
mongoServer = await MongoMemoryServer.create();
return mongoServer.getUri();
}

if (['development', 'production'].includes(env || '')) {
return mongoDBURL as string;
if (['development', 'production'].includes(env)) {
if (!mongoDBURL) throw new Error('MONGO_URL is not defined');
return mongoDBURL;
}

throw new Error(`Unsupported NODE_ENV: ${env}`);
};

export async function startDatabase(uri: string | null = null): Promise<Db> {
export async function getDatabase(): Promise<Db> {
return database ?? (await startDatabase());
}

export async function startDatabase(uri: null | string = null): Promise<Db> {
if (client && database) {
return database;
}

const dbURI = uri || await getRightMongoDBURL();
const dbURI = uri ?? (await getRightMongoDBURL());

client = new MongoClient(dbURI);
await client.connect();
database = client.db();
return database;
}


export async function getDatabase(): Promise<Db> {
return database || await startDatabase();
}

export async function stopDatabase(): Promise<void> {
if (client) {
await client.close();
Expand Down
63 changes: 31 additions & 32 deletions src/database/stores.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,73 @@
import { getDatabase } from './mongo-common';
import { ObjectId } from 'mongodb';

import getUserName from '../utils/git-user-name';
import { getDatabase } from './mongo-common';

// Define the Store interface
interface Store {
_id?: string;
name: string;
export interface Store {
_id: ObjectId;
addedBy?: string;
metadata?: string;
name: string;
}

const collectionName = 'stores';

// Create a Store
async function createStore(store: Store): Promise<Store | null> {
async function createStore(store: Store): Promise<null | Store> {
const database = await getDatabase();
store.addedBy = getUserName();

const storeToInsert = { ...store, _id: store._id ? new ObjectId(store._id) : undefined };
const { insertedId } = await database.collection(collectionName).insertOne(storeToInsert);

// Return the store document with the inserted _id
return await database.collection(collectionName).findOne({ _id: insertedId }) as Store | null;
}

// Get all stores
async function getStores(): Promise<Store[]> {
const database = await getDatabase();
const stores = await database.collection(collectionName).find({}).toArray();
return stores.map(store => ({
_id: store._id?.toString(),
name: store.name,
addedBy: store.addedBy,
})) as Store[];
return (await database.collection(collectionName).findOne({ _id: insertedId })) as null | Store;
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned document’s _id is an ObjectId. If your API or callers expect a string ID, convert it using inserted._id.toString() before returning.

Suggested change
return (await database.collection(collectionName).findOne({ _id: insertedId })) as null | Store;
const insertedStore = await database.collection(collectionName).findOne({ _id: insertedId });
if (!insertedStore) return null;
return {
...insertedStore,
_id: insertedId.toString(),
} as Store;

Copilot uses AI. Check for mistakes.
}

// Delete a store by id
async function deleteStore(_id: string): Promise<{ message: string }> {
const database = await getDatabase();

const result = await database.collection(collectionName).deleteOne({
_id: new ObjectId(_id),
});

if (result.deletedCount === 0) {
return { message: "No store found with that id" };
return { message: 'No store found with that id' };
}

return { message: "Store deleted" };
return { message: 'Store deleted' };
}

// Get all stores
async function getStores(): Promise<Store[]> {
const database = await getDatabase();
const stores = await database.collection<Store>(collectionName).find({}).toArray();
return stores.map(store => ({
_id: store._id,
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map the ObjectId to a string (e.g. store._id.toString()) so that JSON serialization and clients receive a plain string identifier.

Suggested change
_id: store._id,
_id: store._id.toString(),

Copilot uses AI. Check for mistakes.
addedBy: store.addedBy,
name: store.name,
}));
}

// Update a store
async function updateStore(id: string, store: Partial<Store>): Promise<Store | null> {
async function updateStore(id: string, store: Partial<Store>): Promise<null | Store> {
const database = await getDatabase();
delete store._id;

await database.collection(collectionName).updateOne(
{ _id: new ObjectId(id) },
{ $set: store }
);
await database.collection(collectionName).updateOne({ _id: new ObjectId(id) }, { $set: store });

const updated = await database.collection(collectionName).findOne({ _id: new ObjectId(id) });
const updated = await database
.collection<Store>(collectionName)
.findOne({ _id: new ObjectId(id) });
if (!updated) return null;
return {
_id: updated._id?.toString(),
name: updated.name,
_id: new ObjectId(updated._id),
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid wrapping an existing ObjectId in new ObjectId(...), which will generate a new ID. Use the returned updated._id directly or call updated._id.toString() if a string is required.

Suggested change
_id: new ObjectId(updated._id),
_id: updated._id,

Copilot uses AI. Check for mistakes.
addedBy: updated.addedBy,
metadata: updated.metadata,
} as Store;
name: updated.name,
};
}


export { createStore, getStores, deleteStore, updateStore };
export { createStore, deleteStore, getStores, updateStore };
Loading
Loading