Skip to main content

Project Model

The Project model represents projects synced from Plane.so, containing project metadata and relationships to work items and labels.

Schema Definition

model Project {
id String @id @default(uuid())
planeId String @unique
name String?
identifier String?
description String?
totalMembers Int? @map("total_members")
totalCycles Int? @map("total_cycles")
totalModules Int? @map("total_modules")
isMember Boolean? @map("is_member")
integrationId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
labels Label[]
integration PlaneIntegration @relation(fields: [integrationId], references: [id], onDelete: Cascade)
workItems WorkItem[]

@@map("projects")
}

Field Descriptions

Primary Identification

FieldTypeDescription
idStringUUID primary key
planeIdStringUnique Plane.so project identifier

Basic Information

FieldTypeDescription
nameString?Project name
identifierString?Project identifier/code (e.g., "MOBILE", "WEB")
descriptionString?Project description

Statistics

FieldTypeDescription
totalMembersInt?Total number of project members
totalCyclesInt?Total number of development cycles
totalModulesInt?Total number of modules/components

Permissions

FieldTypeDescription
isMemberBoolean?Whether current user is a member of this project

Relations

FieldTypeDescription
integrationIdStringReference to PlaneIntegration.id

Metadata

FieldTypeDescription
createdAtDateTimeProject sync timestamp
updatedAtDateTimeLast update timestamp

Relationships

Belongs To

  • integration: PlaneIntegration - The Plane integration this project belongs to (Cascade delete)

One-to-Many

  • labels: Label[] - Labels associated with this project
  • workItems: WorkItem[] - Work items in this project

Usage Examples

Create Synced Project

const project = await prisma.project.create({
data: {
planeId: "plane-project-123",
name: "Mobile App Development",
identifier: "MOBILE",
description: "Main mobile application project",
totalMembers: 8,
totalCycles: 12,
totalModules: 5,
isMember: true,
integrationId: "plane-integration-456"
}
});

Update Project Statistics

await prisma.project.update({
where: { planeId: "plane-project-123" },
data: {
totalMembers: 9,
totalWorkItems: 45,
updatedAt: new Date()
}
});

Get Projects with Relations

const projects = await prisma.project.findMany({
include: {
integration: true,
labels: {
select: {
name: true,
color: true
}
},
workItems: {
where: { state: "in_progress" },
select: {
name: true,
priority: true
}
},
_count: {
select: {
workItems: true,
labels: true
}
}
}
});

Find Projects by Integration

const integrationProjects = await prisma.project.findMany({
where: {
integrationId: "plane-integration-456",
isMember: true
},
orderBy: { name: "asc" }
});

Sync Logic

Initial Project Sync

async function syncProjects(integrationId: string) {
const integration = await prisma.planeIntegration.findUnique({
where: { id: integrationId }
});

// Fetch projects from Plane API
const projects = await fetchPlaneProjects(integration);

// Upsert projects in database
for (const planeProject of projects) {
await prisma.project.upsert({
where: { planeId: planeProject.id },
update: {
name: planeProject.name,
description: planeProject.description,
totalMembers: planeProject.total_members,
isMember: planeProject.is_member,
updatedAt: new Date()
},
create: {
planeId: planeProject.id,
name: planeProject.name,
identifier: planeProject.identifier,
description: planeProject.description,
totalMembers: planeProject.total_members,
isMember: planeProject.is_member,
integrationId: integrationId
}
});
}
}

Project Statistics Calculation

async function updateProjectStats(projectId: string) {
const workItemStats = await prisma.workItem.groupBy({
by: ['state'],
where: { projectId },
_count: true
});

const labelCount = await prisma.label.count({
where: { projectId }
});

await prisma.project.update({
where: { id: projectId },
data: {
totalWorkItems: workItemStats.reduce((sum, stat) => sum + stat._count, 0),
// Update other stats...
updatedAt: new Date()
}
});
}

Performance Considerations

Indexing

  • Unique index on planeId for API sync operations
  • Index on integrationId for filtering by integration
  • Index on isMember for member-only queries
  • Index on updatedAt for sync status queries

Query Optimization

  • Use select to fetch only needed fields
  • Include relations only when necessary
  • Use pagination for large project lists
  • Cache frequently accessed project data

Bulk Operations

// Bulk update project statistics
const projectUpdates = projects.map(project => ({
where: { planeId: project.planeId },
data: {
totalMembers: project.total_members,
updatedAt: new Date()
}
}));

await prisma.$transaction(
projectUpdates.map(update => prisma.project.update(update))
);

Best Practices

Data Consistency

  • Always sync through the integration API
  • Validate data before saving
  • Handle API errors gracefully
  • Implement retry logic for failed syncs

Access Control

  • Respect Plane project permissions
  • Filter projects based on user membership
  • Implement proper authorization checks

Monitoring

  • Track sync success/failure rates
  • Monitor API rate limit usage
  • Log project changes for auditing
  • Alert on sync failures

Cleanup

  • Remove projects that are no longer accessible
  • Archive old project data if needed
  • Clean up orphaned records regularly