Skip to content

Commit 6da5665

Browse files
chore: tenant based apis (#390)
* refactor app * package-lock app-htmx @acme/ deps * better transform api response * extract-tenants:apiHandler * cleanup extract-tenants:apiHandler * remove handler (api) from extract-repository * sort deps --------- Co-authored-by: David Abram <[email protected]>
1 parent 6940151 commit 6da5665

File tree

18 files changed

+310
-281
lines changed

18 files changed

+310
-281
lines changed

apps/app-htmx/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"esbuild": "^0.19.9"
1515
},
1616
"dependencies": {
17+
"@acme/extract-functions": "*",
18+
"@acme/extract-schema": "*",
1719
"@acme/source-control": "*",
1820
"@acme/super-schema": "*",
1921
"@clerk/fastify": "0.6.30",
@@ -23,4 +25,4 @@
2325
"fastify": "4.25.2",
2426
"nunjucks": "3.2.4"
2527
}
26-
}
28+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { GitHubSourceControl, GitlabSourceControl, type SourceControl } from "@acme/source-control"
2+
import type { Tenant } from "@acme/super-schema"
3+
import type { LibSQLDatabase } from "drizzle-orm/libsql"
4+
import { clerkClient } from "@clerk/fastify";
5+
import { tenantDb } from "./tenant-db";
6+
7+
const getUserForgeAccessToken = async (userId: string, forge: "github" | "gitlab") => {
8+
const userTokens = await clerkClient.users.getUserOauthAccessToken(userId, `oauth_${forge}`);
9+
if (userTokens[0] === undefined) throw new Error("no token");
10+
return userTokens[0].token;
11+
}
12+
13+
type BaseExtractContext = {
14+
db: LibSQLDatabase,
15+
integrations: {
16+
sourceControl: SourceControl | null
17+
}
18+
}
19+
20+
type ExtractContextOptions = {
21+
tenant: Tenant;
22+
userId: string;
23+
forge: "github" | "gitlab";
24+
}
25+
26+
export const extractContext: {
27+
<TExtendedContext extends { integrations?: object } | object>(opts: ExtractContextOptions, ctx: TExtendedContext): Promise<TExtendedContext & BaseExtractContext>;
28+
} = async ({ tenant, userId, forge }, ctx) => {
29+
const token = await getUserForgeAccessToken(userId, forge);
30+
31+
let sourceControl = null;
32+
if (forge === "github") sourceControl = new GitHubSourceControl({ auth: token });
33+
else if (forge === "gitlab") sourceControl = new GitlabSourceControl(token);
34+
35+
const ctxIntegrations = ('integrations' in ctx) ? ctx.integrations : {};
36+
37+
return {
38+
...ctx,
39+
db: tenantDb(tenant),
40+
integrations: {
41+
...ctxIntegrations,
42+
sourceControl
43+
}
44+
}
45+
}

apps/app-htmx/src/functions/fetch-repository.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

apps/app-htmx/src/functions/get-repositories.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import type { LibSQLDatabase } from "drizzle-orm/libsql";
44

55
export const getRepositories = async (db: LibSQLDatabase) => {
66
const repos = await db.select({
7+
id: repositories.id,
78
forge: repositories.forgeType,
89
name: repositories.name,
910
org: namespaces.name,
1011
projectId: repositories.externalId,
1112
}).from(repositories).innerJoin(namespaces, eq(repositories.namespaceId, namespaces.id)).all()
1213

13-
return repos;
14+
return repos.map(repo=>({
15+
...repo,
16+
key: `${repo.forge}-${repo.projectId}`,
17+
}));
1418
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Tenant } from "@acme/super-schema";
2+
import { createClient } from "@libsql/client";
3+
import { drizzle } from "drizzle-orm/libsql";
4+
import { AppConfig } from "src/app-config";
5+
6+
export const tenantDb = (tenant:Tenant)=> {
7+
const client = createClient({
8+
url: tenant.dbUrl,
9+
authToken: AppConfig.tenantDatabaseAuthToken
10+
});
11+
return drizzle(client);
12+
}

apps/app-htmx/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { clerkPlugin } from "@clerk/fastify";
77
import { Home } from "./pages/page.home.js";
88
import fastifyFormbody from "@fastify/formbody";
99
import { SignIn } from "./pages/page.sign-in.js";
10-
import { ExtractRepository } from "./pages/repository/extract.js";
1110
import { RegisterRepository } from "./pages/repository/register.js";
1211
import { AppConfig } from "./app-config.js";
1312
import { StartTransform } from "./pages/start-transform.js";
13+
import { StartExtract } from "./pages/start-extract.js";
1414

1515
AppConfig; // ensure loaded before starting server
1616

@@ -30,7 +30,7 @@ await fastify.register(fastifyFormbody);
3030

3131
fastify.get('/', Home);
3232
fastify.get('/sign-in',SignIn);
33-
fastify.post('/repository/extract', ExtractRepository);
33+
fastify.post('/extract', StartExtract);
3434
fastify.post('/repository/register', RegisterRepository);
3535
fastify.post('/transform', StartTransform);
3636

apps/app-htmx/src/pages/page.home.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const Home: RouteHandlerMethod = async (request, reply) => {
3030
const tenantQuery = query.tenant || tenantList.tenantList[0]!.name;
3131
const tenant = tenantList.tenantList.find(t => t.name === tenantQuery);
3232
if (!tenant) return reply.redirect(303, "/");
33-
const targetTenantId = tenant.id;
3433

3534
const db = drizzle(createClient({
3635
url: tenant.dbUrl,
@@ -46,8 +45,7 @@ export const Home: RouteHandlerMethod = async (request, reply) => {
4645
...page,
4746
...htmx,
4847
...tenantList,
49-
targetTenant: tenant,
50-
targetTenantId,
48+
tenant,
5149
repos,
5250
dates: {
5351
yesterday: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().slice(0, 10),

apps/app-htmx/src/pages/repository/extract.ts

Lines changed: 0 additions & 72 deletions
This file was deleted.
Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { RouteHandlerMethod } from "fastify";
22
import { getAuth } from "@clerk/fastify"
33
import { z } from "zod";
4-
import { tryFetchRepository } from "src/functions/fetch-repository";
54
import { tenantListContext } from "src/context/tenant-list.context";
5+
import { type GetRepositoryFunction, getRepository } from "@acme/extract-functions";
6+
import { namespaces, repositories } from "@acme/extract-schema";
7+
import { extractContext } from "src/functions/extract-context";
68

79
const RegisterInput = z.object({
810
target_tenant_id: z.coerce.number(),
@@ -12,6 +14,13 @@ const RegisterInput = z.object({
1214
project_id: z.string().optional()
1315
})
1416

17+
const getRepositoryEntitiesContext = {
18+
entities: {
19+
namespaces: namespaces,
20+
repositories: repositories,
21+
},
22+
} satisfies Pick<Parameters<GetRepositoryFunction>[1], 'entities'>;
23+
1524
export const RegisterRepository: RouteHandlerMethod = async (request, reply) => {
1625
const auth = getAuth(request);
1726
if (!auth.userId) return reply.status(404).send(); // hide actions from unauthenticated users
@@ -21,37 +30,49 @@ export const RegisterRepository: RouteHandlerMethod = async (request, reply) =>
2130
const safeInput = RegisterInput.safeParse(request.body);
2231
if (!safeInput.success) return reply.status(400).send();
2332

33+
const targetTenantId = safeInput.data.target_tenant_id;
34+
const tenant = tenantList.find(tenant => tenant.id === targetTenantId);
35+
if (!tenant) return reply.view("component.log.html", { error: `Invalid target_tenant_id: ${targetTenantId}` });
36+
37+
const { userId } = auth;
38+
const { forge } = safeInput.data;
39+
2440
const input = {
25-
userId: auth.userId,
26-
forge: safeInput.data.forge,
41+
externalRepositoryId: Number(safeInput.data.project_id) || 0,
2742
namespaceName: safeInput.data.owner || "",
2843
repositoryName: safeInput.data.repo || "",
29-
repositoryId: Number(safeInput.data.project_id) || 0
3044
}
3145

32-
const { repository, namespace } = await tryFetchRepository(input);
46+
const getRepositoryContext = await extractContext({ tenant, userId, forge }, getRepositoryEntitiesContext);
3347

34-
if (!repository || !namespace) return reply.view("component.log.html",
35-
safeInput.data.forge === "github" ? { error: `Repository ${input.namespaceName}/${input.repositoryName} not found` }
36-
: { error: `Project ${input.repositoryId} not found` });
48+
try {
49+
const { repository, namespace } = await getRepository(input, getRepositoryContext);
3750

38-
const targetTenantId = safeInput.data.target_tenant_id;
39-
const tenant = tenantList.find(tenant=>tenant.id === targetTenantId);
40-
if (!tenant) return reply.view("component.log.html", { error: `Invalid target_tenant_id: ${targetTenantId}` });
51+
if (repository._createdAt?.getTime() !== repository._updatedAt?.getTime()) {
52+
return reply.view("component.log.html", { log: `Repository : ${namespace.name}/${repository.name} is already registered.` });
53+
}
4154

42-
const repo = {
43-
name: repository.name,
44-
org: namespace.name,
45-
forge: repository.forgeType,
46-
projectId: repository.externalId
47-
}
48-
49-
return reply.view("component.repository.html", {
50-
repo,
51-
targetTenantId,
52-
dates: {
53-
yesterday: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().slice(0, 10),
54-
today: new Date().toISOString().slice(0, 10)
55+
const repo = {
56+
key: `${forge}-${repository.externalId}`,
57+
name: repository.name,
58+
org: namespace.name,
59+
forge,
60+
projectId: repository.externalId,
5561
}
56-
});
62+
63+
return reply.view("component.repository.html", {
64+
repo,
65+
tenant,
66+
dates: {
67+
yesterday: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().slice(0, 10),
68+
today: new Date().toISOString().slice(0, 10)
69+
}
70+
});
71+
72+
} catch (error) {
73+
console.log(error);
74+
const errorMessage = error instanceof Error? error.message : error;
75+
return reply.view("component.log.html", { error: errorMessage });
76+
}
77+
5778
}

0 commit comments

Comments
 (0)