Improve website github sync (#4259)
This commit is contained in:
@ -40,12 +40,10 @@ if (
|
|||||||
) {
|
) {
|
||||||
markdown(
|
markdown(
|
||||||
getMdSection(
|
getMdSection(
|
||||||
'CLA',
|
'Welcome!',
|
||||||
`
|
`
|
||||||
Hello there and welcome to our project!
|
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).
|
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.`,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { graphql } from '@octokit/graphql';
|
||||||
|
|
||||||
|
import { Repository } from '@/app/contributors/api/types';
|
||||||
|
|
||||||
|
export async function fetchAssignableUsers(
|
||||||
|
query: typeof graphql,
|
||||||
|
): Promise<Set<string>> {
|
||||||
|
const { repository } = await query<Repository>(`
|
||||||
|
query {
|
||||||
|
repository(owner: "twentyhq", name: "twenty") {
|
||||||
|
assignableUsers(first: 100) {
|
||||||
|
nodes {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return new Set(repository.assignableUsers.nodes.map((user) => user.login));
|
||||||
|
}
|
||||||
@ -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<PullRequestNode | IssueNode> = [],
|
||||||
|
): Promise<Array<PullRequestNode | IssueNode>> {
|
||||||
|
const { repository } = await query<Repository>(
|
||||||
|
`
|
||||||
|
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<PullRequestNode | IssueNode> = [
|
||||||
|
...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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<PullRequestNode | IssueNode> = [],
|
|
||||||
): Promise<Array<PullRequestNode | IssueNode>> {
|
|
||||||
const { repository } = await query<RepoData>(
|
|
||||||
`
|
|
||||||
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<PullRequestNode | IssueNode> = [
|
|
||||||
...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<Set<string>> {
|
|
||||||
const { repository } = await query<RepoData>(`
|
|
||||||
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<PullRequestNode>;
|
|
||||||
const fetchedIssues = (await fetchData(
|
|
||||||
lastIssueCursor,
|
|
||||||
true,
|
|
||||||
)) as Array<IssueNode>;
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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<PullRequestNode>;
|
||||||
|
const fetchedIssues = (await fetchIssuesPRs(
|
||||||
|
query,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
)) as Array<IssueNode>;
|
||||||
|
|
||||||
|
savePRsToDB(fetchedPRs, assignableUsers);
|
||||||
|
saveIssuesToDB(fetchedIssues, assignableUsers);
|
||||||
|
|
||||||
|
return new Response('Data synced', {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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<IssueNode>,
|
||||||
|
assignableUsers: Set<string>,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<PullRequestNode>,
|
||||||
|
assignableUsers: Set<string>,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<PullRequestNode | IssueNode> = [],
|
||||||
|
): Promise<Array<PullRequestNode | IssueNode>> {
|
||||||
|
const { search } = await query<SearchIssuesPRsQuery>(
|
||||||
|
`
|
||||||
|
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<PullRequestNode | IssueNode> = [
|
||||||
|
...accumulatedData,
|
||||||
|
...search.edges.map(({ node }) => node),
|
||||||
|
];
|
||||||
|
const pageInfo = search.pageInfo;
|
||||||
|
|
||||||
|
if (pageInfo.hasNextPage) {
|
||||||
|
return searchIssuesPRs(
|
||||||
|
query,
|
||||||
|
pageInfo.endCursor,
|
||||||
|
isIssues,
|
||||||
|
newAccumulatedData,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return newAccumulatedData;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
packages/twenty-website/src/app/contributors/api/types.tsx
Normal file
86
packages/twenty-website/src/app/contributors/api/types.tsx
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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<PullRequestNode>;
|
||||||
|
const fetchedIssues = (await searchIssuesPRs(
|
||||||
|
query,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
)) as Array<IssueNode>;
|
||||||
|
|
||||||
|
savePRsToDB(fetchedPRs, assignableUsers);
|
||||||
|
saveIssuesToDB(fetchedIssues, assignableUsers);
|
||||||
|
|
||||||
|
return new Response('Data synced', {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import { compareSemanticVersions } from '@/shared-utils/compareSemanticVersions';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
|
||||||
import matter from 'gray-matter';
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
|
import { getReleases } from '@/app/releases/get-releases';
|
||||||
|
|
||||||
export interface ReleaseNote {
|
export interface ReleaseNote {
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -12,56 +9,12 @@ export interface ReleaseNote {
|
|||||||
content: string;
|
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<ReleaseNote[]> {
|
|
||||||
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 ``;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|
||||||
const host = request.nextUrl.hostname;
|
const host = request.nextUrl.hostname;
|
||||||
const protocol = request.nextUrl.protocol;
|
const protocol = request.nextUrl.protocol;
|
||||||
const baseUrl = `${protocol}//${host}`;
|
const baseUrl = `${protocol}//${host}`;
|
||||||
|
|
||||||
console.log(baseUrl);
|
console.log(baseUrl);
|
||||||
|
|
||||||
return NextResponse.json(await getReleases(baseUrl), { status: 200 })
|
return NextResponse.json(await getReleases(baseUrl), { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
47
packages/twenty-website/src/app/releases/get-releases.tsx
Normal file
47
packages/twenty-website/src/app/releases/get-releases.tsx
Normal file
@ -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<ReleaseNote[]> {
|
||||||
|
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 ``;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { Line } from '@/app/_components/releases/Line';
|
|||||||
import { Release } from '@/app/_components/releases/Release';
|
import { Release } from '@/app/_components/releases/Release';
|
||||||
import { Title } from '@/app/_components/releases/StyledTitle';
|
import { Title } from '@/app/_components/releases/StyledTitle';
|
||||||
import { ContentContainer } from '@/app/_components/ui/layout/ContentContainer';
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: 'Twenty - Releases',
|
title: 'Twenty - Releases',
|
||||||
|
|||||||
@ -49,6 +49,18 @@ const migrate = async () => {
|
|||||||
throw new Error('Unsupported database driver');
|
throw new Error('Unsupported database driver');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOne = (model: SQLiteTableWithColumns<any>, 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<any>) => {
|
const findAll = (model: SQLiteTableWithColumns<any>) => {
|
||||||
if (isSqliteDriver) {
|
if (isSqliteDriver) {
|
||||||
return sqliteDb.select().from(model).all();
|
return sqliteDb.select().from(model).all();
|
||||||
@ -92,4 +104,4 @@ const insertMany = async (
|
|||||||
throw new Error('Unsupported database driver');
|
throw new Error('Unsupported database driver');
|
||||||
};
|
};
|
||||||
|
|
||||||
export { findAll, insertMany, migrate };
|
export { findAll, findOne, insertMany, migrate };
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
export function compareSemanticVersions(a: string, b: string) {
|
export function compareSemanticVersions(a: string, b: string) {
|
||||||
const a1 = a.split('.');
|
const a1 = a.split('.');
|
||||||
const b1 = b.split('.');
|
const b1 = b.split('.');
|
||||||
|
|
||||||
const len = Math.min(a1.length, b1.length);
|
const len = Math.min(a1.length, b1.length);
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const a2 = +a1[i] || 0;
|
const a2 = +a1[i] || 0;
|
||||||
const b2 = +b1[i] || 0;
|
const b2 = +b1[i] || 0;
|
||||||
|
|
||||||
if (a2 !== b2) {
|
if (a2 !== b2) {
|
||||||
return a2 > b2 ? 1 : -1;
|
return a2 > b2 ? 1 : -1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return b1.length - a1.length;
|
|
||||||
}
|
}
|
||||||
|
return b1.length - a1.length;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user