Skip to content

Commit

Permalink
Order project list by edited at (#623)
Browse files Browse the repository at this point in the history
* Order project list by edited at

* Add tests
  • Loading branch information
neoxelox authored Nov 18, 2024
1 parent a91ff8d commit 1ecff0b
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 120 deletions.
3 changes: 1 addition & 2 deletions apps/web/src/app/(private)/_data-access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ export const getFirstProjectCached = cache(
export const getActiveProjectsCached = cache(
async ({ workspaceId }: { workspaceId: number }) => {
const projectsScope = new ProjectsRepository(workspaceId)
const result =
await projectsScope.findAllActiveDocumentsWithAgreggatedData()
const result = await projectsScope.findAllActiveWithAgreggatedData()
const projects = result.unwrap()

return projects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { ROUTES } from '$/services/routes'
import Link from 'next/link'

type ProjectWithAgreggatedData = Project & {
documentCount: number
lastCreatedAtDocument: Date | null
lastEditedAt: Date | null
}
export function ProjectsTable({
projects,
Expand All @@ -31,7 +30,6 @@ export function ProjectsTable({
<TableHeader>
<TableRow verticalPadding>
<TableHead>Name</TableHead>
<TableHead>Prompts</TableHead>
<TableHead>Edited</TableHead>
<TableHead>Created</TableHead>
<TableHead />
Expand All @@ -52,12 +50,7 @@ export function ProjectsTable({
</TableCell>
<TableCell>
<Text.H5 color='foregroundMuted'>
{project.documentCount || '-'}
</Text.H5>
</TableCell>
<TableCell>
<Text.H5 color='foregroundMuted'>
{relativeTime(project.lastCreatedAtDocument)}
{relativeTime(project.lastEditedAt)}
</Text.H5>
</TableCell>
<TableCell>
Expand Down
183 changes: 117 additions & 66 deletions packages/core/src/repositories/projectsRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,161 @@
import { and, eq } from 'drizzle-orm'
import { beforeEach, describe, expect, it } from 'vitest'

import { Workspace } from '../browser'
import { ProviderApiKey, User, Workspace } from '../browser'
import { database } from '../client'
import { Providers } from '../constants'
import { createProject, createWorkspace, helpers } from '../tests/factories'
import { documentVersions } from '../schema'
import { mergeCommit } from '../services/commits'
import {
createDocumentVersion,
createDraft,
createProject,
createProviderApiKey,
createWorkspace,
destroyDocumentVersion,
helpers,
updateDocumentVersion,
} from '../tests/factories'
import { ProjectsRepository } from './projectsRepository'

describe('ProjectsRepository', async () => {
let repository: ProjectsRepository
let workspace: Workspace

const provider = { type: Providers.OpenAI, name: 'OpenAI' }
let user: User
let provider: ProviderApiKey

beforeEach(async () => {
const { workspace: newWorkspace } = await createWorkspace()
workspace = newWorkspace
const { workspace: w, userData: u } = await createWorkspace()
workspace = w
user = u
provider = await createProviderApiKey({
workspace,
type: Providers.OpenAI,
name: 'OpenAI',
user,
})
repository = new ProjectsRepository(workspace.id)
})

describe('findAllActiveDocumentsWithAgreggatedData', () => {
it('should return active projects with aggregated data', async () => {
// Create test data
const { project: project1 } = await createProject({
describe('findAllActiveWithAgreggatedData', () => {
it('returns active projects ordered by lastEditedAt and createdAt', async () => {
// When there are no projects
let result = await repository.findAllActiveWithAgreggatedData()

expect(result.ok).toBe(true)
expect(result.unwrap()).toEqual([])

// After creating project0 and project1
let { project: project0, commit: commit0 } = await createProject({
name: 'project0',
workspace,
providers: [provider],
documents: {
foo: helpers.createPrompt({ provider: provider.name }),
},
documents: {},
skipMerge: true,
})

const { project: project2 } = await createProject({
let { project: project1, commit: commit1 } = await createProject({
name: 'project1',
workspace,
documents: {
bar: helpers.createPrompt({ provider: provider.name }),
},
documents: {},
skipMerge: true,
})

// Execute the method
const result = await repository.findAllActiveDocumentsWithAgreggatedData()
result = await repository.findAllActiveWithAgreggatedData()

// Assert the result
expect(result.ok).toBe(true)
const projects = result.unwrap()

expect(projects).toHaveLength(2)

const project1Result = projects.find((p) => p.id === project1.id)
expect(project1Result).toBeDefined()
expect(project1Result?.documentCount).toBe(1)
expect(project1Result?.lastCreatedAtDocument).toBeDefined()
expect(result.unwrap()).toEqual([
{ ...project1, lastEditedAt: null },
{ ...project0, lastEditedAt: null },
])

const project2Result = projects.find((p) => p.id === project2.id)
expect(project2Result).toBeDefined()
expect(project2Result?.documentCount).toBe(1)
expect(project2Result?.lastCreatedAtDocument).toBeDefined()
})

it('should return projects with zero document count when no documents exist', async () => {
const { project } = await createProject({
// After adding a document to project1 and modifying it
let { documentVersion: document1 } = await createDocumentVersion({
workspace,
providers: [provider],
user,
commit: commit1,
path: 'document1',
content: helpers.createPrompt({ provider, content: 'content1' }),
})
document1 = await updateDocumentVersion({
document: document1,
commit: commit1,
content: helpers.createPrompt({ provider, content: 'newContent1' }),
})

const result = await repository.findAllActiveDocumentsWithAgreggatedData()
result = await repository.findAllActiveWithAgreggatedData()

expect(result.ok).toBe(true)
const projects = result.unwrap()
expect(result.unwrap()).toEqual([
{ ...project1, lastEditedAt: document1.updatedAt },
{ ...project0, lastEditedAt: null },
])

// After publishing project1 and adding a document to project0
commit1 = await mergeCommit(commit1).then((r) => r.unwrap())
document1 = await database
.select()
.from(documentVersions)
.where(
and(
eq(documentVersions.documentUuid, document1.documentUuid),
eq(documentVersions.commitId, commit1.id),
),
)
.then((d) => d[0]!)
let { documentVersion: document0 } = await createDocumentVersion({
workspace,
user,
commit: commit0,
path: 'document0',
content: helpers.createPrompt({ provider, content: 'content0' }),
})

expect(projects).toHaveLength(1)
expect(projects[0]?.id).toBe(project.id)
expect(projects[0]?.documentCount).toBe(0)
expect(projects[0]?.lastCreatedAtDocument).toBeNull()
})
result = await repository.findAllActiveWithAgreggatedData()

it('should include projects without merged commits', async () => {
await createProject({
expect(result.ok).toBe(true)
expect(result.unwrap()).toEqual([
{ ...project0, lastEditedAt: document0.updatedAt },
{ ...project1, lastEditedAt: document1.updatedAt },
])

// After creating project2 and deleting a document from project1
commit1 = await createDraft({ project: project1, user }).then(
(c) => c.commit,
)
let { project: project2 } = await createProject({
name: 'project2',
workspace,
providers: [provider],
documents: {},
skipMerge: true,
})
document1 = await destroyDocumentVersion({
document: document1,
commit: commit1,
}).then((d) => d!)

const result = await repository.findAllActiveDocumentsWithAgreggatedData()
result = await repository.findAllActiveWithAgreggatedData()

expect(result.ok).toBe(true)
const projects = result.unwrap()

expect(projects).toHaveLength(1)
})
expect(result.unwrap()).toEqual([
{ ...project1, lastEditedAt: document1.updatedAt },
{ ...project2, lastEditedAt: null },
{ ...project0, lastEditedAt: document0.updatedAt },
])

it('should not include deleted projects', async () => {
const { project: deletedProject } = await createProject({
// After creating project3 and deleting it
await createProject({
name: 'project3',
workspace,
deletedAt: new Date(),
})

const { project: activeProject } = await createProject({
workspace,
})

const result = await repository.findAllActiveDocumentsWithAgreggatedData()
result = await repository.findAllActiveWithAgreggatedData()

expect(result.ok).toBe(true)
const projects = result.unwrap()

expect(projects).toHaveLength(1)
expect(projects[0]?.id).toBe(activeProject.id)
expect(projects.find((p) => p.id === deletedProject.id)).toBeUndefined()
expect(result.unwrap()).toEqual([
{ ...project1, lastEditedAt: document1.updatedAt },
{ ...project2, lastEditedAt: null },
{ ...project0, lastEditedAt: document0.updatedAt },
])
})
})
})
50 changes: 11 additions & 39 deletions packages/core/src/repositories/projectsRepository.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
and,
count,
eq,
getTableColumns,
isNotNull,
isNull,
max,
sql,
} from 'drizzle-orm'
import { and, desc, eq, getTableColumns, isNull, max, sql } from 'drizzle-orm'

import { Project } from '../browser'
import { NotFoundError, Result } from '../lib'
Expand Down Expand Up @@ -58,54 +49,35 @@ export class ProjectsRepository extends RepositoryLegacy<typeof tt, Project> {
return Result.ok(result)
}

async findAllActiveDocumentsWithAgreggatedData() {
const lastMergedCommit = this.db.$with('lastMergedCommit').as(
this.db
.select({
projectId: commits.projectId,
maxVersion: max(commits.version).as('maxVersion'),
})
.from(commits)
.where(and(isNull(commits.deletedAt), isNotNull(commits.mergedAt)))
.groupBy(commits.projectId),
)
async findAllActiveWithAgreggatedData() {
const aggredatedData = this.db.$with('aggredatedData').as(
this.db
.with(lastMergedCommit)
.select({
id: this.scope.id,
documentCount: count(documentVersions.id).as('documentCount'),
lastCreatedAtDocument: max(documentVersions.createdAt).as(
'lastCreatedAtDocument',
),
lastEditedAt: max(documentVersions.updatedAt).as('lastEditedAt'),
})
.from(this.scope)
.innerJoin(commits, eq(commits.projectId, this.scope.id))
.innerJoin(
lastMergedCommit,
and(
eq(lastMergedCommit.projectId, this.scope.id),
eq(commits.version, lastMergedCommit.maxVersion),
),
)
.innerJoin(documentVersions, eq(documentVersions.commitId, commits.id))
.where(isNull(this.scope.deletedAt))
.where(and(isNull(this.scope.deletedAt), isNull(commits.deletedAt)))
.groupBy(this.scope.id),
)

const result = await this.db
.with(aggredatedData)
.select({
...this.scope._.selectedFields,
documentCount:
sql<number>`CAST(CASE WHEN ${aggredatedData.documentCount} IS NULL THEN 0 ELSE ${aggredatedData.documentCount} END AS INTEGER)`.as(
'documentCount',
),
lastCreatedAtDocument: aggredatedData.lastCreatedAtDocument,
lastEditedAt: aggredatedData.lastEditedAt,
})
.from(this.scope)
.leftJoin(aggredatedData, eq(aggredatedData.id, this.scope.id))
.where(isNull(this.scope.deletedAt))
.orderBy(
desc(
sql`COALESCE(${aggredatedData.lastEditedAt}, ${this.scope.createdAt})`,
),
desc(this.scope.id),
)

return Result.ok(result)
}
Expand Down
Loading

0 comments on commit 1ecff0b

Please sign in to comment.