feat: menambahkan kolom slug dan implement slug di module project
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[slug]` on the table `project` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- A unique constraint covering the columns `[id_project,slug]` on the table `task` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `slug` to the `project` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `slug` to the `task` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `project` ADD COLUMN `slug` VARCHAR(255) NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `task` ADD COLUMN `slug` VARCHAR(255) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `project_slug_key` ON `project`(`slug`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `task_slug_idx` ON `task`(`slug`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `task_id_project_slug_key` ON `task`(`id_project`, `slug`);
|
||||||
@@ -205,6 +205,7 @@ model User {
|
|||||||
model Project {
|
model Project {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
id_admin Int //mentor project
|
id_admin Int //mentor project
|
||||||
|
slug String @unique @db.VarChar(255)
|
||||||
project_icon ProjectIcon @default(code)
|
project_icon ProjectIcon @default(code)
|
||||||
project_name String @db.VarChar(255)
|
project_name String @db.VarChar(255)
|
||||||
description String @db.Text
|
description String @db.Text
|
||||||
@@ -245,6 +246,7 @@ model ProjectMember {
|
|||||||
model Task {
|
model Task {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
id_project Int
|
id_project Int
|
||||||
|
slug String @db.VarChar(255)
|
||||||
title String @db.VarChar(255)
|
title String @db.VarChar(255)
|
||||||
description String @db.Text
|
description String @db.Text
|
||||||
deadline_at DateTime
|
deadline_at DateTime
|
||||||
@@ -255,8 +257,10 @@ model Task {
|
|||||||
project Project @relation(fields: [id_project], references: [id], map: "task_id_project_foreign")
|
project Project @relation(fields: [id_project], references: [id], map: "task_id_project_foreign")
|
||||||
submissions TaskSubmission[]
|
submissions TaskSubmission[]
|
||||||
|
|
||||||
|
@@unique([id_project, slug])
|
||||||
@@index([id_project], map: "task_id_project_foreign")
|
@@index([id_project], map: "task_id_project_foreign")
|
||||||
@@index([deadline_at])
|
@@index([deadline_at])
|
||||||
|
@@index([slug])
|
||||||
@@map("task")
|
@@map("task")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -151,13 +151,13 @@ async function main() {
|
|||||||
|
|
||||||
// 10. Project
|
// 10. Project
|
||||||
const proj1 = await prisma.project.create({
|
const proj1 = await prisma.project.create({
|
||||||
data: { id_admin: admin1.id, project_icon: 'code', project_name: 'Internify Platform Dev', description: 'Upgrading the core Internify platform backend and web client.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
data: { id_admin: admin1.id, project_icon: 'code', project_name: 'Internify Platform Dev', slug: 'internify-platform-dev', description: 'Upgrading the core Internify platform backend and web client.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
||||||
});
|
});
|
||||||
const proj2 = await prisma.project.create({
|
const proj2 = await prisma.project.create({
|
||||||
data: { id_admin: admin2.id, project_icon: 'shield', project_name: 'Auth Gateway Module', description: 'Securing API gateway with unified OAuth2 authentication.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
data: { id_admin: admin2.id, project_icon: 'shield', project_name: 'Auth Gateway Module', slug: 'auth-gateway-module', description: 'Securing API gateway with unified OAuth2 authentication.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
||||||
});
|
});
|
||||||
const proj3 = await prisma.project.create({
|
const proj3 = await prisma.project.create({
|
||||||
data: { id_admin: admin1.id, project_icon: 'chart', project_name: 'Recruitment Analytics Dashboard', description: 'Developing analytical charts for recruitment metrics.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
data: { id_admin: admin1.id, project_icon: 'chart', project_name: 'Recruitment Analytics Dashboard', slug: 'recruitment-analytics-dashboard', description: 'Developing analytical charts for recruitment metrics.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), status: 'active' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 11. LamaranMagang
|
// 11. LamaranMagang
|
||||||
@@ -173,13 +173,13 @@ async function main() {
|
|||||||
|
|
||||||
// 12. Task
|
// 12. Task
|
||||||
const task1 = await prisma.task.create({
|
const task1 = await prisma.task.create({
|
||||||
data: { id_project: proj1.id, title: 'Database Setup', description: 'Setup database schema and write initial seed script.', deadline_at: new Date('2026-08-01'), submission_type: 'url_link' }
|
data: { id_project: proj1.id, title: 'Database Setup', slug: 'database-setup', description: 'Setup database schema and write initial seed script.', deadline_at: new Date('2026-08-01'), submission_type: 'url_link' }
|
||||||
});
|
});
|
||||||
const task2 = await prisma.task.create({
|
const task2 = await prisma.task.create({
|
||||||
data: { id_project: proj1.id, title: 'API Specification', description: 'Draft comprehensive swagger API document.', deadline_at: new Date('2026-08-15'), submission_type: 'file_upload' }
|
data: { id_project: proj1.id, title: 'API Specification', slug: 'api-specification', description: 'Draft comprehensive swagger API document.', deadline_at: new Date('2026-08-15'), submission_type: 'file_upload' }
|
||||||
});
|
});
|
||||||
const task3 = await prisma.task.create({
|
const task3 = await prisma.task.create({
|
||||||
data: { id_project: proj2.id, title: 'Implement JWT Validation', description: 'Setup middleware and keys for JWT validation.', deadline_at: new Date('2026-08-10'), submission_type: 'url_link' }
|
data: { id_project: proj2.id, title: 'Implement JWT Validation', slug: 'implement-jwt-validation', description: 'Setup middleware and keys for JWT validation.', deadline_at: new Date('2026-08-10'), submission_type: 'url_link' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 13. User
|
// 13. User
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
function slugify(value, fallback = 'item') {
|
||||||
|
if (!value || typeof value !== 'string') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
let slug = value
|
||||||
|
.toString()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
slug = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+$/.test(slug)) {
|
||||||
|
slug = `${fallback}-${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
slugify,
|
||||||
|
};
|
||||||
@@ -420,6 +420,44 @@ class ProjectRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findBySlug(slug) {
|
||||||
|
return prisma.project.findUnique({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
},
|
||||||
|
include: this.projectDetailInclude(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByIdOrSlug(identifier) {
|
||||||
|
if (/^\d+$/.test(String(identifier))) {
|
||||||
|
return this.findById(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findBySlug(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSlugExists(slug, excludeProjectId = null) {
|
||||||
|
const where = {
|
||||||
|
slug,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (excludeProjectId) {
|
||||||
|
where.id = {
|
||||||
|
not: parseInt(excludeProjectId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await prisma.project.findFirst({
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(project);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ProjectRepository();
|
module.exports = new ProjectRepository();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
} = require("../../../helpers/error");
|
} = require("../../../helpers/error");
|
||||||
|
const { slugify } = require("../../../helpers/utils/slug");
|
||||||
|
|
||||||
class ProjectService {
|
class ProjectService {
|
||||||
async createProject(projectPayload, actor) {
|
async createProject(projectPayload, actor) {
|
||||||
@@ -72,8 +73,11 @@ class ProjectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectSlug = await this.generateUniqueProjectSlug(projectData.project_name);
|
||||||
|
|
||||||
const newProjectData = {
|
const newProjectData = {
|
||||||
id_admin: parseInt(actor.id),
|
id_admin: parseInt(actor.id),
|
||||||
|
slug: projectSlug,
|
||||||
project_icon: projectData.project_icon,
|
project_icon: projectData.project_icon,
|
||||||
project_name: projectData.project_name,
|
project_name: projectData.project_name,
|
||||||
description: projectData.description || "",
|
description: projectData.description || "",
|
||||||
@@ -110,6 +114,7 @@ class ProjectService {
|
|||||||
|
|
||||||
const mappedProjects = projects.map((project) => ({
|
const mappedProjects = projects.map((project) => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
|
slug: project.slug,
|
||||||
project_icon: project.project_icon,
|
project_icon: project.project_icon,
|
||||||
project_name: project.project_name,
|
project_name: project.project_name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
@@ -131,7 +136,7 @@ class ProjectService {
|
|||||||
|
|
||||||
async getProjectById(id, actor = {}) {
|
async getProjectById(id, actor = {}) {
|
||||||
try {
|
try {
|
||||||
const project = await projectRepository.findById(id);
|
const project = await projectRepository.findByIdOrSlug(id);
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return error(new NotFoundError("Project not found"));
|
return error(new NotFoundError("Project not found"));
|
||||||
@@ -160,7 +165,7 @@ class ProjectService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingProject = await projectRepository.findById(id);
|
const existingProject = await projectRepository.findByIdOrSlug(id);
|
||||||
|
|
||||||
if (!existingProject) {
|
if (!existingProject) {
|
||||||
return error(new NotFoundError("Project not found"));
|
return error(new NotFoundError("Project not found"));
|
||||||
@@ -193,7 +198,17 @@ class ProjectService {
|
|||||||
updateData.description = "";
|
updateData.description = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedProject = await projectRepository.updateById(id, updateData);
|
if (
|
||||||
|
updateData.project_name !== undefined &&
|
||||||
|
updateData.project_name !== existingProject.project_name
|
||||||
|
) {
|
||||||
|
updateData.slug = await this.generateUniqueProjectSlug(
|
||||||
|
updateData.project_name,
|
||||||
|
existingProject.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProject = await projectRepository.updateById(existingProject.id, updateData);
|
||||||
|
|
||||||
return data(this.mapProjectDetail(updatedProject));
|
return data(this.mapProjectDetail(updatedProject));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -213,7 +228,7 @@ class ProjectService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingProject = await projectRepository.findById(id);
|
const existingProject = await projectRepository.findByIdOrSlug(id);
|
||||||
|
|
||||||
if (!existingProject) {
|
if (!existingProject) {
|
||||||
return error(new NotFoundError("Project not found"));
|
return error(new NotFoundError("Project not found"));
|
||||||
@@ -223,7 +238,7 @@ class ProjectService {
|
|||||||
return data(this.mapProjectDetail(existingProject));
|
return data(this.mapProjectDetail(existingProject));
|
||||||
}
|
}
|
||||||
|
|
||||||
const archivedProject = await projectRepository.archiveById(id);
|
const archivedProject = await projectRepository.archiveById(existingProject.id);
|
||||||
|
|
||||||
return data(this.mapProjectDetail(archivedProject));
|
return data(this.mapProjectDetail(archivedProject));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -283,6 +298,7 @@ class ProjectService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
|
slug: project.slug,
|
||||||
project_icon: project.project_icon,
|
project_icon: project.project_icon,
|
||||||
project_name: project.project_name,
|
project_name: project.project_name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
@@ -351,6 +367,7 @@ class ProjectService {
|
|||||||
|
|
||||||
const mappedProjects = projects.map((project) => ({
|
const mappedProjects = projects.map((project) => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
|
slug: project.slug,
|
||||||
project_icon: project.project_icon,
|
project_icon: project.project_icon,
|
||||||
project_name: project.project_name,
|
project_name: project.project_name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
@@ -430,6 +447,7 @@ class ProjectService {
|
|||||||
|
|
||||||
const mappedProjects = projects.map((project) => ({
|
const mappedProjects = projects.map((project) => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
|
slug: project.slug,
|
||||||
project_icon: project.project_icon,
|
project_icon: project.project_icon,
|
||||||
project_name: project.project_name,
|
project_name: project.project_name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
@@ -536,6 +554,20 @@ class ProjectService {
|
|||||||
return error(err);
|
return error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateUniqueProjectSlug(projectName, excludeProjectId = null) {
|
||||||
|
const baseSlug = slugify(projectName, 'project');
|
||||||
|
|
||||||
|
let slug = baseSlug;
|
||||||
|
let counter = 2;
|
||||||
|
|
||||||
|
while (await projectRepository.findSlugExists(slug, excludeProjectId)) {
|
||||||
|
slug = `${baseSlug}-${counter}`;
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ProjectService();
|
module.exports = new ProjectService();
|
||||||
|
|||||||
Reference in New Issue
Block a user