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
26 changes: 1 addition & 25 deletions backend/controllers/experience.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,9 @@ const deleteProject = async (req, res) => {
}
};

// Experience Text
const getExpText = async (req, res) => {
try {
const userId = req.user.id;
const data = await experienceService.getExperienceText(userId, req.supabase);
res.json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
};

const putExpText = async (req, res) => {
try {
const userId = req.user.id;
const { id, text } = req.body;
const data = await experienceService.saveExperienceText(userId, id, text, req.supabase);
res.json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
};

module.exports = {
getProjects,
postProject,
putProject,
deleteProject,
getExpText,
putExpText
deleteProject
};
20 changes: 20 additions & 0 deletions backend/controllers/roles.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const supabase = require('../supabaseClient');

const getRolesBank = async (req, res) => {
try {
const { data, error } = await req.supabase
.from('roles_dictionary')
.select('*')
.order('name');

if (error) throw new Error(error.message);

res.json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
};

module.exports = {
getRolesBank
};
30 changes: 30 additions & 0 deletions backend/migrations/003_add_experience_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Bank of roles and tags
CREATE TABLE IF NOT EXISTS roles_dictionary (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
category TEXT -- e.g., 'role', 'tag'
);

-- Relational table for tracking user experience
CREATE TABLE IF NOT EXISTS user_experiences (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL, -- Link to user
role_id INTEGER REFERENCES roles_dictionary(id) ON DELETE CASCADE,
status TEXT CHECK (status IN ('current', 'previous')),
years INTEGER, -- Manual override (e.g., 0 for no experience, 1, 2...)
start_date DATE, -- Optional: used to dynamically calculate duration
end_date DATE -- Optional: marks the end of previous roles
);

-- Seed initial roles
INSERT INTO roles_dictionary (name, category) VALUES
('Software Engineer', 'role'),
('Frontend Developer', 'role'),
('Backend Developer', 'role'),
('Fullstack Developer', 'role'),
('DevOps Engineer', 'role'),
('Data Scientist', 'role'),
('Product Manager', 'role'),
('UI', 'tag'),
('UX', 'tag')
ON CONFLICT (name) DO NOTHING;
27 changes: 26 additions & 1 deletion backend/repositories/profile.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,33 @@ const createProfile = async (userId, updateData, client) => {
.single();
};

const findUserExperiences = async (userId, client) => {
return await client
.from('user_experiences')
.select('*, roles_dictionary(*)')
.eq('user_id', userId);
};

const syncUserExperiences = async (userId, experiences, client) => {
// Simplified sync: delete all and re-insert
await client.from('user_experiences').delete().eq('user_id', userId);
if (experiences && experiences.length > 0) {
const rows = experiences.map(exp => ({
user_id: userId,
role_id: exp.role_id,
status: exp.status,
years: exp.years || null,
start_date: exp.start_date || null,
end_date: exp.end_date || null
}));
await client.from('user_experiences').insert(rows);
}
};

module.exports = {
findFirstProfile,
updateProfile,
createProfile
createProfile,
findUserExperiences,
syncUserExperiences
};
5 changes: 0 additions & 5 deletions backend/routes/experience.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,4 @@ router.get('/projects', authenticate, experienceController.getProjects);
router.post('/projects', authenticate, experienceController.postProject);
router.put('/projects/:id', authenticate, experienceController.putProject);
router.delete('/projects/:id', authenticate, experienceController.deleteProject);

// Text Routes
router.get('/text', authenticate, experienceController.getExpText);
router.put('/text', authenticate, experienceController.putExpText);

module.exports = router;
8 changes: 8 additions & 0 deletions backend/routes/roles.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const rolesController = require('../controllers/roles.controller');
const { authenticate } = require('../middleware/auth');

router.get('/', authenticate, rolesController.getRolesBank);

module.exports = router;
18 changes: 18 additions & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,21 @@ CREATE TABLE IF NOT EXISTS rss_jobs (
seniority TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Bank of roles and tags
CREATE TABLE IF NOT EXISTS roles_dictionary (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
category TEXT -- e.g., 'role', 'tag'
);

-- Relational table for tracking user experience
CREATE TABLE IF NOT EXISTS user_experiences (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL, -- Link to user
role_id INTEGER REFERENCES roles_dictionary(id) ON DELETE CASCADE,
status TEXT CHECK (status IN ('current', 'previous')),
years INTEGER, -- Manual override (e.g., 0 for no experience, 1, 2...)
start_date DATE, -- Optional: used to dynamically calculate duration
end_date DATE -- Optional: marks the end of previous roles
);
2 changes: 2 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const { startMailPolling } = require('./cron/mail-poll-cron');
const notificationsRoutes = require('./routes/notifications.routes');
const rssRoutes = require('./routes/rss.routes');
const { startRssPolling } = require('./cron/rss-poll-cron');
const rolesRoutes = require('./routes/roles.routes');

// --- Mount Routes ---
app.use('/auth', userRoutes);
Expand All @@ -49,6 +50,7 @@ app.use('/api/messages', messagesRoutes);
app.use('/api/events', eventsRoutes);
app.use('/api/notifications', notificationsRoutes);
app.use('/api/rss-feeds', rssRoutes);
app.use('/api/roles-bank', rolesRoutes);

// Health check
app.get('/api/health', (req, res) => {
Expand Down
15 changes: 14 additions & 1 deletion backend/services/profile.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const getProfile = async (userId, supabaseClient) => {

let profile = data || {};

const { data: experiencesData, error: expError } = await profileRepository.findUserExperiences(userId, supabaseClient);
if (expError) throw new Error(expError.message);

profile.experiences = experiencesData || [];

// Transformation logic
if (profile.cv_data) {
profile.cvData = profile.cv_data;
Expand All @@ -20,7 +25,7 @@ const getProfile = async (userId, supabaseClient) => {
};

const upsertProfile = async (userId, payload, supabaseClient) => {
const { id, ...updateData } = payload;
const { id, experiences, ...updateData } = payload;

// Mapping frontend keys to database keys
if ('cvData' in updateData) {
Expand All @@ -37,6 +42,14 @@ const upsertProfile = async (userId, payload, supabaseClient) => {
: await profileRepository.createProfile(userId, updateData, supabaseClient);

if (error) throw new Error(error.message);

if (experiences !== undefined) {
if (experiences.filter(exp => exp.status === 'current').length > 1) {
throw new Error("Only one current experience is allowed.");
}
await profileRepository.syncUserExperiences(userId, experiences, supabaseClient);
}

return data;
};

Expand Down
17 changes: 0 additions & 17 deletions backend/tests/controllers/experience.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,4 @@ describe('experience.controller', () => {
});
});

describe('getExpText', () => {
it('should return data', async () => {
const { req, res } = buildReqRes();
experienceService.getExperienceText.mockResolvedValue({ text: 'exp' });
await controller.getExpText(req, res);
expect(res.json).toHaveBeenCalledWith({ text: 'exp' });
});
});

describe('putExpText', () => {
it('should return data', async () => {
const { req, res } = buildReqRes({ body: { id: 1, text: 'updated' } });
experienceService.saveExperienceText.mockResolvedValue({ text: 'updated' });
await controller.putExpText(req, res);
expect(res.json).toHaveBeenCalledWith({ text: 'updated' });
});
});
});
50 changes: 50 additions & 0 deletions backend/tests/controllers/roles.controller.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const rolesController = require('../../controllers/roles.controller');

describe('Roles Controller', () => {
let mockReq;
let mockRes;

beforeEach(() => {
mockRes = {
json: jest.fn(),
status: jest.fn().mockReturnThis()
};
});

it('should return roles bank successfully (Sunny Day)', async () => {
mockReq = {
supabase: {
from: jest.fn().mockReturnValue({
select: jest.fn().mockReturnValue({
order: jest.fn().mockResolvedValue({
data: [{ id: 1, name: 'Software Engineer' }],
error: null
})
})
})
}
};

await rolesController.getRolesBank(mockReq, mockRes);
expect(mockRes.json).toHaveBeenCalledWith([{ id: 1, name: 'Software Engineer' }]);
});

it('should handle db error gracefully (Rainy Day)', async () => {
mockReq = {
supabase: {
from: jest.fn().mockReturnValue({
select: jest.fn().mockReturnValue({
order: jest.fn().mockResolvedValue({
data: null,
error: { message: 'Database error' }
})
})
})
}
};

await rolesController.getRolesBank(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Database error' });
});
});
Loading
Loading