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
27 changes: 26 additions & 1 deletion BACKEND/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import passport from "./config/passport.js";
import swaggerUi from 'swagger-ui-express';
import swaggerSpec from './swagger.js';
import { stripeWebhook } from "./controllers/checkout.controller.js";
import { expressMiddleware } from "@as-integrations/express4";
import { apolloServer } from "./graphql/server.js";
import { optionalProtect } from "./middleware/auth.js";

// Import error handlers
import { notFoundHandler, errorHandler } from "./middleware/errorMiddleware.js";
Expand Down Expand Up @@ -57,6 +60,18 @@ if (process.env.VITE_API_URL) {
}

const app = express();
await apolloServer.start();

app.use(
"/graphql",
express.json(),
optionalProtect,
expressMiddleware(apolloServer, {
context: async ({ req }) => ({
user: req.user || null,
}),
})
);
app.use(
helmet({
contentSecurityPolicy: {
Expand Down Expand Up @@ -132,4 +147,14 @@ if (process.env.NODE_ENV === "production") {
});
}

export default app;
// ============= ERROR HANDLERS (ALWAYS AT THE BOTTOM) =============
// 404 handler for unmatched routes (API routes that don't exist)
app.use(notFoundHandler);
// Global error handler
app.use(errorHandler);

export default app;




73 changes: 73 additions & 0 deletions BACKEND/graphql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# GraphQL API

## Endpoint

`/graphql`

Apollo Sandbox is available at:

http://localhost:5000/graphql

## Product Query Example

```graphql
query GetProducts {
products {
_id
name
price
image
description
category
brand
stock
averageRating
reviewCount
}
}
```

## Product Mutation Example

```graphql
mutation CreateProduct {
createProduct(
name: "Sample Product"
price: 99.99
image: "https://example.com/image.png"
description: "A sample product"
category: "General"
brand: "DemoBrand"
stock: 10
) {
_id
name
price
}
}
```

## User Query Example

```graphql
query GetUsers {
users {
_id
name
email
avatar
}
}
```

## Order Query Example

```graphql
query {
orders {
_id
totalAmount
paymentStatus
}
}
```
101 changes: 101 additions & 0 deletions BACKEND/graphql/resolvers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Product from "../models/product.model.js";
import User from "../models/user.model.js";
import Order from "../models/order.model.js";
import {
indexProduct,
deleteProductFromIndex,
} from "../services/elasticsearch.service.js";
import redis from "../config/redis.js";

async function invalidateProductCache() {
if (!redis) return;
try {
const keys = await redis.keys("products:*");
if (keys.length) {
await redis.del(...keys);
}
} catch (error) {
console.warn("[Redis] Cache invalidation error:", error.message);
}
}

export const resolvers = {
Query: {
products: async () => Product.find({ isDeleted: false }),
product: async (_, { id }) =>
Product.findOne({
_id: id,
isDeleted: false,
}),

users: async () => User.find(),
user: async (_, { id }) => User.findById(id),

orders: async () => Order.find().populate("user"),
order: async (_, { id }) => Order.findById(id).populate("user"),
},
Mutation: {
createProduct: async (_, args, context) => {
if (!context.user) {
throw new Error("Unauthorized");
}

const product = await Product.create(args);
await indexProduct(product);
await invalidateProductCache();
return product;
},

updateProduct: async (_, { id, ...updates }, context) => {
if (!context.user) {
throw new Error("Unauthorized");
}

const product = await Product.findByIdAndUpdate(id, updates, {
new: true,
});
if (product) {
await indexProduct(product);
await invalidateProductCache();
}
return product;
},

deleteProduct: async (_, { id }, context) => {
if (!context.user) {
throw new Error("Unauthorized");
}

const deleted = await Product.findByIdAndUpdate(
id,
{ isDeleted: true },
{ new: true }
);
if (!deleted) {
return false;
}

await deleteProductFromIndex(id);
await invalidateProductCache();
return true;
},

createUser: async (_, args, context) => {
if (!context.user) {
throw new Error("Unauthorized");
}

return await User.create(args);
},

deleteUser: async (_, { id }, context) => {
if (!context.user) {
throw new Error("Unauthorized");
}

const deleted = await User.findByIdAndDelete(id);
return !!deleted;
},
},
};

71 changes: 71 additions & 0 deletions BACKEND/graphql/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export const typeDefs = `#graphql
type Product {
_id: ID!
name: String!
price: Float!
image: String!
description: String
category: String
brand: String
stock: Int
averageRating: Float
reviewCount: Int
}

type User {
_id: ID!
name: String!
email: String!
avatar: String
}

type Order {
_id: ID!
totalAmount: Float!
paymentStatus: String!
stripeSessionId: String!
}

type Query {
products: [Product]
product(id: ID!): Product

users: [User]
user(id: ID!): User

orders: [Order]
order(id: ID!): Order
}

type Mutation {
createProduct(
name: String!
price: Float!
image: String!
description: String
category: String
brand: String
stock: Int
): Product

updateProduct(
id: ID!
name: String
price: Float
image: String
description: String
category: String
brand: String
stock: Int
): Product

deleteProduct(id: ID!): Boolean

createUser(
name: String!
email: String!
): User

deleteUser(id: ID!): Boolean
}
`;
8 changes: 8 additions & 0 deletions BACKEND/graphql/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApolloServer } from "@apollo/server";
import { typeDefs } from "./schema.js";
import { resolvers } from "./resolvers.js";

export const apolloServer = new ApolloServer({
typeDefs,
resolvers,
});
Loading
Loading