From 90722673f83222bd33c4f5e8e047f3ffb8e2f247 Mon Sep 17 00:00:00 2001 From: Nathan Lamy Date: Mon, 25 Aug 2025 19:12:28 +0200 Subject: [PATCH] feat: add meals to database --- README.md | 4 + app/controllers/internals_controller.ts | 43 +++++++++ app/controllers/meals_controller.ts | 96 +++++++++++++++++++ app/models/course.ts | 15 +++ app/models/meal.ts | 26 +++++ app/models/meal_registration.ts | 12 +++ app/validators/meal.ts | 40 ++++++++ .../1756124105948_create_meals_table.ts | 18 ++++ .../1756124135995_create_courses_table.ts | 18 ++++ ...738666_create_meals_registrations_table.ts | 17 ++++ start/env.ts | 2 +- start/routes.ts | 21 ++-- 12 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 README.md create mode 100644 app/controllers/meals_controller.ts create mode 100644 app/models/course.ts create mode 100644 app/models/meal.ts create mode 100644 app/models/meal_registration.ts create mode 100644 app/validators/meal.ts create mode 100644 database/migrations/1756124105948_create_meals_table.ts create mode 100644 database/migrations/1756124135995_create_courses_table.ts create mode 100644 database/migrations/1756125738666_create_meals_registrations_table.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..dda8d58 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Clean up avant une nouvelle année scolaire : +- Effacer les mails de tous les utilisateurs (users.email) dans la base de données. +- Mettre la dernière activité à 1 mois pour l'autocomplete (auth) +- Supprimer les repas (meals) (et les plats associés avec cascade) diff --git a/app/controllers/internals_controller.ts b/app/controllers/internals_controller.ts index 06f577e..4699b24 100644 --- a/app/controllers/internals_controller.ts +++ b/app/controllers/internals_controller.ts @@ -1,3 +1,6 @@ +import MealRegistration from '#models/meal_registration' +import env from '#start/env' +import { updateMealsRegistrationsValidator } from '#validators/meal' import type { HttpContext } from '@adonisjs/core/http' import redis from '@adonisjs/redis/services/main' import { DateTime } from 'luxon' @@ -20,6 +23,46 @@ export default class InternalsController { console.log(`Colles fetching for class ${className} completed.`) }) } + + // POST /internals/fetch-meals + async fetchMeals({ response }: HttpContext) { + await redis.publish( + 'jobs_queue', + JSON.stringify({ + type: 3, // Fetch meals + class_name: env.get('DEFAULT_CLASS_NAME'), + }) + ) + return response.send({ success: true, message: 'Fetching meals...' }) + } + + // POST /internals/meals-registrations + async updateMealsRegistrations({ response, request }: HttpContext) { + const { userId, meals } = await request.validateUsing(updateMealsRegistrationsValidator) + + // Remove all existing registrations for the user + await MealRegistration.query().where('user_id', userId).delete() + for (const mealData of meals) { + const meal = await MealRegistration.query() + .where({ + date: mealData.date, + type: mealData.meal_type === 'lunch' ? 0 : 1, + }) + .first() + if (meal) { + // Create a new registration + await MealRegistration.create({ + mealId: meal.id, + userId: userId, + }) + } + } + + return response.send({ + success: true, + message: 'Meal registrations updated successfully', + }) + } } async function queue(className: string) { diff --git a/app/controllers/meals_controller.ts b/app/controllers/meals_controller.ts new file mode 100644 index 0000000..a9d133f --- /dev/null +++ b/app/controllers/meals_controller.ts @@ -0,0 +1,96 @@ +import Meal from '#models/meal' +import { createMealValidator } from '#validators/meal' +import type { HttpContext } from '@adonisjs/core/http' +import redis from '@adonisjs/redis/services/main' +import { DateTime } from 'luxon' + +export default class MealsController { + // POST /meals + public async create({ request }: HttpContext) { + const data = await request.validateUsing(createMealValidator) + + if ('meals' in data) { + // Remove all already submittable meals + await Meal.query().where('submittable', true).update({ submittable: false }) + // Set new meals as submittable + for (const mealData of data.meals) { + const meal = await Meal.query() + .where('date', mealData.date) + .where('type', mealData.meal_type === 'lunch' ? 0 : 1) + .first() + if (meal) { + meal.submittable = true + await meal.save() + } else { + console.warn('Meal to set as submittable not found:', mealData) + } + } + return { message: 'Meals updated successfully' } + } + + const { date, type, courses } = data + const mealType = type === 'Déjeuner' ? 0 : 1 + + // Check if a meal with the same date and type already exists + const existingMeal = await Meal.query() + .where({ date, type: mealType }) + .preload('courses') + .first() + if (existingMeal) { + // Check if the existing meal has the same courses + const existingCourseNames = existingMeal.courses.map((course) => course.description) + const newCourseNames = courses.map((course) => course.description) + if ( + existingCourseNames.length === newCourseNames.length && + existingCourseNames.every((name) => newCourseNames.includes(name)) + ) { + return existingMeal + } + + // If not, delete the existing meal (and its courses, thanks to cascade delete) + await existingMeal.delete() + } + + // Create the new meal + const meal = await Meal.create({ + date: DateTime.fromJSDate(date), + type: mealType, + }) + await meal.related('courses').createMany(courses) + return meal + } + + // GET /meals + public async index({}: HttpContext) { + // TODO: Check if the user is registered for each meal + return Meal.query().orderBy('date', 'asc').preload('courses') + } + + // POST /meals/:id + public async registerForMeal({ params, auth, response }: HttpContext) { + const meal = await Meal.find(params.id) + if (!meal) { + return response.notFound({ message: 'Meal not found' }) + } + if (!meal.submittable) { + return response.badRequest({ message: 'Meal is not submittable' }) + } + + // Register the user for the meal + await redis.publish( + 'jobs_queue', + JSON.stringify({ + type: 4, // Submit meals + meal: { + date: meal.date.toFormat('yyyy-MM-dd'), + meal_type: meal.type === 0 ? 'lunch' : 'dinner', + }, + class_name: auth.user!.className, + user_id: auth.user!.id, + // TODO: add user credentials + }) + ) + + return { success: true, message: 'Registration in progress' } + } +} diff --git a/app/models/course.ts b/app/models/course.ts new file mode 100644 index 0000000..8b84c43 --- /dev/null +++ b/app/models/course.ts @@ -0,0 +1,15 @@ +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Course extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare mealId: number + + @column() + declare name: string + + @column() + declare description: string +} diff --git a/app/models/meal.ts b/app/models/meal.ts new file mode 100644 index 0000000..b0ccba9 --- /dev/null +++ b/app/models/meal.ts @@ -0,0 +1,26 @@ +import { DateTime } from 'luxon' +import { BaseModel, column, computed, hasMany } from '@adonisjs/lucid/orm' +import Course from './course.js' +import type { HasMany } from '@adonisjs/lucid/types/relations' + +export default class Meal extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare date: DateTime + + @column() + declare type: 0 | 1 // 0 = lunch, 1 = dinner + + @column() + declare submittable: boolean + + @computed() + get name() { + return this.type === 0 ? 'Déjeuner' : 'Dîner' + } + + @hasMany(() => Course) + declare courses: HasMany +} diff --git a/app/models/meal_registration.ts b/app/models/meal_registration.ts new file mode 100644 index 0000000..a97bd26 --- /dev/null +++ b/app/models/meal_registration.ts @@ -0,0 +1,12 @@ +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class MealRegistration extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare mealId: number + + @column() + declare userId: number +} diff --git a/app/validators/meal.ts b/app/validators/meal.ts new file mode 100644 index 0000000..914b3e0 --- /dev/null +++ b/app/validators/meal.ts @@ -0,0 +1,40 @@ +import vine from '@vinejs/vine' + +const meals = vine.array( + vine.object({ + date: vine.string(), // YYYY-MM-DD + meal_type: vine.string().in(['dinner', 'lunch']), + }) +) + +export const createMealValidator = vine.compile( + vine.union([ + vine.union.if( + (data) => vine.helpers.isObject(data) && 'meals' in data, + vine.object({ + meals, + }) + ), + vine.union.else( + vine.object({ + date: vine.date(), + type: vine.string().in(['Déjeuner', 'Dîner']), + courses: vine + .array( + vine.object({ + name: vine.string().minLength(1).maxLength(255), + description: vine.string().minLength(1), + }) + ) + .minLength(1), + }) + ), + ]) +) + +export const updateMealsRegistrationsValidator = vine.compile( + vine.object({ + userId: vine.number().min(1), + meals, + }) +) diff --git a/database/migrations/1756124105948_create_meals_table.ts b/database/migrations/1756124105948_create_meals_table.ts new file mode 100644 index 0000000..09284c2 --- /dev/null +++ b/database/migrations/1756124105948_create_meals_table.ts @@ -0,0 +1,18 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'meals' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.date('date').notNullable() + table.integer('type').notNullable() // 0 = lunch, 1 = dinner + table.boolean('submittable').defaultTo(false) + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/migrations/1756124135995_create_courses_table.ts b/database/migrations/1756124135995_create_courses_table.ts new file mode 100644 index 0000000..561f494 --- /dev/null +++ b/database/migrations/1756124135995_create_courses_table.ts @@ -0,0 +1,18 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'courses' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.integer('meal_id').unsigned().references('id').inTable('meals').onDelete('CASCADE') + table.string('name').notNullable() + table.text('description').notNullable() + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/migrations/1756125738666_create_meals_registrations_table.ts b/database/migrations/1756125738666_create_meals_registrations_table.ts new file mode 100644 index 0000000..777ed0d --- /dev/null +++ b/database/migrations/1756125738666_create_meals_registrations_table.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'meals_registrations' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.integer('meal_id').unsigned().references('id').inTable('meals').onDelete('CASCADE') + table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE') + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/start/env.ts b/start/env.ts index 7e68b9a..8ffc23b 100644 --- a/start/env.ts +++ b/start/env.ts @@ -74,5 +74,5 @@ export default await Env.create(new URL('../', import.meta.url), { API_BEARER_TOKEN: Env.schema.string(), - MENUS_PATH: Env.schema.string(), + DEFAULT_CLASS_NAME: Env.schema.string(), }) diff --git a/start/routes.ts b/start/routes.ts index a3c3ec1..fe9444c 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -67,20 +67,21 @@ const InternalsController = () => import('#controllers/internals_controller') router .group(() => { router.post('/back-fetch', [InternalsController, 'backFetch']) + router.post('/fetch-meals', [InternalsController, 'fetchMeals']) + router.post('/meals-registrations', [InternalsController, 'updateMealsRegistrations']) }) .prefix('/internals') .use(middleware.internal()) +const MealsController = () => import('#controllers/meals_controller') +router + .group(() => { + router.post('/', [MealsController, 'create']).use(middleware.internal()) + router.get('/', [MealsController, 'index']).use(middleware.auth()) + router.post('/:id', [MealsController, 'registerForMeal']).use(middleware.auth()) + }) + .prefix('/meals') + router.get('/health', async () => { return { status: 'ok' } }) - -// BETA: Serve menus.json file -import fs from 'node:fs/promises' -import env from './env.js' - -router.get('/menus', async () => { - // Return menus.json file - const data = await fs.readFile(env.get('MENUS_PATH'), 'utf-8') - return JSON.parse(data) -})