diff --git a/infra/k6/package.json b/infra/k6/package.json index 40a1b73..cd1384a 100644 --- a/infra/k6/package.json +++ b/infra/k6/package.json @@ -8,7 +8,6 @@ "test:teams": "k6 run scenarios/teams.js", "test:teams-members": "k6 run scenarios/teams-members.js", "test:teams-invitations": "k6 run scenarios/teams-invitations.js", - "test:teams-settings": "k6 run scenarios/teams-settings.js", "test:teams-me": "k6 run scenarios/teams-me.js", "test:projects": "k6 run scenarios/projects.js", "test:users": "k6 run scenarios/users.js", diff --git a/infra/k6/scenarios/boards-columns.js b/infra/k6/scenarios/boards-columns.js index be15015..71d6c54 100644 --- a/infra/k6/scenarios/boards-columns.js +++ b/infra/k6/scenarios/boards-columns.js @@ -44,13 +44,13 @@ export default function () { description: 'k6 columns scenario project', visibility: 'public', }; - const createProjectRes = client.post(`/teams/${team.slug}/projects`, project, { + const createProjectRes = client.post(`/teams/${team.id}/projects`, project, { tags: { name: 'post-teams-projects' }, }); const projectId = createProjectRes.json().projectId; check(createProjectRes, { - 'POST /teams/:slug/projects: has projectId': (r) => r.json().projectId !== undefined, + 'POST /teams/:id/projects: has projectId': (r) => r.json().projectId !== undefined, }); sleep(1); @@ -125,7 +125,7 @@ export default function () { sleep(1); // --- delete project --- - client.delete(`/teams/${team.slug}/projects/${projectId}`, { + client.delete(`/teams/${team.id}/projects/${projectId}`, { tags: { name: 'delete-teams-projects' }, }); diff --git a/infra/k6/scenarios/boards-views.js b/infra/k6/scenarios/boards-views.js index 35d8188..a4eec3d 100644 --- a/infra/k6/scenarios/boards-views.js +++ b/infra/k6/scenarios/boards-views.js @@ -44,13 +44,13 @@ export default function () { description: 'k6 views scenario project', visibility: 'public', }; - const createProjectRes = client.post(`/teams/${team.slug}/projects`, project, { + const createProjectRes = client.post(`/teams/${team.id}/projects`, project, { tags: { name: 'post-teams-projects' }, }); const projectId = createProjectRes.json().projectId; check(createProjectRes, { - 'POST /teams/:slug/projects: has projectId': (r) => r.json().projectId !== undefined, + 'POST /teams/:id/projects: has projectId': (r) => r.json().projectId !== undefined, }); sleep(1); @@ -121,7 +121,7 @@ export default function () { sleep(1); // --- delete project --- - client.delete(`/teams/${team.slug}/projects/${projectId}`, { + client.delete(`/teams/${team.id}/projects/${projectId}`, { tags: { name: 'delete-teams-projects' }, }); diff --git a/infra/k6/scenarios/boards.js b/infra/k6/scenarios/boards.js index 6cfff8a..5d1da42 100644 --- a/infra/k6/scenarios/boards.js +++ b/infra/k6/scenarios/boards.js @@ -43,13 +43,13 @@ export default function () { description: 'k6 boards scenario project', visibility: 'public', }; - const createProjectRes = client.post(`/teams/${team.slug}/projects`, project, { + const createProjectRes = client.post(`/teams/${team.id}/projects`, project, { tags: { name: 'post-teams-projects' }, }); const projectId = createProjectRes.json().projectId; check(createProjectRes, { - 'POST /teams/:slug/projects: has projectId': (r) => r.json().projectId !== undefined, + 'POST /teams/:id/projects: has projectId': (r) => r.json().projectId !== undefined, }); sleep(1); @@ -106,7 +106,7 @@ export default function () { sleep(1); // --- delete project --- - client.delete(`/teams/${team.slug}/projects/${projectId}`, { + client.delete(`/teams/${team.id}/projects/${projectId}`, { tags: { name: 'delete-teams-projects' }, }); diff --git a/infra/k6/scenarios/projects.js b/infra/k6/scenarios/projects.js index 9fe8785..9734f17 100644 --- a/infra/k6/scenarios/projects.js +++ b/infra/k6/scenarios/projects.js @@ -42,7 +42,7 @@ export default function () { description: 'description for k6_test_project', visibility: 'public', }; - const createRes = client.post(`/teams/${team.slug}/projects`, project, { + const createRes = client.post(`/teams/${team.id}/projects`, project, { tags: { name: 'post-teams-projects' }, }); const projectId = createRes.json().projectId; @@ -54,7 +54,7 @@ export default function () { const updatedProject = { name: newProjectName, }; - client.patch(`/teams/${team.slug}/projects/${projectId}`, updatedProject, { + client.patch(`/teams/${team.id}/projects/${projectId}`, updatedProject, { tags: { name: 'patch-teams-projects' }, }); @@ -63,7 +63,7 @@ export default function () { // --- get all teams projects --- const getAllRes = client.get( - `/teams/${team.slug}/projects`, + `/teams/${team.id}/projects`, {}, { tags: { name: 'get-teams-projects' } }, ); @@ -74,7 +74,7 @@ export default function () { // --- get one team project --- client.get( - `/teams/${team.slug}/projects/${projectId}`, + `/teams/${team.id}/projects/${projectId}`, {}, { tags: { name: 'teams-projects-id' } }, ); @@ -83,13 +83,13 @@ export default function () { // --- generate share token --- const shareTokenRes = client.post( - `/teams/${team.slug}/projects/${projectId}/share`, + `/teams/${team.id}/projects/${projectId}/share`, {}, { tags: { name: 'teams-projects-generate-token' } }, ); check(shareTokenRes, { - 'POST /teams/:slug/projects/:id/share: has token': (r) => + 'POST /teams/:id/projects/:id/share: has token': (r) => r.json().payload.token !== undefined, }); @@ -98,7 +98,7 @@ export default function () { // --- archive project --- client.post( - `/teams/${team.slug}/projects/${projectId}/archive`, + `/teams/${team.id}/projects/${projectId}/archive`, {}, { tags: { name: 'teams-projects-archive' } }, ); @@ -108,7 +108,7 @@ export default function () { // --- delete project --- client.delete( - `/teams/${team.slug}/projects/${projectId}`, + `/teams/${team.id}/projects/${projectId}`, {}, { tags: { name: 'delete-teams-projects' } }, ); diff --git a/infra/k6/scenarios/teams-invitations.js b/infra/k6/scenarios/teams-invitations.js index fb6278f..5031120 100644 --- a/infra/k6/scenarios/teams-invitations.js +++ b/infra/k6/scenarios/teams-invitations.js @@ -39,9 +39,9 @@ export default function () { sleep(1); - // --- GET /teams/:slug/invitations --- + // --- GET /teams/:id/invitations --- const listRes = client.get( - `/teams/${team.slug}/invitations`, + `/teams/${team.id}/invitations`, {}, { tags: { name: 'teams-invitations-list' }, @@ -56,9 +56,9 @@ export default function () { const invite = items.length ? items[0] : null; if (invite && invite.code) { - // --- GET /teams/:slug/invitations/:code --- + // --- GET /teams/:id/invitations/:code --- client.get( - `/teams/${team.slug}/invitations/${invite.code}`, + `/teams/${team.id}/invitations/${invite.code}`, {}, { tags: { name: 'teams-invitations-get' }, @@ -67,16 +67,16 @@ export default function () { sleep(1); - // --- PATCH /teams/:slug/invitations/:code --- + // --- PATCH /teams/:id/invitations/:code --- client.patch( - `/teams/${team.slug}/invitations/${invite.code}`, + `/teams/${team.id}/invitations/${invite.code}`, { role: 'member' }, { tags: { name: 'teams-invitations-update' } }, ); sleep(1); - // --- POST /teams/:slug/invitations/:code/accept --- + // --- POST /teams/:id/invitations/:code/accept --- if (__ITER === 0 && invite.email && userByEmail[invite.email]) { const invitedUser = userByEmail[invite.email]; const { client: invitedClient } = getAuthUser(invitedUser, { @@ -84,7 +84,7 @@ export default function () { }); invitedClient.post( - `/teams/${team.slug}/invitations/${invite.code}/accept`, + `/teams/${team.id}/invitations/${invite.code}/accept`, {}, { tags: { name: 'teams-invitations-accept' } }, ); @@ -93,10 +93,10 @@ export default function () { sleep(1); - // --- POST /teams/:slug/invitations --- + // --- POST /teams/:id/invitations --- const randomEmail = `k6_invite_${__VU}_${__ITER}@tasktracker.local`; client.post( - `/teams/${team.slug}/invitations`, + `/teams/${team.id}/invitations`, { email: randomEmail, role: 'member' }, { tags: { name: 'teams-invitations-create' }, @@ -105,9 +105,9 @@ export default function () { sleep(1); - // --- POST /teams/:slug/invitations (duplicate) --- + // --- POST /teams/:id/invitations (duplicate) --- client.post( - `/teams/${team.slug}/invitations`, + `/teams/${team.id}/invitations`, { email: randomEmail, role: 'member' }, { tags: { name: 'teams-invitations-create-duplicate' }, diff --git a/infra/k6/scenarios/teams-members.js b/infra/k6/scenarios/teams-members.js index 7a71241..7114f95 100644 --- a/infra/k6/scenarios/teams-members.js +++ b/infra/k6/scenarios/teams-members.js @@ -33,9 +33,9 @@ export default function () { sleep(1); - // --- GET /teams/:slug/members --- + // --- GET /teams/:id/members --- const membersRes = client.get( - `/teams/${team.slug}/members`, + `/teams/${team.id}/members`, {}, { tags: { name: 'teams-members-list' }, @@ -50,7 +50,7 @@ export default function () { // --- PATCH /teams/:slug/members/:userId --- if (target && target.id) { client.patch( - `/teams/${team.slug}/members/${target.id}`, + `/teams/${team.id}/members/${target.id}`, { role: 'member' }, { tags: { name: 'teams-members-update' } }, ); diff --git a/infra/k6/scenarios/teams-settings.js b/infra/k6/scenarios/teams-settings.js deleted file mode 100644 index fa20923..0000000 --- a/infra/k6/scenarios/teams-settings.js +++ /dev/null @@ -1,51 +0,0 @@ -import { SharedArray } from 'k6/data'; -import { sleep } from 'k6'; -import { GET_OPTIONS } from '../common/config.js'; -import getAuthUser from '../shared/get-auth-user.js'; - -const users = new SharedArray('test users', function () { - return JSON.parse(open('../data/users.json')); -}); -const teams = new SharedArray('test teams', function () { - return JSON.parse(open('../data/teams.json')); -}); -const tags = new SharedArray('test tags', function () { - return JSON.parse(open('../data/tags.json')); -}); - -const baseOptions = GET_OPTIONS(); -baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, { - 'http_req_duration{name:auth-sign-in}': ['p(95)<333'], - 'http_req_duration{name:teams-tags-sync}': ['p(95)<333'], -}); - -export const options = baseOptions; - -function pickTags(count = 3) { - if (!tags.length) return ['k6_tag']; - const start = (__VU - 1) % tags.length; - const selected = []; - for (let i = 0; i < Math.min(count, tags.length); i++) { - const idx = (start + i) % tags.length; - selected.push(tags[idx].name); - } - return selected; -} - -export default function () { - const idx = (__VU - 1) % teams.length; - const team = teams[idx]; - const user = users[idx % users.length]; - const { client } = getAuthUser(user); - - sleep(1); - - // --- PUT /teams/:slug/tags --- - client.put( - `/teams/${team.slug}/tags`, - { tags: pickTags(3) }, - { tags: { name: 'teams-tags-sync' } }, - ); - - sleep(1); -} diff --git a/infra/k6/scenarios/teams.js b/infra/k6/scenarios/teams.js index 4ee64f7..c8ae202 100644 --- a/infra/k6/scenarios/teams.js +++ b/infra/k6/scenarios/teams.js @@ -34,31 +34,25 @@ export default function () { sleep(1); // --- POST /teams --- - const slug = randomStr(10); const team = { - name: 'k6_team_' + slug, + name: 'k6_team_' + randomStr(10), description: randomStr(15), - slug: slug, }; - client.post('/teams', team, { tags: { name: 'teams-create' } }); - - sleep(1); - - // --- GET /check-slug/:slug --- - client.get(`/teams/check-slug/${slug}`, {}, { tags: { name: 'teams-check-slug' } }); - + const teamRes = client.post('/teams', team, { tags: { name: 'teams-create' } }); + console.log(teamRes); + const teamId = teamRes.json().teamId; sleep(1); - // --- GET /:slug --- - client.get(`/teams/${slug}`, {}, { tags: { name: 'teams-find-one' } }); + // --- GET /:teamId --- + client.get(`/teams/${teamId}`, {}, { tags: { name: 'teams-find-one' } }); sleep(1); - // --- PATCH /:slug --- + // --- PATCH /:teamId --- const updatedTeam = { description: randomStr(25), }; - client.patch(`/teams/${slug}`, updatedTeam, { + client.patch(`/teams/${teamId}`, updatedTeam, { tags: { name: 'teams-update' }, }); @@ -67,7 +61,7 @@ export default function () { // --- Update team avatar --- const fdAvatar = new FormData(); fdAvatar.append('file', http.file(avatar, 'avatar.png', 'image/png')); - fdAvatar.append('slug', slug); + fdAvatar.append('teamId', teamId); fdAvatar.append('context', 'team.avatar'); client.post('/upload', fdAvatar.body(), { @@ -83,7 +77,7 @@ export default function () { // --- Update team banner --- const fdBanner = new FormData(); fdBanner.append('file', http.file(avatar, 'avatar.png', 'image/png')); - fdBanner.append('slug', slug); + fdBanner.append('teamId', teamId); fdBanner.append('context', 'team.banner'); client.post('/upload', fdBanner.body(), { rawBody: true, @@ -95,8 +89,8 @@ export default function () { sleep(1); - // --- DELETE /:slug --- - client.delete(`/teams/${slug}`, { + // --- DELETE /:teamId --- + client.delete(`/teams/${teamId}`, { tags: { name: 'teams-delete' }, }); diff --git a/infra/k6/scripts/clear-k6-data.ts b/infra/k6/scripts/clear-k6-data.ts index ccee9fe..a1c8fa6 100644 --- a/infra/k6/scripts/clear-k6-data.ts +++ b/infra/k6/scripts/clear-k6-data.ts @@ -11,7 +11,6 @@ async function clearDB(db: PostgresJsDatabase) { return await db.transaction(async (tx) => { await tx.delete(sc.users).where(sql`${sc.users.email} LIKE 'k6_user_%'`); await tx.delete(sc.teams).where(sql`${sc.teams.name} LIKE 'k6_team_%'`); - await tx.delete(sc.tags).where(sql`${sc.tags.name} LIKE 'k6_tag_%'`); }); } diff --git a/infra/k6/scripts/seed-k6-data.ts b/infra/k6/scripts/seed-k6-data.ts index 79bfc24..a550b06 100644 --- a/infra/k6/scripts/seed-k6-data.ts +++ b/infra/k6/scripts/seed-k6-data.ts @@ -13,7 +13,6 @@ async function seed_db(db: PostgresJsDatabase) { const COUNT = 1000; const OUT_USERS_FILE = resolve(process.cwd(), 'infra/k6/data/users.json'); const OUT_TEAMS_FILE = resolve(process.cwd(), 'infra/k6/data/teams.json'); - const OUT_TAGS_FILE = resolve(process.cwd(), 'infra/k6/data/tags.json'); console.log(`Start seeding using pg driver...`); @@ -26,16 +25,12 @@ async function seed_db(db: PostgresJsDatabase) { const activitiesToInsert = []; const usersData = []; const teamsData = []; - const tagsData = []; const teamsToInsert = []; - const tagsToInsert = []; - const teamsToTagsToInsert = []; const teamMembersToInsert = []; for (let i = 0; i < COUNT; i++) { const userId = createId(); const teamId = createId(); - const tagId = createId(); const email = `k6_user_${i}@tasktracker.com`; const user = { @@ -50,13 +45,8 @@ async function seed_db(db: PostgresJsDatabase) { id: teamId, ownerId: userId, name: `k6_team_${i}`, - slug: `k6_team_${i}`, description: `description team - ${i}`, }; - const tag = { - id: tagId, - name: `k6_tag_${i}`, - }; const teamMember = { teamId: teamId, userId: userId, @@ -67,18 +57,12 @@ async function seed_db(db: PostgresJsDatabase) { usersToInsert.push(user); teamsToInsert.push(team); - tagsToInsert.push(tag); - teamsToTagsToInsert.push({ - teamId, - tagId, - }); teamMembersToInsert.push(teamMember); securityToInsert.push({ userId, passwordHash }); notificationsToInsert.push({ userId }); usersData.push({ email, password }); teamsData.push(team); - tagsData.push(tag); for (let j = 0; j < 10; j++) { activitiesToInsert.push({ @@ -107,15 +91,12 @@ async function seed_db(db: PostgresJsDatabase) { await tx.insert(sc.userActivity).values(chunk); } await tx.insert(sc.teams).values(teamsToInsert); - await tx.insert(sc.tags).values(tagsToInsert); - await tx.insert(sc.teamsToTags).values(teamsToTagsToInsert); await tx.insert(sc.teamMembers).values(teamMembersToInsert); }); const filesToSave = [ { path: OUT_USERS_FILE, data: usersData }, { path: OUT_TEAMS_FILE, data: teamsData }, - { path: OUT_TAGS_FILE, data: tagsData }, ]; for (const { path, data } of filesToSave) { @@ -126,7 +107,6 @@ async function seed_db(db: PostgresJsDatabase) { console.log(`Success! Created ${COUNT} entries for each entity`); console.log(`User data saved to: ${OUT_USERS_FILE}`); console.log(`Teams data saved to: ${OUT_TEAMS_FILE}`); - console.log(`Tags data saved to: ${OUT_TAGS_FILE}`); } async function seed_redis(redis: Redis) { @@ -141,7 +121,6 @@ async function seed_redis(redis: Redis) { id: string; ownerId: string; name: string; - slug: string; description: string; }[]; @@ -177,7 +156,6 @@ async function seed_redis(redis: Redis) { invitesData.push({ code, email: invitee.email, - teamSlug: team.slug, }); } }); diff --git a/migrations/0009_true_avengers.sql b/migrations/0009_true_avengers.sql new file mode 100644 index 0000000..a9b2aa6 --- /dev/null +++ b/migrations/0009_true_avengers.sql @@ -0,0 +1,8 @@ +ALTER TABLE "base"."tags" DISABLE ROW LEVEL SECURITY; +ALTER TABLE "base"."teams_to_tags" DISABLE ROW LEVEL SECURITY; +DROP TABLE "base"."tags" CASCADE; +DROP TABLE "base"."teams_to_tags" CASCADE; +ALTER TABLE "base"."teams" DROP CONSTRAINT "teams_slug_unique"; +DROP INDEX "base"."team_active_slug_idx"; +DROP INDEX "base"."team_slug_idx"; +ALTER TABLE "base"."teams" DROP COLUMN "slug"; \ No newline at end of file diff --git a/migrations/meta/0009_snapshot.json b/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..e37f3df --- /dev/null +++ b/migrations/meta/0009_snapshot.json @@ -0,0 +1,1513 @@ +{ + "id": "9afd9304-3a2f-40df-986d-b9fcd4f0e596", + "prevId": "02dde4b4-75ab-4b50-97aa-5feb5ffaf31c", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_preferences": { + "name": "user_preferences", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'system'" + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "recovery_email": { + "name": "recovery_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "headline": { + "name": "headline", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "vacation_start": { + "name": "vacation_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vacation_end": { + "name": "vacation_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vacation_message": { + "name": "vacation_message", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'none'" + }, + "pronouns": { + "name": "pronouns", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'none'" + }, + "pronouns_custom": { + "name": "pronouns_custom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_identities": { + "name": "user_identities", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_identities_user_id_users_id_fk": { + "name": "user_identities_user_id_users_id_fk", + "tableFrom": "user_identities", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_user_id_idx": { + "name": "provider_user_id_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_role_idx": { + "name": "member_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_owner_idx": { + "name": "team_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_deleted_at_idx": { + "name": "team_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.project_shares": { + "name": "project_shares", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "token_idx": { + "name": "token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_share_project_id_idx": { + "name": "project_share_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_shares_project_id_projects_id_fk": { + "name": "project_shares_project_id_projects_id_fk", + "tableFrom": "project_shares", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_shares_token_unique": { + "name": "project_shares_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.projects": { + "name": "projects", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "task_sequence": { + "name": "task_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "project_visibility", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "project_team_key_idx": { + "name": "project_team_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_name_idx": { + "name": "project_team_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_id_idx": { + "name": "project_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_id_idx": { + "name": "project_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.board_columns": { + "name": "board_columns", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "column_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "visibility": { + "name": "visibility", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "position": { + "name": "position", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#64748b'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "board_columns_board_id_boards_id_fk": { + "name": "board_columns_board_id_boards_id_fk", + "tableFrom": "board_columns", + "tableTo": "boards", + "schemaTo": "base", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.boards_views": { + "name": "boards_views", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "board_type", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'kanban'" + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "position": { + "name": "position", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "boards_views_board_id_boards_id_fk": { + "name": "boards_views_board_id_boards_id_fk", + "tableFrom": "boards_views", + "tableTo": "boards", + "schemaTo": "base", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.boards": { + "name": "boards", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "position": { + "name": "position", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_board_name_idx": { + "name": "project_board_name_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "boards_project_id_projects_id_fk": { + "name": "boards_project_id_projects_id_fk", + "tableFrom": "boards", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boards_owner_id_users_id_fk": { + "name": "boards_owner_id_users_id_fk", + "tableFrom": "boards", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "owner", + "admin", + "lead", + "moderator", + "member", + "viewer" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "active", + "banned", + "inactive" + ] + }, + "base.project_status": { + "name": "project_status", + "schema": "base", + "values": [ + "active", + "archived", + "template" + ] + }, + "base.project_visibility": { + "name": "project_visibility", + "schema": "base", + "values": [ + "public", + "private" + ] + }, + "base.board_type": { + "name": "board_type", + "schema": "base", + "values": [ + "kanban", + "calendar", + "gantt_matrix" + ] + }, + "base.column_status": { + "name": "column_status", + "schema": "base", + "values": [ + "backlog", + "todo", + "in_progress", + "done", + "canceled" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 823654f..1118b8c 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1780843802831, "tag": "0008_quiet_loners", "breakpoints": false + }, + { + "idx": 9, + "version": "7", + "when": 1780853186023, + "tag": "0009_true_avengers", + "breakpoints": false } ] } \ No newline at end of file diff --git a/package.json b/package.json index 455678d..a339281 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "k6:projects": "pnpm --filter @project/performance-tests test:projects", "k6:teams-members": "pnpm --filter @project/performance-tests test:teams-members", "k6:teams-invitations": "pnpm --filter @project/performance-tests test:teams-invitations", - "k6:teams-settings": "pnpm --filter @project/performance-tests test:teams-settings", "k6:teams-me": "pnpm --filter @project/performance-tests test:teams-me", "k6:users": "pnpm --filter @project/performance-tests test:users", "k6:boards": "pnpm --filter @project/performance-tests test:boards:all", @@ -79,7 +78,6 @@ "postgres": "^3.4.9", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "transliteration": "^2.6.1", "ua-parser-js": "^2.0.9", "winston": "^3.19.0", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bb838f..8800cf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,9 +137,6 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 - transliteration: - specifier: ^2.6.1 - version: 2.6.1 ua-parser-js: specifier: ^2.0.9 version: 2.0.9 @@ -4268,11 +4265,6 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - transliteration@2.6.1: - resolution: {integrity: sha512-hJ9BhrQAOnNTbpOr1MxsNjZISkn7ppvF5TKUeFmTE1mG4ZPD/XVxF0L0LUoIUCWmQyxH0gJpVtfYLAWf298U9w==} - engines: {node: '>=20.0.0'} - hasBin: true - triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -8828,8 +8820,6 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - transliteration@2.6.1: {} - triple-beam@1.4.1: {} ts-api-utils@1.4.3(typescript@5.9.3): diff --git a/src/projects/application/controller/projects/controller.ts b/src/projects/application/controller/projects/controller.ts index 390a31f..665df56 100644 --- a/src/projects/application/controller/projects/controller.ts +++ b/src/projects/application/controller/projects/controller.ts @@ -13,14 +13,14 @@ import { CreateProjectDto, CreateShareTokenDto, UpdateProjectDto } from '../../d import { ProjectStatus } from '@core/projects/domain/entities'; import { ProjectsFacade } from '../../projects.facade'; -@ApiBaseController('teams/:slug/projects', 'Team Projects', true) +@ApiBaseController('teams/:teamId/projects', 'Team Projects', true) export class ProjectsController { constructor(private readonly facade: ProjectsFacade) {} @Get() @FindAllProjectsSwagger() - async findAll(@Param('slug') slug: string, @GetUserId() userId: string) { - return this.facade.getTeamProjects(slug, userId); + async findAll(@Param('teamId') teamId: string, @GetUserId() userId: string) { + return this.facade.getTeamProjects(teamId, userId); } @Get(':id') @@ -28,62 +28,62 @@ export class ProjectsController { @FindOneProjectSwagger() async getOne( @Param('id') id: string, - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId?: string, @Query('token') token?: string, ) { - return this.facade.getDetail(id, slug, userId, token); + return this.facade.getDetail(id, teamId, userId, token); } @Post(':id/share') @CreateShareTokenSwagger() async generateShareToken( @Param('id') id: string, - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId: string, @Body() dto: CreateShareTokenDto, ) { - return this.facade.generateShareToken(id, slug, userId, dto); + return this.facade.generateShareToken(id, teamId, userId, dto); } @Post(':id/archive') @ArchiveProjectSwagger() async archive( @Param('id') id: string, - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId: string, ) { - return this.facade.setStatus(id, slug, userId, ProjectStatus.Archived); + return this.facade.setStatus(id, teamId, userId, ProjectStatus.Archived); } @Post() @CreateProjectSwagger() async create( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId: string, @Body() dto: CreateProjectDto, ) { - return this.facade.create(userId, slug, dto); + return this.facade.create(userId, teamId, dto); } @Patch(':id') @UpdateProjectSwagger() async update( @Param('id') id: string, - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId: string, @Body() dto: UpdateProjectDto, ) { - return this.facade.update(id, slug, userId, dto); + return this.facade.update(id, teamId, userId, dto); } @Delete(':id') @RemoveProjectSwagger() async remove( @Param('id') id: string, - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() userId: string, ) { - return this.facade.delete(id, slug, userId); + return this.facade.delete(id, teamId, userId); } } diff --git a/src/projects/application/controller/projects/swagger.ts b/src/projects/application/controller/projects/swagger.ts index a82f3c4..16d65bc 100644 --- a/src/projects/application/controller/projects/swagger.ts +++ b/src/projects/application/controller/projects/swagger.ts @@ -16,7 +16,7 @@ import { CreateShareTokenResponse } from '@core/projects/application/dtos/projec export const CreateProjectSwagger = () => applyDecorators( ApiOperation({ summary: 'Создать новый проект в команде' }), - ApiParam({ name: 'slug', type: 'string' }), + ApiParam({ name: 'teamId', type: 'string' }), ApiBody({ type: CreateProjectDto.Output }), ApiResponse({ status: 201, @@ -33,7 +33,7 @@ export const CreateProjectSwagger = () => export const FindAllProjectsSwagger = () => applyDecorators( ApiOperation({ summary: 'Получить список всех проектов команды' }), - ApiParam({ name: 'slug', type: 'string' }), + ApiParam({ name: 'teamId', type: 'string' }), ApiResponse({ status: 200, description: 'Список проектов получен', @@ -127,8 +127,8 @@ export const CreateShareTokenSwagger = () => 'Создает защищенный токен доступа к проекту. Если expiresAt не указан, по умолчанию ставится доступ на 3 месяца.', }), ApiParam({ - name: 'slug', - description: 'Slug команды', + name: 'teamId', + description: 'ID команды', type: 'string', }), ApiParam({ diff --git a/src/projects/application/dtos/projects.dto.ts b/src/projects/application/dtos/projects.dto.ts index 46e17b6..f2492ac 100644 --- a/src/projects/application/dtos/projects.dto.ts +++ b/src/projects/application/dtos/projects.dto.ts @@ -74,7 +74,6 @@ export class CreateShareTokenResponse extends createZodDto(CreateShareTokenRespo const TeamShortSchema = z.object({ id: z.string().describe('ID команды'), name: z.string().describe('Название команды'), - slug: z.string().describe('Слаг команды'), role: z.string().describe('Роль пользователя в команде'), }); diff --git a/src/projects/application/projects.facade.ts b/src/projects/application/projects.facade.ts index 3fb6dc9..3a71ad6 100644 --- a/src/projects/application/projects.facade.ts +++ b/src/projects/application/projects.facade.ts @@ -23,36 +23,36 @@ export class ProjectsFacade { private readonly findByTeamQ: FindProjectsByTeamQuery, ) {} - public async create(userId: string, slug: string, dto: CreateProjectDto) { - return this.createProjectUC.execute(userId, slug, dto); + public async create(userId: string, teamId: string, dto: CreateProjectDto) { + return this.createProjectUC.execute(userId, teamId, dto); } - public async update(id: string, slug: string, userId: string, dto: UpdateProjectDto) { - return this.updateProjectUC.execute(id, slug, userId, dto); + public async update(id: string, teamId: string, userId: string, dto: UpdateProjectDto) { + return this.updateProjectUC.execute(id, teamId, userId, dto); } - public async delete(id: string, slug: string, userId: string) { - return this.deleteProjectUC.execute(id, slug, userId); + public async delete(id: string, teamId: string, userId: string) { + return this.deleteProjectUC.execute(id, teamId, userId); } - public async setStatus(id: string, slug: string, userId: string, status: ProjectStatus) { - return this.setStatusUC.execute(id, slug, userId, status); + public async setStatus(id: string, teamId: string, userId: string, status: ProjectStatus) { + return this.setStatusUC.execute(id, teamId, userId, status); } public async generateShareToken( id: string, - slug: string, + teamId: string, userId: string, dto: CreateShareTokenDto, ) { - return this.generateTokenUC.execute(id, slug, userId, dto); + return this.generateTokenUC.execute(id, teamId, userId, dto); } - public async getDetail(id: string, slug: string, userId?: string, token?: string) { - return this.getDetailQ.execute(id, slug, userId, token); + public async getDetail(id: string, teamId: string, userId?: string, token?: string) { + return this.getDetailQ.execute(id, teamId, userId, token); } - public async getTeamProjects(slug: string, userId: string) { - return this.findByTeamQ.execute(slug, userId); + public async getTeamProjects(teamId: string, userId: string) { + return this.findByTeamQ.execute(teamId, userId); } } diff --git a/src/projects/application/use-cases/create-project.use-case.ts b/src/projects/application/use-cases/create-project.use-case.ts index a2cc6de..e7d0d23 100644 --- a/src/projects/application/use-cases/create-project.use-case.ts +++ b/src/projects/application/use-cases/create-project.use-case.ts @@ -12,8 +12,8 @@ export class CreateProjectUseCase { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(userId: string, slug: string, dto: CreateProjectDto) { - const { team } = await this.policy.ensureTeamAccess(slug, userId, 'admin'); + public async execute(userId: string, teamId: string, dto: CreateProjectDto) { + const { team } = await this.policy.ensureTeamAccess(teamId, userId, 'admin'); const data = { ...dto, diff --git a/src/projects/application/use-cases/delete-project.use-case.ts b/src/projects/application/use-cases/delete-project.use-case.ts index b5d3e71..4957663 100644 --- a/src/projects/application/use-cases/delete-project.use-case.ts +++ b/src/projects/application/use-cases/delete-project.use-case.ts @@ -11,8 +11,8 @@ export class DeleteProjectUseCase { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(id: string, slug: string, userId: string) { - const { project } = await this.policy.validateProjectAccess(id, slug, userId, 'admin'); + public async execute(id: string, teamId: string, userId: string) { + const { project } = await this.policy.validateProjectAccess(id, teamId, userId, 'admin'); const result = await this.projectsRepo.delete(project.id); if (!result) { diff --git a/src/projects/application/use-cases/find-project.query.ts b/src/projects/application/use-cases/find-project.query.ts index c5b41d9..da30aac 100644 --- a/src/projects/application/use-cases/find-project.query.ts +++ b/src/projects/application/use-cases/find-project.query.ts @@ -20,7 +20,7 @@ export class FindProjectQuery { */ public async execute( projectId: string, - slug: string, + teamId: string, userId?: string, shareToken?: string, minRole: keyof typeof ROLE_PRIORITY = 'viewer', @@ -42,12 +42,12 @@ export class FindProjectQuery { return this.findPublic(project, shareToken); } - return this.findPrivate(project, slug, userId, minRole); + return this.findPrivate(project, teamId, userId, minRole); } private findPrivate = async ( project: Project, - slug: string, + teamId: string, userId?: string, minRole: keyof typeof ROLE_PRIORITY = 'viewer', ) => { @@ -61,7 +61,7 @@ export class FindProjectQuery { ); } - const team = await this.findTeamQ.execute(slug); + const team = await this.findTeamQ.execute(teamId); if (!team || team.id !== project.teamId) { throw new BaseException( { diff --git a/src/projects/application/use-cases/find-projects-by-team.query.ts b/src/projects/application/use-cases/find-projects-by-team.query.ts index f9f8760..80670e8 100644 --- a/src/projects/application/use-cases/find-projects-by-team.query.ts +++ b/src/projects/application/use-cases/find-projects-by-team.query.ts @@ -11,8 +11,8 @@ export class FindProjectsByTeamQuery { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(slug: string, userId: string) { - const { team, member } = await this.policy.ensureTeamAccess(slug, userId, 'viewer'); + public async execute(teamId: string, userId: string) { + const { team, member } = await this.policy.ensureTeamAccess(teamId, userId, 'viewer'); const projects = await this.projectsRepo.findByTeam(team.id); const items = projects.map((p) => ProjectsMapper.toListResponse(p, member)); @@ -20,7 +20,6 @@ export class FindProjectsByTeamQuery { team: { id: team.id, name: team.name, - slug: team.slug, role: member.role, }, // TODO: реализовать полноценную пагинацию для проектов команды. diff --git a/src/projects/application/use-cases/generate-share-token.use-case.ts b/src/projects/application/use-cases/generate-share-token.use-case.ts index 7afd5ce..f4fbf0d 100644 --- a/src/projects/application/use-cases/generate-share-token.use-case.ts +++ b/src/projects/application/use-cases/generate-share-token.use-case.ts @@ -13,8 +13,8 @@ export class GenerateShareTokenUseCase { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(id: string, slug: string, userId: string, dto: CreateShareTokenDto) { - const { project } = await this.policy.validateProjectAccess(id, slug, userId); + public async execute(id: string, teamId: string, userId: string, dto: CreateShareTokenDto) { + const { project } = await this.policy.validateProjectAccess(id, teamId, userId); let expiresAt: Date; diff --git a/src/projects/application/use-cases/get-project-detail.query.ts b/src/projects/application/use-cases/get-project-detail.query.ts index d69ec4e..d5e04d4 100644 --- a/src/projects/application/use-cases/get-project-detail.query.ts +++ b/src/projects/application/use-cases/get-project-detail.query.ts @@ -6,10 +6,10 @@ import { FindProjectQuery } from './find-project.query'; export class GetProjectDetailQuery { constructor(private readonly findProjectQuery: FindProjectQuery) {} - public async execute(id: string, slug: string, userId?: string, token?: string) { + public async execute(id: string, teamId: string, userId?: string, token?: string) { const { project, member } = await this.findProjectQuery.execute( id, - slug, + teamId, userId, token, 'viewer', diff --git a/src/projects/application/use-cases/set-project-status.use-case.ts b/src/projects/application/use-cases/set-project-status.use-case.ts index 9e5240e..479b390 100644 --- a/src/projects/application/use-cases/set-project-status.use-case.ts +++ b/src/projects/application/use-cases/set-project-status.use-case.ts @@ -12,8 +12,8 @@ export class SetProjectStatusUseCase { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(id: string, slug: string, userId: string, status: ProjectStatus) { - const { project } = await this.policy.validateProjectAccess(id, slug, userId); + public async execute(id: string, teamId: string, userId: string, status: ProjectStatus) { + const { project } = await this.policy.validateProjectAccess(id, teamId, userId); const result = await this.projectsRepo.update(project.id, { status }); if (!result) { diff --git a/src/projects/application/use-cases/update-project.use-case.ts b/src/projects/application/use-cases/update-project.use-case.ts index eaf8a7b..68ffccf 100644 --- a/src/projects/application/use-cases/update-project.use-case.ts +++ b/src/projects/application/use-cases/update-project.use-case.ts @@ -12,8 +12,8 @@ export class UpdateProjectUseCase { private readonly policy: ProjectAccessPolicy, ) {} - public async execute(id: string, slug: string, userId: string, dto: UpdateProjectDto) { - const { project } = await this.policy.validateProjectAccess(id, slug, userId); + public async execute(id: string, teamId: string, userId: string, dto: UpdateProjectDto) { + const { project } = await this.policy.validateProjectAccess(id, teamId, userId); const { isPublic, key, ...data } = dto; const result = await this.projectsRepo.update(project.id, { diff --git a/src/projects/domain/policy/project-access.policy.ts b/src/projects/domain/policy/project-access.policy.ts index 89a4df9..fc92248 100644 --- a/src/projects/domain/policy/project-access.policy.ts +++ b/src/projects/domain/policy/project-access.policy.ts @@ -17,11 +17,11 @@ export class ProjectAccessPolicy { * Проверка доступа к команде (используется, например, при создании проекта) */ public async ensureTeamAccess( - slug: string, + teamId: string, userId: string, minRole: keyof typeof ROLE_PRIORITY = 'viewer', ) { - const team = await this.findTeamQ.execute(slug); + const team = await this.findTeamQ.execute(teamId); if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, @@ -56,11 +56,11 @@ export class ProjectAccessPolicy { */ public async validateProjectAccess( projectId: string, - slug: string, + teamId: string, userId: string, minRole: keyof typeof ROLE_PRIORITY = 'admin', ) { - const { team, member } = await this.ensureTeamAccess(slug, userId, minRole); + const { team, member } = await this.ensureTeamAccess(teamId, userId, minRole); const project = await this.projectsRepo.findOne(projectId); if (!project || project.teamId !== team.id) { @@ -77,7 +77,7 @@ export class ProjectAccessPolicy { } /** - * Проверка доступа к проекту по projectId (без slug) + * Проверка доступа к проекту по projectId */ public async validateProjectAccessById( projectId: string, diff --git a/src/shared/media/dtos/upload-file.dto.ts b/src/shared/media/dtos/upload-file.dto.ts index 72ae1bb..cd9b479 100644 --- a/src/shared/media/dtos/upload-file.dto.ts +++ b/src/shared/media/dtos/upload-file.dto.ts @@ -19,13 +19,13 @@ export const UploadMediaSchema = z.object({ }) .describe('Контекст загрузки (тип сущности и тип медиа)'), file: FileSchema, - slug: z + teamId: z .string({ - error: 'Slug должен быть строкой', + error: 'Team ID должен быть строкой', }) - .min(1, 'Slug не может быть пустым') + .min(1, 'Team ID не может быть пустым') .optional() - .describe('Уникальный идентификатор (slug) команды. Обязателен для контекстов team.*'), + .describe('Уникальный идентификатор команды. Обязателен для контекстов team.*'), }); export class UploadMediaDto extends createZodDto(UploadMediaSchema) {} diff --git a/src/shared/media/interfaces/media.interface.ts b/src/shared/media/interfaces/media.interface.ts index 471f7df..9c29a7c 100644 --- a/src/shared/media/interfaces/media.interface.ts +++ b/src/shared/media/interfaces/media.interface.ts @@ -11,7 +11,7 @@ export interface UpdateMediaUser { export interface UpdateMediaTeam { entity: { type: 'team'; - slug: string; + id: string; }; path: string; type: 'avatar' | 'banner'; diff --git a/src/shared/media/strategies/team-media.strategy.ts b/src/shared/media/strategies/team-media.strategy.ts index 6f7bcf2..79bc050 100644 --- a/src/shared/media/strategies/team-media.strategy.ts +++ b/src/shared/media/strategies/team-media.strategy.ts @@ -8,7 +8,7 @@ export class TeamMediaStrategy implements MediaDispatchStrategy { createPayload(dto: UploadMediaDto, userId: string, path: string): UpdateMediaTeam { return { - entity: { type: 'team', slug: dto.slug! }, + entity: { type: 'team', id: dto.teamId! }, type: dto.context.split('.').pop() as 'avatar' | 'banner', initiatorId: userId, path, diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts index 3f5f8b0..b298f1b 100644 --- a/src/shared/media/workers/media.worker.ts +++ b/src/shared/media/workers/media.worker.ts @@ -14,7 +14,7 @@ export class MediaProcessor extends WorkerHost { super(); } - async process(job: Job<{ original: string; context: string; userId: string; slug?: string }>) { + async process(job: Job<{ original: string; context: string; userId: string }>) { if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; const { original: originalFilePath, context } = job.data; diff --git a/src/teams/application/controller/index.ts b/src/teams/application/controller/index.ts index 5e1b219..cb32def 100644 --- a/src/teams/application/controller/index.ts +++ b/src/teams/application/controller/index.ts @@ -1,5 +1,4 @@ export { MeController } from './me/controller'; export { TeamsController } from './teams/controller'; export { TeamsMembersController } from './members/controller'; -export { TeamsSettingsController } from './settings/controller'; export { TeamsInvitationsController } from './invitations/controller'; diff --git a/src/teams/application/controller/invitations/controller.ts b/src/teams/application/controller/invitations/controller.ts index f653a70..0a42247 100644 --- a/src/teams/application/controller/invitations/controller.ts +++ b/src/teams/application/controller/invitations/controller.ts @@ -12,34 +12,34 @@ import type { JwtPayload } from '@shared/types'; import { InviteMemberDto, UpdateInvitationDto } from '../../dtos'; import { TeamsFacade } from '../../team.facade'; -@ApiBaseController('teams/:slug/invitations', 'Teams Invitations', true) +@ApiBaseController('teams/:teamId/invitations', 'Teams Invitations', true) export class TeamsInvitationsController { constructor(private readonly facade: TeamsFacade) {} @Get() @GetTeamInvitationsSwagger() - async getAll(@Param('slug') slug: string, @GetUserId() userId: string) { - return this.facade.getInvitations(slug, userId); + async getAll(@Param('teamId') teamId: string, @GetUserId() userId: string) { + return this.facade.getInvitations(teamId, userId); } @Get(':code') @GetTeamInvitationSwagger() async getOne( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @Param('code') code: string, @GetUser() user: JwtPayload, ) { - return this.facade.getInvitation(slug, code, user.sub, user.email); + return this.facade.getInvitation(teamId, code, user.sub, user.email); } @Post() @InviteMemberSwagger() async invite( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @GetUserId() inviterId: string, @Body() dto: InviteMemberDto, ) { - return this.facade.invite(slug, inviterId, dto); + return this.facade.invite(teamId, inviterId, dto); } @Post(':code/accept') @@ -51,21 +51,21 @@ export class TeamsInvitationsController { @Patch(':code') @UpdateTeamInvitationSwagger() async update( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @Param('code') code: string, @GetUserId() userId: string, @Body() dto: UpdateInvitationDto, ) { - return this.facade.updateInvitation(slug, code, userId, dto); + return this.facade.updateInvitation(teamId, code, userId, dto); } @Delete(':code') @DeleteTeamInvitationSwagger() async decline( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @Param('code') code: string, @GetUser() user: JwtPayload, ) { - return this.facade.declineInvitation(slug, code, user.sub, user.email); + return this.facade.declineInvitation(teamId, code, user.sub, user.email); } } diff --git a/src/teams/application/controller/invitations/swagger.ts b/src/teams/application/controller/invitations/swagger.ts index 338fbad..365c343 100644 --- a/src/teams/application/controller/invitations/swagger.ts +++ b/src/teams/application/controller/invitations/swagger.ts @@ -45,7 +45,10 @@ export const InviteMemberSwagger = () => ' Если нет — ему уйдет письмо на указанный Email.', }), ApiBody({ type: InviteMemberDto.Output }), - ApiParam({ name: 'slug', description: 'Слаг команды, в которую приглашаем' }), + ApiParam({ + name: 'teamId', + description: 'Уникальный идентификатор команды, в которую приглашаем', + }), ApiResponse({ status: 201, description: 'Инвайт создан и отправлен', @@ -91,7 +94,7 @@ export const GetTeamInvitationsSwagger = () => summary: 'Получить список всех приглашений в команду', description: 'Возвращает все активные инвайты команды. Доступно только owner/admin.', }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiResponse({ status: 200, description: 'Список приглашений команды', @@ -111,7 +114,7 @@ export const GetTeamInvitationSwagger = () => description: 'Возвращает данные инвайта по коду в рамках команды. Доступно только owner/admin.', }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiParam({ name: 'code', description: 'Код инвайта' }), ApiResponse({ status: 200, @@ -132,7 +135,7 @@ export const UpdateTeamInvitationSwagger = () => description: 'Позволяет изменить только поле role у существующего инвайта. TTL сохраняется.', }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiParam({ name: 'code', description: 'Код инвайта' }), ApiBody({ type: UpdateInvitationDto.Output }), ApiResponse({ @@ -154,7 +157,7 @@ export const DeleteTeamInvitationSwagger = () => description: 'Удаляет инвайт и чистит индексы в Redis (team:invites и user:invites). Доступно только owner/admin.', }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiParam({ name: 'code', description: 'Код инвайта' }), ApiResponse({ status: 200, diff --git a/src/teams/application/controller/members/controller.ts b/src/teams/application/controller/members/controller.ts index b4e9b22..b3e8ece 100644 --- a/src/teams/application/controller/members/controller.ts +++ b/src/teams/application/controller/members/controller.ts @@ -4,34 +4,34 @@ import { GetMembersSwagger, RemoveMemberSwagger, UpdateMemberSwagger } from './s import type { UpdateMemberDto } from '../../dtos/member.dto'; import { TeamsFacade } from '../../team.facade'; -@ApiBaseController('teams/:slug', 'Teams Members', true) +@ApiBaseController('teams/:teamId', 'Teams Members', true) export class TeamsMembersController { constructor(private readonly facade: TeamsFacade) {} @Get('members') @GetMembersSwagger() - async getMembers(@Param('slug') slug: string) { - return this.facade.getMembers(slug); + async getMembers(@Param('teamId') teamId: string) { + return this.facade.getMembers(teamId); } @Patch('members/:userId') @UpdateMemberSwagger() async updateMember( - @Param('slug') slug: string, + @Param('teamId') teamId: string, @Param('userId') targetUserId: string, @GetUserId() currentUserId: string, @Body() dto: UpdateMemberDto, ) { - return this.facade.updateMember(slug, currentUserId, targetUserId, dto); + return this.facade.updateMember(teamId, currentUserId, targetUserId, dto); } @Delete('members/:userId') @RemoveMemberSwagger() async removeMember( - @Param('slug') slug: string, - @Param('userId') targerUserId: string, + @Param('teamId') teamId: string, + @Param('userId') targetUserId: string, @GetUserId() currentUserId: string, ) { - return this.facade.removeMember(slug, currentUserId, targerUserId); + return this.facade.removeMember(teamId, currentUserId, targetUserId); } } diff --git a/src/teams/application/controller/members/swagger.ts b/src/teams/application/controller/members/swagger.ts index 68a46da..b955087 100644 --- a/src/teams/application/controller/members/swagger.ts +++ b/src/teams/application/controller/members/swagger.ts @@ -47,7 +47,7 @@ export const FindInvitesSwagger = () => export const GetMembersSwagger = () => applyDecorators( ApiOperation({ summary: 'Получить список всех участников команды' }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiResponse({ status: 200, description: 'Список участников получен', @@ -68,7 +68,7 @@ export const UpdateMemberSwagger = () => ' Владелец команды (Owner) не может понизить свою роль через этот эндпоинт.', }), ApiBody({ type: UpdateMemberDto.Output }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiParam({ name: 'userId', description: 'ID пользователя, чьи права редактируются' }), ApiResponse({ status: 200, @@ -85,7 +85,7 @@ export const UpdateMemberSwagger = () => export const RemoveMemberSwagger = () => applyDecorators( ApiOperation({ summary: 'Удалить участника из команды' }), - ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'teamId', description: 'Уникальный идентификатор команды' }), ApiParam({ name: 'userId', description: 'ID пользователя' }), ApiResponse({ status: 200, diff --git a/src/teams/application/controller/settings/controller.ts b/src/teams/application/controller/settings/controller.ts deleted file mode 100644 index 4f691e2..0000000 --- a/src/teams/application/controller/settings/controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Body, Param, Put } from '@nestjs/common'; -import { ApiBaseController } from '@shared/decorators'; -import { SyncTeamTagsSwagger } from './swagger'; -import { SyncTagsDto } from '../../dtos'; -import { TeamsFacade } from '../../team.facade'; - -@ApiBaseController('teams/:slug', 'Teams Settings', true) -export class TeamsSettingsController { - constructor(private readonly facade: TeamsFacade) {} - - @Put('tags') - @SyncTeamTagsSwagger() - async syncTags(@Param('slug') slug: string, @Body() dto: SyncTagsDto) { - return this.facade.syncTags(slug, dto.tags); - } -} diff --git a/src/teams/application/controller/settings/swagger.ts b/src/teams/application/controller/settings/swagger.ts deleted file mode 100644 index 504e633..0000000 --- a/src/teams/application/controller/settings/swagger.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { applyDecorators, SetMetadata } from '@nestjs/common'; -import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ActionResponse } from '@shared/dtos'; -import { ApiForbidden, ApiNotFound, ApiUnauthorized } from '@shared/error'; -import { SyncTagsDto } from '../../dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; - -export const SyncTeamTagsSwagger = () => - applyDecorators( - ApiOperation({ summary: 'Синхронизировать теги команды' }), - ApiBody({ type: SyncTagsDto.Output }), - ApiResponse({ status: 200, description: 'Теги обновлены', type: ActionResponse.Output }), - ApiForbidden(), - ApiNotFound(), - ApiUnauthorized(), - - SetMetadata(ZOD_RESPONSE_TOKEN, ActionResponse), - ); diff --git a/src/teams/application/controller/teams/controller.ts b/src/teams/application/controller/teams/controller.ts index b4546e0..c1182a0 100644 --- a/src/teams/application/controller/teams/controller.ts +++ b/src/teams/application/controller/teams/controller.ts @@ -5,7 +5,6 @@ import { FindOneTeamSwagger, RemoveTeamSwagger, UpdateTeamSwagger, - CheckSlugSwagger, } from './swagger'; import { CreateTeamDto, UpdateTeamDto } from '../../dtos'; import { TeamsFacade } from '../../team.facade'; @@ -20,32 +19,22 @@ export class TeamsController { return this.facade.createTeam(userId, dto); } - @Get('check-slug/:slug') - @CheckSlugSwagger() - async checkSlug(@Param('slug') slug: string) { - return this.facade.checkSlug(slug); - } - - @Get(':slug') + @Get(':id') @FindOneTeamSwagger() - async findOne(@Param('slug') slug: string) { - return this.facade.getTeamBySlug(slug); + async findOne(@Param('id') id: string) { + return this.facade.getTeamById(id); } - @Patch(':slug') + @Patch(':id') @UpdateTeamSwagger() - async update( - @Param('slug') slug: string, - @GetUserId() userId: string, - @Body() dto: UpdateTeamDto, - ) { - return this.facade.updateTeam(slug, userId, dto); + async update(@Param('id') id: string, @GetUserId() userId: string, @Body() dto: UpdateTeamDto) { + return this.facade.updateTeam(id, userId, dto); } - @Delete(':slug') + @Delete(':id') @RemoveTeamSwagger() @HttpCode(HttpStatus.OK) - async remove(@Param('slug') slug: string, @GetUserId() userId: string) { - return this.facade.deleteTeam(slug, userId); + async remove(@Param('id') id: string, @GetUserId() userId: string) { + return this.facade.deleteTeam(id, userId); } } diff --git a/src/teams/application/controller/teams/swagger.ts b/src/teams/application/controller/teams/swagger.ts index a93888f..a5fb6fc 100644 --- a/src/teams/application/controller/teams/swagger.ts +++ b/src/teams/application/controller/teams/swagger.ts @@ -1,15 +1,10 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; -import { - ApiConflict, - ApiForbidden, - ApiNotFound, - ApiUnauthorized, - ApiValidationError, -} from '@shared/error'; -import { CreateTeamDto, UpdateTeamDto, CheckSlugResponse, TeamResponse } from '../../dtos'; +import { ApiForbidden, ApiNotFound, ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { CreateTeamDto, UpdateTeamDto, TeamResponse } from '../../dtos'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { CreateTeamResponse } from '@core/teams/application/dtos/team.dto'; export const CreateTeamSwagger = () => applyDecorators( @@ -18,40 +13,18 @@ export const CreateTeamSwagger = () => ApiResponse({ status: 201, description: 'Команда успешно создана', - type: ActionResponse.Output, + type: CreateTeamResponse.Output, }), - ApiConflict('Команда с таким slug уже существует'), ApiValidationError(), ApiUnauthorized(), - SetMetadata(ZOD_RESPONSE_TOKEN, ActionResponse), - ); - -export const CheckSlugSwagger = () => - applyDecorators( - ApiOperation({ - summary: 'Проверить доступность слага', - description: 'Проверяет, свободен ли уникальный адрес команды для использования.', - }), - ApiParam({ - name: 'slug', - description: 'Желаемый слаг команды', - example: 'my-super-team', - }), - ApiResponse({ - status: 200, - description: 'Результат проверки доступности', - type: CheckSlugResponse.Output, - }), - ApiUnauthorized(), - - SetMetadata(ZOD_RESPONSE_TOKEN, CheckSlugResponse), + SetMetadata(ZOD_RESPONSE_TOKEN, CreateTeamResponse), ); export const FindOneTeamSwagger = () => applyDecorators( - ApiOperation({ summary: 'Получить детальную информацию о команде по slug' }), - ApiParam({ name: 'slug', description: 'Уникальный идентификатор (слаг) команды' }), + ApiOperation({ summary: 'Получить детальную информацию о команде' }), + ApiParam({ name: 'id', description: 'Уникальный идентификатор команды' }), ApiResponse({ status: 200, description: 'Данные команды получены', @@ -67,7 +40,10 @@ export const UpdateTeamSwagger = () => applyDecorators( ApiOperation({ summary: 'Обновить данные команды' }), ApiBody({ type: UpdateTeamDto.Output }), - ApiParam({ name: 'slug', description: 'Слаг команды для редактирования' }), + ApiParam({ + name: 'id', + description: 'Уникальный идентификатор команды для редактирования', + }), ApiResponse({ status: 200, description: 'Команда успешно обновлена', @@ -84,7 +60,7 @@ export const UpdateTeamSwagger = () => export const RemoveTeamSwagger = () => applyDecorators( ApiOperation({ summary: 'Удалить команду' }), - ApiParam({ name: 'slug', description: 'Слаг команды для удаления' }), + ApiParam({ name: 'id', description: 'Уникальный идентификатор команды для удаления' }), ApiResponse({ status: 200, description: 'Команда успешно удалена', diff --git a/src/teams/application/dtos/index.ts b/src/teams/application/dtos/index.ts index efa61a0..2fae57e 100644 --- a/src/teams/application/dtos/index.ts +++ b/src/teams/application/dtos/index.ts @@ -14,11 +14,7 @@ export { export { CreateTeamDto, UpdateTeamDto, - FindTagsQuery, - SyncTagsDto, UserTeamResponse, - TagResponse, - CheckSlugResponse, UserTeamsResponse, TeamResponse, } from './team.dto'; diff --git a/src/teams/application/dtos/team.dto.ts b/src/teams/application/dtos/team.dto.ts index d8a9839..63d2ed7 100644 --- a/src/teams/application/dtos/team.dto.ts +++ b/src/teams/application/dtos/team.dto.ts @@ -1,6 +1,7 @@ import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; import { AvatarResponseSchema, createPaginationSchema } from '@shared/schemas'; +import { ActionResponseSchema } from '@shared/dtos'; export const CreateTeamSchema = z.object({ name: z.string().min(2).max(100).describe('Название команды, отображаемое в интерфейсе'), @@ -9,26 +10,16 @@ export const CreateTeamSchema = z.object({ .min(10) .max(500) .describe('Краткое описание деятельности или целей команды'), - slug: z.string().optional().describe('Уникальная ссылка на изображение команду'), - tags: z - .array(z.string()) - .optional() - .superRefine((items, ctx) => { - if (!items) return; - const lowerItems = items.map((i) => i.toLowerCase()); - const hasDuplicates = new Set(lowerItems).size !== items.length; - - if (hasDuplicates) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Теги в списке не должны повторяться (регистр не важен)', - }); - } - }) - .describe('Список строковых названий тегов для классификации'), }); export class CreateTeamDto extends createZodDto(CreateTeamSchema) {} + +const CreateTeamResponseSchema = ActionResponseSchema.extend({ + teamId: z.string().describe('Уникальный идентификатор команды в системе'), +}); + +export class CreateTeamResponse extends createZodDto(CreateTeamResponseSchema) {} + export class UpdateTeamDto extends createZodDto( CreateTeamSchema.partial().refine((data) => Object.keys(data).length > 0, { error: 'Необходимо передать хотя бы одно поле для обновления', @@ -36,57 +27,6 @@ export class UpdateTeamDto extends createZodDto( }), ) {} -export const TagSchema = z.object({ - id: z.string().describe('Уникальный идентификатор тега (CUID2)'), - name: z.string().min(1).max(50).describe('Название тега (например, "Backend", "Design")'), -}); - -export const SyncTagsSchema = z.object({ - tags: z - .array(z.string()) - .min(1, 'Список тегов не может быть пустым') - .max(15, 'Нельзя добавить более 15 тегов за раз') - .superRefine((items, ctx) => { - if (!items) return; - const lowerItems = items.map((i) => i.toLowerCase()); - const hasDuplicates = new Set(lowerItems).size !== items.length; - - if (hasDuplicates) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Теги в списке не должны повторяться (регистр не важен)', - }); - } - }) - .describe( - 'Массив названий тегов для привязки к команде. Если тега нет в базе, он будет создан.', - ), -}); - -const FindTagsQuerySchema = z.object({ - search: z.string().optional().describe('Поисковый запрос для фильтрации тегов по названию'), - page: z.coerce.number().int().min(1).default(1).describe('Номер страницы (от 1)'), - limit: z.coerce - .number() - .int() - .min(1) - .max(100) - .default(20) - .describe('Количество возвращаемых результатов (1-100)'), -}); - -export class TagResponse extends createZodDto(createPaginationSchema(TagSchema)) {} -export class SyncTagsDto extends createZodDto(SyncTagsSchema) {} -export class FindTagsQuery extends createZodDto(FindTagsQuerySchema) {} - -export const CheckSlugResponseSchema = z.object({ - available: z - .boolean() - .describe('Флаг доступности: true — адрес свободен, false — уже занят или некорректен'), -}); - -export class CheckSlugResponse extends createZodDto(CheckSlugResponseSchema) {} - export const TeamPermissionsSchema = z.object({ canEdit: z.boolean().describe('Разрешено ли редактировать настройки и профиль команды'), canDelete: z @@ -100,7 +40,6 @@ export const TeamPermissionsSchema = z.object({ export const UserTeamSchema = z.object({ id: z.string().describe('Уникальный ID команды'), name: z.string().describe('Название команды'), - slug: z.string().describe('Уникальный URL-путь команды'), description: z.string().nullable().describe('Краткое описание команды'), avatar: AvatarResponseSchema, role: z.string().describe('Системное название роли пользователя'), @@ -119,7 +58,6 @@ export class UserTeamsResponse extends createZodDto(createPaginationSchema(UserT export const TeamResponseSchema = z.object({ id: z.string().describe('Уникальный ID команды'), - slug: z.string().describe('Слаг команды'), name: z.string().describe('Название команды'), description: z.string().nullable().describe('Описание команды'), avatarUrl: z diff --git a/src/teams/application/mappers/member.mapper.ts b/src/teams/application/mappers/member.mapper.ts index 5765213..530bc0a 100644 --- a/src/teams/application/mappers/member.mapper.ts +++ b/src/teams/application/mappers/member.mapper.ts @@ -34,7 +34,6 @@ export class TeamMemberMapper { return { id: row.id, name: row.name, - slug: row.slug, description: row.description, avatar, role: role, diff --git a/src/teams/application/team.facade.ts b/src/teams/application/team.facade.ts index bc96eca..cdf7b54 100644 --- a/src/teams/application/team.facade.ts +++ b/src/teams/application/team.facade.ts @@ -15,12 +15,10 @@ export class TeamsFacade { private readonly getInvitationQ: UC.GetInvitationQuery, private readonly getInvitationsQ: UC.GetInvitationsQuery, private readonly getTeamMembersQ: UC.GetTeamMembersQuery, - private readonly checkSlugQ: UC.CheckTeamSlugQuery, private readonly createTeamUc: UC.CreateTeamUseCase, private readonly deleteTeamUc: UC.DeleteTeamUseCase, private readonly updateTeamUc: UC.UpdateTeamUseCase, - private readonly syncTagsUc: UC.SyncTeamTagsUseCase, private readonly updateMemberUc: UC.UpdateTeamMemberUseCase, private readonly removeMemberUc: UC.RemoveTeamMemberUseCase, @@ -33,49 +31,46 @@ export class TeamsFacade { private readonly getMyInvitesUc: UC.GetMyInvitesUseCase, ) {} - public checkSlug = (slug: string) => this.checkSlugQ.execute(slug); + public getTeamById = (teamId: string) => this.findTeamQ.execute(teamId); - public getTeamBySlug = (slug: string) => this.findTeamQ.execute(slug); - - public getInvitation = (slug: string, code: string, userId: string, userEmail: string) => - this.getInvitationQ.execute(slug, code, userId, userEmail); + public getInvitation = (teamId: string, code: string, userId: string, userEmail: string) => + this.getInvitationQ.execute(teamId, code, userId, userEmail); public createTeam = (ownerId: string, dto: CreateTeamDto) => this.createTeamUc.execute(ownerId, dto); - public updateTeam = (slug: string, userId: string, dto: UpdateTeamDto) => - this.updateTeamUc.execute(slug, userId, dto); + public updateTeam = (teamId: string, userId: string, dto: UpdateTeamDto) => + this.updateTeamUc.execute(teamId, userId, dto); - public deleteTeam = (slug: string, userId: string) => this.deleteTeamUc.execute(slug, userId); + public deleteTeam = (teamId: string, userId: string) => + this.deleteTeamUc.execute(teamId, userId); - public getMembers = (slug: string) => this.getTeamMembersQ.execute(slug); + public getMembers = (teamId: string) => this.getTeamMembersQ.execute(teamId); - public updateMember = (slug: string, curr: string, target: string, dto: UpdateMemberDto) => - this.updateMemberUc.execute(slug, curr, target, dto); + public updateMember = (teamId: string, curr: string, target: string, dto: UpdateMemberDto) => + this.updateMemberUc.execute(teamId, curr, target, dto); - public removeMember = (slug: string, curr: string, target: string) => - this.removeMemberUc.execute(slug, curr, target); + public removeMember = (teamId: string, curr: string, target: string) => + this.removeMemberUc.execute(teamId, curr, target); - public getInvitations = (slug: string, userId?: string) => - this.getInvitationsQ.execute(slug, userId); + public getInvitations = (teamId: string, userId?: string) => + this.getInvitationsQ.execute(teamId, userId); - public invite = (slug: string, inviterId: string, dto: InviteMemberDto) => - this.sendInviteUc.execute(slug, inviterId, dto); + public invite = (teamId: string, inviterId: string, dto: InviteMemberDto) => + this.sendInviteUc.execute(teamId, inviterId, dto); public acceptInvite = (code: string, userId: string, email: string) => this.acceptInviteUc.execute(code, userId, email); - public declineInvitation = (slug: string, code: string, userId: string, userEmail: string) => - this.declineInvitationUc.execute(slug, code, userId, userEmail); + public declineInvitation = (teamId: string, code: string, userId: string, userEmail: string) => + this.declineInvitationUc.execute(teamId, code, userId, userEmail); public updateInvitation = ( - slug: string, + teamId: string, code: string, userId: string, dto: UpdateInvitationDto, - ) => this.updateInvitationUc.execute(slug, code, userId, dto); - - public syncTags = (slug: string, tags: string[]) => this.syncTagsUc.execute(slug, tags); + ) => this.updateInvitationUc.execute(teamId, code, userId, dto); public getMyTeams = (userId: string, pagination: any) => this.getMyTeamsUc.execute(userId, pagination); diff --git a/src/teams/application/use-cases/base/check-team-slug.query.ts b/src/teams/application/use-cases/base/check-team-slug.query.ts deleted file mode 100644 index 46a3083..0000000 --- a/src/teams/application/use-cases/base/check-team-slug.query.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ITeamsRepository } from '@core/teams/domain/repository'; -import { Inject, Injectable } from '@nestjs/common'; - -@Injectable() -export class CheckTeamSlugQuery { - constructor(@Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository) {} - - async execute(slug: string) { - const normalizedSlug = slug.trim().toLowerCase(); - - const available = await this.teamsRepo.isSlugAvailable(normalizedSlug); - - return { - available, - message: available - ? `Slug ${normalizedSlug} доступен для использования` - : `Slug ${normalizedSlug} уже занят`, - }; - } -} diff --git a/src/teams/application/use-cases/base/create-team.use-case.ts b/src/teams/application/use-cases/base/create-team.use-case.ts index c6b8cc7..00b41d2 100644 --- a/src/teams/application/use-cases/base/create-team.use-case.ts +++ b/src/teams/application/use-cases/base/create-team.use-case.ts @@ -2,7 +2,6 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { CreateTeamDto } from '../../dtos'; import { BaseException } from '@shared/error'; -import { slugify } from 'transliteration'; @Injectable() export class CreateTeamUseCase { @@ -12,36 +11,11 @@ export class CreateTeamUseCase { ) {} async execute(userId: string, dto: CreateTeamDto) { - const baseSlug = slugify(dto.slug || dto.name, { lowercase: true, separator: '-' }); - const existingTeam = await this.teamsRepo.findBySlug(baseSlug); - - if (existingTeam) { - throw new BaseException( - { - code: 'SLUG_ALREADY_EXISTS', - message: `Ссылка "${baseSlug}" уже занята другой командой`, - details: [{ target: 'slug', value: baseSlug }], - }, - HttpStatus.CONFLICT, - ); - } - - const { tags, ...teamData } = dto; - const uniqueTags = tags ? [...new Set(tags.map((tag) => tag.toLowerCase()))] : []; - try { - const result = await this.teamsRepo.create( - userId, - { - ...teamData, - slug: baseSlug, - }, - uniqueTags, - ); + const result = await this.teamsRepo.create(userId, dto); return { ...result, - slug: baseSlug, message: 'Команда успешно создана', }; } catch (error) { diff --git a/src/teams/application/use-cases/base/delete-team.use-case.ts b/src/teams/application/use-cases/base/delete-team.use-case.ts index 134bdf6..0855390 100644 --- a/src/teams/application/use-cases/base/delete-team.use-case.ts +++ b/src/teams/application/use-cases/base/delete-team.use-case.ts @@ -9,14 +9,14 @@ export class DeleteTeamUseCase { private readonly teamsRepo: ITeamsRepository, ) {} - async execute(slug: string, userId: string) { - const team = await this.teamsRepo.findBySlug(slug); + async execute(teamId: string, userId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', - message: `Команда ${slug} не найдена`, + message: `Команда ${teamId} не найдена`, }, HttpStatus.NOT_FOUND, ); diff --git a/src/teams/application/use-cases/base/find-team.query.ts b/src/teams/application/use-cases/base/find-team.query.ts index 793c20e..b2d0b1c 100644 --- a/src/teams/application/use-cases/base/find-team.query.ts +++ b/src/teams/application/use-cases/base/find-team.query.ts @@ -8,8 +8,8 @@ export class FindTeamQuery { private readonly repository: ITeamsRepository, ) {} - async execute(slug: string) { + async execute(teamId: string) { //TODO: add avatarURL handling - return this.repository.findBySlug(slug); + return this.repository.findById(teamId); } } diff --git a/src/teams/application/use-cases/base/get-all-tags.use-case.ts b/src/teams/application/use-cases/base/get-all-tags.use-case.ts deleted file mode 100644 index aaa6764..0000000 --- a/src/teams/application/use-cases/base/get-all-tags.use-case.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { FindTagsQuery } from '../../dtos'; -import { ITeamsRepository } from '@core/teams/domain/repository'; - -@Injectable() -export class GetAllTagsUseCase { - constructor( - @Inject('ITeamsRepository') - private readonly teamsRepo: ITeamsRepository, - ) {} - - async execute(query: FindTagsQuery) { - const safePage = Math.max(query.page ?? 1, 1); - const safeLimit = Math.min(Math.max(query.limit ?? 20, 1), 50); - const offset = (safePage - 1) * safeLimit; - - const { data, total } = await this.teamsRepo.findAllTags({ - search: query.search, - limit: safeLimit, - offset, - }); - - const totalPages = total === 0 ? 0 : Math.ceil(total / safeLimit); - - return { - items: data, - meta: { - hasNextPage: safePage < totalPages, - hasPrevPage: safePage > 1, - total, - totalPages, - page: safePage, - limit: safeLimit, - }, - }; - } -} diff --git a/src/teams/application/use-cases/base/sync-team-tags.use-case.ts b/src/teams/application/use-cases/base/sync-team-tags.use-case.ts deleted file mode 100644 index 5199ad0..0000000 --- a/src/teams/application/use-cases/base/sync-team-tags.use-case.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ITeamsRepository } from '@core/teams/domain/repository'; -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; - -@Injectable() -export class SyncTeamTagsUseCase { - constructor( - @Inject('ITeamsRepository') - private readonly teamsRepo: ITeamsRepository, - ) {} - - async execute(slug: string, tags: string[]) { - const team = await this.teamsRepo.findBySlug(slug); - - if (!team) { - throw new BaseException( - { - code: 'TEAM_NOT_FOUND', - message: 'Команда не найдена', - }, - HttpStatus.NOT_FOUND, - ); - } - - const normalizedTags = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; - - const isSynced = await this.teamsRepo.syncTags(team.id, normalizedTags); - - if (!isSynced) { - throw new BaseException( - { - code: 'TAGS_SYNC_FAILED', - message: 'Не удалось обновить теги команды', - details: [{ target: 'tags', count: normalizedTags.length }], - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - return { - success: true, - message: 'Теги команды успешно обновлены', - }; - } -} diff --git a/src/teams/application/use-cases/base/update-team.use-case.ts b/src/teams/application/use-cases/base/update-team.use-case.ts index 745d438..253e797 100644 --- a/src/teams/application/use-cases/base/update-team.use-case.ts +++ b/src/teams/application/use-cases/base/update-team.use-case.ts @@ -10,14 +10,14 @@ export class UpdateTeamUseCase { private readonly teamsRepo: ITeamsRepository, ) {} - async execute(slug: string, userId: string, dto: UpdateTeamDto) { - const team = await this.teamsRepo.findBySlug(slug); + async execute(teamId: string, userId: string, dto: UpdateTeamDto) { + const team = await this.teamsRepo.findById(teamId); if (!team?.id) { throw new BaseException( { code: 'TEAM_NOT_FOUND', - message: `Команда ${slug} не найдена`, + message: `Команда ${teamId} не найдена`, }, HttpStatus.NOT_FOUND, ); @@ -37,10 +37,8 @@ export class UpdateTeamUseCase { ); } - const { tags, ...data } = dto; - try { - const result = await this.teamsRepo.update(team.id, data, tags); + const result = await this.teamsRepo.update(team.id, dto); return { ...result, diff --git a/src/teams/application/use-cases/index.ts b/src/teams/application/use-cases/index.ts index a359b89..08d0693 100644 --- a/src/teams/application/use-cases/index.ts +++ b/src/teams/application/use-cases/index.ts @@ -1,10 +1,8 @@ -import { CheckTeamSlugQuery } from './base/check-team-slug.query'; import { FindTeamQuery } from './base/find-team.query'; import { FindTeamMemberQuery } from './members/find-team-member.query'; import { GetInvitationQuery } from './invitions/get-invitation.query'; import { GetInvitationsQuery } from './invitions/get-invitations.query'; import { GetTeamMembersQuery } from './members/get-team-members.query'; -import { GetAllTagsUseCase } from './base/get-all-tags.use-case'; import { GetMyInvitesUseCase } from './invitions/get-my-invites.use-case'; import { GetMyTeamsUseCase } from './base/get-my-teams.use-case'; @@ -13,20 +11,17 @@ import { CreateTeamUseCase } from './base/create-team.use-case'; import { DeleteTeamUseCase } from './base/delete-team.use-case'; import { RemoveTeamMemberUseCase } from './members/remove-team-member.use-case'; import { SendInvitationUseCase } from './invitions/send-invitation.use-case'; -import { SyncTeamTagsUseCase } from './base/sync-team-tags.use-case'; import { UpdateTeamUseCase } from './base/update-team.use-case'; import { UpdateTeamMemberUseCase } from './members/update-team-member.use-case'; import { UpdateInvitationUseCase } from './invitions/update-invitation.use-case'; import { DeclineInvitationUseCase } from './invitions/decline-invitation.use-case'; export { - CheckTeamSlugQuery, FindTeamQuery, FindTeamMemberQuery, GetInvitationQuery, GetInvitationsQuery, GetTeamMembersQuery, - GetAllTagsUseCase, GetMyInvitesUseCase, GetMyTeamsUseCase, AcceptInvitationUseCase, @@ -34,7 +29,6 @@ export { DeleteTeamUseCase, RemoveTeamMemberUseCase, SendInvitationUseCase, - SyncTeamTagsUseCase, UpdateTeamUseCase, UpdateTeamMemberUseCase, UpdateInvitationUseCase, @@ -42,13 +36,11 @@ export { }; export const TeamQueries = [ - CheckTeamSlugQuery, FindTeamQuery, FindTeamMemberQuery, GetInvitationQuery, GetInvitationsQuery, GetTeamMembersQuery, - GetAllTagsUseCase, GetMyInvitesUseCase, GetMyTeamsUseCase, ]; @@ -59,7 +51,6 @@ export const TeamUseCases = [ DeleteTeamUseCase, RemoveTeamMemberUseCase, SendInvitationUseCase, - SyncTeamTagsUseCase, UpdateTeamUseCase, UpdateTeamMemberUseCase, UpdateInvitationUseCase, diff --git a/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts index e13c696..1a9abaa 100644 --- a/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts @@ -16,8 +16,8 @@ export class DeclineInvitationUseCase { @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, ) {} - async execute(slug: string, code: string, userId: string, userEmail: string) { - const team = await this.getTeamOrThrow(slug); + async execute(teamId: string, code: string, userId: string, userEmail: string) { + const team = await this.getTeamOrThrow(teamId); const invite = await this.getInviteOrThrow(code); this.validateInviteOwnership(invite, team.id); @@ -56,8 +56,8 @@ export class DeclineInvitationUseCase { ); } - private async getTeamOrThrow(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + private async getTeamOrThrow(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, diff --git a/src/teams/application/use-cases/invitions/get-invitation.query.ts b/src/teams/application/use-cases/invitions/get-invitation.query.ts index 2336765..bb7ee3c 100644 --- a/src/teams/application/use-cases/invitions/get-invitation.query.ts +++ b/src/teams/application/use-cases/invitions/get-invitation.query.ts @@ -14,8 +14,8 @@ export class GetInvitationQuery { @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, ) {} - async execute(slug: string, code: string, userId: string, userEmail: string) { - const team = await this.getTeamOrThrow(slug); + async execute(teamId: string, code: string, userId: string, userEmail: string) { + const team = await this.getTeamOrThrow(teamId); const invite = await this.getInviteOrThrow(code); this.validateInviteOwnership(invite, team.id); @@ -24,8 +24,8 @@ export class GetInvitationQuery { return { code, ...invite }; } - private async getTeamOrThrow(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + private async getTeamOrThrow(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, diff --git a/src/teams/application/use-cases/invitions/get-invitations.query.ts b/src/teams/application/use-cases/invitions/get-invitations.query.ts index ab62072..ed1f839 100644 --- a/src/teams/application/use-cases/invitions/get-invitations.query.ts +++ b/src/teams/application/use-cases/invitions/get-invitations.query.ts @@ -14,8 +14,8 @@ export class GetInvitationsQuery { @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, ) {} - async execute(slug: string, userId: string) { - const team = await this.getTeamOrThrow(slug); + async execute(teamId: string, userId: string) { + const team = await this.getTeamOrThrow(teamId); await this.ensureAdminPermissions(team.id, userId); const teamKey = this.TEAM_INVITES_KEY(team.id); @@ -68,8 +68,8 @@ export class GetInvitationsQuery { }; } - private async getTeamOrThrow(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + private async getTeamOrThrow(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, diff --git a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts index f970066..d02e930 100644 --- a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts @@ -29,8 +29,8 @@ export class SendInvitationUseCase { private readonly policy: TeamMemberPolicy, ) {} - async execute(slug: string, inviterId: string, dto: InviteMemberDto) { - const team = await this.getTeamOrThrow(slug); + async execute(teamId: string, inviterId: string, dto: InviteMemberDto) { + const team = await this.getTeamOrThrow(teamId); const inviter = await this.getInviterOrThrow(team.id, inviterId); this.validatePermissions(inviter.role as TeamRole, dto.role as TeamRole); @@ -47,8 +47,8 @@ export class SendInvitationUseCase { return { success: true, message: `Приглашение отправлено на ${dto.email.toLowerCase()}` }; } - private async getTeamOrThrow(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + private async getTeamOrThrow(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, diff --git a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts index 84fde3c..8ccceb0 100644 --- a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts @@ -18,8 +18,8 @@ export class UpdateInvitationUseCase { private readonly policy: TeamMemberPolicy, ) {} - async execute(slug: string, code: string, userId: string, dto: UpdateInvitationDto) { - const team = await this.getTeamOrThrow(slug); + async execute(teamId: string, code: string, userId: string, dto: UpdateInvitationDto) { + const team = await this.getTeamOrThrow(teamId); const member = await this.getMemberOrThrow(team.id, userId); const key = this.INVITES_KEY(code); @@ -37,8 +37,8 @@ export class UpdateInvitationUseCase { }; } - private async getTeamOrThrow(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + private async getTeamOrThrow(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, diff --git a/src/teams/application/use-cases/members/get-team-members.query.ts b/src/teams/application/use-cases/members/get-team-members.query.ts index 8c8ceb8..3919430 100644 --- a/src/teams/application/use-cases/members/get-team-members.query.ts +++ b/src/teams/application/use-cases/members/get-team-members.query.ts @@ -12,12 +12,12 @@ export class GetTeamMembersQuery { private readonly cfg: ConfigService, ) {} - async execute(slug: string) { - const team = await this.teamsRepo.findBySlug(slug); + async execute(teamId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) { throw new BaseException( - { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + { code: 'TEAM_NOT_FOUND', message: `Команда ${teamId} не найдена` }, HttpStatus.NOT_FOUND, ); } diff --git a/src/teams/application/use-cases/members/remove-team-member.use-case.ts b/src/teams/application/use-cases/members/remove-team-member.use-case.ts index a8c219f..aabab45 100644 --- a/src/teams/application/use-cases/members/remove-team-member.use-case.ts +++ b/src/teams/application/use-cases/members/remove-team-member.use-case.ts @@ -12,11 +12,11 @@ export class RemoveTeamMemberUseCase { private readonly policy: TeamMemberPolicy, ) {} - async execute(slug: string, currentUserId: string, targetUserId: string) { - const team = await this.teamsRepo.findBySlug(slug); + async execute(teamId: string, currentUserId: string, targetUserId: string) { + const team = await this.teamsRepo.findById(teamId); if (!team) { throw new BaseException( - { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + { code: 'TEAM_NOT_FOUND', message: `Команда ${teamId} не найдена` }, HttpStatus.NOT_FOUND, ); } diff --git a/src/teams/application/use-cases/members/update-team-member.use-case.ts b/src/teams/application/use-cases/members/update-team-member.use-case.ts index 84813dd..1084ffd 100644 --- a/src/teams/application/use-cases/members/update-team-member.use-case.ts +++ b/src/teams/application/use-cases/members/update-team-member.use-case.ts @@ -13,7 +13,12 @@ export class UpdateTeamMemberUseCase { private readonly teamMemberPolicy: TeamMemberPolicy, ) {} - async execute(slug: string, currentUserId: string, targetUserId: string, dto: UpdateMemberDto) { + async execute( + teamId: string, + currentUserId: string, + targetUserId: string, + dto: UpdateMemberDto, + ) { if (currentUserId === targetUserId) { throw new BaseException( { code: 'SELF_EDIT_RESTRICTED', message: 'Вы не можете редактировать свои данные' }, @@ -21,10 +26,10 @@ export class UpdateTeamMemberUseCase { ); } - const team = await this.teamsRepo.findBySlug(slug); + const team = await this.teamsRepo.findById(teamId); if (!team) { throw new BaseException( - { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + { code: 'TEAM_NOT_FOUND', message: `Команда ${teamId} не найдена` }, HttpStatus.NOT_FOUND, ); } diff --git a/src/teams/domain/entities/teams.domain.ts b/src/teams/domain/entities/teams.domain.ts index 9ffee16..e4d97cb 100644 --- a/src/teams/domain/entities/teams.domain.ts +++ b/src/teams/domain/entities/teams.domain.ts @@ -1,5 +1,5 @@ import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { teams, teamMembers, tags, teamsToTags } from '../../infrastructure/persistence/models'; +import { teams, teamMembers } from '../../infrastructure/persistence/models'; export type Team = InferSelectModel; export type NewTeam = InferInsertModel; @@ -7,16 +7,6 @@ export type NewTeam = InferInsertModel; export type TeamMember = InferSelectModel; export type NewTeamMember = InferInsertModel; -export type Tag = InferSelectModel; -export type NewTag = InferInsertModel; - -export type TeamToTag = InferSelectModel; -export type NewTeamToTag = InferInsertModel; - export type TeamWithMembers = Team & { members: TeamMember[]; }; - -export type TeamWithTags = Team & { - tags: Tag[]; -}; diff --git a/src/teams/domain/repository/teams.repository.interface.ts b/src/teams/domain/repository/teams.repository.interface.ts index 4179800..f7886b4 100644 --- a/src/teams/domain/repository/teams.repository.interface.ts +++ b/src/teams/domain/repository/teams.repository.interface.ts @@ -1,4 +1,4 @@ -import type { Team, NewTeam, NewTeamMember, Tag } from '../entities'; +import type { Team, NewTeam, NewTeamMember } from '../entities'; type TResponse = { success: boolean; teamId: string }; @@ -17,7 +17,6 @@ export type RawMemberRow = { export type RawMemberTeams = { id: string; name: string; - slug: string; description: string | null; avatarUrl: string | null; role: string; @@ -25,28 +24,19 @@ export type RawMemberTeams = { }; export interface ITeamsRepository { - create(ownerId: string, dto: NewTeam, tags?: string[]): Promise; - update(id: string, dto: Partial, tags?: string[]): Promise; + create(ownerId: string, dto: NewTeam): Promise; + update(id: string, dto: Partial): Promise; remove(id: string, userId: string): Promise; - isSlugAvailable(slug: string): Promise; - findMember(teamId: string, userId: string): Promise; findMembers(teamId: string): Promise; - findBySlug(slug: string): Promise; + findById(teamId: string): Promise; findByUser( userId: string, // TODO: ADD ZOD QUERY pagination: { search?: string; limit?: number; offset?: number }, ): Promise; - findAllTags(options: { - search?: string; - limit?: number; - offset?: number; - }): Promise<{ data: Tag[]; total: number }>; - syncTags(teamId: string, tagNames: string[]): Promise; - updateTeamAvatar(teamId: string, url: string): Promise; updateTeamBanner(teamId: string, url: string): Promise; diff --git a/src/teams/infrastructure/listeners/update-media.listener.ts b/src/teams/infrastructure/listeners/update-media.listener.ts index 3a9f2fd..02c4515 100644 --- a/src/teams/infrastructure/listeners/update-media.listener.ts +++ b/src/teams/infrastructure/listeners/update-media.listener.ts @@ -11,7 +11,7 @@ export class UpdateTeamMediaListener extends WorkerHost { constructor( @Inject('ITeamsRepository') private readonly repository: ITeamsRepository, - private readonly poilcy: TeamMemberPolicy, + private readonly policy: TeamMemberPolicy, ) { super(); } @@ -22,21 +22,21 @@ export class UpdateTeamMediaListener extends WorkerHost { const { initiatorId, entity, type, path } = job.data; try { - const teamId = await this.validatePermissionsAndGetTeamId(entity.slug, initiatorId); + const teamId = await this.validatePermissionsAndGetTeamId(entity.id, initiatorId); await this.executeMediaUpdate(teamId, type, path); - await job.log(`Successfully updated ${type} for team ${entity.slug}`); + await job.log(`Successfully updated ${type} for team ${entity.id}`); } catch (error) { await job.log( - `Failed to update ${type} for team ${entity.slug}: ${error instanceof Error ? error.message : String(error)}`, + `Failed to update ${type} for team ${entity.id}: ${error instanceof Error ? error.message : String(error)}`, ); throw error; } } - private async validatePermissionsAndGetTeamId(slug: string, userId: string): Promise { - const team = await this.repository.findBySlug(slug); + private async validatePermissionsAndGetTeamId(teamId: string, userId: string): Promise { + const team = await this.repository.findById(teamId); if (!team) { throw new UnrecoverableError('Команда не найдена'); } @@ -47,7 +47,7 @@ export class UpdateTeamMediaListener extends WorkerHost { throw new UnrecoverableError('Не состоит в этой команде'); } - const hasAccess = this.poilcy.canUpdateMedia(member.role as TeamRole); + const hasAccess = this.policy.canUpdateMedia(member.role as TeamRole); if (!hasAccess) { throw new UnrecoverableError('Недостаточно прав для обновления медиа'); diff --git a/src/teams/infrastructure/persistence/models/index.ts b/src/teams/infrastructure/persistence/models/index.ts index f97c6e3..2a40eb0 100644 --- a/src/teams/infrastructure/persistence/models/index.ts +++ b/src/teams/infrastructure/persistence/models/index.ts @@ -1,2 +1,2 @@ -export { tags, teamMembers, teams, teamsToTags } from './teams.model'; +export { teamMembers, teams } from './teams.model'; export { type TeamRole, roleEnum, statusEnum } from './enums'; diff --git a/src/teams/infrastructure/persistence/models/teams.model.ts b/src/teams/infrastructure/persistence/models/teams.model.ts index f79de53..751f983 100644 --- a/src/teams/infrastructure/persistence/models/teams.model.ts +++ b/src/teams/infrastructure/persistence/models/teams.model.ts @@ -2,8 +2,6 @@ import { primaryKey, timestamp, text, varchar, index } from 'drizzle-orm/pg-core import { createId } from '@paralleldrive/cuid2'; import { roleEnum, statusEnum } from './enums'; import { baseSchema, users } from '@shared/entities'; -import { uniqueIndex } from 'drizzle-orm/pg-core'; -import { isNull } from 'drizzle-orm'; export const teams = baseSchema.table( 'teams', @@ -11,7 +9,6 @@ export const teams = baseSchema.table( id: text('id') .primaryKey() .$defaultFn(() => createId()), - slug: varchar('slug', { length: 120 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), description: text('description'), avatarUrl: text('avatar_url'), @@ -26,8 +23,6 @@ export const teams = baseSchema.table( deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), }, (t) => ({ - uniqueActiveSlug: uniqueIndex('team_active_slug_idx').on(t.slug).where(isNull(t.deletedAt)), - slugIdx: index('team_slug_idx').on(t.slug), ownerIdx: index('team_owner_idx').on(t.ownerId), softDeleteIdx: index('team_deleted_at_idx').on(t.deletedAt), }), @@ -55,26 +50,3 @@ export const teamMembers = baseSchema.table( userRoleIdx: index('member_role_idx').on(t.userId, t.role), }), ); - -export const tags = baseSchema.table('tags', { - id: text('id') - .primaryKey() - .$defaultFn(() => createId()), - name: varchar('name', { length: 50 }).unique().notNull(), -}); - -export const teamsToTags = baseSchema.table( - 'teams_to_tags', - { - teamId: text('team_id') - .references(() => teams.id, { onDelete: 'cascade' }) - .notNull(), - tagId: text('tag_id') - .references(() => tags.id, { onDelete: 'cascade' }) - .notNull(), - }, - (t) => ({ - pk: primaryKey({ columns: [t.teamId, t.tagId] }), - tagIdx: index('teams_to_tags_tag_id_idx').on(t.tagId), - }), -); diff --git a/src/teams/infrastructure/persistence/repositories/teams.repository.ts b/src/teams/infrastructure/persistence/repositories/teams.repository.ts index 1e545c6..8b51c7b 100644 --- a/src/teams/infrastructure/persistence/repositories/teams.repository.ts +++ b/src/teams/infrastructure/persistence/repositories/teams.repository.ts @@ -2,7 +2,7 @@ import { Inject } from '@nestjs/common'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import * as schema from '../models'; import * as scUsers from '@core/user/infrastructure/persistence/models'; -import { and, asc, count, desc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm'; +import { and, desc, eq, ilike, isNull } from 'drizzle-orm'; import type { NewTeam, NewTeamMember, Team, TeamMember } from '@core/teams/domain/entities'; import { ITeamsRepository } from '@core/teams/domain/repository'; @@ -12,15 +12,6 @@ export class TeamsRepository implements ITeamsRepository { private readonly db: DatabaseService, ) {} - public isSlugAvailable = async (slug: string) => { - const result = await this.db - .select({ id: schema.teams.id }) - .from(schema.teams) - .where(eq(schema.teams.slug, slug)); - - return result.length === 0; - }; - public addMember = async (dto: NewTeamMember) => { const result = await this.db .insert(schema.teamMembers) @@ -32,32 +23,13 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public create = async (ownerId: string, dto: NewTeam, tags?: string[]) => { + public create = async (ownerId: string, dto: NewTeam) => { return this.db.transaction(async (tx) => { const [{ teamId }] = await tx .insert(schema.teams) .values({ ...dto, ownerId }) .returning({ teamId: schema.teams.id }); - if (tags?.length) { - const names = tags.map((n) => ({ name: n })); - - const insertedTags = await tx - .insert(schema.tags) - .values(names) - .onConflictDoUpdate({ - target: schema.tags.name, - set: { name: sql`${schema.tags.name}` }, - }) - .returning({ id: schema.tags.id }); - - if (insertedTags.length > 0) { - const tags = insertedTags.map((t) => ({ teamId, tagId: t.id })); - - await tx.insert(schema.teamsToTags).values(tags); - } - } - await tx.insert(schema.teamMembers).values({ teamId, userId: ownerId, @@ -73,7 +45,7 @@ export class TeamsRepository implements ITeamsRepository { }); }; - public update = async (id: string, dto: Partial, tags?: string[]) => { + public update = async (id: string, dto: Partial) => { return this.db.transaction(async (tx) => { const [{ teamId }] = await tx .update(schema.teams) @@ -81,10 +53,6 @@ export class TeamsRepository implements ITeamsRepository { .where(eq(schema.teams.id, id)) .returning({ teamId: schema.teams.id }); - if (tags?.length) { - // TODO: FEAT AT FEATURE - } - return { success: true, teamId, @@ -92,14 +60,11 @@ export class TeamsRepository implements ITeamsRepository { }); }; - public remove = async (teamId: string, userId) => { - const suffix = Date.now().toString(); - + public remove = async (teamId: string, userId: string) => { const result = await this.db .update(schema.teams) .set({ deletedAt: new Date().toISOString(), - slug: sql`${schema.teams.slug} || '-' || ${suffix}`, }) .where(and(eq(schema.teams.id, teamId), eq(schema.teams.ownerId, userId))); @@ -140,7 +105,6 @@ export class TeamsRepository implements ITeamsRepository { .select({ id: schema.teams.id, name: schema.teams.name, - slug: schema.teams.slug, description: schema.teams.description, avatarUrl: schema.teams.avatarUrl, role: schema.teamMembers.role, @@ -156,34 +120,8 @@ export class TeamsRepository implements ITeamsRepository { return query; }; - public findAllTags = async (options: { search?: string; limit?: number; offset?: number }) => { - const cleanSearch = options.search?.trim(); - const escapedSearch = cleanSearch?.replace(/([%_\\])/g, '\\$1'); - - const whereCondition = escapedSearch - ? ilike(schema.tags.name, `%${escapedSearch}%`) - : undefined; - - const [data, [{ total }]] = await Promise.all([ - this.db - .select() - .from(schema.tags) - .where(whereCondition) - .limit(options.limit) - .offset(options.offset) - .orderBy(asc(schema.tags.name)), - - this.db.select({ total: count() }).from(schema.tags).where(whereCondition), - ]); - - return { - data, - total: Number(total ?? 0), - }; - }; - - public findBySlug = async (slug: string) => { - const [team] = await this.db.select().from(schema.teams).where(eq(schema.teams.slug, slug)); + public findById = async (teamId: string) => { + const [team] = await this.db.select().from(schema.teams).where(eq(schema.teams.id, teamId)); if (!team) return null; return team; }; @@ -198,32 +136,6 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public syncTags = async (teamId: string, tagNames: string[]) => { - await this.db.transaction(async (tx) => { - await tx.delete(schema.teamsToTags).where(eq(schema.teamsToTags.teamId, teamId)); - - if (tagNames.length === 0) { - return; - } - - await tx - .insert(schema.tags) - .values(tagNames.map((name) => ({ name }))) - .onConflictDoNothing({ target: schema.tags.name }); - - const existingTags = await tx - .select({ id: schema.tags.id }) - .from(schema.tags) - .where(inArray(schema.tags.name, tagNames)); - - await tx - .insert(schema.teamsToTags) - .values(existingTags.map((tag) => ({ teamId, tagId: tag.id }))); - }); - - return true; - }; - public updateMember = async (teamId: string, userId: string, dto: Partial) => { const { role, status } = dto; diff --git a/src/teams/teams.module.ts b/src/teams/teams.module.ts index 995aa92..922783f 100644 --- a/src/teams/teams.module.ts +++ b/src/teams/teams.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { TeamsInvitationsController, - TeamsSettingsController, TeamsMembersController, TeamsController, MeController, @@ -25,7 +24,6 @@ const REPOSITORY = { provide: 'ITeamsRepository', useClass: TeamsRepository }; ], controllers: [ TeamsInvitationsController, - TeamsSettingsController, TeamsMembersController, TeamsController, MeController,