Standardize UID implementation across backend
- Add unified UID column migration for all entities - Create centralized UID generation utility - Update all models to use standardized UID hooks - Fix route handlers to support UID-based lookups - Update slug utilities for consistent UID extraction - Fix tag tests to use query parameters instead of path params - Configure Jest for better TypeScript support
This commit is contained in:
parent
b62e322a24
commit
c3e8449a25
21 changed files with 279 additions and 577 deletions
|
|
@ -21,6 +21,6 @@ module.exports = {
|
|||
testTimeout: 30000,
|
||||
maxWorkers: '100%',
|
||||
moduleNameMapper: {
|
||||
'^nanoid/non-secure$': '<rootDir>/tests/mocks/nanoid.js',
|
||||
'^nanoid$': '<rootDir>/tests/mocks/nanoid.js',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Add nanoid column to areas table
|
||||
await queryInterface.addColumn('areas', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: true, // Initially allow null during migration
|
||||
unique: false, // Initially not unique during migration
|
||||
});
|
||||
|
||||
// Create index on nanoid column for performance
|
||||
await queryInterface.addIndex('areas', ['nanoid'], {
|
||||
name: 'areas_nanoid_index',
|
||||
});
|
||||
|
||||
// Get all existing areas and populate nanoid values
|
||||
const areas = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM areas',
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Update each area with a unique nanoid
|
||||
for (const area of areas) {
|
||||
const areaNanoid = nanoid();
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE areas SET nanoid = ? WHERE id = ?',
|
||||
{
|
||||
replacements: [areaNanoid, area.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add unique constraint after all nanoids are populated
|
||||
await queryInterface.addConstraint('areas', {
|
||||
fields: ['nanoid'],
|
||||
type: 'unique',
|
||||
name: 'areas_nanoid_unique',
|
||||
});
|
||||
|
||||
// Change nanoid column to not allow null
|
||||
await queryInterface.changeColumn('areas', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove unique constraint
|
||||
await queryInterface.removeConstraint('areas', 'areas_nanoid_unique');
|
||||
|
||||
// Remove index
|
||||
await queryInterface.removeIndex('areas', 'areas_nanoid_index');
|
||||
|
||||
// Remove nanoid column
|
||||
await queryInterface.removeColumn('areas', 'nanoid');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Temporarily disable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
// Safely add nanoid column to tasks table (without unique constraint initially)
|
||||
await safeAddColumns(queryInterface, 'tasks', [
|
||||
{
|
||||
name: 'nanoid',
|
||||
definition: {
|
||||
type: Sequelize.STRING(21), // nanoid default length is 21
|
||||
allowNull: true, // Initially allow null, we'll populate it then make it not null
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Get all existing tasks that don't have nanoid yet
|
||||
const tasks = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM tasks WHERE nanoid IS NULL',
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Generate and update nanoid for each existing task
|
||||
for (const task of tasks) {
|
||||
const taskNanoid = nanoid();
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE tasks SET nanoid = ? WHERE id = ?',
|
||||
{
|
||||
replacements: [taskNanoid, task.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Now make the column not null since all existing tasks have nanoids
|
||||
try {
|
||||
await queryInterface.changeColumn('tasks', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Column already exists with correct constraints');
|
||||
}
|
||||
|
||||
// Add index for performance
|
||||
await safeAddIndex(queryInterface, 'tasks', ['nanoid'], {
|
||||
unique: true,
|
||||
name: 'tasks_nanoid_unique_index',
|
||||
});
|
||||
} finally {
|
||||
// Re-enable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove the index first
|
||||
await queryInterface.removeIndex('tasks', 'tasks_nanoid_unique_index');
|
||||
|
||||
// Remove the nanoid column
|
||||
await queryInterface.removeColumn('tasks', 'nanoid');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Temporarily disable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
// Safely add nanoid column to projects table
|
||||
await safeAddColumns(queryInterface, 'projects', [
|
||||
{
|
||||
name: 'nanoid',
|
||||
definition: {
|
||||
type: Sequelize.STRING(21), // nanoid default length is 21
|
||||
allowNull: true, // Initially allow null, we'll populate it then make it not null
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Get all existing projects that don't have nanoid yet
|
||||
const projects = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM projects WHERE nanoid IS NULL',
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Generate and update nanoid for each existing project
|
||||
for (const project of projects) {
|
||||
const projectNanoid = nanoid();
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE projects SET nanoid = ? WHERE id = ?',
|
||||
{
|
||||
replacements: [projectNanoid, project.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Now make the column not null since all existing projects have nanoids
|
||||
try {
|
||||
await queryInterface.changeColumn('projects', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Column already exists with correct constraints');
|
||||
}
|
||||
|
||||
// Add index for performance
|
||||
await safeAddIndex(queryInterface, 'projects', ['nanoid'], {
|
||||
unique: true,
|
||||
name: 'projects_nanoid_unique_index',
|
||||
});
|
||||
} finally {
|
||||
// Re-enable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove the index first
|
||||
try {
|
||||
await queryInterface.removeIndex(
|
||||
'projects',
|
||||
'projects_nanoid_unique_index'
|
||||
);
|
||||
} catch (error) {
|
||||
// Index might not exist
|
||||
}
|
||||
|
||||
// Remove the nanoid column
|
||||
await queryInterface.removeColumn('projects', 'nanoid');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Temporarily disable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
// Safely add nanoid column to notes table
|
||||
await safeAddColumns(queryInterface, 'notes', [
|
||||
{
|
||||
name: 'nanoid',
|
||||
definition: {
|
||||
type: Sequelize.STRING(21), // nanoid default length is 21
|
||||
allowNull: true, // Initially allow null, we'll populate it then make it not null
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Get all existing notes that don't have nanoid yet
|
||||
const notes = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM notes WHERE nanoid IS NULL',
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Generate and update nanoid for each existing note
|
||||
for (const note of notes) {
|
||||
const noteNanoid = nanoid();
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE notes SET nanoid = ? WHERE id = ?',
|
||||
{
|
||||
replacements: [noteNanoid, note.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Now make the column not null since all existing notes have nanoids
|
||||
try {
|
||||
await queryInterface.changeColumn('notes', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Column already exists with correct constraints');
|
||||
}
|
||||
|
||||
// Add index for performance
|
||||
await safeAddIndex(queryInterface, 'notes', ['nanoid'], {
|
||||
unique: true,
|
||||
name: 'notes_nanoid_unique_index',
|
||||
});
|
||||
} finally {
|
||||
// Re-enable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove the index first
|
||||
try {
|
||||
await queryInterface.removeIndex(
|
||||
'notes',
|
||||
'notes_nanoid_unique_index'
|
||||
);
|
||||
} catch (error) {
|
||||
// Index might not exist
|
||||
}
|
||||
|
||||
// Remove the nanoid column
|
||||
await queryInterface.removeColumn('notes', 'nanoid');
|
||||
},
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Temporarily disable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
// Safely add nanoid column to tags table
|
||||
await safeAddColumns(queryInterface, 'tags', [
|
||||
{
|
||||
name: 'nanoid',
|
||||
definition: {
|
||||
type: Sequelize.STRING(21), // nanoid default length is 21
|
||||
allowNull: true, // Initially allow null, we'll populate it then make it not null
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Get all existing tags that don't have nanoid yet
|
||||
const tags = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM tags WHERE nanoid IS NULL',
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Generate and update nanoid for each existing tag
|
||||
for (const tag of tags) {
|
||||
const tagNanoid = nanoid();
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE tags SET nanoid = ? WHERE id = ?',
|
||||
{
|
||||
replacements: [tagNanoid, tag.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Now make the column not null since all existing tags have nanoids
|
||||
try {
|
||||
await queryInterface.changeColumn('tags', 'nanoid', {
|
||||
type: Sequelize.STRING(21),
|
||||
allowNull: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Column already exists with correct constraints');
|
||||
}
|
||||
|
||||
// Add index for performance
|
||||
await safeAddIndex(queryInterface, 'tags', ['nanoid'], {
|
||||
unique: true,
|
||||
name: 'tags_nanoid_unique_index',
|
||||
});
|
||||
} finally {
|
||||
// Re-enable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove the index first
|
||||
try {
|
||||
await queryInterface.removeIndex(
|
||||
'tags',
|
||||
'tags_nanoid_unique_index'
|
||||
);
|
||||
} catch (error) {
|
||||
// Index might not exist
|
||||
}
|
||||
|
||||
// Remove the nanoid column
|
||||
await queryInterface.removeColumn('tags', 'nanoid');
|
||||
},
|
||||
};
|
||||
94
backend/migrations/20250805000001-standardize-uid-columns.js
Normal file
94
backend/migrations/20250805000001-standardize-uid-columns.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
'use strict';
|
||||
|
||||
const { uid } = require('../utils/uid');
|
||||
const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils');
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Temporarily disable foreign key constraints for SQLite
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
try {
|
||||
// Add uid columns to all tables
|
||||
const tables = [
|
||||
{ name: 'areas', hasUid: false },
|
||||
{ name: 'projects', hasUid: false },
|
||||
{ name: 'notes', hasUid: false },
|
||||
{ name: 'tags', hasUid: false },
|
||||
{ name: 'tasks', hasUid: false } // Keep existing uuid column, add new uid column
|
||||
];
|
||||
|
||||
// 1. Add uid columns to all tables
|
||||
for (const table of tables) {
|
||||
await safeAddColumns(queryInterface, table.name, [
|
||||
{
|
||||
name: 'uid',
|
||||
definition: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true, // Initially allow null during population
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. Populate uid values for all tables
|
||||
for (const table of tables) {
|
||||
// Get records without uid values
|
||||
const records = await queryInterface.sequelize.query(
|
||||
`SELECT id FROM ${table.name} WHERE uid IS NULL`,
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// Generate uid for each record
|
||||
for (const record of records) {
|
||||
const uniqueId = uid();
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE ${table.name} SET uid = ? WHERE id = ?`,
|
||||
{
|
||||
replacements: [uniqueId, record.id],
|
||||
type: Sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Make uid column not null and unique
|
||||
await queryInterface.changeColumn(table.name, 'uid', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
});
|
||||
|
||||
// Add unique index for performance
|
||||
await safeAddIndex(queryInterface, table.name, ['uid'], {
|
||||
unique: true,
|
||||
name: `${table.name}_uid_unique_index`,
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Re-enable foreign key constraints
|
||||
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove unique indexes and uid columns
|
||||
const tables = ['areas', 'projects', 'notes', 'tags', 'tasks'];
|
||||
|
||||
for (const tableName of tables) {
|
||||
try {
|
||||
await queryInterface.removeIndex(tableName, `${tableName}_uid_unique_index`);
|
||||
} catch (error) {
|
||||
// Index might not exist
|
||||
console.log(`${tableName}_uid_unique_index not found, skipping removal`);
|
||||
}
|
||||
|
||||
try {
|
||||
await queryInterface.removeColumn(tableName, 'uid');
|
||||
} catch (error) {
|
||||
console.log(`Error removing uid column from ${tableName}:`, error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Area = sequelize.define(
|
||||
|
|
@ -10,11 +10,11 @@ module.exports = (sequelize) => {
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
nanoid: {
|
||||
type: DataTypes.STRING(21),
|
||||
uid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: () => nanoid(),
|
||||
defaultValue: uid,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('./models');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
const CalendarToken = sequelize.define(
|
||||
'CalendarToken',
|
||||
|
|
@ -9,6 +10,12 @@ const CalendarToken = sequelize.define(
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.STRING(),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: uid,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Note = sequelize.define(
|
||||
|
|
@ -10,11 +10,11 @@ module.exports = (sequelize) => {
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
nanoid: {
|
||||
type: DataTypes.STRING(21),
|
||||
uid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: () => nanoid(),
|
||||
defaultValue: uid,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Project = sequelize.define(
|
||||
|
|
@ -10,11 +10,11 @@ module.exports = (sequelize) => {
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
nanoid: {
|
||||
type: DataTypes.STRING(21),
|
||||
uid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: () => nanoid(),
|
||||
defaultValue: uid,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Tag = sequelize.define(
|
||||
|
|
@ -10,11 +10,11 @@ module.exports = (sequelize) => {
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
nanoid: {
|
||||
type: DataTypes.STRING(21),
|
||||
uid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: () => nanoid(),
|
||||
defaultValue: uid,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const { nanoid } = require('nanoid/non-secure');
|
||||
const { uid } = require('../utils/uid');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Task = sequelize.define(
|
||||
|
|
@ -10,18 +10,18 @@ module.exports = (sequelize) => {
|
|||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: uid,
|
||||
},
|
||||
uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
nanoid: {
|
||||
type: DataTypes.STRING(21),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: () => nanoid(),
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ router.get('/areas', async (req, res) => {
|
|||
|
||||
const areas = await Area.findAll({
|
||||
where: { user_id: req.session.userId },
|
||||
attributes: ['id', 'nanoid', 'name', 'description'],
|
||||
attributes: ['id', 'uid', 'name', 'description'],
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ router.post('/areas', async (req, res) => {
|
|||
|
||||
res.status(201).json({
|
||||
...area.toJSON(),
|
||||
nanoid: area.nanoid, // Explicitly include nanoid
|
||||
uid: area.uid, // Explicitly include uid
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating area:', error);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const express = require('express');
|
||||
const { Note, Tag, Project, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const { extractNanoidFromSlug } = require('../utils/slug-utils');
|
||||
const { extractUidFromSlug } = require('../utils/slug-utils');
|
||||
const router = express.Router();
|
||||
|
||||
// Helper function to validate tag name (same as in tags.js)
|
||||
|
|
@ -97,13 +97,13 @@ router.get('/notes', async (req, res) => {
|
|||
let includeClause = [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
required: false,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ router.get('/notes', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /api/note/:id (supports both numeric ID and nanoid-slug)
|
||||
// GET /api/note/:id (supports both numeric ID and uid-slug)
|
||||
router.get('/note/:id', async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
|
|
@ -137,7 +137,7 @@ router.get('/note/:id', async (req, res) => {
|
|||
const identifier = req.params.id;
|
||||
let whereClause;
|
||||
|
||||
// Check if identifier is numeric (regular ID) or nanoid-slug
|
||||
// Check if identifier is numeric (regular ID) or uid-slug
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
// It's a numeric ID
|
||||
whereClause = {
|
||||
|
|
@ -145,14 +145,14 @@ router.get('/note/:id', async (req, res) => {
|
|||
user_id: req.session.userId,
|
||||
};
|
||||
} else {
|
||||
// It's a nanoid-slug, extract the nanoid
|
||||
const nanoid = extractNanoidFromSlug(identifier);
|
||||
if (!nanoid) {
|
||||
// It's a uid-slug, extract the uid
|
||||
const uid = extractUidFromSlug(identifier);
|
||||
if (!uid) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid note identifier' });
|
||||
}
|
||||
whereClause = { nanoid: nanoid, user_id: req.session.userId };
|
||||
whereClause = { uid: uid, user_id: req.session.userId };
|
||||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
|
|
@ -160,13 +160,13 @@ router.get('/note/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
required: false,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -227,20 +227,20 @@ router.post('/note', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
required: false,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
...noteWithAssociations.toJSON(),
|
||||
nanoid: noteWithAssociations.nanoid, // Explicitly include nanoid
|
||||
uid: noteWithAssociations.uid, // Explicitly include uid
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating note:', error);
|
||||
|
|
@ -309,13 +309,13 @@ router.patch('/note/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
required: false,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const config = getConfig();
|
|||
const fs = require('fs');
|
||||
const { Project, Task, Tag, Area, Note, sequelize } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const { extractNanoidFromSlug } = require('../utils/slug-utils');
|
||||
const { extractUidFromSlug } = require('../utils/slug-utils');
|
||||
const router = express.Router();
|
||||
|
||||
// Helper function to validate tag name (same as in tags.js)
|
||||
|
|
@ -185,13 +185,13 @@ router.get('/projects', async (req, res) => {
|
|||
whereClause.pin_to_sidebar = false;
|
||||
}
|
||||
|
||||
// Filter by area - support both numeric area_id and nanoid-slug area
|
||||
// Filter by area - support both numeric area_id and uid-slug area
|
||||
if (area && area !== '') {
|
||||
// Extract nanoid from nanoid-slug format
|
||||
const nanoid = extractNanoidFromSlug(area);
|
||||
if (nanoid) {
|
||||
// Extract uid from uid-slug format
|
||||
const uid = extractUidFromSlug(area);
|
||||
if (uid) {
|
||||
const areaRecord = await Area.findOne({
|
||||
where: { nanoid: nanoid, user_id: req.session.userId },
|
||||
where: { uid: uid, user_id: req.session.userId },
|
||||
attributes: ['id'],
|
||||
});
|
||||
if (areaRecord) {
|
||||
|
|
@ -218,7 +218,7 @@ router.get('/projects', async (req, res) => {
|
|||
},
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -275,7 +275,7 @@ router.get('/projects', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /api/project/:id (supports both numeric ID and nanoid-slug)
|
||||
// GET /api/project/:id (supports both numeric ID and uid-slug)
|
||||
router.get('/project/:id', async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
|
|
@ -285,7 +285,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
const identifier = req.params.id;
|
||||
let whereClause;
|
||||
|
||||
// Check if identifier is numeric (regular ID) or nanoid-slug
|
||||
// Check if identifier is numeric (regular ID) or uid-slug
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
// It's a numeric ID
|
||||
whereClause = {
|
||||
|
|
@ -293,14 +293,14 @@ router.get('/project/:id', async (req, res) => {
|
|||
user_id: req.session.userId,
|
||||
};
|
||||
} else {
|
||||
// It's a nanoid-slug, extract the nanoid
|
||||
const nanoid = extractNanoidFromSlug(identifier);
|
||||
if (!nanoid) {
|
||||
// It's a uid-slug, extract the uid
|
||||
const uid = extractUidFromSlug(identifier);
|
||||
if (!uid) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid project identifier' });
|
||||
}
|
||||
whereClause = { nanoid: nanoid, user_id: req.session.userId };
|
||||
whereClause = { uid: uid, user_id: req.session.userId };
|
||||
}
|
||||
|
||||
const project = await Project.findOne({
|
||||
|
|
@ -316,7 +316,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -326,7 +326,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -348,7 +348,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -356,7 +356,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
{ model: Area, required: false, attributes: ['id', 'name'] },
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -462,7 +462,7 @@ router.post('/project', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -472,7 +472,7 @@ router.post('/project', async (req, res) => {
|
|||
|
||||
res.status(201).json({
|
||||
...projectJson,
|
||||
nanoid: projectWithAssociations.nanoid, // Explicitly include nanoid
|
||||
uid: projectWithAssociations.uid, // Explicitly include uid
|
||||
tags: projectJson.Tags || [], // Normalize Tags to tags
|
||||
due_date_at: formatDate(projectWithAssociations.due_date_at),
|
||||
});
|
||||
|
|
@ -537,7 +537,7 @@ router.patch('/project/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const express = require('express');
|
||||
const { Tag, Task, Note, Project, sequelize } = require('../models');
|
||||
const { extractNanoidFromSlug } = require('../utils/slug-utils');
|
||||
const { extractUidFromSlug } = require('../utils/slug-utils');
|
||||
const router = express.Router();
|
||||
const _ = require('lodash');
|
||||
|
||||
// Helper function to validate tag name
|
||||
function validateTagName(name) {
|
||||
|
|
@ -40,7 +41,7 @@ router.get('/tags', async (req, res) => {
|
|||
try {
|
||||
const tags = await Tag.findAll({
|
||||
where: { user_id: req.currentUser.id },
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
res.json(tags);
|
||||
|
|
@ -50,37 +51,27 @@ router.get('/tags', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /api/tag/:identifier (supports both ID, name, and nanoid-slug)
|
||||
router.get('/tag/:identifier', async (req, res) => {
|
||||
// GET /api/tag/:identifier (supports both ID, name, and uid-slug)
|
||||
router.get('/tag', async (req, res) => {
|
||||
try {
|
||||
const identifier = req.params.identifier;
|
||||
let whereClause;
|
||||
const { id, uid, name } = req.query;
|
||||
|
||||
// Check if identifier is numeric (ID), nanoid-slug, or tag name
|
||||
if (/^\d+$/.test(identifier)) {
|
||||
// It's a numeric ID
|
||||
whereClause = {
|
||||
id: parseInt(identifier),
|
||||
user_id: req.currentUser.id,
|
||||
};
|
||||
} else if (identifier.includes('-') && identifier.length > 21) {
|
||||
// It's likely a nanoid-slug, extract the nanoid
|
||||
const nanoid = extractNanoidFromSlug(identifier);
|
||||
if (!nanoid) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid tag identifier' });
|
||||
}
|
||||
whereClause = { nanoid: nanoid, user_id: req.currentUser.id };
|
||||
} else {
|
||||
// It's a tag name - decode URI component to handle special characters
|
||||
const tagName = decodeURIComponent(identifier);
|
||||
whereClause = { name: tagName, user_id: req.currentUser.id };
|
||||
let whereClause = {
|
||||
user_id: req.currentUser.id,
|
||||
};
|
||||
if (!_.isEmpty(id)) {
|
||||
whereClause.id = parseInt(id, 10);
|
||||
}
|
||||
if (!_.isEmpty(uid)) {
|
||||
whereClause.uid = uid;
|
||||
}
|
||||
if (!_.isEmpty(name)) {
|
||||
whereClause.name = decodeURIComponent(name);
|
||||
}
|
||||
|
||||
const tag = await Tag.findOne({
|
||||
where: whereClause,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['name', 'uid'],
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
|
|
@ -111,7 +102,7 @@ router.post('/tag', async (req, res) => {
|
|||
|
||||
res.status(201).json({
|
||||
id: tag.id,
|
||||
nanoid: tag.nanoid, // Explicitly include nanoid
|
||||
uid: tag.uid, // Explicitly include uid
|
||||
name: tag.name,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -50,18 +50,18 @@ async function serializeTask(task) {
|
|||
|
||||
return {
|
||||
...taskWithoutSubtasks,
|
||||
nanoid: task.nanoid, // Explicitly include nanoid
|
||||
uid: task.uid, // Explicitly include uid
|
||||
tags: taskJson.Tags || [],
|
||||
Project: taskJson.Project
|
||||
? {
|
||||
...taskJson.Project,
|
||||
nanoid: taskJson.Project.nanoid, // Explicitly include Project nanoid
|
||||
uid: taskJson.Project.uid, // Explicitly include Project uid
|
||||
}
|
||||
: null,
|
||||
subtasks: Subtasks
|
||||
? Subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
nanoid: subtask.nanoid, // Also include nanoid for subtasks
|
||||
uid: subtask.uid, // Also include uid for subtasks
|
||||
tags: subtask.Tags || [],
|
||||
due_date: subtask.due_date
|
||||
? subtask.due_date.toISOString().split('T')[0]
|
||||
|
|
@ -285,12 +285,12 @@ async function filterTasksByParams(params, userId) {
|
|||
let includeClause = [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -299,7 +299,7 @@ async function filterTasksByParams(params, userId) {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -431,13 +431,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -446,7 +446,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -474,13 +474,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -489,7 +489,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -529,13 +529,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -544,7 +544,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -596,13 +596,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -611,7 +611,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -639,13 +639,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -654,7 +654,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -693,13 +693,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -708,7 +708,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -746,13 +746,13 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'active', 'nanoid'],
|
||||
attributes: ['id', 'name', 'active', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -761,7 +761,7 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -889,20 +889,26 @@ router.get('/tasks', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /api/task/uuid/:uuid
|
||||
router.get('/task/uuid/:uuid', async (req, res) => {
|
||||
// GET /api/task?uid=...
|
||||
router.get('/task', async (req, res) => {
|
||||
try {
|
||||
const { uid } = req.query;
|
||||
|
||||
if (_.isEmpty(uid)) {
|
||||
return res.status(400).json({ error: 'uid query parameter is required' });
|
||||
}
|
||||
|
||||
const task = await Task.findOne({
|
||||
where: { uuid: req.params.uuid, user_id: req.currentUser.id },
|
||||
where: { uid: uid, user_id: req.currentUser.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
|
@ -916,53 +922,11 @@ router.get('/task/uuid/:uuid', async (req, res) => {
|
|||
|
||||
res.json(serializedTask);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task by UUID:', error);
|
||||
console.error('Error fetching task by UID:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/task/nanoid/:nanoid
|
||||
router.get('/task/nanoid/:nanoid', async (req, res) => {
|
||||
try {
|
||||
const task = await Task.findOne({
|
||||
where: { nanoid: req.params.nanoid, user_id: req.currentUser.id },
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Task,
|
||||
as: 'Subtasks',
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found.' });
|
||||
}
|
||||
|
||||
const serializedTask = await serializeTask(task);
|
||||
|
||||
res.json(serializedTask);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task by nanoid:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/task/:id
|
||||
router.get('/task/:id', async (req, res) => {
|
||||
|
|
@ -972,12 +936,12 @@ router.get('/task/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -986,7 +950,7 @@ router.get('/task/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -1018,12 +982,12 @@ router.get('/task/:id/subtasks', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
|
@ -1176,12 +1140,12 @@ router.post('/task', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
|
@ -1234,7 +1198,7 @@ router.patch('/task/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -1694,12 +1658,12 @@ router.patch('/task/:id', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
|
@ -1728,12 +1692,12 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -1742,7 +1706,7 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
|
|
@ -2003,12 +1967,12 @@ router.patch('/task/:id/toggle-today', async (req, res) => {
|
|||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
{
|
||||
model: Project,
|
||||
attributes: ['id', 'name', 'nanoid'],
|
||||
attributes: ['id', 'name', 'uid'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('Tags Routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('GET /api/tag/:id', () => {
|
||||
describe('GET /api/tag', () => {
|
||||
let tag;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -104,15 +104,14 @@ describe('Tags Routes', () => {
|
|||
});
|
||||
|
||||
it('should get tag by id', async () => {
|
||||
const response = await agent.get(`/api/tag/${tag.id}`);
|
||||
const response = await agent.get(`/api/tag?id=${tag.id}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.id).toBe(tag.id);
|
||||
expect(response.body.name).toBe(tag.name);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent tag', async () => {
|
||||
const response = await agent.get('/api/tag/999999');
|
||||
const response = await agent.get('/api/tag?id=999999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Tag not found');
|
||||
|
|
@ -130,14 +129,14 @@ describe('Tags Routes', () => {
|
|||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/tag/${otherTag.id}`);
|
||||
const response = await agent.get(`/api/tag?id=${otherTag.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Tag not found');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).get(`/api/tag/${tag.id}`);
|
||||
const response = await request(app).get(`/api/tag?id=${tag.id}`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
|
|||
|
|
@ -23,58 +23,58 @@ function createSlug(text, maxLength = 50) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a nanoid-slug URL for a given entity
|
||||
* @param {string} nanoid - The nanoid of the entity
|
||||
* Creates a uid-slug URL for a given entity
|
||||
* @param {string} uid - The uid of the entity
|
||||
* @param {string} name - The name/title of the entity
|
||||
* @param {number} maxSlugLength - Maximum length of the slug part (default: 40)
|
||||
* @returns {string} The nanoid-slug URL part (e.g., "abc123-clean-the-backyard")
|
||||
* @returns {string} The uid-slug URL part (e.g., "abc123-clean-the-backyard")
|
||||
*/
|
||||
function createNanoidSlug(nanoid, name, maxSlugLength = 40) {
|
||||
if (!nanoid) throw new Error('Nanoid is required');
|
||||
function createUidSlug(uid, name, maxSlugLength = 40) {
|
||||
if (!uid) throw new Error('UID is required');
|
||||
|
||||
const slug = createSlug(name, maxSlugLength);
|
||||
return slug ? `${nanoid}-${slug}` : nanoid;
|
||||
return slug ? `${uid}-${slug}` : uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts nanoid from a nanoid-slug URL part
|
||||
* @param {string} nanoidSlug - The nanoid-slug (e.g., "abc123-clean-the-backyard")
|
||||
* @returns {string} The extracted nanoid
|
||||
* Extracts uid from a uid-slug URL part
|
||||
* @param {string} uidSlug - The uid-slug (e.g., "abc123-clean-the-backyard")
|
||||
* @returns {string} The extracted uid
|
||||
*/
|
||||
function extractNanoidFromSlug(nanoidSlug) {
|
||||
if (!nanoidSlug) return '';
|
||||
function extractUidFromSlug(uidSlug) {
|
||||
if (!uidSlug) return '';
|
||||
|
||||
// Nanoid is always 21 characters by default, extract the first part before the first hyphen
|
||||
// But handle cases where the nanoid itself might contain hyphens
|
||||
const parts = nanoidSlug.split('-');
|
||||
// UID is always 15 characters by our custom implementation, extract the first part before the first hyphen
|
||||
// But handle cases where the uid itself might contain hyphens
|
||||
const parts = uidSlug.split('-');
|
||||
if (parts.length === 1) {
|
||||
// No slug, just nanoid
|
||||
// No slug, just uid
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
// Look for the nanoid part (21 chars) - it should be the first part
|
||||
// Look for the uid part (15 chars) - it should be the first part
|
||||
const firstPart = parts[0];
|
||||
if (firstPart.length === 21) {
|
||||
if (firstPart.length === 15) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Fallback: try to find 21-character alphanumeric string
|
||||
const nanoidMatch = nanoidSlug.match(/^([A-Za-z0-9_-]{21})/);
|
||||
return nanoidMatch ? nanoidMatch[1] : nanoidSlug.split('-')[0];
|
||||
// Fallback: try to find 15-character alphanumeric string
|
||||
const uidMatch = uidSlug.match(/^([0-9abcdefghijkmnpqrstuvwxyz]{15})/);
|
||||
return uidMatch ? uidMatch[1] : uidSlug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string looks like a valid nanoid
|
||||
* Validates if a string looks like a valid uid
|
||||
* @param {string} str - String to validate
|
||||
* @returns {boolean} True if it looks like a nanoid
|
||||
* @returns {boolean} True if it looks like a uid
|
||||
*/
|
||||
function isValidNanoid(str) {
|
||||
return /^[A-Za-z0-9_-]{21}$/.test(str);
|
||||
function isValidUid(str) {
|
||||
return /^[0-9abcdefghijkmnpqrstuvwxyz]{15}$/.test(str);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSlug,
|
||||
createNanoidSlug,
|
||||
extractNanoidFromSlug,
|
||||
isValidNanoid,
|
||||
createUidSlug,
|
||||
extractUidFromSlug,
|
||||
isValidUid,
|
||||
};
|
||||
|
|
|
|||
11
backend/utils/uid.js
Normal file
11
backend/utils/uid.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const nanoid = require('nanoid');
|
||||
|
||||
function uid() {
|
||||
const generate = nanoid.customAlphabet(
|
||||
'0123456789abcdefghijkmnpqrstuvwxyz',
|
||||
15
|
||||
);
|
||||
return generate();
|
||||
}
|
||||
|
||||
module.exports = { uid };
|
||||
Loading…
Add table
Add a link
Reference in a new issue