diff --git a/packages/twenty-utils/dangerfile.ts b/packages/twenty-utils/dangerfile.ts index 2e7d43eb7..424fff73f 100644 --- a/packages/twenty-utils/dangerfile.ts +++ b/packages/twenty-utils/dangerfile.ts @@ -40,12 +40,10 @@ if ( ) { markdown( getMdSection( - 'CLA', + 'Welcome!', ` -Hello there and welcome to our project! -By submitting your Pull Request, you acknowledge that you agree with the terms of our [Contributor License Agreement](https://github.com/twentyhq/twenty/blob/main/.github/CLA.md). -Although we don't have a dedicated legal counsel, having this kind of agreement can protect us from potential legal issues or patent trolls. -Thank you for your understanding.`, +Hello there, congrats on your first PR! We're excited to have you contributing to this project. +By submitting your Pull Request, you acknowledge that you agree with the terms of our [Contributor License Agreement](https://github.com/twentyhq/twenty/blob/main/.github/CLA.md).`, ), ); } diff --git a/packages/twenty-website/src/app/contributors/api/fetch-assignable-users.tsx b/packages/twenty-website/src/app/contributors/api/fetch-assignable-users.tsx new file mode 100644 index 000000000..a244980c0 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/fetch-assignable-users.tsx @@ -0,0 +1,21 @@ +import { graphql } from '@octokit/graphql'; + +import { Repository } from '@/app/contributors/api/types'; + +export async function fetchAssignableUsers( + query: typeof graphql, +): Promise> { + const { repository } = await query(` + query { + repository(owner: "twentyhq", name: "twenty") { + assignableUsers(first: 100) { + nodes { + login + } + } + } + } + `); + + return new Set(repository.assignableUsers.nodes.map((user) => user.login)); +} diff --git a/packages/twenty-website/src/app/contributors/api/fetch-issues-prs.tsx b/packages/twenty-website/src/app/contributors/api/fetch-issues-prs.tsx new file mode 100644 index 000000000..fbac2a072 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/fetch-issues-prs.tsx @@ -0,0 +1,102 @@ +import { graphql } from '@octokit/graphql'; + +import { + IssueNode, + PullRequestNode, + Repository, +} from '@/app/contributors/api/types'; + +export async function fetchIssuesPRs( + query: typeof graphql, + cursor: string | null = null, + isIssues: boolean = false, + accumulatedData: Array = [], +): Promise> { + const { repository } = await query( + ` + query ($cursor: String) { + repository(owner: "twentyhq", name: "twenty") { + pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${isIssues}) { + nodes { + id + title + body + url + createdAt + updatedAt + closedAt + mergedAt + author { + resourcePath + login + avatarUrl(size: 460) + url + } + labels(first: 100) { + nodes { + id + name + color + description + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + issues(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${!isIssues}) { + nodes { + id + title + body + url + createdAt + updatedAt + closedAt + author { + resourcePath + login + avatarUrl + url + } + labels(first: 100) { + nodes { + id + name + color + description + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + { cursor }, + ); + + const newAccumulatedData: Array = [ + ...accumulatedData, + ...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes), + ]; + const pageInfo = isIssues + ? repository.issues.pageInfo + : repository.pullRequests.pageInfo; + + if (pageInfo.hasNextPage) { + return fetchIssuesPRs( + query, + pageInfo.endCursor, + isIssues, + newAccumulatedData, + ); + } else { + return newAccumulatedData; + } +} diff --git a/packages/twenty-website/src/app/contributors/api/generate/route.tsx b/packages/twenty-website/src/app/contributors/api/generate/route.tsx deleted file mode 100644 index 6afd29b61..000000000 --- a/packages/twenty-website/src/app/contributors/api/generate/route.tsx +++ /dev/null @@ -1,335 +0,0 @@ -export const dynamic = 'force-dynamic'; - -import { global } from '@apollo/client/utilities/globals'; -import { graphql } from '@octokit/graphql'; - -import { insertMany, migrate } from '@/database/database'; -import { - issueLabelModel, - issueModel, - labelModel, - pullRequestLabelModel, - pullRequestModel, - userModel, -} from '@/database/model'; - -interface LabelNode { - id: string; - name: string; - color: string; - description: string; -} - -export interface AuthorNode { - resourcePath: string; - login: string; - avatarUrl: string; - url: string; -} - -interface PullRequestNode { - id: string; - title: string; - body: string; - url: string; - createdAt: string; - updatedAt: string; - closedAt: string; - mergedAt: string; - author: AuthorNode; - labels: { - nodes: LabelNode[]; - }; -} - -interface IssueNode { - id: string; - title: string; - body: string; - url: string; - createdAt: string; - updatedAt: string; - closedAt: string; - author: AuthorNode; - labels: { - nodes: LabelNode[]; - }; -} - -interface PageInfo { - hasNextPage: boolean; - endCursor: string | null; -} - -interface PullRequests { - nodes: PullRequestNode[]; - pageInfo: PageInfo; -} - -interface Issues { - nodes: IssueNode[]; - pageInfo: PageInfo; -} - -interface AssignableUserNode { - login: string; -} - -interface AssignableUsers { - nodes: AssignableUserNode[]; -} - -interface RepoData { - repository: { - pullRequests: PullRequests; - issues: Issues; - assignableUsers: AssignableUsers; - }; -} - -const query = graphql.defaults({ - headers: { - Authorization: 'bearer ' + global.process.env.GITHUB_TOKEN, - }, -}); - -async function fetchData( - cursor: string | null = null, - isIssues: boolean = false, - accumulatedData: Array = [], -): Promise> { - const { repository } = await query( - ` - query ($cursor: String) { - repository(owner: "twentyhq", name: "twenty") { - pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${isIssues}) { - nodes { - id - title - body - url - createdAt - updatedAt - closedAt - mergedAt - author { - resourcePath - login - avatarUrl(size: 460) - url - } - labels(first: 100) { - nodes { - id - name - color - description - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - issues(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${!isIssues}) { - nodes { - id - title - body - url - createdAt - updatedAt - closedAt - author { - resourcePath - login - avatarUrl - url - } - labels(first: 100) { - nodes { - id - name - color - description - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - `, - { cursor }, - ); - - const newAccumulatedData: Array = [ - ...accumulatedData, - ...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes), - ]; - const pageInfo = isIssues - ? repository.issues.pageInfo - : repository.pullRequests.pageInfo; - - if (pageInfo.hasNextPage) { - return fetchData(pageInfo.endCursor, isIssues, newAccumulatedData); - } else { - return newAccumulatedData; - } -} - -async function fetchAssignableUsers(): Promise> { - const { repository } = await query(` - query { - repository(owner: "twentyhq", name: "twenty") { - assignableUsers(first: 100) { - nodes { - login - } - } - } - } - `); - - return new Set(repository.assignableUsers.nodes.map((user) => user.login)); -} - -export async function GET() { - if (!global.process.env.GITHUB_TOKEN) { - return new Response('No GitHub token provided', { status: 500 }); - } - - await migrate(); - - // TODO if we ever hit API Rate Limiting - const lastPRCursor = null; - const lastIssueCursor = null; - - const assignableUsers = await fetchAssignableUsers(); - const fetchedPRs = (await fetchData(lastPRCursor)) as Array; - const fetchedIssues = (await fetchData( - lastIssueCursor, - true, - )) as Array; - - for (const pr of fetchedPRs) { - if (pr.author == null) { - continue; - } - await insertMany( - userModel, - [ - { - id: pr.author.login, - avatarUrl: pr.author.avatarUrl, - url: pr.author.url, - isEmployee: assignableUsers.has(pr.author.login) ? '1' : '0', - }, - ], - { onConflictKey: 'id' }, - ); - - await insertMany( - pullRequestModel, - [ - { - id: pr.id, - title: pr.title, - body: pr.body, - url: pr.url, - createdAt: pr.createdAt, - updatedAt: pr.updatedAt, - closedAt: pr.closedAt, - mergedAt: pr.mergedAt, - authorId: pr.author.login, - }, - ], - { onConflictKey: 'id' }, - ); - - for (const label of pr.labels.nodes) { - await insertMany( - labelModel, - [ - { - id: label.id, - name: label.name, - color: label.color, - description: label.description, - }, - ], - { onConflictKey: 'id' }, - ); - await insertMany(pullRequestLabelModel, [ - { - pullRequestId: pr.id, - labelId: label.id, - }, - ]); - } - } - - for (const issue of fetchedIssues) { - if (issue.author == null) { - continue; - } - await insertMany( - userModel, - [ - { - id: issue.author.login, - avatarUrl: issue.author.avatarUrl, - url: issue.author.url, - isEmployee: assignableUsers.has(issue.author.login) ? '1' : '0', - }, - ], - { onConflictKey: 'id' }, - ); - - await insertMany( - issueModel, - [ - { - id: issue.id, - title: issue.title, - body: issue.body, - url: issue.url, - createdAt: issue.createdAt, - updatedAt: issue.updatedAt, - closedAt: issue.closedAt, - authorId: issue.author.login, - }, - ], - { onConflictKey: 'id' }, - ); - - for (const label of issue.labels.nodes) { - await insertMany( - labelModel, - [ - { - id: label.id, - name: label.name, - color: label.color, - description: label.description, - }, - ], - { onConflictKey: 'id' }, - ); - await insertMany(issueLabelModel, [ - { - pullRequestId: issue.id, - labelId: label.id, - }, - ]); - } - } - - return new Response('Data synced', { - status: 200, - }); -} diff --git a/packages/twenty-website/src/app/contributors/api/init/route.tsx b/packages/twenty-website/src/app/contributors/api/init/route.tsx new file mode 100644 index 000000000..e3181c6e4 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/init/route.tsx @@ -0,0 +1,46 @@ +export const dynamic = 'force-dynamic'; + +import { global } from '@apollo/client/utilities/globals'; +import { graphql } from '@octokit/graphql'; + +import { fetchAssignableUsers } from '@/app/contributors/api/fetch-assignable-users'; +import { fetchIssuesPRs } from '@/app/contributors/api/fetch-issues-prs'; +import { saveIssuesToDB } from '@/app/contributors/api/save-issues-to-db'; +import { savePRsToDB } from '@/app/contributors/api/save-prs-to-db'; +import { IssueNode, PullRequestNode } from '@/app/contributors/api/types'; +import { migrate } from '@/database/database'; + +export async function GET() { + if (!global.process.env.GITHUB_TOKEN) { + return new Response('No GitHub token provided', { status: 500 }); + } + + const query = graphql.defaults({ + headers: { + Authorization: 'bearer ' + global.process.env.GITHUB_TOKEN, + }, + }); + + await migrate(); + + const assignableUsers = await fetchAssignableUsers(query); + const fetchedPRs = (await fetchIssuesPRs( + query, + null, + false, + [], + )) as Array; + const fetchedIssues = (await fetchIssuesPRs( + query, + null, + true, + [], + )) as Array; + + savePRsToDB(fetchedPRs, assignableUsers); + saveIssuesToDB(fetchedIssues, assignableUsers); + + return new Response('Data synced', { + status: 200, + }); +} diff --git a/packages/twenty-website/src/app/contributors/api/save-issues-to-db.tsx b/packages/twenty-website/src/app/contributors/api/save-issues-to-db.tsx new file mode 100644 index 000000000..4ab8bdf02 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/save-issues-to-db.tsx @@ -0,0 +1,69 @@ +import { IssueNode } from '@/app/contributors/api/types'; +import { insertMany } from '@/database/database'; +import { + issueLabelModel, + issueModel, + labelModel, + userModel, +} from '@/database/model'; + +export async function saveIssuesToDB( + issues: Array, + assignableUsers: Set, +) { + for (const issue of issues) { + if (issue.author == null) { + continue; + } + await insertMany( + userModel, + [ + { + id: issue.author.login, + avatarUrl: issue.author.avatarUrl, + url: issue.author.url, + isEmployee: assignableUsers.has(issue.author.login) ? '1' : '0', + }, + ], + { onConflictKey: 'id' }, + ); + + await insertMany( + issueModel, + [ + { + id: issue.id, + title: issue.title, + body: issue.body, + url: issue.url, + createdAt: issue.createdAt, + updatedAt: issue.updatedAt, + closedAt: issue.closedAt, + authorId: issue.author.login, + }, + ], + { onConflictKey: 'id' }, + ); + + for (const label of issue.labels.nodes) { + await insertMany( + labelModel, + [ + { + id: label.id, + name: label.name, + color: label.color, + description: label.description, + }, + ], + { onConflictKey: 'id' }, + ); + await insertMany(issueLabelModel, [ + { + pullRequestId: issue.id, + labelId: label.id, + }, + ]); + } + } +} diff --git a/packages/twenty-website/src/app/contributors/api/save-prs-to-db.tsx b/packages/twenty-website/src/app/contributors/api/save-prs-to-db.tsx new file mode 100644 index 000000000..3cb93350c --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/save-prs-to-db.tsx @@ -0,0 +1,70 @@ +import { PullRequestNode } from '@/app/contributors/api/types'; +import { insertMany } from '@/database/database'; +import { + labelModel, + pullRequestLabelModel, + pullRequestModel, + userModel, +} from '@/database/model'; + +export async function savePRsToDB( + prs: Array, + assignableUsers: Set, +) { + for (const pr of prs) { + if (pr.author == null) { + continue; + } + await insertMany( + userModel, + [ + { + id: pr.author.login, + avatarUrl: pr.author.avatarUrl, + url: pr.author.url, + isEmployee: assignableUsers.has(pr.author.login) ? '1' : '0', + }, + ], + { onConflictKey: 'id' }, + ); + + await insertMany( + pullRequestModel, + [ + { + id: pr.id, + title: pr.title, + body: pr.body, + url: pr.url, + createdAt: pr.createdAt, + updatedAt: pr.updatedAt, + closedAt: pr.closedAt, + mergedAt: pr.mergedAt, + authorId: pr.author.login, + }, + ], + { onConflictKey: 'id' }, + ); + + for (const label of pr.labels.nodes) { + await insertMany( + labelModel, + [ + { + id: label.id, + name: label.name, + color: label.color, + description: label.description, + }, + ], + { onConflictKey: 'id' }, + ); + await insertMany(pullRequestLabelModel, [ + { + pullRequestId: pr.id, + labelId: label.id, + }, + ]); + } + } +} diff --git a/packages/twenty-website/src/app/contributors/api/search-issues-prs.tsx b/packages/twenty-website/src/app/contributors/api/search-issues-prs.tsx new file mode 100644 index 000000000..e06318aa3 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/search-issues-prs.tsx @@ -0,0 +1,99 @@ +import { graphql } from '@octokit/graphql'; + +import { + IssueNode, + PullRequestNode, + SearchIssuesPRsQuery, +} from '@/app/contributors/api/types'; + +export async function searchIssuesPRs( + query: typeof graphql, + cursor: string | null = null, + isIssues: boolean = false, + accumulatedData: Array = [], +): Promise> { + const { search } = await query( + ` + query searchPullRequestsAndIssues($cursor: String) { + search(query: "repo:twentyhq/twenty ${ + isIssues ? 'is:issue' : 'is:pr' + } updated:>2024-02-27", type: ISSUE, first: 100, after: $cursor) { + edges { + node { + ... on PullRequest { + id + title + body + url + createdAt + updatedAt + closedAt + mergedAt + author { + resourcePath + login + avatarUrl(size: 460) + url + } + labels(first: 100) { + nodes { + id + name + color + description + } + } + } + ... on Issue { + id + title + body + url + createdAt + updatedAt + closedAt + author { + resourcePath + login + avatarUrl + url + } + labels(first: 100) { + nodes { + id + name + color + description + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + { + cursor, + }, + ); + const newAccumulatedData: Array = [ + ...accumulatedData, + ...search.edges.map(({ node }) => node), + ]; + const pageInfo = search.pageInfo; + + if (pageInfo.hasNextPage) { + return searchIssuesPRs( + query, + pageInfo.endCursor, + isIssues, + newAccumulatedData, + ); + } else { + return newAccumulatedData; + } +} diff --git a/packages/twenty-website/src/app/contributors/api/types.tsx b/packages/twenty-website/src/app/contributors/api/types.tsx new file mode 100644 index 000000000..0f3968d59 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/types.tsx @@ -0,0 +1,86 @@ +export interface LabelNode { + id: string; + name: string; + color: string; + description: string; +} + +export interface AuthorNode { + resourcePath: string; + login: string; + avatarUrl: string; + url: string; +} + +export interface PullRequestNode { + id: string; + title: string; + body: string; + url: string; + createdAt: string; + updatedAt: string; + closedAt: string; + mergedAt: string; + author: AuthorNode; + labels: { + nodes: LabelNode[]; + }; +} + +export interface IssueNode { + id: string; + title: string; + body: string; + url: string; + createdAt: string; + updatedAt: string; + closedAt: string; + author: AuthorNode; + labels: { + nodes: LabelNode[]; + }; +} + +export interface PageInfo { + hasNextPage: boolean; + endCursor: string | null; +} + +export interface PullRequests { + nodes: PullRequestNode[]; + pageInfo: PageInfo; +} + +export interface Issues { + nodes: IssueNode[]; + pageInfo: PageInfo; +} + +export interface AssignableUserNode { + login: string; +} + +export interface AssignableUsers { + nodes: AssignableUserNode[]; +} + +export interface Repository { + repository: { + pullRequests: PullRequests; + issues: Issues; + assignableUsers: AssignableUsers; + }; +} + +export interface SearchEdgeNode { + node: IssueNode | PullRequestNode; +} + +export interface SearchEdges { + edges: SearchEdgeNode[]; + pageInfo: PageInfo; +} + +export interface SearchIssuesPRsQuery { + search: SearchEdges; +} diff --git a/packages/twenty-website/src/app/contributors/api/update/route.tsx b/packages/twenty-website/src/app/contributors/api/update/route.tsx new file mode 100644 index 000000000..e5bcbb299 --- /dev/null +++ b/packages/twenty-website/src/app/contributors/api/update/route.tsx @@ -0,0 +1,55 @@ +import { graphql } from '@octokit/graphql'; +import { desc } from 'drizzle-orm'; + +import { fetchAssignableUsers } from '@/app/contributors/api/fetch-assignable-users'; +import { saveIssuesToDB } from '@/app/contributors/api/save-issues-to-db'; +import { savePRsToDB } from '@/app/contributors/api/save-prs-to-db'; +import { searchIssuesPRs } from '@/app/contributors/api/search-issues-prs'; +import { IssueNode, PullRequestNode } from '@/app/contributors/api/types'; +import { findOne } from '@/database/database'; +import { issueModel, pullRequestModel } from '@/database/model'; + +export async function GET() { + if (!global.process.env.GITHUB_TOKEN) { + return new Response('No GitHub token provided', { status: 500 }); + } + + const query = graphql.defaults({ + headers: { + Authorization: 'bearer ' + global.process.env.GITHUB_TOKEN, + }, + }); + + const assignableUsers = await fetchAssignableUsers(query); + + const mostRecentPR = findOne( + pullRequestModel, + desc(pullRequestModel.updatedAt), + ); + + const mostRecentIssue = findOne(issueModel, desc(issueModel.updatedAt)); + + if (!mostRecentPR || !mostRecentIssue) { + return new Response('Run Init command first', { status: 400 }); + } + + const fetchedPRs = (await searchIssuesPRs( + query, + null, + false, + [], + )) as Array; + const fetchedIssues = (await searchIssuesPRs( + query, + null, + true, + [], + )) as Array; + + savePRsToDB(fetchedPRs, assignableUsers); + saveIssuesToDB(fetchedIssues, assignableUsers); + + return new Response('Data synced', { + status: 200, + }); +} diff --git a/packages/twenty-website/src/app/releases/api/route.tsx b/packages/twenty-website/src/app/releases/api/route.tsx index 7b3e8ceeb..a67aea09b 100644 --- a/packages/twenty-website/src/app/releases/api/route.tsx +++ b/packages/twenty-website/src/app/releases/api/route.tsx @@ -1,9 +1,6 @@ -import { compareSemanticVersions } from '@/shared-utils/compareSemanticVersions'; -import fs from 'fs'; -import matter from 'gray-matter'; - -import { NextRequest, NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server'; +import { getReleases } from '@/app/releases/get-releases'; export interface ReleaseNote { slug: string; @@ -12,56 +9,12 @@ export interface ReleaseNote { content: string; } - -const BASE_URL = 'https://twenty.com/'; - -// WARNING: This API is used by twenty-front, not just by twenty-website -// Make sure you don't change it without updating twenty-front at the same time -export async function getReleases(baseUrl?: string): Promise { - const files = fs.readdirSync('src/content/releases'); - const releasenotes: ReleaseNote[] = []; - - for (const fileName of files) { - if (!fileName.endsWith('.md') && !fileName.endsWith('.mdx')) { - continue; - } - const file = fs.readFileSync(`src/content/releases/${fileName}`, 'utf-8'); - const { data: frontmatter, content } = matter(file); - - let updatedContent; - if(baseUrl) { - updatedContent = content.replace(/!\[(.*?)\]\((?!http)(.*?)\)/g, (match, alt, src) => { - // Check if src is a relative path (not starting with http:// or https://) - if (!src.startsWith('/')) { - src = `${baseUrl}/${src}`; - } else { - src = `${baseUrl}${src}`; - } - return `![${alt}](${src})`; - }); - } - - releasenotes.push({ - slug: fileName.slice(0, -4), - date: frontmatter.Date, - release: frontmatter.release, - content: updatedContent ?? content, - }); - } - - releasenotes.sort((a, b) => compareSemanticVersions(b.release, a.release)); - - return releasenotes; -} - - export async function GET(request: NextRequest) { - const host = request.nextUrl.hostname; const protocol = request.nextUrl.protocol; const baseUrl = `${protocol}//${host}`; - + console.log(baseUrl); - return NextResponse.json(await getReleases(baseUrl), { status: 200 }) + return NextResponse.json(await getReleases(baseUrl), { status: 200 }); } diff --git a/packages/twenty-website/src/app/releases/get-releases.tsx b/packages/twenty-website/src/app/releases/get-releases.tsx new file mode 100644 index 000000000..5b215b043 --- /dev/null +++ b/packages/twenty-website/src/app/releases/get-releases.tsx @@ -0,0 +1,47 @@ +import fs from 'fs'; +import matter from 'gray-matter'; + +import { ReleaseNote } from '@/app/releases/api/route'; +import { compareSemanticVersions } from '@/shared-utils/compareSemanticVersions'; + +// WARNING: This API is used by twenty-front, not just by twenty-website +// Make sure you don't change it without updating twenty-front at the same time +export async function getReleases(baseUrl?: string): Promise { + const files = fs.readdirSync('src/content/releases'); + const releasenotes: ReleaseNote[] = []; + + for (const fileName of files) { + if (!fileName.endsWith('.md') && !fileName.endsWith('.mdx')) { + continue; + } + const file = fs.readFileSync(`src/content/releases/${fileName}`, 'utf-8'); + const { data: frontmatter, content } = matter(file); + + let updatedContent; + if (baseUrl) { + updatedContent = content.replace( + /!\[(.*?)\]\((?!http)(.*?)\)/g, + (match: string, alt: string, src: string) => { + // Check if src is a relative path (not starting with http:// or https://) + if (!src.startsWith('/')) { + src = `${baseUrl}/${src}`; + } else { + src = `${baseUrl}${src}`; + } + return `![${alt}](${src})`; + }, + ); + } + + releasenotes.push({ + slug: fileName.slice(0, -4), + date: frontmatter.Date, + release: frontmatter.release, + content: updatedContent ?? content, + }); + } + + releasenotes.sort((a, b) => compareSemanticVersions(b.release, a.release)); + + return releasenotes; +} diff --git a/packages/twenty-website/src/app/releases/page.tsx b/packages/twenty-website/src/app/releases/page.tsx index 330235d85..b6e5dfdcc 100644 --- a/packages/twenty-website/src/app/releases/page.tsx +++ b/packages/twenty-website/src/app/releases/page.tsx @@ -5,7 +5,7 @@ import { Line } from '@/app/_components/releases/Line'; import { Release } from '@/app/_components/releases/Release'; import { Title } from '@/app/_components/releases/StyledTitle'; import { ContentContainer } from '@/app/_components/ui/layout/ContentContainer'; -import { getReleases } from '@/app/releases/api/route'; +import { getReleases } from '@/app/releases/get-releases'; export const metadata: Metadata = { title: 'Twenty - Releases', diff --git a/packages/twenty-website/src/database/database.ts b/packages/twenty-website/src/database/database.ts index dd2402027..49fdfe075 100644 --- a/packages/twenty-website/src/database/database.ts +++ b/packages/twenty-website/src/database/database.ts @@ -49,6 +49,18 @@ const migrate = async () => { throw new Error('Unsupported database driver'); }; +const findOne = (model: SQLiteTableWithColumns, orderBy: any) => { + if (isSqliteDriver) { + return sqliteDb.select().from(model).orderBy(orderBy).limit(1).execute(); + } + + if (isPgDriver) { + return pgDb.select().from(model).orderBy(orderBy).limit(1).execute(); + } + + throw new Error('Unsupported database driver'); +}; + const findAll = (model: SQLiteTableWithColumns) => { if (isSqliteDriver) { return sqliteDb.select().from(model).all(); @@ -92,4 +104,4 @@ const insertMany = async ( throw new Error('Unsupported database driver'); }; -export { findAll, insertMany, migrate }; +export { findAll, findOne, insertMany, migrate }; diff --git a/packages/twenty-website/src/shared-utils/compareSemanticVersions.ts b/packages/twenty-website/src/shared-utils/compareSemanticVersions.ts index 0fbec4b9f..ff97a2697 100644 --- a/packages/twenty-website/src/shared-utils/compareSemanticVersions.ts +++ b/packages/twenty-website/src/shared-utils/compareSemanticVersions.ts @@ -1,16 +1,16 @@ export function compareSemanticVersions(a: string, b: string) { - const a1 = a.split('.'); - const b1 = b.split('.'); - - const len = Math.min(a1.length, b1.length); - - for (let i = 0; i < len; i++) { - const a2 = +a1[i] || 0; - const b2 = +b1[i] || 0; - - if (a2 !== b2) { - return a2 > b2 ? 1 : -1; - } + const a1 = a.split('.'); + const b1 = b.split('.'); + + const len = Math.min(a1.length, b1.length); + + for (let i = 0; i < len; i++) { + const a2 = +a1[i] || 0; + const b2 = +b1[i] || 0; + + if (a2 !== b2) { + return a2 > b2 ? 1 : -1; } - return b1.length - a1.length; - } \ No newline at end of file + } + return b1.length - a1.length; +}