mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
const excludeAttributes = [];
|
||||
|
||||
const adminExclude = [
|
||||
...excludeAttributes,
|
||||
// admins can see audit fields, so nothing extra excluded
|
||||
];
|
||||
|
||||
const userExclude = [
|
||||
...excludeAttributes,
|
||||
// regular users cannot see audit trails
|
||||
"createdBy", "updatedBy", "deletedBy", "deletedAt",
|
||||
];
|
||||
|
||||
const jsonbSchemas = {};
|
||||
|
||||
const computedAttributes = [];
|
||||
|
||||
const filterableFields = {}
|
||||
|
||||
module.exports = { adminExclude, userExclude, excludeAttributes, jsonbSchemas, computedAttributes, filterableFields };
|
||||
@@ -0,0 +1,123 @@
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("../../config/db.config");
|
||||
const { mdl_UserGroups } = require('../users/user_groups.mdl');
|
||||
|
||||
const TaskList = sequelize.define('TaskList', {
|
||||
task_list_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
||||
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, filterable: true },
|
||||
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
updatedBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
deletedBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
}, {
|
||||
tableName: 'task_lists',
|
||||
paranoid: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// ─── Junction: TaskList ↔ UserGroups ──────────────────────────────────────────
|
||||
const TaskListGroup = sequelize.define('TaskListGroup', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, onDelete: 'CASCADE', },
|
||||
group_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: 'user_groups', key: 'group_id' }, onDelete: 'CASCADE', },
|
||||
assignedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, allowNull: false, filterable: true },
|
||||
assignedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||
}, {
|
||||
tableName: 'task_list_groups',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['task_list_id', 'group_id'], name: 'uq_task_list_group' },
|
||||
{ fields: ['task_list_id'], name: 'idx_tlg_task_list_id' },
|
||||
{ fields: ['group_id'], name: 'idx_tlg_group_id' },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const Task = sequelize.define('Task', {
|
||||
task_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
||||
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, hidden: true, filterable: false },
|
||||
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
|
||||
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
||||
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
|
||||
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, order: 2.1, filterable: true, comment: 'Position within the task list — display order only. Locking is driven solely by explicit task_prerequisites.' },
|
||||
accepts_submissions: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, order: 2.25, filterable: true, comment: 'When false, no new TaskCompletion submissions are accepted for this task (existing completions are unaffected).' },
|
||||
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
|
||||
auto_marked_at: { type: DataTypes.DATE, allowNull: true, filterable: true, comment: 'Set only by the taskOverdue cron sweep when it auto-flips status; never touched by user-driven completion.' },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
updatedBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
deletedBy: { type: DataTypes.INTEGER, allowNull: true, filterable: true },
|
||||
}, {
|
||||
tableName: 'tasks',
|
||||
paranoid: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
const TaskRequirement = sequelize.define('TaskRequirement', {
|
||||
requirement_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' } },
|
||||
type: { type: DataTypes.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson', 'submit_text'), allowNull: false, filterable: true },
|
||||
|
||||
// ── visit_link ──────────────────────────────────────────────────────────
|
||||
link_url: { type: DataTypes.STRING, allowNull: true, filterable: false },
|
||||
link_label: { type: DataTypes.STRING, allowNull: true, filterable: false },
|
||||
|
||||
// ── upload_file ─────────────────────────────────────────────────────────
|
||||
allowed_file_types: { type: DataTypes.JSONB, allowNull: true, filterable: false },
|
||||
max_file_count: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1, filterable: false },
|
||||
|
||||
// ── read_course / read_unit / read_lesson ──────────────────────────────────
|
||||
reference_id: { type: DataTypes.UUID, allowNull: true, comment: 'course_id | unit_id | lesson_id depending on type', filterable: false },
|
||||
reference_label: { type: DataTypes.STRING, allowNull: true, comment: 'Cached display name so we do not always join', filterable: false },
|
||||
|
||||
// ── submit_text ─────────────────────────────────────────────────────────
|
||||
prompt: { type: DataTypes.TEXT, allowNull: true, comment: 'Instructions shown above the free-text response box.', filterable: false },
|
||||
|
||||
// ── upload_file / submit_text ──────────────────────────────────────────
|
||||
requires_review: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, filterable: true, comment: 'If true, a submission only counts as complete once an admin approves it.' },
|
||||
|
||||
order: { type: DataTypes.INTEGER, defaultValue: 0, filterable: true },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.INTEGER, allowNull: true },
|
||||
updatedBy: { type: DataTypes.INTEGER, allowNull: true },
|
||||
deletedBy: { type: DataTypes.INTEGER, allowNull: true },
|
||||
}, {
|
||||
tableName: 'task_requirements',
|
||||
paranoid: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// ─── Junction: Task ↔ Task (explicit prerequisite graph) ──────────────────────
|
||||
const TaskPrerequisite = sequelize.define('TaskPrerequisite', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE', },
|
||||
prerequisite_task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE', },
|
||||
}, {
|
||||
tableName: 'task_prerequisites',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['task_id', 'prerequisite_task_id'], name: 'uq_task_prerequisite' },
|
||||
{ fields: ['task_id'], name: 'idx_tp_task_id' },
|
||||
{ fields: ['prerequisite_task_id'], name: 'idx_tp_prerequisite_task_id' },
|
||||
],
|
||||
});
|
||||
|
||||
TaskList.hasMany(Task, { foreignKey: 'task_list_id', as: 'tasks' });
|
||||
Task.belongsTo(TaskList, { foreignKey: 'task_list_id', as: 'taskList' });
|
||||
Task.hasMany(TaskRequirement, { foreignKey: 'task_id', as: 'requirements' });
|
||||
TaskRequirement.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
||||
|
||||
// TaskList ↔ UserGroups (many-to-many through TaskListGroup)
|
||||
TaskList.belongsToMany(mdl_UserGroups, { through: TaskListGroup, foreignKey: 'task_list_id', otherKey: 'group_id', as: 'groups', });
|
||||
mdl_UserGroups.belongsToMany(TaskList, { through: TaskListGroup, foreignKey: 'group_id', otherKey: 'task_list_id', as: 'taskLists', });
|
||||
|
||||
// Task ↔ Task (self-referential many-to-many through TaskPrerequisite)
|
||||
// 'prerequisites' — tasks THIS task depends on. 'dependents' — tasks that depend on THIS task.
|
||||
Task.belongsToMany(Task, { through: TaskPrerequisite, foreignKey: 'task_id', otherKey: 'prerequisite_task_id', as: 'prerequisites' });
|
||||
Task.belongsToMany(Task, { through: TaskPrerequisite, foreignKey: 'prerequisite_task_id', otherKey: 'task_id', as: 'dependents' });
|
||||
|
||||
module.exports = { Task, TaskList, TaskRequirement, TaskListGroup, TaskPrerequisite, mdl_UserGroups };
|
||||
@@ -0,0 +1,40 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: task_completion.attributes.js
|
||||
* Type of Program: Attributes / Field Config
|
||||
* Description: Attribute exclusion lists for TaskCompletion and TaskCompletionFile.
|
||||
* Admins see audit fields; clients see only their own completion data.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 13, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
|
||||
// ─── Shared base exclusions (neither role needs these) ────────────────────────
|
||||
const excludeAttributes = [];
|
||||
|
||||
// ─── Admin: can see audit trails ──────────────────────────────────────────────
|
||||
const adminExclude = [
|
||||
...excludeAttributes,
|
||||
];
|
||||
|
||||
// ─── Client: no audit trails ──────────────────────────────────────────────────
|
||||
const clientExclude = [
|
||||
...excludeAttributes,
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'deletedBy',
|
||||
'deletedAt',
|
||||
'storage_key',
|
||||
];
|
||||
|
||||
const jsonbSchemas = {};
|
||||
const computedAttributes = [];
|
||||
const filterableFields = {};
|
||||
|
||||
module.exports = {
|
||||
adminExclude,
|
||||
clientExclude,
|
||||
excludeAttributes,
|
||||
jsonbSchemas,
|
||||
computedAttributes,
|
||||
filterableFields,
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: task_completion.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize models for `task_completions` and `task_completion_files`.
|
||||
* A completion is created when a user turns in work for a task.
|
||||
* Multiple completions are allowed (resubmit anytime).
|
||||
* Files are stored separately — one completion can have many files.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 13, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('../users/users.mdl');
|
||||
const { Task } = require('./task.mdl');
|
||||
|
||||
// ─── TaskCompletion ────────────────────────────────────────────────────────────
|
||||
const TaskCompletion = sequelize.define('TaskCompletion', {
|
||||
completion_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
order: 3,
|
||||
},
|
||||
task_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'tasks', key: 'task_id' },
|
||||
onDelete: 'CASCADE',
|
||||
order: 4,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: 'users', key: 'user_id' },
|
||||
onDelete: 'CASCADE',
|
||||
order: 5,
|
||||
},
|
||||
note: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: 'Optional note the user attaches when submitting.',
|
||||
order: 1,
|
||||
},
|
||||
response_text: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: 'The learner\'s free-text answer, for submit_text requirements.',
|
||||
order: 1.5,
|
||||
},
|
||||
submitted_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
allowNull: false,
|
||||
order: 2,
|
||||
},
|
||||
|
||||
// ── Review workflow (requires_review requirements only) ──────────────────
|
||||
status: {
|
||||
type: DataTypes.ENUM('submitted', 'approved', 'rejected'),
|
||||
allowNull: false,
|
||||
defaultValue: 'submitted',
|
||||
order: 2.1,
|
||||
filterable: true,
|
||||
},
|
||||
reviewed_by: { type: DataTypes.BIGINT, allowNull: true, order: 2.2 },
|
||||
reviewed_at: { type: DataTypes.DATE, allowNull: true, order: 2.3 },
|
||||
review_note: { type: DataTypes.TEXT, allowNull: true, comment: 'Optional note the admin leaves when approving/rejecting.', order: 2.4 },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, order: 6 },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, order: 7 },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, order: 8 },
|
||||
}, {
|
||||
tableName: 'task_completions',
|
||||
paranoid: true,
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['task_id'], name: 'idx_tc_task_id' },
|
||||
{ fields: ['user_id'], name: 'idx_tc_user_id' },
|
||||
{ fields: ['task_id', 'user_id'], name: 'idx_tc_task_user' },
|
||||
],
|
||||
});
|
||||
|
||||
// ─── TaskCompletionFile ────────────────────────────────────────────────────────
|
||||
const TaskCompletionFile = sequelize.define('TaskCompletionFile', {
|
||||
file_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
completion_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'task_completions', key: 'completion_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
file_url: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: 'Storage path or CDN URL of the uploaded file.',
|
||||
},
|
||||
file_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: 'Original filename as uploaded by the user.',
|
||||
},
|
||||
file_size: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'File size in bytes.',
|
||||
},
|
||||
mime_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: 'MIME type e.g. application/pdf, image/png.',
|
||||
},
|
||||
storage_key: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: 'S3 object key (e.g. "images/uuid.jpg"), used by the download proxy endpoint.',
|
||||
},
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
}, {
|
||||
tableName: 'task_completion_files',
|
||||
paranoid: true,
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['completion_id'], name: 'idx_tcf_completion_id' },
|
||||
],
|
||||
});
|
||||
|
||||
// ─── Associations ──────────────────────────────────────────────────────────────
|
||||
Task.hasMany(TaskCompletion, { foreignKey: 'task_id', as: 'completions' });
|
||||
TaskCompletion.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
||||
|
||||
mdl_Users.hasMany(TaskCompletion, { foreignKey: 'user_id', as: 'taskCompletions' });
|
||||
TaskCompletion.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
|
||||
|
||||
TaskCompletion.hasMany(TaskCompletionFile, { foreignKey: 'completion_id', as: 'files' });
|
||||
TaskCompletionFile.belongsTo(TaskCompletion, { foreignKey: 'completion_id', as: 'completion' });
|
||||
|
||||
module.exports = { TaskCompletion, TaskCompletionFile };
|
||||
@@ -0,0 +1,158 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: task_progress.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize models for tracking user progress on task requirements.
|
||||
*
|
||||
* TaskLinkVisit — tracks when a user visits a visit_link requirement.
|
||||
* UPSERT key: (requirement_id, user_id)
|
||||
* Revisiting updates visited_at.
|
||||
*
|
||||
* TaskProgress — tracks completion of read_course / read_unit / read_lesson.
|
||||
* UPSERT key: (requirement_id, user_id, reference_id)
|
||||
* Lessons: completed flipped directly by client.
|
||||
* Units/Courses: completed derived from child completion.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 13, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('../users/users.mdl');
|
||||
const { Task, TaskRequirement } = require('./task.mdl');
|
||||
|
||||
// ─── TaskLinkVisit ─────────────────────────────────────────────────────────────
|
||||
const TaskLinkVisit = sequelize.define('TaskLinkVisit', {
|
||||
visit_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
task_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'tasks', key: 'task_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
requirement_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'task_requirements', key: 'requirement_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: 'users', key: 'user_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
visited_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
allowNull: false,
|
||||
comment: 'Updated on every revisit via UPSERT.',
|
||||
},
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
}, {
|
||||
tableName: 'task_link_visits',
|
||||
timestamps: true,
|
||||
paranoid: false, // visits are never soft-deleted
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['requirement_id', 'user_id'],
|
||||
name: 'uq_tlv_requirement_user',
|
||||
},
|
||||
{ fields: ['task_id'], name: 'idx_tlv_task_id' },
|
||||
{ fields: ['user_id'], name: 'idx_tlv_user_id' },
|
||||
{ fields: ['requirement_id'], name: 'idx_tlv_requirement_id' },
|
||||
],
|
||||
});
|
||||
|
||||
// ─── TaskProgress ──────────────────────────────────────────────────────────────
|
||||
const TaskProgress = sequelize.define('TaskProgress', {
|
||||
progress_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
task_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'tasks', key: 'task_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
requirement_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: { model: 'task_requirements', key: 'requirement_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: 'users', key: 'user_id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
reference_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
comment: 'course_id | unit_id | lesson_id depending on type.',
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('read_course', 'read_unit', 'read_lesson'),
|
||||
allowNull: false,
|
||||
},
|
||||
completed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
},
|
||||
completed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: 'Set when completed flips true. Cleared when it reverts to false.',
|
||||
},
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
|
||||
}, {
|
||||
tableName: 'task_progress',
|
||||
timestamps: true,
|
||||
paranoid: false, // progress rows are never soft-deleted
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['requirement_id', 'user_id', 'reference_id'],
|
||||
name: 'uq_tp_requirement_user_reference',
|
||||
},
|
||||
{ fields: ['task_id'], name: 'idx_tp_task_id' },
|
||||
{ fields: ['user_id'], name: 'idx_tp_user_id' },
|
||||
{ fields: ['requirement_id'], name: 'idx_tp_requirement_id' },
|
||||
{ fields: ['type'], name: 'idx_tp_type' },
|
||||
],
|
||||
});
|
||||
|
||||
// ─── Associations ──────────────────────────────────────────────────────────────
|
||||
Task.hasMany(TaskLinkVisit, { foreignKey: 'task_id', as: 'linkVisits' });
|
||||
TaskLinkVisit.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
||||
|
||||
TaskRequirement.hasMany(TaskLinkVisit, { foreignKey: 'requirement_id', as: 'visits' });
|
||||
TaskLinkVisit.belongsTo(TaskRequirement, { foreignKey: 'requirement_id', as: 'requirement' });
|
||||
|
||||
mdl_Users.hasMany(TaskLinkVisit, { foreignKey: 'user_id', as: 'linkVisits' });
|
||||
TaskLinkVisit.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
|
||||
|
||||
Task.hasMany(TaskProgress, { foreignKey: 'task_id', as: 'progress' });
|
||||
TaskProgress.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
||||
|
||||
TaskRequirement.hasMany(TaskProgress, { foreignKey: 'requirement_id', as: 'progress' });
|
||||
TaskProgress.belongsTo(TaskRequirement, { foreignKey: 'requirement_id', as: 'requirement' });
|
||||
|
||||
mdl_Users.hasMany(TaskProgress, { foreignKey: 'user_id', as: 'taskProgress' });
|
||||
TaskProgress.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
|
||||
|
||||
module.exports = { TaskLinkVisit, TaskProgress };
|
||||
Reference in New Issue
Block a user