Add proper ORM and postgres support (#3978)

* Add postgresql support

* Fixes

* Fix perfs
This commit is contained in:
Charles Bochet 2024-02-14 17:53:50 +01:00 committed by GitHub
parent 94ad0e33ec
commit 4613f64910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2143 additions and 344 deletions

View File

@ -22,6 +22,7 @@
"@hello-pangea/dnd": "^16.2.0",
"@hookform/resolvers": "^3.1.1",
"@jsdevtools/rehype-toc": "^3.0.2",
"@libsql/client": "^0.4.3",
"@mdx-js/react": "^3.0.0",
"@nestjs/apollo": "^11.0.5",
"@nestjs/axios": "^3.0.1",
@ -76,6 +77,7 @@
"deep-equal": "^2.2.2",
"docusaurus-node-polyfills": "^1.0.0",
"dotenv-cli": "^7.2.1",
"drizzle-orm": "^0.29.3",
"esbuild-plugin-svgr": "^2.1.0",
"file-type": "16.5.4",
"framer-motion": "^10.12.17",
@ -238,6 +240,7 @@
"cross-var": "^1.1.0",
"danger": "^11.3.0",
"dotenv-cli": "^7.2.1",
"drizzle-kit": "^0.20.14",
"eslint": "^8.53.0",
"eslint-config-next": "14.0.4",
"eslint-config-prettier": "^9.1.0",

View File

@ -1,2 +1,5 @@
BASE_URL=http://localhost:3000
GITHUB_TOKEN=your_github_token
DATABASE_DRIVER=sqlite # or pg
DATABASE_PG_URL=postgres://website:website@localhost:5432/website # only if using postgres

View File

@ -7,6 +7,11 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"database:generate:pg": "drizzle-kit generate:pg --config=src/database/postgres/drizzle-posgres.config.ts",
"database:generate:sqlite": "drizzle-kit generate:sqlite --config=src/database/sqlite/drizzle-sqlite.config.ts"
},
"dependencies": {
"postgres": "^3.4.3"
}
}

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import Link from 'next/link';
export interface User {
login: string;
id: string;
avatarUrl: string;
}
@ -66,13 +66,10 @@ const AvatarGrid = ({ users }: { users: User[] }) => {
return (
<AvatarGridContainer>
{users.map((user) => (
<Link
href={`/developers/contributors/${user.login}`}
key={`l_${user.login}`}
>
<AvatarItem key={user.login}>
<img src={user.avatarUrl} alt={user.login} />
<span className="username">{user.login}</span>
<Link href={`/developers/contributors/${user.id}`} key={`l_${user.id}`}>
<AvatarItem key={user.id}>
<img src={user.avatarUrl} alt={user.id} />
<span className="username">{user.id}</span>
</AvatarItem>
</Link>
))}

View File

@ -10,6 +10,9 @@ export const ActivityLog = ({
}: {
data: { value: number; day: string }[];
}) => {
if (!data.length) {
return null;
}
return (
<CardContainer>
<Title>Activity</Title>

View File

@ -90,10 +90,12 @@ export const ProfileCard = ({
<StyledGithubIcon size="M" color="rgba(0,0,0,1)" />
</a>
</h3>
<p className="duration">
Contributing since{' '}
{format(new Date(firstContributionAt), 'MMMM yyyy')}
</p>
{firstContributionAt && (
<p className="duration">
Contributing since{' '}
{format(new Date(firstContributionAt), 'MMMM yyyy')}
</p>
)}
</Details>
</ProfileContainer>
);

View File

@ -1,4 +1,3 @@
import Database from 'better-sqlite3';
import { Metadata } from 'next';
import { Background } from '@/app/components/oss-friends/Background';
@ -9,12 +8,8 @@ import { ProfileCard } from '@/app/developers/contributors/[slug]/components/Pro
import { ProfileInfo } from '@/app/developers/contributors/[slug]/components/ProfileInfo';
import { PullRequests } from '@/app/developers/contributors/[slug]/components/PullRequests';
import { ThankYou } from '@/app/developers/contributors/[slug]/components/ThankYou';
interface Contributor {
login: string;
avatarUrl: string;
pullRequestCount: number;
}
import { findAll } from '@/database/database';
import { pullRequestModel, userModel } from '@/database/model';
export function generateMetadata({
params,
@ -27,134 +22,109 @@ export function generateMetadata({
}
export default async function ({ params }: { params: { slug: string } }) {
const db = new Database('db.sqlite', { readonly: true });
const contributors = await findAll(userModel);
const contributor = db
.prepare(
`
SELECT
u.login,
u.avatarUrl,
(SELECT COUNT(*) FROM pullRequests WHERE authorId = u.id) AS pullRequestCount,
(SELECT COUNT(*) FROM issues WHERE authorId = u.id) AS issuesCount
FROM
users u
WHERE
u.login = :user_id
`,
)
.get({ user_id: params.slug }) as Contributor;
const contributor = contributors.find(
(contributor) => contributor.id === params.slug,
);
const pullRequestActivity = db
.prepare(
`
SELECT
COUNT(*) as value,
DATE(createdAt) as day
FROM
pullRequests
WHERE
authorId = (SELECT id FROM users WHERE login = :user_id)
GROUP BY
DATE(createdAt)
ORDER BY
DATE(createdAt)
`,
)
.all({ user_id: params.slug }) as { value: number; day: string }[];
if (!contributor) {
return;
}
// Latest PRs.
const pullRequestList = db
.prepare(
`
SELECT
id,
title,
body,
url,
createdAt,
updatedAt,
closedAt,
mergedAt,
authorId
FROM
pullRequests
WHERE
authorId = (SELECT id FROM users WHERE login = :user_id)
ORDER BY
DATE(createdAt) DESC
LIMIT
10
`,
)
.all({ user_id: params.slug }) as {
title: string;
createdAt: string;
url: string;
id: string;
mergedAt: string | null;
authorId: string;
}[];
const pullRequests = await findAll(pullRequestModel);
const mergedPullRequests = pullRequests
.filter((pr) => pr.mergedAt !== null)
.filter(
(pr) =>
![
'dependabot',
'cyborch',
'emilienchvt',
'Samox',
'charlesBochet',
'gitstart-app',
'thaisguigon',
'lucasbordeau',
'magrinj',
'Weiko',
'gitstart-twenty',
'bosiraphael',
'martmull',
'FelixMalfait',
'thomtrp',
'Bonapara',
'nimraahmed',
].includes(pr.authorId),
);
const mergedPullRequests = db
.prepare(
`
SELECT * FROM (
SELECT
merged_pr_counts.*,
(RANK() OVER(ORDER BY merged_count) - 1) / CAST( total_authors as float) * 100 as rank_percentage
FROM
(
SELECT
authorId,
COUNT(*) FILTER (WHERE mergedAt IS NOT NULL) as merged_count
FROM
pullRequests pr
JOIN
users u ON pr.authorId = u.id
WHERE
u.isEmployee = FALSE
GROUP BY
authorId
) AS merged_pr_counts
CROSS JOIN
(
SELECT COUNT(DISTINCT authorId) as total_authors
FROM pullRequests pr
JOIN
users u ON pr.authorId = u.id
WHERE
u.isEmployee = FALSE
) AS author_counts
) WHERE authorId = (SELECT id FROM users WHERE login = :user_id)
`,
)
.all({ user_id: params.slug }) as {
merged_count: number;
rank_percentage: number;
}[];
const contributorPullRequests = pullRequests.filter(
(pr) => pr.authorId === contributor.id,
);
const mergedContributorPullRequests = contributorPullRequests.filter(
(pr) => pr.mergedAt !== null,
);
db.close();
const mergedContributorPullRequestsByContributor = mergedPullRequests.reduce(
(acc, pr) => {
acc[pr.authorId] = (acc[pr.authorId] || 0) + 1;
return acc;
},
{},
);
const mergedContributorPullRequestsByContributorArray = Object.entries(
mergedContributorPullRequestsByContributor,
)
.map(([authorId, value]) => ({ authorId, value }))
.sort((a, b) => b.value - a.value);
const contributorRank =
((mergedContributorPullRequestsByContributorArray.findIndex(
(contributor) => contributor.authorId === params.slug,
) +
1) /
contributors.length) *
100;
const pullRequestActivity = contributorPullRequests.reduce((acc, pr) => {
const date = new Date(pr.createdAt).toISOString().split('T')[0];
acc[date] = (acc[date] || 0) + 1;
return acc;
}, []);
const pullRequestActivityArray = Object.entries(pullRequestActivity)
.map(([day, value]) => ({ day, value }))
.sort((a, b) => new Date(a.day).getTime() - new Date(b.day).getTime());
return (
<>
<Background />
<ContentContainer>
<Breadcrumb active={contributor.login} />
<Breadcrumb active={contributor.id} />
<ProfileCard
username={contributor.login}
username={contributor.id}
avatarUrl={contributor.avatarUrl}
firstContributionAt={pullRequestActivity[0].day}
firstContributionAt={pullRequestActivityArray[0]?.day}
/>
<ProfileInfo
mergedPRsCount={mergedPullRequests[0].merged_count}
rank={(100 - Number(mergedPullRequests[0].rank_percentage)).toFixed(
0,
)}
activeDays={pullRequestActivity.length}
mergedPRsCount={mergedContributorPullRequests.length}
rank={Math.ceil(Number(contributorRank)).toFixed(0)}
activeDays={pullRequestActivityArray.length}
/>
<ActivityLog data={pullRequestActivityArray} />
<PullRequests
list={
contributorPullRequests.slice(0, 9) as {
id: string;
title: string;
url: string;
createdAt: string;
mergedAt: string | null;
authorId: string;
}[]
}
/>
<ActivityLog data={pullRequestActivity} />
<PullRequests list={pullRequestList} />
<ThankYou authorId={contributor.login} />
</ContentContainer>
</>

View File

@ -1,23 +0,0 @@
import Database from 'better-sqlite3';
export async function GET(
request: Request,
{ params }: { params: { slug: string } },
) {
const db = new Database('db.sqlite', { readonly: true });
if (
params.slug !== 'users' &&
params.slug !== 'labels' &&
params.slug !== 'pullRequests' &&
params.slug !== 'issues'
) {
return Response.json({ error: 'Invalid table name' }, { status: 400 });
}
const rows = db.prepare('SELECT * FROM ' + params.slug).all();
db.close();
return Response.json(rows);
}

View File

@ -1,7 +1,14 @@
import { graphql } from '@octokit/graphql';
import Database from 'better-sqlite3';
const db = new Database('db.sqlite', { verbose: console.log });
import { insertMany, migrate } from '@/database/database';
import {
issueLabelModel,
issueModel,
labelModel,
pullRequestLabelModel,
pullRequestModel,
userModel,
} from '@/database/model';
interface LabelNode {
id: string;
@ -188,176 +195,132 @@ async function fetchAssignableUsers(): Promise<Set<string>> {
return new Set(repository.assignableUsers.nodes.map((user) => user.login));
}
const initDb = () => {
db.prepare(
`
CREATE TABLE IF NOT EXISTS pullRequests (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
url TEXT,
createdAt TEXT,
updatedAt TEXT,
closedAt TEXT,
mergedAt TEXT,
authorId TEXT,
FOREIGN KEY (authorId) REFERENCES users(id)
);
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
url TEXT,
createdAt TEXT,
updatedAt TEXT,
closedAt TEXT,
authorId TEXT,
FOREIGN KEY (authorId) REFERENCES users(id)
);
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
login TEXT,
avatarUrl TEXT,
url TEXT,
isEmployee BOOLEAN
);
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS labels (
id TEXT PRIMARY KEY,
name TEXT,
color TEXT,
description TEXT
);
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS pullRequestLabels (
pullRequestId TEXT,
labelId TEXT,
FOREIGN KEY (pullRequestId) REFERENCES pullRequests(id),
FOREIGN KEY (labelId) REFERENCES labels(id)
);
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS issueLabels (
issueId TEXT,
labelId TEXT,
FOREIGN KEY (issueId) REFERENCES issues(id),
FOREIGN KEY (labelId) REFERENCES labels(id)
);
`,
).run();
};
export async function GET() {
initDb();
await migrate();
// TODO if we ever hit API Rate Limiting
const lastPRCursor = null;
const lastIssueCursor = null;
const assignableUsers = await fetchAssignableUsers();
const prs = (await fetchData(lastPRCursor)) as Array<PullRequestNode>;
const issues = (await fetchData(lastIssueCursor, true)) as Array<IssueNode>;
const fetchedPRs = (await fetchData(lastPRCursor)) as Array<PullRequestNode>;
const fetchedIssues = (await fetchData(
lastIssueCursor,
true,
)) as Array<IssueNode>;
const insertPR = db.prepare(
'INSERT INTO pullRequests (id, title, body, url, createdAt, updatedAt, closedAt, mergedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
);
const insertIssue = db.prepare(
'INSERT INTO issues (id, title, body, url, createdAt, updatedAt, closedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
);
const insertUser = db.prepare(
'INSERT INTO users (id, login, avatarUrl, url, isEmployee) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
);
const insertLabel = db.prepare(
'INSERT INTO labels (id, name, color, description) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
);
const insertPullRequestLabel = db.prepare(
'INSERT INTO pullRequestLabels (pullRequestId, labelId) VALUES (?, ?)',
);
const insertIssueLabel = db.prepare(
'INSERT INTO issueLabels (issueId, labelId) VALUES (?, ?)',
);
for (const pr of prs) {
console.log(pr);
for (const pr of fetchedPRs) {
if (pr.author == null) {
continue;
}
insertUser.run(
pr.author.resourcePath,
pr.author.login,
pr.author.avatarUrl,
pr.author.url,
assignableUsers.has(pr.author.login) ? 1 : 0,
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' },
);
insertPR.run(
pr.id,
pr.title,
pr.body,
pr.url,
pr.createdAt,
pr.updatedAt,
pr.closedAt,
pr.mergedAt,
pr.author.resourcePath,
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) {
insertLabel.run(label.id, label.name, label.color, label.description);
insertPullRequestLabel.run(pr.id, label.id);
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 issues) {
for (const issue of fetchedIssues) {
if (issue.author == null) {
continue;
}
insertUser.run(
issue.author.resourcePath,
issue.author.login,
issue.author.avatarUrl,
issue.author.url,
assignableUsers.has(issue.author.login) ? 1 : 0,
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' },
);
insertIssue.run(
issue.id,
issue.title,
issue.body,
issue.url,
issue.createdAt,
issue.updatedAt,
issue.closedAt,
issue.author.resourcePath,
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) {
insertLabel.run(label.id, label.name, label.color, label.description);
insertIssueLabel.run(issue.id, label.id);
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,
},
]);
}
}
db.close();
return new Response('Data synced', { status: 200 });
}

View File

@ -1,41 +1,44 @@
import Database from 'better-sqlite3';
import AvatarGrid from '@/app/components/AvatarGrid';
import { Header } from '@/app/components/developers/contributors/Header';
import { Background } from '@/app/components/oss-friends/Background';
import { ContentContainer } from '@/app/components/oss-friends/ContentContainer';
import { findAll } from '@/database/database';
import { pullRequestModel, userModel } from '@/database/model';
interface Contributor {
login: string;
id: string;
avatarUrl: string;
pullRequestCount: number;
}
const Contributors = async () => {
const db = new Database('db.sqlite', { readonly: true });
const contributors = await findAll(userModel);
const pullRequests = await findAll(pullRequestModel);
const contributors = db
.prepare(
`SELECT
u.login,
u.avatarUrl,
COUNT(pr.id) AS pullRequestCount
FROM
users u
JOIN
pullRequests pr ON u.id = pr.authorId
WHERE
u.isEmployee = FALSE
AND u.login NOT IN ('dependabot', 'cyborch', 'emilienchvt', 'Samox')
GROUP BY
u.id
ORDER BY
pullRequestCount DESC;
`,
const pullRequestByAuthor = pullRequests.reduce((acc, pr) => {
acc[pr.authorId] = acc[pr.authorId] ? acc[pr.authorId] + 1 : 1;
return acc;
}, {});
const fitlerContributors = contributors
.filter((contributor) => contributor.isEmployee === '0')
.filter(
(contributor) =>
![
'dependabot',
'cyborch',
'emilienchvt',
'Samox',
'nimraahmed',
'gitstart-app',
].includes(contributor.id),
)
.all() as Contributor[];
.map((contributor) => {
contributor.pullRequestCount = pullRequestByAuthor[contributor.id] || 0;
db.close();
return contributor;
})
.sort((a, b) => b.pullRequestCount - a.pullRequestCount)
.filter((contributor) => contributor.pullRequestCount > 0);
return (
<>
@ -43,7 +46,7 @@ const Contributors = async () => {
<ContentContainer>
<Header />
<div>
<AvatarGrid users={contributors} />
<AvatarGrid users={fitlerContributors as Contributor[]} />
</div>
</ContentContainer>
</>

View File

@ -0,0 +1,70 @@
import { createClient } from '@libsql/client';
import { drizzle as sqliteDrizzle } from 'drizzle-orm/libsql';
import { migrate as sqliteMigrate } from 'drizzle-orm/libsql/migrator';
import { drizzle as pgDrizzle } from 'drizzle-orm/postgres-js';
import { migrate as postgresMigrate } from 'drizzle-orm/postgres-js/migrator';
import { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core';
import postgres from 'postgres';
import 'dotenv/config';
// Todo: Deprecate SQLite once prototyping is complete, this is making things impossible to type properly
const databaseDriver = process.env.DATABASE_DRIVER;
const sqliteClient = createClient({
url: 'file:twenty-website.sqlite',
});
const pgClient = postgres(`${process.env.DATABASE_PG_URL}`);
const sqliteDb = sqliteDrizzle(sqliteClient, { logger: true });
const pgDb = pgDrizzle(pgClient, { logger: true });
const isSqliteDriver = databaseDriver === 'sqlite';
const migrate = async () => {
if (isSqliteDriver) {
await sqliteMigrate(sqliteDb, {
migrationsFolder: './src/database/sqlite/migrations',
});
} else {
await postgresMigrate(pgDb, {
migrationsFolder: './src/database/postgres/migrations',
});
}
};
const findAll = (model: SQLiteTableWithColumns<any>) => {
return isSqliteDriver
? sqliteDb.select().from(model).all()
: pgDb.select().from(model).execute();
};
// Todo: rework typing
const insertMany = async (
model: SQLiteTableWithColumns<any>,
data: any,
options?: { onConflictKey?: string },
) => {
if (isSqliteDriver) {
const query = sqliteDb.insert(model).values(data);
if (options?.onConflictKey) {
return query
.onConflictDoNothing({
target: [model[options.onConflictKey]],
})
.execute();
}
return query.execute();
}
const query = pgDb.insert(model).values(data);
if (options?.onConflictKey) {
return query
.onConflictDoNothing({
target: [model[options.onConflictKey]],
})
.execute();
}
return query.execute();
};
export { findAll, insertMany, migrate };

View File

@ -0,0 +1,47 @@
import {
pgIssueLabels,
pgIssues,
pgLabels,
pgPullRequestLabels,
pgPullRequests,
pgUsers,
} from '@/database/postgres/schema-postgres';
import {
sqlLiteIssueLabels,
sqlLiteIssues,
sqlLiteLabels,
sqlLitePullRequestLabels,
sqlLitePullRequests,
sqlLiteUsers,
} from '@/database/sqlite/schema-sqlite';
const databaseDriver = process.env.DATABASE_DRIVER;
const isSqliteDriver = databaseDriver === 'sqlite';
export const userModel = isSqliteDriver ? sqlLiteUsers : pgUsers;
export const pullRequestModel = isSqliteDriver
? sqlLitePullRequests
: pgPullRequests;
export const issueModel = isSqliteDriver ? sqlLiteIssues : pgIssues;
export const labelModel = isSqliteDriver ? sqlLiteLabels : pgLabels;
export const pullRequestLabelModel = isSqliteDriver
? sqlLitePullRequestLabels
: pgPullRequestLabels;
export const issueLabelModel = isSqliteDriver
? sqlLiteIssueLabels
: pgIssueLabels;
export type User = typeof sqlLiteUsers.$inferSelect;
export type PullRequest = typeof sqlLitePullRequests.$inferSelect;
export type Issue = typeof sqlLiteIssues.$inferSelect;
export type Label = typeof sqlLiteLabels.$inferSelect;
export type PullRequestLabel = typeof sqlLitePullRequestLabels.$inferSelect;
export type IssueLabel = typeof sqlLiteIssueLabels.$inferSelect;
export type UserInsert = typeof sqlLiteUsers.$inferInsert;
export type PullRequestInsert = typeof sqlLitePullRequests.$inferInsert;
export type IssueInsert = typeof sqlLiteIssues.$inferInsert;
export type LabelInsert = typeof sqlLiteLabels.$inferInsert;
export type PullRequestLabelInsert =
typeof sqlLitePullRequestLabels.$inferInsert;
export type IssueLabelInsert = typeof sqlLiteIssueLabels.$inferInsert;

View File

@ -0,0 +1,14 @@
import { Config } from 'drizzle-kit';
import 'dotenv/config';
const pgConfig = {
schema: './src/database/postgres/schema-postgres.ts',
out: './src/database/postgres/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_PG_URL ?? '',
},
} satisfies Config;
export default pgConfig;

View File

@ -0,0 +1,84 @@
CREATE TABLE IF NOT EXISTS "issueLabels" (
"issueId" text,
"labelId" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "issues" (
"id" text PRIMARY KEY NOT NULL,
"externalId" text,
"title" text,
"body" text,
"url" text,
"createdAt" text,
"updatedAt" text,
"closedAt" text,
"authorId" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "labels" (
"id" text PRIMARY KEY NOT NULL,
"externalId" text,
"name" text,
"color" text,
"description" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "pullRequestLabels" (
"pullRequestExternalId" text,
"labelId" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "pullRequests" (
"id" text PRIMARY KEY NOT NULL,
"title" text,
"body" text,
"url" text,
"createdAt" text,
"updatedAt" text,
"closedAt" text,
"mergedAt" text,
"authorId" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" text PRIMARY KEY NOT NULL,
"avatarUrl" text,
"url" text,
"isEmployee" text
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "issueLabels" ADD CONSTRAINT "issueLabels_issueId_issues_id_fk" FOREIGN KEY ("issueId") REFERENCES "issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "issueLabels" ADD CONSTRAINT "issueLabels_labelId_labels_id_fk" FOREIGN KEY ("labelId") REFERENCES "labels"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "issues" ADD CONSTRAINT "issues_authorId_users_id_fk" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "pullRequestLabels" ADD CONSTRAINT "pullRequestLabels_pullRequestExternalId_pullRequests_id_fk" FOREIGN KEY ("pullRequestExternalId") REFERENCES "pullRequests"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "pullRequestLabels" ADD CONSTRAINT "pullRequestLabels_labelId_labels_id_fk" FOREIGN KEY ("labelId") REFERENCES "labels"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "pullRequests" ADD CONSTRAINT "pullRequests_authorId_users_id_fk" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@ -0,0 +1,343 @@
{
"id": "a7895a79-44a3-4fad-b750-f89d8c04d85c",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
"tables": {
"issueLabels": {
"name": "issueLabels",
"schema": "",
"columns": {
"issueId": {
"name": "issueId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"labelId": {
"name": "labelId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"issueLabels_issueId_issues_id_fk": {
"name": "issueLabels_issueId_issues_id_fk",
"tableFrom": "issueLabels",
"tableTo": "issues",
"columnsFrom": [
"issueId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"issueLabels_labelId_labels_id_fk": {
"name": "issueLabels_labelId_labels_id_fk",
"tableFrom": "issueLabels",
"tableTo": "labels",
"columnsFrom": [
"labelId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"issues": {
"name": "issues",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"externalId": {
"name": "externalId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"updatedAt": {
"name": "updatedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"closedAt": {
"name": "closedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"authorId": {
"name": "authorId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"issues_authorId_users_id_fk": {
"name": "issues_authorId_users_id_fk",
"tableFrom": "issues",
"tableTo": "users",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"labels": {
"name": "labels",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"externalId": {
"name": "externalId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pullRequestLabels": {
"name": "pullRequestLabels",
"schema": "",
"columns": {
"pullRequestExternalId": {
"name": "pullRequestExternalId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"labelId": {
"name": "labelId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"pullRequestLabels_pullRequestExternalId_pullRequests_id_fk": {
"name": "pullRequestLabels_pullRequestExternalId_pullRequests_id_fk",
"tableFrom": "pullRequestLabels",
"tableTo": "pullRequests",
"columnsFrom": [
"pullRequestExternalId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"pullRequestLabels_labelId_labels_id_fk": {
"name": "pullRequestLabels_labelId_labels_id_fk",
"tableFrom": "pullRequestLabels",
"tableTo": "labels",
"columnsFrom": [
"labelId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pullRequests": {
"name": "pullRequests",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"updatedAt": {
"name": "updatedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"closedAt": {
"name": "closedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"mergedAt": {
"name": "mergedAt",
"type": "text",
"primaryKey": false,
"notNull": false
},
"authorId": {
"name": "authorId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"pullRequests_authorId_users_id_fk": {
"name": "pullRequests_authorId_users_id_fk",
"tableFrom": "pullRequests",
"tableTo": "users",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isEmployee": {
"name": "isEmployee",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1707921820164,
"tag": "0000_absent_giant_man",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,52 @@
import { pgTable, text } from 'drizzle-orm/pg-core';
export const pgUsers = pgTable('users', {
id: text('id').primaryKey(),
avatarUrl: text('avatarUrl'),
url: text('url'),
isEmployee: text('isEmployee'),
});
export const pgPullRequests = pgTable('pullRequests', {
id: text('id').primaryKey(),
name: text('title'),
body: text('body'),
url: text('url'),
createdAt: text('createdAt'),
updatedAt: text('updatedAt'),
closedAt: text('closedAt'),
mergedAt: text('mergedAt'),
authorId: text('authorId').references(() => pgUsers.id),
});
export const pgIssues = pgTable('issues', {
id: text('id').primaryKey(),
externalId: text('externalId'),
title: text('title'),
body: text('body'),
url: text('url'),
createdAt: text('createdAt'),
updatedAt: text('updatedAt'),
closedAt: text('closedAt'),
authorId: text('authorId').references(() => pgUsers.id),
});
export const pgLabels = pgTable('labels', {
id: text('id').primaryKey(),
externalId: text('externalId'),
name: text('name'),
color: text('color'),
description: text('description'),
});
export const pgPullRequestLabels = pgTable('pullRequestLabels', {
pullRequestId: text('pullRequestExternalId').references(
() => pgPullRequests.id,
),
labelId: text('labelId').references(() => pgLabels.id),
});
export const pgIssueLabels = pgTable('issueLabels', {
issueId: text('issueId').references(() => pgIssues.id),
labelId: text('labelId').references(() => pgLabels.id),
});

View File

@ -0,0 +1,14 @@
import { Config } from 'drizzle-kit';
import 'dotenv/config';
const sqliteConfig = {
schema: './src/database/sqlite/schema-sqlite.ts',
out: './src/database/sqlite/migrations',
driver: 'libsql',
dbCredentials: {
url: 'twenty-website.sqlite',
},
} satisfies Config;
export default sqliteConfig;

View File

@ -0,0 +1,54 @@
CREATE TABLE `issueLabels` (
`issueId` text,
`labelId` text,
FOREIGN KEY (`issueId`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`labelId`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `issues` (
`id` text PRIMARY KEY NOT NULL,
`externalId` text,
`title` text,
`body` text,
`url` text,
`createdAt` text,
`updatedAt` text,
`closedAt` text,
`authorId` text,
FOREIGN KEY (`authorId`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `labels` (
`id` text PRIMARY KEY NOT NULL,
`externalId` text,
`name` text,
`color` text,
`description` text
);
--> statement-breakpoint
CREATE TABLE `pullRequestLabels` (
`pullRequestExternalId` text,
`labelId` text,
FOREIGN KEY (`pullRequestExternalId`) REFERENCES `pullRequests`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`labelId`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `pullRequests` (
`id` text PRIMARY KEY NOT NULL,
`title` text,
`body` text,
`url` text,
`createdAt` text,
`updatedAt` text,
`closedAt` text,
`mergedAt` text,
`authorId` text,
FOREIGN KEY (`authorId`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` text PRIMARY KEY NOT NULL,
`avatarUrl` text,
`url` text,
`isEmployee` text
);

View File

@ -0,0 +1,367 @@
{
"version": "5",
"dialect": "sqlite",
"id": "033cd768-53b9-4c60-99ee-be070ea7abd6",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"issueLabels": {
"name": "issueLabels",
"columns": {
"issueId": {
"name": "issueId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"labelId": {
"name": "labelId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"issueLabels_issueId_issues_id_fk": {
"name": "issueLabels_issueId_issues_id_fk",
"tableFrom": "issueLabels",
"tableTo": "issues",
"columnsFrom": [
"issueId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"issueLabels_labelId_labels_id_fk": {
"name": "issueLabels_labelId_labels_id_fk",
"tableFrom": "issueLabels",
"tableTo": "labels",
"columnsFrom": [
"labelId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"issues": {
"name": "issues",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"externalId": {
"name": "externalId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updatedAt": {
"name": "updatedAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"closedAt": {
"name": "closedAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"authorId": {
"name": "authorId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"issues_authorId_users_id_fk": {
"name": "issues_authorId_users_id_fk",
"tableFrom": "issues",
"tableTo": "users",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"labels": {
"name": "labels",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"externalId": {
"name": "externalId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pullRequestLabels": {
"name": "pullRequestLabels",
"columns": {
"pullRequestExternalId": {
"name": "pullRequestExternalId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"labelId": {
"name": "labelId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pullRequestLabels_pullRequestExternalId_pullRequests_id_fk": {
"name": "pullRequestLabels_pullRequestExternalId_pullRequests_id_fk",
"tableFrom": "pullRequestLabels",
"tableTo": "pullRequests",
"columnsFrom": [
"pullRequestExternalId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"pullRequestLabels_labelId_labels_id_fk": {
"name": "pullRequestLabels_labelId_labels_id_fk",
"tableFrom": "pullRequestLabels",
"tableTo": "labels",
"columnsFrom": [
"labelId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"pullRequests": {
"name": "pullRequests",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updatedAt": {
"name": "updatedAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"closedAt": {
"name": "closedAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"mergedAt": {
"name": "mergedAt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"authorId": {
"name": "authorId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"pullRequests_authorId_users_id_fk": {
"name": "pullRequests_authorId_users_id_fk",
"tableFrom": "pullRequests",
"tableTo": "users",
"columnsFrom": [
"authorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"isEmployee": {
"name": "isEmployee",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1707920913359,
"tag": "0000_small_karma",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,52 @@
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const sqlLiteUsers = sqliteTable('users', {
id: text('id').primaryKey(),
avatarUrl: text('avatarUrl'),
url: text('url'),
isEmployee: text('isEmployee'),
});
export const sqlLitePullRequests = sqliteTable('pullRequests', {
id: text('id').primaryKey(),
name: text('title'),
body: text('body'),
url: text('url'),
createdAt: text('createdAt'),
updatedAt: text('updatedAt'),
closedAt: text('closedAt'),
mergedAt: text('mergedAt'),
authorId: text('authorId').references(() => sqlLiteUsers.id),
});
export const sqlLiteIssues = sqliteTable('issues', {
id: text('id').primaryKey(),
externalId: text('externalId'),
title: text('title'),
body: text('body'),
url: text('url'),
createdAt: text('createdAt'),
updatedAt: text('updatedAt'),
closedAt: text('closedAt'),
authorId: text('authorId').references(() => sqlLiteUsers.id),
});
export const sqlLiteLabels = sqliteTable('labels', {
id: text('id').primaryKey(),
externalId: text('externalId'),
name: text('name'),
color: text('color'),
description: text('description'),
});
export const sqlLitePullRequestLabels = sqliteTable('pullRequestLabels', {
pullRequestId: text('pullRequestExternalId').references(
() => sqlLitePullRequests.id,
),
labelId: text('labelId').references(() => sqlLiteLabels.id),
});
export const sqlLiteIssueLabels = sqliteTable('issueLabels', {
issueId: text('issueId').references(() => sqlLiteIssues.id),
labelId: text('labelId').references(() => sqlLiteLabels.id),
});

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

784
yarn.lock

File diff suppressed because it is too large Load Diff