Skip to content

Commit 3cf8a3e

Browse files
committed
feat(api): add schemas endpoints for dynamic creation via api key
1 parent b1d9e36 commit 3cf8a3e

5 files changed

Lines changed: 109 additions & 1 deletion

File tree

backend/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,14 @@ const projectRoute = require('./routes/projects');
7171
const dataRoute = require('./routes/data');
7272
const userAuthRoute = require('./routes/userAuth');
7373
const storageRoute = require('./routes/storage');
74+
const schemaRoute = require('./routes/schemas');
7475

7576
// ROUTES SETUP
7677
app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth
7778
app.use('/api/projects', dashboardLimiter, projectRoute); // Project Mgmt
7879
app.use('/api/userAuth', limiter, logger, userAuthRoute);
7980
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
80-
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
81+
app.use('/api/schemas', limiter, cors(adminCorsOptions), logger, schemaRoute);
8182
app.use('/api/storage', limiter, cors(adminCorsOptions), logger, storageRoute);
8283

8384
app.get('/api/server-ip', async (req, res) => {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const { createSchemaApiKeySchema } = require('../utils/input.validation');
2+
const Project = require('../models/Project');
3+
const { v4: uuidv4 } = require('uuid');
4+
const { deleteProjectById, setProjectById, deleteProjectByApiKeyCache } = require('../services/redisCaching');
5+
const { z } = require('zod');
6+
7+
module.exports.checkSchema = async (req, res) => {
8+
try {
9+
const { collectionName } = req.params;
10+
const project = req.project;
11+
12+
if (!project) return res.status(401).json({ error: "Project missing from request." });
13+
14+
const collectionConfig = project.collections.find(c => c.name === collectionName);
15+
16+
if (!collectionConfig) {
17+
return res.status(404).json({ error: "Schema/Collection not found" });
18+
}
19+
20+
res.status(200).json({ message: "Schema exists", collection: collectionConfig });
21+
} catch (err) {
22+
res.status(500).json({ error: err.message });
23+
}
24+
};
25+
26+
module.exports.createSchema = async (req, res) => {
27+
try {
28+
const { name, fields } = createSchemaApiKeySchema.parse(req.body);
29+
let project = req.project; // From verifyApiKey
30+
31+
// Need the full document to call .save(), req.project from cache might be a lean object.
32+
// It's safer to fetch the mongoose model instance to modify it.
33+
const projectId = project._id;
34+
const fullProject = await Project.findById(projectId);
35+
36+
if (!fullProject) return res.status(404).json({ error: 'Project not found' });
37+
38+
const exists = fullProject.collections.find(c => c.name === name);
39+
if (exists) return res.status(400).json({ error: 'Collection/Schema already exists' });
40+
41+
if (!fullProject.jwtSecret) fullProject.jwtSecret = uuidv4();
42+
43+
// Convert the incoming API payload (name, type, required) into the UrBackend schema format (key, type, required)
44+
const transformedFields = (fields || []).map(f => {
45+
// Capitalize first letter of type mapping "string" -> "String" requirement
46+
const mappedType = f.type.charAt(0).toUpperCase() + f.type.slice(1).toLowerCase();
47+
return {
48+
key: f.name,
49+
type: mappedType,
50+
required: f.required === true
51+
};
52+
});
53+
54+
fullProject.collections.push({ name: name, model: transformedFields });
55+
await fullProject.save();
56+
57+
// Clear redis cache
58+
await deleteProjectById(projectId.toString());
59+
await setProjectById(projectId.toString(), fullProject);
60+
await deleteProjectByApiKeyCache(fullProject.apiKey); // In case it was cached by hashed key
61+
// NOTE: req.hashedApiKey is available from middleware if we strictly want that.
62+
if (req.hashedApiKey) {
63+
await deleteProjectByApiKeyCache(req.hashedApiKey);
64+
}
65+
66+
const projectObj = fullProject.toObject();
67+
delete projectObj.apiKey;
68+
delete projectObj.jwtSecret;
69+
70+
res.status(201).json({ message: "Schema created successfully", project: projectObj });
71+
} catch (err) {
72+
if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors });
73+
res.status(500).json({ error: err.message });
74+
}
75+
};

backend/routes/schemas.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const verifyApiKey = require('../middleware/verifyApiKey');
4+
const { checkSchema, createSchema } = require("../controllers/schema.controller");
5+
6+
// GET Route to check if a schema exists
7+
// GET /api/schemas/error_logs
8+
router.get('/:collectionName', verifyApiKey, checkSchema);
9+
10+
// POST Route to create a new schema
11+
// POST /api/schemas
12+
router.post('/', verifyApiKey, createSchema);
13+
14+
module.exports = router;

backend/test_schemas.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// We will need a project API key to test this.
2+
// Before running this test, you'll need the dev server running, or try starting it up in another terminal.
3+
const http = require('http');
4+
5+
async function getAdminToken() {
6+
// You'd normally fetch or login...
7+
// Let's do a curl test instead manually if we don't have a known API key
8+
}

backend/utils/input.validation.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ module.exports.createCollectionSchema = z.object({
5454
})).optional()
5555
});
5656

57+
// Create Collection Schema (API Key based)
58+
module.exports.createSchemaApiKeySchema = z.object({
59+
name: z.string().min(1, "Collection Name is required"),
60+
fields: z.array(z.object({
61+
name: z.string(),
62+
type: z.enum(['string', 'number', 'boolean', 'date', 'String', 'Number', 'Boolean', 'Date']),
63+
required: z.boolean().optional()
64+
})).optional()
65+
});
66+
5767
module.exports.sanitize = (obj) => {
5868
const clean = {};
5969
for (const key in obj) {

0 commit comments

Comments
 (0)