Sprint Challenge: Relational Databases

Challenge Overview

Demonstrate your mastery of relational databases by building a complete database application.

In this sprint challenge, you'll build a complete database application that demonstrates your understanding of:

Project Setup

Getting Started

  1. Fork and clone the starter repository
  2. Install dependencies using npm
  3. Set up your database configuration
  4. Run the initial migrations
View Starter Repository

Project Requirements

Database Schema

  • Design and implement a normalized database schema
  • Create appropriate table relationships
  • Include necessary indexes and constraints
  • Document your schema design

Your schema should apply normalization principles to avoid data redundancy while maintaining data integrity. Your tables should use appropriate data types and include essential constraints like primary and foreign keys.

// Example migration for a normalized project schema
exports.up = function(knex) {
  return knex.schema
    .createTable('projects', table => {
      table.increments('id');
      table.string('name').notNullable();
      table.text('description');
      table.boolean('completed').defaultTo(false);
      table.timestamps(true, true);
    })
    .createTable('resources', table => {
      table.increments('id');
      table.string('name').notNullable().unique();
      table.text('description');
    })
    .createTable('tasks', table => {
      table.increments('id');
      table.integer('project_id')
        .notNullable()
        .references('id')
        .inTable('projects')
        .onDelete('CASCADE');
      table.string('description').notNullable();
      table.boolean('completed').defaultTo(false);
      table.timestamps(true, true);
    })
    .createTable('project_resources', table => {
      table.integer('project_id')
        .notNullable()
        .references('id')
        .inTable('projects')
        .onDelete('CASCADE');
      table.integer('resource_id')
        .notNullable()
        .references('id')
        .inTable('resources')
        .onDelete('CASCADE');
      table.primary(['project_id', 'resource_id']);
    });
};
                

Data Access Layer

  • Implement CRUD operations for all tables
  • Create complex queries using joins
  • Write efficient database access methods

Your data access layer should abstract database operations and provide a clean API for your application. Implement helper functions for common operations and use joins to retrieve related data efficiently.

// Example data access methods
const db = require('../data/db-config');

async function getAllProjects() {
  return db('projects')
    .select('*');
}

async function getProjectById(id) {
  // Get project with related tasks and resources
  const project = await db('projects')
    .where('projects.id', id)
    .first();
    
  if (project) {
    // Get project tasks
    const tasks = await db('tasks')
      .where('project_id', id);
      
    // Get project resources
    const resources = await db('resources')
      .join('project_resources', 'resources.id', 'project_resources.resource_id')
      .where('project_resources.project_id', id)
      .select('resources.*');
      
    return {
      ...project,
      tasks,
      resources
    };
  }
  
  return null;
}

async function addProject(project) {
  // Transaction to handle related resources
  return db.transaction(async trx => {
    const [projectId] = await trx('projects').insert(project);
    
    if (project.resources && project.resources.length > 0) {
      const resourceRecords = project.resources.map(resourceId => ({
        project_id: projectId,
        resource_id: resourceId
      }));
      
      await trx('project_resources').insert(resourceRecords);
    }
    
    return getProjectById(projectId);
  });
}
                

API Endpoints

  • Create RESTful endpoints for all entities
  • Implement proper error handling
  • Validate incoming data
  • Return appropriate status codes and responses

Your API should follow RESTful conventions with endpoints for all necessary operations. Implement validation to ensure data integrity and use appropriate status codes and error messages.

// Example API endpoints
const router = require('express').Router();
const Projects = require('../models/project-model');

// Get all projects
router.get('/', async (req, res, next) => {
  try {
    const projects = await Projects.getAllProjects();
    res.status(200).json(projects);
  } catch (err) {
    next(err);
  }
});

// Get project by ID
router.get('/:id', async (req, res, next) => {
  try {
    const project = await Projects.getProjectById(req.params.id);
    
    if (project) {
      res.status(200).json(project);
    } else {
      res.status(404).json({ message: 'Project not found' });
    }
  } catch (err) {
    next(err);
  }
});

// Create a new project
router.post('/', validateProject, async (req, res, next) => {
  try {
    const newProject = await Projects.addProject(req.body);
    res.status(201).json(newProject);
  } catch (err) {
    next(err);
  }
});

// Middleware for validation
function validateProject(req, res, next) {
  const { name } = req.body;
  
  if (!name || name.trim() === '') {
    return res.status(400).json({ message: 'Project name is required' });
  }
  
  next();
}
                

Testing

  • Write tests for database operations
  • Test API endpoints
  • Ensure proper error handling

Implement comprehensive tests to verify that your database operations and API endpoints work correctly. Use a testing database to avoid affecting production data.

// Example test for project endpoint
const request = require('supertest');
const server = require('../api/server');
const db = require('../data/db-config');

describe('projects endpoints', () => {
  beforeEach(async () => {
    await db.migrate.rollback();
    await db.migrate.latest();
    await db.seed.run();
  });
  
  describe('GET /api/projects', () => {
    it('returns all projects', async () => {
      const res = await request(server).get('/api/projects');
      
      expect(res.status).toBe(200);
      expect(res.body).toHaveLength(3); // Assuming 3 seed projects
      expect(res.body[0]).toHaveProperty('id');
      expect(res.body[0]).toHaveProperty('name');
    });
  });
  
  describe('POST /api/projects', () => {
    it('creates a new project', async () => {
      const newProject = {
        name: 'Test Project',
        description: 'A test project',
        completed: false
      };
      
      const res = await request(server)
        .post('/api/projects')
        .send(newProject);
        
      expect(res.status).toBe(201);
      expect(res.body).toHaveProperty('id');
      expect(res.body.name).toBe(newProject.name);
    });
    
    it('returns 400 if name is missing', async () => {
      const invalidProject = {
        description: 'Missing name field'
      };
      
      const res = await request(server)
        .post('/api/projects')
        .send(invalidProject);
        
      expect(res.status).toBe(400);
      expect(res.body).toHaveProperty('message');
    });
  });
});
                

Submission Guidelines

To submit your project:

  1. Push your code to GitHub
  2. Ensure all tests are passing
  3. Update the README with setup instructions
  4. Submit your repository link through the submission form
Submit Project

Resources

Assessment

Your project will be assessed based on:

View Assessment Rubric

Next Steps

After completing the Sprint Challenge: