diff --git a/backend/.gitignore b/backend/.gitignore index 1dcef2d9f..f95ba41c3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ node_modules -.env \ No newline at end of file +.env +notes.txt \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 1cef5c7af..308da749f 100644 --- a/backend/app.js +++ b/backend/app.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose') const cors = require('cors') const dotenv = require('dotenv'); const app = express(); +const GC = require('./utils/GC'); dotenv.config(); // Middleware @@ -53,16 +54,22 @@ app.get('/', (req, res) => { res.status(200).json({ status: "success", message: "urBackend API is running ЁЯЪА" }) }); -// ЁЯЫбя╕П FAULT TOLERANCE: Global Error Handler +// FAULT TOLERANCE: Global Error Handler app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + return res.status(400).json({ + error: "Invalid JSON format", + message: "Check your request body syntax. Stray characters outside the JSON object are not allowed." + }); + } + console.error("ЁЯФе Unhandled Error:", err.stack); res.status(500).json({ error: "Something went wrong!", message: err.message }); }); - -// ЁЯЫбя╕П DB CONNECTION & SERVER START +// DB CONNECTION & SERVER START // (Only connect if NOT in Test Mode) if (process.env.NODE_ENV !== 'test') { diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js index 162e87054..ddc064be4 100644 --- a/backend/controllers/data.controller.js +++ b/backend/controllers/data.controller.js @@ -1,41 +1,31 @@ -const { sanitize } = require("../utils/input.validation") -const { ObjectId } = require('mongodb'); +const { sanitize } = require("../utils/input.validation"); const mongoose = require('mongoose'); const Project = require("../models/Project"); +const { getConnection } = require("../utils/connectionManager"); +const { getCompiledModel } = require("../utils/injectModel"); +// 1. INSERT DATA module.exports.insertData = async (req, res) => { try { const { collectionName } = req.params; - const project = req.project; - - // Latest Project Data fetch karein (taaki accurate usage mile) - const currentProject = await Project.findById(project._id); + const project = req.project; // Middleware se mila project metadata - const collectionConfig = currentProject.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); const schemaRules = collectionConfig.model; const incomingData = req.body; const cleanData = {}; - // --- VALIDATION & SANITIZATION --- + // Validation & Sanitization for (const field of schemaRules) { if (field.required && incomingData[field.key] === undefined) { - return res.status(400).json({ - error: `Field '${field.key}' is required.`, - field: field.key - }); + return res.status(400).json({ error: `Field '${field.key}' is required.` }); } if (incomingData[field.key] !== undefined) { - // Type checking logic (same as before)... + // Type Checks if (field.type === 'Number' && typeof incomingData[field.key] !== 'number') return res.status(400).send(`Field '${field.key}' must be a Number.`); if (field.type === 'Boolean' && typeof incomingData[field.key] !== 'boolean') return res.status(400).send(`Field '${field.key}' must be a Boolean.`); - cleanData[field.key] = incomingData[field.key]; } } @@ -43,194 +33,134 @@ module.exports.insertData = async (req, res) => { const safeData = sanitize(cleanData); Object.assign(cleanData, safeData); - // CHECK DB LIMIT --- - // Calculate approx size in bytes - const docSize = Buffer.byteLength(JSON.stringify(cleanData)); - - if (currentProject.databaseUsed + docSize > currentProject.databaseLimit) { - return res.status(403).send("Database limit exceeded. Delete some data to free up space."); + // Usage Limit Check (Internal Only) + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(cleanData)); + if ((project.databaseUsed || 0) + docSize > project.databaseLimit) { + return res.status(403).send("Database limit exceeded."); + } } - // Insert Data - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); - const result = await collection.insertOne(cleanData); + // Get Connection & Model + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); - // UPDATE USAGE --- - currentProject.databaseUsed += docSize; - await currentProject.save(); + const result = await Model.create(cleanData); - res.status(201).json({ - message: "Data inserted successfully", - insertedId: result.insertedId, - data: cleanData - }); + // Update Project Metadata (Internal Only) + if (!project.isExternal) { + project.databaseUsed = (project.databaseUsed || 0) + docSize; + await project.save(); + } + res.status(201).json(result); } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + console.error("Insert Error:", err); + res.status(500).json({ error: err.message }); } -} - +}; +// 2. GET ALL DATA module.exports.getAllData = async (req, res) => { try { const { collectionName } = req.params; const project = req.project; const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - const finalCollectionName = `${project._id}_${collectionName}`; + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - const data = await mongoose.connection.db - .collection(finalCollectionName) - .find({}) - .toArray(); + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + const data = await Model.find({}).limit(100); res.json(data); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + console.log("error----------------") + res.status(500).json({ error: err.message }); } -} +}; +// 3. GET SINGLE DOC module.exports.getSingleDoc = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - // Security Check const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - - const finalCollectionName = `${project._id}_${collectionName}`; + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - // Find document by _id - const doc = await mongoose.connection.db - .collection(finalCollectionName) - .findOne({ _id: new ObjectId(id) }); // ID Conversion is important + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); - if (!doc) { - return res.status(404).send("Document not found."); - } + const doc = await Model.findById(id); + if (!doc) return res.status(404).send("Document not found."); res.json(doc); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} - - - +}; +// 4. UPDATE DATA module.exports.updateSingleData = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - const incomingData = req.body; // Naya data + const incomingData = req.body; - // Security & Config Find const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - // --- VALIDATION REPEAT (Important) --- + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); + + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + + // Basic Schema Validation const schemaRules = collectionConfig.model; for (const key in incomingData) { - // 1. Check if field exists in schema const fieldRule = schemaRules.find(f => f.key === key); - if (!fieldRule) { - - // Not Found - return res.status(400).send(`Field '${key}' is not defined in the schema.`); - } - - // Check Type - if (fieldRule.type === 'Number' && typeof incomingData[key] !== 'number') { - return res.status(400).send(`Field '${key}' must be a Number.`); - } - if (fieldRule.type === 'Boolean' && typeof incomingData[key] !== 'boolean') { - return res.status(400).send(`Field '${key}' must be a Boolean.`); - } - // String check is loose in JS, usually fine. + if (!fieldRule) return res.status(400).send(`Field '${key}' not in schema.`); + // Type checks... (Number, Boolean etc.) } + const result = await Model.findByIdAndUpdate(id, { $set: incomingData }, { new: true }); + if (!result) return res.status(404).send("Document not found."); - const safeData = sanitize(incomingData); - incomingData = safeData; - - - const finalCollectionName = `${project._id}_${collectionName}`; - - const result = await mongoose.connection.db - .collection(finalCollectionName) - .updateOne( - { _id: new ObjectId(id) }, - { $set: incomingData } - ); - - if (result.matchedCount === 0) { - return res.status(404).send("Document not found to update."); - } - - res.json({ message: "Document updated successfully", id, updatedFields: incomingData }); - + res.json({ message: "Updated", data: result }); } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} - - - +}; +// 5. DELETE DATA module.exports.deleteSingleDoc = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - // Latest Project Fetch - const currentProject = await Project.findById(project._id); - - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - // First find the document to know size - const docToDelete = await collection.findOne({ _id: new ObjectId(id) }); + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + const docToDelete = await Model.findById(id); if (!docToDelete) return res.status(404).send("Document not found."); - // Calculate size to subtract - // It also counts _id field - const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(docToDelete)); + } - // Delete - await collection.deleteOne({ _id: new ObjectId(id) }); + await Model.deleteOne({ _id: id }); - // Update Usage (Ensure it doesn't go below 0) - currentProject.databaseUsed = Math.max(0, currentProject.databaseUsed - docSize); - await currentProject.save(); + if (!project.isExternal) { + project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); + await project.save(); + } res.json({ message: "Document deleted", id }); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 714d54b9d..15ccf6a16 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,9 +1,13 @@ const mongoose = require("mongoose") const Project = require("../models/Project") +const Log = require("../models/Log") const { createProjectSchema, createCollectionSchema } = require('../utils/input.validation'); const { generateApiKey, hashApiKey } = require('../utils/api'); const { z } = require('zod'); - +const { encrypt } = require('../utils/encryption'); +const { URL } = require('url'); +const { getConnection } = require("../utils/connectionManager"); +const { getCompiledModel } = require("../utils/injectModel") // CONFIGURATION @@ -87,8 +91,59 @@ module.exports.regenerateApiKey = async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } +}; + + +//function to validate monguri +const isSafeUri = (uri) => { + try { + const parsed = new URL(uri); + const host = parsed.hostname.toLowerCase(); + const badHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1']; + return !badHosts.includes(host); + } catch (e) { return false; } +}; + + + +module.exports.updateExternalConfig = async (req, res) => { + try { + const { projectId } = req.params; + const { dbUri, storageUrl, storageKey, storageProvider } = req.body; + + if (!dbUri && (!storageUrl || !storageKey)) { + return res.status(400).json({ + error: "At least one configuration (Database or Storage) must be provided." + }); + } + + if (dbUri && !isSafeUri(dbUri)) return res.status(400).json({ error: "Invalid DB URI" }); + + const configToEncrypt = {}; + if (dbUri) configToEncrypt.dbUri = dbUri; + if (storageUrl && storageKey) { + configToEncrypt.storageUrl = storageUrl; + configToEncrypt.storageKey = storageKey; + configToEncrypt.storageProvider = storageProvider || 'supabase'; + } + + const encryptedConfig = encrypt(JSON.stringify(configToEncrypt)); + + const project = await Project.findOneAndUpdate( + { _id: projectId, owner: req.user._id }, + { $set: { externalConfig: encryptedConfig, isExternal: true } }, + { new: true } + ); + + if (!project) return res.status(404).json({ error: "Project not found." }); + + res.status(200).json({ message: "Configuration saved successfully." }); + } catch (error) { + res.status(500).json({ error: error.message }); + } } + module.exports.createCollection = async (req, res) => { try { const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body); @@ -122,13 +177,28 @@ module.exports.getData = async (req, res) => { const project = await Project.findOne({ _id: projectId, owner: req.user._id }); if (!project) return res.status(404).json({ error: "Project not found." }); - const finalCollectionName = `${project._id}_${collectionName}`; - const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); + // const finalCollectionName = `${project._id}_${collectionName}`; - let data = []; - if (collectionsList.length > 0) { - data = await mongoose.connection.db.collection(finalCollectionName).find({}).limit(50).toArray(); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ + error: "Collection not found", + collection: collectionName + }); } + + const connection = await getConnection(projectId); + const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); + + const data = await model.find({}).limit(50); + + // let data = []; + // if (collectionsList.length > 0) { + // data = await mongoose.connection.db.collection(finalCollectionName).find({}).limit(50).toArray(); + // } + res.json(data); } catch (err) { res.status(500).json({ error: err.message }); @@ -143,18 +213,31 @@ module.exports.insertData = async (req, res) => { const finalCollectionName = `${project._id}_${collectionName}`; const incomingData = req.body; - const docSize = Buffer.byteLength(JSON.stringify(incomingData)); - const limit = project.databaseLimit || 50 * 1024 * 1024; - if ((project.databaseUsed || 0) + docSize > limit) { - return res.status(403).json({ error: "Database limit exceeded. Delete some data." }); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ error: "Collection configuration not found." }); + } + + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(incomingData)); + + const limit = project.databaseLimit || 20 * 1024 * 1024; + + if ((project.databaseUsed || 0) + docSize > limit) { + return res.status(403).json({ error: "Database limit exceeded. Delete some data." }); + } } - const result = await mongoose.connection.db - .collection(finalCollectionName) - .insertOne(incomingData); + const connection = await getConnection(projectId); + const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + const result = await model.create(incomingData); - project.databaseUsed = (project.databaseUsed || 0) + docSize; + if (!project.isExternal) { + project.databaseUsed = (project.databaseUsed || 0) + docSize; + } await project.save(); res.json(result); @@ -166,27 +249,40 @@ module.exports.insertData = async (req, res) => { module.exports.deleteRow = async (req, res) => { try { const { projectId, collectionName, id } = req.params; + const project = await Project.findOne({ _id: projectId, owner: req.user._id }); if (!project) return res.status(404).json({ error: "Project not found." }); - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ error: "Collection not found." }); + } + + const connection = await getConnection(projectId); + const Model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + const docToDelete = await Model.findById(id); + if (!docToDelete) { + return res.status(404).json({ error: "Document not found." }); + } - const docToDelete = await collection.findOne({ _id: new mongoose.Types.ObjectId(id) }); + const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); - if (docToDelete) { - const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); - await collection.deleteOne({ _id: new mongoose.Types.ObjectId(id) }); + + await Model.deleteOne({ _id: id }); + + if (!project.isExternal) { project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); await project.save(); } - res.json({ success: true }); + res.json({ success: true, message: "Document deleted successfully" }); + } catch (err) { - console.log(err) + console.error("Delete Error:", err); res.status(500).json({ error: err.message }); } -} +}; module.exports.listFiles = async (req, res) => { try { diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index b75688364..cedcb33b9 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -28,6 +28,7 @@ module.exports = function (req, res, next) { // Proceed to the next middleware or route handler next(); } catch (err) { + console.log("err---------------2") console.error(err); // Do not expose detailed error information in production diff --git a/backend/models/Project.js b/backend/models/Project.js index a56b9fd4f..2a8497f22 100644 --- a/backend/models/Project.js +++ b/backend/models/Project.js @@ -15,6 +15,13 @@ const collectionSchema = new mongoose.Schema({ model: [fieldSchema] }); +const externalConfigSchema = new mongoose.Schema({ + encrypted: { type: String, select: false }, + iv: { type: String, select: false }, + tag: { type: String, select: false } +}, { _id: false }); + + const projectSchema = new mongoose.Schema({ name: { type: String, required: true }, description: String, @@ -34,13 +41,19 @@ const projectSchema = new mongoose.Schema({ collections: [collectionSchema], // STORAGE LIMITS (Files) - storageUsed: { type: Number, default: 0 }, // in bytes - storageLimit: { type: Number, default: 100 * 1024 * 1024 }, // 100MB for Files + storageUsed: { type: Number, default: 0 }, + storageLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Files // DATABASE LIMITS (JSON Docs) - databaseUsed: { type: Number, default: 0 }, // in bytes - databaseLimit: { type: Number, default: 50 * 1024 * 1024 } // 50MB for Data (approx 50,000 large docs) + databaseUsed: { type: Number, default: 0 }, // + databaseLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Data + + externalConfig: { + type: externalConfigSchema, + default: null + }, + isExternal: { type: Boolean, default: false } }, { timestamps: true }); module.exports = mongoose.model('Project', projectSchema); \ No newline at end of file diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 2a403502d..bde8d17fd 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -20,6 +20,7 @@ const { deleteAllFiles, deleteProject, updateProject, + updateExternalConfig, analytics } = require("../controllers/project.controller") @@ -65,6 +66,9 @@ router.delete('/:projectId', authMiddleware, verifyEmail, deleteProject); // UPDATE PROJECT router.patch('/:projectId', authMiddleware, updateProject); +// UPDATE EXTERNAL CONFIG +router.patch('/:projectId/byod-config', authMiddleware, updateExternalConfig); + // INSERT DATA (Dashboard) router.post('/:projectId/collections/:collectionName/data', authMiddleware, verifyEmail, insertData); diff --git a/backend/utils/GC.js b/backend/utils/GC.js new file mode 100644 index 000000000..1dfb9b044 --- /dev/null +++ b/backend/utils/GC.js @@ -0,0 +1,21 @@ +const registry = require("./registry"); +function garbageCollect() { + setInterval(() => { + console.log("20 minutes passed"); + const now = new Date(); + for (const [key, value] of registry) { + if (now - value.lastAccessed > 20 * 60 * 1000) { + + if (value.readyState === 1) { + value.close(); + registry.delete(key); + } + + } + } + }, 20 * 60 * 1000); +} + +garbageCollect(); + +module.exports = garbageCollect; diff --git a/backend/utils/connectionManager.js b/backend/utils/connectionManager.js new file mode 100644 index 000000000..123d577f4 --- /dev/null +++ b/backend/utils/connectionManager.js @@ -0,0 +1,57 @@ +const registry = require("./registry"); +const Project = require("../models/Project"); +const { decrypt } = require("./encryption"); +const mongoose = require("mongoose"); + + +async function getConnection(projectId) { + const key = projectId.toString(); + if (registry.has(key)) { + const cachedConn = registry.get(key); + if (cachedConn.readyState === 1) { + cachedConn.lastAccessed = new Date(); + return cachedConn; + } + } + + const projectDoc = await Project.findById(projectId) + .select("+externalConfig.iv +externalConfig.tag +externalConfig.encrypted"); + if (!projectDoc) throw new Error("Project not found"); + if (!projectDoc.isExternal) { + return mongoose.connection; + } + + let config; + try { + const decryptedConfig = decrypt(projectDoc.externalConfig); + config = JSON.parse(decryptedConfig); + } catch { + throw new Error("Invalid or corrupted external config"); + } + + const connection = mongoose.createConnection(config.dbUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + connection.on("connected", () => { + console.log(`DB connected for project ${projectId}`); + }); + + connection.on("error", (err) => { + console.error("Connection error:", err); + }); + + connection.on("close", () => { + registry.delete(key); + console.log(`DB connection closed for project ${key}`); + }); + + + connection.lastAccessed = new Date(); + registry.set(key, connection); + + return connection; +} + +module.exports = { getConnection }; diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js new file mode 100644 index 000000000..df37002f3 --- /dev/null +++ b/backend/utils/encryption.js @@ -0,0 +1,47 @@ +const crypto = require("crypto"); + +const algorithm = "aes-256-gcm"; +const ivLength = 16; + +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; + +function encrypt(plainText) { + if (!ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY missing in .env"); + + const iv = crypto.randomBytes(ivLength); + + const cipher = crypto.createCipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + + let encrypted = cipher.update(plainText, "utf8", "hex"); + encrypted += cipher.final("hex"); + + const tag = cipher.getAuthTag(); + + return { + iv: iv.toString("hex"), + encrypted: encrypted, + tag: tag.toString("hex") + }; +} + +function decrypt(encryptedData) { + try { + const iv = Buffer.from(encryptedData.iv, "hex"); + const tag = Buffer.from(encryptedData.tag, "hex"); + const encryptedText = Buffer.from(encryptedData.encrypted, "hex"); + + const decipher = crypto.createDecipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + decipher.setAuthTag(tag); + + let decrypted = decipher.update(encryptedText, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; + } catch (error) { + console.error("Decryption failed: Data tampered or wrong key."); + return null; + } +} + + +module.exports = { encrypt, decrypt }; \ No newline at end of file diff --git a/backend/utils/injectModel.js b/backend/utils/injectModel.js new file mode 100644 index 000000000..498eac087 --- /dev/null +++ b/backend/utils/injectModel.js @@ -0,0 +1,66 @@ +const modelRegistry = new WeakMap(); +const mongoose = require("mongoose"); + +const typeMapping = { + String: String, + Number: Number, + Boolean: Boolean, + Date: Date +}; + +function buildMongooseSchema(fieldsArray) { + const schemaDef = {}; + + fieldsArray.forEach(field => { + schemaDef[field.key] = { + type: typeMapping[field.type], + required: !!field.required + }; + }); + + return new mongoose.Schema(schemaDef, { timestamps: true }); +} + + +function getCompiledModel(connection, collectionData, projectId, isExternal) { + + let collectionName = ""; + + if (!isExternal) { + collectionName = `${projectId}_${collectionData.name}` + } else { + collectionName = collectionData.name; + + } + + + // 1. Get per-connection cache + if (!modelRegistry.has(connection)) { + modelRegistry.set(connection, new Map()); + } + + const connectionModels = modelRegistry.get(connection); + + // 2. If already compiled for THIS connection + if (connectionModels.has(collectionName)) { + return connectionModels.get(collectionName); + } + + // 3. If model already exists on connection (edge case) + if (connection.models[collectionName]) { + const existingModel = connection.models[collectionName]; + connectionModels.set(collectionName, existingModel); + return existingModel; + } + + // 4. Build schema + compile + const schema = buildMongooseSchema(collectionData.model); + const model = connection.model(collectionName, schema); + + // 5. Cache it + connectionModels.set(collectionName, model); + + return model; +} + +module.exports = { getCompiledModel }; \ No newline at end of file diff --git a/backend/utils/registry.js b/backend/utils/registry.js new file mode 100644 index 000000000..1a3722137 --- /dev/null +++ b/backend/utils/registry.js @@ -0,0 +1,3 @@ +const registry = new Map(); + +module.exports = registry; diff --git a/frontend/src/components/Layout/Header.jsx b/frontend/src/components/Layout/Header.jsx index 3d57389b1..098118298 100644 --- a/frontend/src/components/Layout/Header.jsx +++ b/frontend/src/components/Layout/Header.jsx @@ -1,7 +1,7 @@ import { useAuth } from '../../context/AuthContext'; import { Menu } from 'lucide-react'; // Import Menu Icon -function Header({ onToggleSidebar }) { // Prop receive kiya +function Header({ onToggleSidebar, showToggle = true }) { // Default showToggle to true const { user } = useAuth(); const initial = user?.email ? user.email[0].toUpperCase() : 'D'; @@ -12,19 +12,18 @@ function Header({ onToggleSidebar }) { // Prop receive kiya borderBottom: '1px solid var(--color-border)', display: 'flex', alignItems: 'center', - justifyContent: 'space-between', // Changed to space-between - padding: '0 1rem', // Reduced padding for mobile + justifyContent: 'space-between', + padding: '0 1rem', position: 'fixed', top: 0, right: 0, - // Mobile fix: left should be 0, margin-left will handle desktop left: 0, - zIndex: 40, - // Desktop: Push header to right of sidebar - // We use a media query logic via inline style or keep consistent - // Better approach: Let CSS handle margins via .main-content structure + zIndex: 1000, width: '100%', - paddingLeft: 'calc(var(--sidebar-width) + 1rem)' // Desktop padding + // Only add paddingLeft if we expect a sidebar (showToggle is a proxy for sidebar existence here) + // But wait, showToggle means "can we toggle it". Even if we can't toggle, if sidebar is gone, padding should be 0 (or standard). + // In Project Mode, showToggle is false. So paddingLeft should be 0 (or standard 1rem from padding above). + paddingLeft: showToggle ? 'calc(var(--sidebar-width) + 1rem)' : '1rem' }} className="responsive-header"> {/* CSS override for mobile padding in style tag below */} @@ -42,14 +41,16 @@ function Header({ onToggleSidebar }) { // Prop receive kiya } `} - {/* Mobile Menu Button */} - + {/* Mobile Menu Button - Only show if toggle is allowed */} + {showToggle && ( + + )} {/* Spacer to push User Profile to right */}
@@ -72,4 +73,4 @@ function Header({ onToggleSidebar }) { // Prop receive kiya ); } -export default Header; \ No newline at end of file +export default Header; diff --git a/frontend/src/components/Layout/MainLayout.jsx b/frontend/src/components/Layout/MainLayout.jsx index a33655756..01c10871b 100644 --- a/frontend/src/components/Layout/MainLayout.jsx +++ b/frontend/src/components/Layout/MainLayout.jsx @@ -1,38 +1,54 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useLocation, matchPath } from 'react-router-dom'; import Sidebar from './Sidebar'; import Header from './Header'; +import ProjectNavbar from './ProjectNavbar'; import logoImage from '../../assets/logo_u.png'; function MainLayout({ children }) { const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const location = useLocation(); + + // Check if we are inside a project route to toggle layout mode + // Paths like /project/:projectId/... + const isProjectRoute = matchPath("/project/:projectId/*", location.pathname); return (Real-time metrics for your backend.
+Real-time usage metrics and logs.