Skip to main content

Database Schema

The MarsAI platform uses MySQL with Sequelize ORM to manage a comprehensive database schema supporting film submissions, jury voting, events, and user management.

Entity Relationship Overview

Users ──────┬───── Movies ────── Awards
            │         │
            │         ├───── Votes
            │         │
            │         └───── Categories (N:N)
            │         └───── Collaborators (N:N)
            │         └───── Juries (N:N with Users)

            └───── Reservations ───── Events

Votes ────── VoteHistories

Sponsors (independent)
NewsletterSubscribers (independent)

Core Models

User Model

Represents all platform users: producers, jury members, and administrators. File: ~/workspace/source/back/src/models/User.js:1
FieldTypeConstraintsDescription
id_userINTEGERPRIMARY KEY, AUTO_INCREMENTUnique user identifier
first_nameSTRING(100)NOT NULLUser’s first name
last_nameSTRING(100)NOT NULLUser’s last name
emailSTRING(255)NOT NULL, UNIQUELogin email address
passwordSTRING(255)NOT NULLBcrypt hashed password
phoneSTRING(20)NULLLandline phone number
mobileSTRING(20)NULLMobile phone number
birth_dateDATENULLDate of birth
streetSTRING(50)NULLStreet address
postal_codeSTRING(10)NULLPostal/ZIP code
citySTRING(50)NULLCity
countrySTRING(50)NULLCountry
biographySTRING(255)NULLProfessional biography
jobENUMNULLACTOR, DIRECTOR, PRODUCER, WRITER, OTHER
portfolioSTRING(50)NULLPortfolio URL
youtubeSTRING(50)NULLYouTube channel
instagramSTRING(50)NULLInstagram handle
linkedinSTRING(50)NULLLinkedIn profile
facebookSTRING(50)NULLFacebook profile
tiktokSTRING(50)NULLTikTok handle
known_by_mars_aiENUMNULLDiscovery method
roleENUMNOT NULL, DEFAULT: ‘PRODUCER’ADMIN, JURY, PRODUCER
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
export default (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    id_user: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    first_name: {
      type: DataTypes.STRING(100),
      allowNull: false
    },
    last_name: {
      type: DataTypes.STRING(100),
      allowNull: false
    },
    email: {
      type: DataTypes.STRING(255),
      allowNull: false,
      unique: true
    },
    password: {
      type: DataTypes.STRING(255),
      allowNull: false  // Bcrypt hashed
    },
    role: {
      type: DataTypes.ENUM('ADMIN', 'JURY', 'PRODUCER'),
      allowNull: false,
      defaultValue: 'PRODUCER'
    },
    // ... other fields
  }, {
    tableName: 'users'
  });

  User.associate = function(models) {
    User.hasMany(models.Movie, { foreignKey: 'id_user' });
    User.hasMany(models.Vote, { foreignKey: 'id_user' });
    User.belongsToMany(models.Movie, {
      as: 'JuryMovies',
      through: 'movies_juries',
      foreignKey: 'id_user',
      otherKey: 'id_movie'
    });
  };

  return User;
};
Associations:
  • hasMany → Movie (as producer)
  • hasMany → Vote (as jury member)
  • belongsToMany → Movie (through movies_juries, as assigned jury)

Movie Model

Represents films submitted to the festival. File: ~/workspace/source/back/src/models/Movie.js:1
FieldTypeConstraintsDescription
id_movieINTEGERPRIMARY KEY, AUTO_INCREMENTUnique movie identifier
titleSTRINGNOT NULLFilm title
descriptionTEXTNULLFilm description
durationINTEGERNULLDuration in minutes
main_languageSTRINGNULLPrimary language
release_yearINTEGERNULLYear of release
nationalitySTRINGNULLCountry of origin
display_pictureSTRINGNULLMain poster image
picture1STRINGNULLAdditional image 1
picture2STRINGNULLAdditional image 2
picture3STRINGNULLAdditional image 3
trailerSTRINGNULLTrailer video file
youtube_linkSTRING(255)NULLYouTube video URL
productionSTRINGNULLProduction company
workshopSTRING(255)NULLWorkshop name
translationSTRING(255)NULLTranslation language
synopsisTEXTNULLFrench synopsis
synopsis_anglaisTEXTNULLEnglish synopsis
subtitleSTRING(255)NULLSubtitle language
ai_toolSTRING(255)NULLAI tool used in production
thumbnailSTRING(255)NULLVideo thumbnail
admin_commentTEXTNULLAdmin notes
jury_commentTEXTNULLJury candidacy message
selection_statusENUMNOT NULL, DEFAULT: ‘submitted’submitted, assigned, to_discuss, candidate, awarded, refused, selected, finalist
youtube_statusSTRING(255)NULLYouTube upload status
youtube_movie_idSTRING(255)NULLYouTube video ID
id_userINTEGERNOT NULL, FOREIGN KEYProducer who submitted
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
The selection_status field tracks a film through the jury process:
  1. submitted: Initial submission state
  2. assigned: Assigned to jury members for first round voting
  3. to_discuss: Moved to second round discussion
  4. candidate: Selected as award candidate
  5. awarded: Film received an award
  6. refused: Rejected by jury
  7. selected/finalist: Legacy statuses
export default (sequelize, DataTypes) => {
  const Movie = sequelize.define('Movie', {
    id_movie: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    title: {
      type: DataTypes.STRING,
      allowNull: false
    },
    selection_status: {
      type: DataTypes.ENUM(
        'submitted',
        'assigned',
        'to_discuss',
        'candidate',
        'awarded',
        'refused',
        'selected',
        'finalist'
      ),
      allowNull: false,
      defaultValue: 'submitted'
    },
    id_user: {
      type: DataTypes.INTEGER,
      allowNull: false
    }
    // ... other fields
  }, {
    tableName: 'movies',
    timestamps: true
  });

  Movie.associate = function(models) {
    Movie.belongsTo(models.User, {
      as: 'Producer',
      foreignKey: 'id_user'
    });
    Movie.hasMany(models.Award, { foreignKey: 'id_movie' });
    Movie.hasMany(models.Vote, { foreignKey: 'id_movie' });
    Movie.belongsToMany(models.Categorie, {
      through: 'movies_categories',
      foreignKey: 'id_movie',
      otherKey: 'id_categorie'
    });
    Movie.belongsToMany(models.Collaborator, {
      through: 'collaborators_movies',
      foreignKey: 'id_movie',
      otherKey: 'id_collaborator'
    });
    Movie.belongsToMany(models.User, {
      as: 'Juries',
      through: 'movies_juries',
      foreignKey: 'id_movie',
      otherKey: 'id_user'
    });
  };

  return Movie;
};
Associations:
  • belongsTo → User (as Producer)
  • hasMany → Award
  • hasMany → Vote
  • belongsToMany → Categorie (through movies_categories)
  • belongsToMany → Collaborator (through collaborators_movies)
  • belongsToMany → User as Juries (through movies_juries)

Vote Model

Jury votes on submitted films. File: ~/workspace/source/back/src/models/Vote.js:1
FieldTypeConstraintsDescription
id_voteINTEGERPRIMARY KEY, AUTO_INCREMENTUnique vote identifier
id_userINTEGERNOT NULL, FOREIGN KEYJury member
id_movieINTEGERNOT NULL, FOREIGN KEYFilm being voted on
noteENUMNOT NULLYES, NO, TO DISCUSS
commentsTEXTNULLJury comments
modification_countINTEGERDEFAULT: 0Number of vote changes
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
export default (sequelize, DataTypes) => {
  const Vote = sequelize.define('Vote', {
    id_vote: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    note: {
      type: DataTypes.ENUM('YES', 'NO', 'TO DISCUSS'),
      allowNull: false
    },
    modification_count: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      allowNull: false
    }
  }, { tableName: 'votes', timestamps: true });

  Vote.associate = function(models) {
    Vote.belongsTo(models.User, { foreignKey: 'id_user' });
    Vote.belongsTo(models.Movie, { foreignKey: 'id_movie' });
    Vote.hasMany(models.VoteHistory, {
      foreignKey: 'id_vote',
      as: 'history'
    });
  };

  return Vote;
};
Associations:
  • belongsTo → User (jury member)
  • belongsTo → Movie
  • hasMany → VoteHistory
The modification_count field tracks how many times a jury member changed their vote.

Award Model

Awards given to films. File: ~/workspace/source/back/src/models/Award.js:1
FieldTypeConstraintsDescription
id_awardINTEGERPRIMARY KEY, AUTO_INCREMENTUnique award identifier
id_movieINTEGERNOT NULL, FOREIGN KEYAwarded film
award_nameSTRING(100)NULLName of the award
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
export default (sequelize, DataTypes) => {
  const Award = sequelize.define('Award', {
    id_award: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    id_movie: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    award_name: DataTypes.STRING(100)
  }, { tableName: 'awards', timestamps: true });

  Award.associate = function(models) {
    Award.belongsTo(models.Movie, { foreignKey: 'id_movie' });
  };

  return Award;
};

Event Model

Festival events (conferences, workshops, webinars). File: ~/workspace/source/back/src/models/Event.js:1
FieldTypeConstraintsDescription
id_eventINTEGERPRIMARY KEY, AUTO_INCREMENTUnique event identifier
nameSTRING(255)NULLEvent name
descriptionTEXTNULLEvent description
start_dateDATENULLStart date/time
end_dateDATENULLEnd date/time
locationSTRING(255)NULLEvent location
event_typeENUMNULLCONFERENCE, WORKSHOP, MEETUP, WEBINAR
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
export default (sequelize, DataTypes) => {
  const Event = sequelize.define('Event', {
    event_type: DataTypes.ENUM('CONFERENCE', 'WORKSHOP', 'MEETUP', 'WEBINAR'),
    // ... other fields
  }, { tableName: 'events', timestamps: true });

  Event.associate = function(models) {
    Event.hasMany(models.Reservation, { foreignKey: 'id_event' });
  };

  return Event;
};
Associations:
  • hasMany → Reservation

Reservation Model

Event reservations/registrations. File: ~/workspace/source/back/src/models/Reservation.js:1
FieldTypeConstraintsDescription
id_reservationINTEGERPRIMARY KEY, AUTO_INCREMENTUnique reservation identifier
id_eventINTEGERNOT NULL, FOREIGN KEYEvent being reserved
first_nameSTRING(100)NULLAttendee first name
last_nameSTRING(100)NULLAttendee last name
emailSTRING(255)NULLContact email
number_seatsINTEGERNULLNumber of seats reserved
reservation_dateDATENULLReservation timestamp
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
Associations:
  • belongsTo → Event

Categorie Model

Film categories/genres (many-to-many with Movies). File: ~/workspace/source/back/src/models/Categorie.js:1
FieldTypeConstraintsDescription
id_categorieINTEGERPRIMARY KEY, AUTO_INCREMENTUnique category identifier
nameSTRINGNULLCategory name
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
Categorie.associate = function(models) {
  Categorie.belongsToMany(models.Movie, {
    through: 'movies_categories',
    foreignKey: 'id_categorie',
    otherKey: 'id_movie'
  });
};
Join Table: movies_categories

Collaborator Model

Film collaborators/crew members (many-to-many with Movies). File: ~/workspace/source/back/src/models/Collaborator.js:1
FieldTypeConstraintsDescription
id_collaboratorINTEGERPRIMARY KEY, AUTO_INCREMENTUnique collaborator identifier
first_nameSTRING(150)NULLFirst name
last_nameSTRING(150)NULLLast name
emailSTRING(255)NULLEmail address
jobSTRING(100)NULLRole/job title
createdAtTIMESTAMPAUTOCreation timestamp
updatedAtTIMESTAMPAUTOLast update timestamp
Collaborator.associate = function(models) {
  Collaborator.belongsToMany(models.Movie, {
    through: 'collaborators_movies',
    foreignKey: 'id_collaborator',
    otherKey: 'id_movie'
  });
};
Join Table: collaborators_movies

VoteHistory Model

Tracks changes to votes for audit purposes.
FieldTypeConstraintsDescription
id_vote_historyINTEGERPRIMARY KEY, AUTO_INCREMENTUnique history record
id_voteINTEGERNOT NULL, FOREIGN KEYOriginal vote
id_userINTEGERNOT NULLJury member
id_movieINTEGERNOT NULLFilm
noteFLOATNOT NULLVote value
commentsTEXTNULLComments
createdAtTIMESTAMPAUTOWhen vote was modified
Associations:
  • belongsTo → Vote

Festival sponsors. File: ~/workspace/source/back/src/models/Sponsor.js:1
FieldTypeConstraintsDescription
id_sponsorINTEGERPRIMARY KEY, AUTO_INCREMENTUnique sponsor identifier
nameSTRING(255)NOT NULLSponsor name
logoSTRING(255)NOT NULLLogo file path
urlSTRING(255)NULLWebsite URL
categorySTRING(100)NULLSponsor tier/category

NewsletterSubscriber Model

Newsletter email subscriptions.
FieldTypeConstraintsDescription
id_subscriberINTEGERPRIMARY KEY, AUTO_INCREMENTUnique subscriber identifier
emailSTRING(255)NOT NULL, UNIQUEEmail address
subscribed_atTIMESTAMPAUTOSubscription date

Join Tables (Many-to-Many)

movies_categories

Links movies to their categories.
FieldTypeDescription
id_movieINTEGERForeign key to movies
id_categorieINTEGERForeign key to categories

collaborators_movies

Links collaborators to movies they worked on.
FieldTypeDescription
id_collaboratorINTEGERForeign key to collaborators
id_movieINTEGERForeign key to movies

movies_juries

Assigns jury members to specific movies for voting.
FieldTypeDescription
id_userINTEGERForeign key to users (jury)
id_movieINTEGERForeign key to movies
This join table enables the platform to assign specific movies to specific jury members rather than all jury members seeing all movies.

Model Loading System

All models are dynamically loaded in ~/workspace/source/back/src/models/index.js:1:
import fs from "fs";
import path from "path";
import { Sequelize } from "sequelize";
import configFile from "../../config/config.cjs";

const env = process.env.NODE_ENV || "development";
const config = configFile[env];
const db = {};

let sequelize = new Sequelize(
  config.database,
  config.username,
  config.password,
  config
);

// Load all model files dynamically
async function loadModels() {
  const files = fs.readdirSync(__dirname)
    .filter(file => file.slice(-3) === ".js" && file !== "index.js");

  for (const file of files) {
    const modelModule = await import(path.join(__dirname, file));
    const model = modelModule.default || modelModule;
    
    if (typeof model === "function") {
      const definedModel = model(sequelize, Sequelize.DataTypes);
      db[definedModel.name] = definedModel;
    }
  }

  // Initialize associations
  Object.keys(db).forEach((modelName) => {
    if (db[modelName].associate) {
      db[modelName].associate(db);
    }
  });
}

await loadModels();

db.sequelize = sequelize;
db.Sequelize = Sequelize;

export default db;
Models are loaded asynchronously using ES modules, and associations are initialized after all models are defined.

Querying with Associations

Example queries using the relationships:
const movie = await db.Movie.findByPk(1, {
  include: [
    { 
      model: db.User, 
      as: 'Producer',
      attributes: ['first_name', 'last_name', 'email']
    }
  ]
});

Database Best Practices

  1. Always use transactions for multi-table operations
  2. Eager load associations to avoid N+1 query problems
  3. Use indexes on foreign keys and frequently queried fields
  4. Validate data at both model and database levels
  5. Use timestamps for audit trails (createdAt, updatedAt)
  6. Soft deletes can be added via paranoid: true if needed