Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "dist/server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npx prisma generate --schema ./prisma/schema && tsup src --out-dir dist --sourcemap inline",
"build": "pnpm exec prisma generate --schema ./prisma/schema && tsup src --out-dir dist --sourcemap inline",
"dev": "ts-node-dev --respawn -r tsconfig-paths/register src/server.ts",
"start": "node -r tsconfig-paths/register dist/server.js",
"prisma:migrate": "prisma migrate dev --schema ./prisma/schema",
Expand All @@ -32,7 +32,6 @@
"google-auth-library": "^10.5.0",
"jsonwebtoken": "^9.0.3",
"pino-pretty": "^13.1.3",
"prisma": "^7.4.2",
"resend": "^6.9.3",
"uuid": "^13.0.0",
"zod": "^4.1.12"
Expand All @@ -45,6 +44,7 @@
"eslint-plugin-perfectionist": "^4.15.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-unused-imports": "^4.2.0",
"prisma": "^7.4.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { OrganizationsRepository } from '@/adapters/outbound/prisma/repositories
import { Route } from '@/adapters/inbound/http/decorators/route-decorator'
import type { FastifyReply, FastifyRequest } from 'fastify'
import { GetAllOrganizationsUseCase } from '@/core/use-cases/organizations/get-all-organizations'
import { getAllOrganizationsQuerySchema } from './schema'

export class GetAllOrganizationsController {
private organizationRepository: OrganizationsRepository
Expand All @@ -14,7 +15,9 @@ export class GetAllOrganizationsController {

@Route('GET', '/organizations')
async execute(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const response = await this.useCase.execute()
const { name, ong_type } = getAllOrganizationsQuerySchema.parse(request.query)

const response = await this.useCase.execute({ name, ong_type })

return reply.status(200).send(response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod'

const ongCategoryEnum = z.enum([
'animais',
'meioAmbiente',
'educacao',
'saude',
'direitosHumanos',
'combateAFome',
'criancasEAdolescentes',
'idosos',
'pessoasComDeficiencia',
'moradoresDeRua',
'igualdadeDeGenero',
'refugiadosEImigrantes',
'protecaoAnimal',
'desenvolvimentoComunitario',
'culturaEArte',
'esporteEInclusao',
'voluntariadoEDoacoes',
'tecnologiaSocial',
'direitosDasMulheres',
'outros'
])

export const getAllOrganizationsQuerySchema = z.object({
name: z.string().optional(),
ong_type: ongCategoryEnum.optional()
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Prisma } from '@prisma-generated'
import { OngCategory, Prisma } from '@prisma-generated'
import { randomUUID } from 'crypto'
import {
OrganizationInterface,
Expand Down Expand Up @@ -65,8 +65,14 @@ export class OrganizationsRepository implements OrganizationInterface {
})
}

getAllOrganizations = async () => {
getAllOrganizations = async (filters?: { name?: string; ong_type?: OngCategory }) => {
return await prisma.organization.findMany({
where: {
organization_profile: {
...(filters?.name && { name: { contains: filters.name, mode: 'insensitive' } }),
...(filters?.ong_type && { ong_type: filters.ong_type })
}
},
Comment on lines +70 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify optional relation cardinality and current repository filter shape.
rg -n "organization_profile OrganizationProfile\\?" prisma/schema/account.prisma -C3
rg -n "getAllOrganizations\\s*=\\s*async" src/adapters/outbound/prisma/repositories/organization-repository.ts -A30

Repository: ifsp-projects/backend

Length of output: 1177


🏁 Script executed:

#!/bin/bash
# 1. Check if organization_profile can be null/missing (relation cardinality)
rg "organization_profile" prisma/schema/account.prisma -B5 -A5

# 2. Check if there are tests for getAllOrganizations
fd ".*test.*" -e ts -e js | xargs grep -l "getAllOrganizations" 2>/dev/null | head -5

# 3. Look for how organizations are created/seeded
fd ".*seed.*" -e ts -e js | head -5

# 4. Check for any migration or validation logic around organization_profile
rg "organization_profile" src/ -l | head -10

Repository: ifsp-projects/backend

Length of output: 1197


🏁 Script executed:

#!/bin/bash
# 1. Check the interface/port definition
cat src/core/domain/ports/repositories/organization-repository.ts

# 2. Look for test files
fd "getAllOrganizations|organization.*test" . -e ts -e js

# 3. Check how this method is used in controllers
rg "getAllOrganizations" src/ -B2 -A2

Repository: ifsp-projects/backend

Length of output: 9161


🏁 Script executed:

#!/bin/bash
# 1. Check the actual usage in the controller to see what return type is expected
cat src/adapters/inbound/http/controllers/organizations/get-all-organizations/index.ts

# 2. Look for any test files to understand intended behavior
find . -type f -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "getAllOrganizations" 2>/dev/null | head -5

# 3. Check if there's any documentation or comments about the relation
rg "organization_profile" src/adapters/outbound/prisma/repositories/organization-repository.ts -B3 -A10

Repository: ifsp-projects/backend

Length of output: 2745


🏁 Script executed:

#!/bin/bash
# Verify if organizations can actually exist without profiles
# Check creation patterns and any data about orgs without profiles

# 1. Look at how organizations are typically created
rg "prisma.organization.create|Organization.create" src/ -B3 -A5

# 2. Check if there are migrations that might shed light on this
find . -name "*.sql" -o -path "*/migrations/*" 2>/dev/null | head -10

# 3. Check the organizational structure to understand the domain
cat src/core/domain/ports/repositories/organization-profile-repository.ts | head -30

# 4. Look for any explicit checks or comments about organization without profile
rg "organization.*profile.*null|organization.*without.*profile" src/ -i

Repository: ifsp-projects/backend

Length of output: 3141


🏁 Script executed:

#!/bin/bash
# Critical check: Does Prisma's empty object filter actually exclude organizations without profiles?
# Let's look at any documentation or tests that show the intended behavior

# 1. Check the return type expectations
rg "getAllOrganizations.*Promise" src/core -A1

# 2. Look for any seeders or fixtures that might show the expected data state
find . -name "*seed*" -o -name "*fixture*" | grep -E "\.(ts|js)$"

# 3. Check if there's any explicit statement about org-profile requirement
rg "organization_profile.*required|must.*profile|without.*profile" src/ -i

# 4. Check the interface return type - it returns Organization[], not OrganizationWithProfileInclude
# This is a key detail. Let's verify what Organization vs OrganizationWithProfileInclude is
rg "type Organization[^P]|interface Organization" src/ -B2 -A5

Repository: ifsp-projects/backend

Length of output: 2793


🏁 Script executed:

#!/bin/bash
# Check the actual return type mismatch and Prisma behavior documentation
# The critical issue: does where: { organization_profile: {} } exclude orgs without profiles?

# 1. Verify the type mismatch - interface says Organization[], implementation includes profile
rg "getAllOrganizations.*=" src/adapters/outbound/prisma/repositories/organization-repository.ts -A25 | grep -E "return|include|where"

# 2. Check if there's any documentation or comments about Prisma relation filtering
rg "organization_profile.*where|where.*organization_profile" src/ -B2 -A2

# 3. Look for any GitHub issues or comments about this
find . -name "*.md" -o -name "*.txt" | xargs grep -i "organization.*profile.*filter" 2>/dev/null

# 4. Check the return type signature consistency
cat > /tmp/check_types.ts << 'EOF'
// The interface definition says:
// getAllOrganizations: (...) => Promise<Organization[]>

// But the implementation does:
// return await prisma.organization.findMany({
//   where: { organization_profile: { ... } },
//   include: { organization_profile: true }
// })

// This should return OrganizationWithProfileInclude[] or Organization[]?
// Type mismatch suggests the implementation is wrong OR the interface is wrong
EOF

# 5. Verify: when organization_profile is in where clause with empty object, 
# does Prisma exclude orgs without profile? This is the core issue.
# From Prisma docs: where relation: {} on optional relation DOES match records WITH the relation
# But to get ALL records (with or without), relation filter should not be in where at all
echo "ANALYSIS: In Prisma, where: { optional_relation: {...} } only matches records WITH that relation"
echo "Therefore, the current code WILL exclude organizations without profiles"

Repository: ifsp-projects/backend

Length of output: 389


🏁 Script executed:

#!/bin/bash
# Check if the return type is actually correct or if there's a mismatch
# The interface says Organization[] but implementation returns with includes

# 1. Look at what getAllOrganizations actually returns in practice
rg "getAllOrganizations" src/core/use-cases/organizations/get-all-organizations/ -A10

# 2. Check how the response is used by the controller
rg "useCase.execute" src/adapters/inbound/http/controllers/organizations/get-all-organizations/ -B3 -A5

# 3. Check if Organization type includes organization_profile or not
# This will tell us if the implementation matches the interface
rg "export.*Organization[^P].*=|type Organization[^P]" src/ -l

# 4. Let's check the generated Prisma types
find . -path "*prisma*" -name "*.ts" | grep -i "index\|types" | head -5

Repository: ifsp-projects/backend

Length of output: 3621


🏁 Script executed:

#!/bin/bash
# Final verification: confirm Prisma's behavior with empty relation objects in where clauses
# and verify if this impacts actual behavior

# 1. Check if organization_profile is ACTUALLY optional or if it's always created with organization
rg "organization_profile" prisma/schema/account.prisma -B5 -A5 | grep -E "model Organization|model OrganizationProfile|organization_profile"

# 2. Look at the full getAllOrganizations implementation once more for precision
sed -n '68,80p' src/adapters/outbound/prisma/repositories/organization-repository.ts

# 3. Check if any tests actually call getAllOrganizations without filters
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) -exec grep -l "getAllOrganizations" {} \;

Repository: ifsp-projects/backend

Length of output: 715


Filtering always requires organization_profile, unintentionally excluding organizations without profiles.

When no filters are provided to getAllOrganizations, the where: { organization_profile: {} } clause still matches only organizations that have a profile, silently excluding organizations without one. Since organization_profile is optional, this causes unexpected data loss for plain GET /organizations requests.

Conditionally apply the organization_profile filter only when actual filters exist (name or ong_type). When no filters are provided, omit the where clause entirely to return all organizations.

Suggested fix
   getAllOrganizations = async (filters?: { name?: string; ong_type?: OngCategory }) => {
+    const name = filters?.name?.trim()
+    const hasProfileFilters = Boolean(name || filters?.ong_type)
+
     return await prisma.organization.findMany({
-      where: {
-        organization_profile: {
-          ...(filters?.name && { name: { contains: filters.name, mode: 'insensitive' } }),
-          ...(filters?.ong_type && { ong_type: filters.ong_type })
-        }
-      },
+      where: hasProfileFilters
+        ? {
+            organization_profile: {
+              is: {
+                ...(name && { name: { contains: name, mode: 'insensitive' } }),
+                ...(filters?.ong_type && { ong_type: filters.ong_type })
+              }
+            }
+          }
+        : undefined,
       include: {
         organization_profile: true
       }
     })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapters/outbound/prisma/repositories/organization-repository.ts` around
lines 70 - 75, The current getAllOrganizations query always includes a where: {
organization_profile: { ... } } clause which excludes organizations without
profiles; change getAllOrganizations so you only add the organization_profile
predicate when filters?.name or filters?.ong_type are present: build the Prisma
where object conditionally (omit organization_profile entirely when no filters)
and only merge ...(filters?.name && ...) and ...(filters?.ong_type && ...) into
organization_profile when filters exist, ensuring the final query omits the
where key when no profile filters are applied.

include: {
organization_profile: true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Organization, Prisma } from '@prisma-generated'
import { OngCategory, Organization, Prisma } from '@prisma-generated'

export type OrganizationWithProfileInclude = Prisma.OrganizationGetPayload<{
include: {
Expand All @@ -10,7 +10,7 @@ export interface OrganizationInterface {
createOrganization: (
payload: Prisma.OrganizationUncheckedCreateInput
) => Promise<Organization>
getAllOrganizations: () => Promise<Organization[]>
getAllOrganizations: (filters?: { name?: string; ong_type?: OngCategory }) => Promise<Organization[]>
getOrganizationById: (
id: string
) => Promise<OrganizationWithProfileInclude | null>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { OrganizationDoesNotExistError } from '@/core/domain/exceptions/organizations'
import { OrganizationsRepository } from '@/adapters/outbound/prisma/repositories/organization-repository'
import { OngCategory } from '@prisma-generated'
import { GetAllOrganizationsUseCaseReturn } from './types'

export class GetAllOrganizationsUseCase {
constructor(
protected readonly organizationsRepository: OrganizationsRepository
) {}
) { }

execute = async (): Promise<GetAllOrganizationsUseCaseReturn> => {
const data = await this.organizationsRepository.getAllOrganizations()
execute = async (filters?: { name?: string; ong_type?: OngCategory }): Promise<GetAllOrganizationsUseCaseReturn> => {
const data = await this.organizationsRepository.getAllOrganizations(filters)

if (!data) {
throw new OrganizationDoesNotExistError()
Expand Down
Loading