feat(infra): Update infra and add support for teams to SST (#186)

## Description
- [x] Adds support for AWS SSO, which makes us (the team) able to use
SST and update the components independently
- [x] Splits the webpage into the landing page (Qwik), and Astro (the
console) in charge of playing. This allows us to pass in Environment
Variables to the console
- ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This
allows us to use Fumadocs _citation needed_ that's much more beautiful,
and supports OpenApi~
- Cloudflare pages with github integration is not working on our new CF
account. So we will have to push the pages deployment manually with
Github actions
- [x] Moves the current set up from my personal CF and AWS accounts to
dedicated Nestri accounts -

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [x] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->
Please approve my PR 🥹


## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
This commit is contained in:
Wanjohi
2025-02-27 18:52:05 +03:00
committed by GitHub
parent 237e016b2d
commit 457aac2258
138 changed files with 4218 additions and 2579 deletions

View File

@@ -1,164 +1,152 @@
import { z } from "zod";
import databaseClient from "../database"
import { fn } from "../utils";
import { groupBy, map, pipe, values } from "remeda"
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { VisibleError } from "../error";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { teamTable } from "./team.sql";
import { createEvent } from "../event";
import { assertActor } from "../actor";
import { and, eq, sql } from "../drizzle";
import { memberTable } from "../member/member.sql";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Teams {
export module Team {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Team.id,
}),
name: z.string().openapi({
description: "Name of the team",
example: Examples.Team.name,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this team was first created",
example: Examples.Team.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this team was last edited",
example: Examples.Team.updatedAt,
}),
// owner: z.boolean().openapi({
// description: "Whether this team is owned by this user",
// example: Examples.Team.owner,
// }),
slug: z.string().openapi({
description: "This is the unique name identifier for the team",
description: "The unique and url-friendly slug of this team",
example: Examples.Team.slug
}),
name: z.string().openapi({
description: "The name of this team",
example: Examples.Team.name
})
})
.openapi({
ref: "Team",
description: "A group of users sharing the same machines for gaming.",
description: "Represents a team on Nestri",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
export const Events = {
Created: createEvent(
"team.created",
z.object({
teamID: z.string().nonempty(),
}),
),
};
const query = {
teams: {
$: {
where: {
members: user.id,
deletedAt: { $isNull: true }
}
},
}
export class WorkspaceExistsError extends VisibleError {
constructor(slug: string) {
super(
"team.slug_exists",
`there is already a workspace named "${slug}"`,
);
}
const res = await db.query(query)
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
slug: group[0].slug,
//@ts-expect-error
owner: group[0].owner === user.id
}))
)
return result
}
export const create = fn(
Info.pick({ slug: true, id: true, name: true }).partial({
id: true,
}), (input) => {
createTransaction(async (tx) => {
const id = input.id ?? createID("team");
const result = await tx.insert(teamTable).values({
id,
slug: input.slug,
name: input.name
})
.onConflictDoNothing()
.returning({ insertedID: teamTable.id })
export const fromSlug = fn(z.string(), async (slug) => {
const db = databaseClient()
if (result.length === 0) throw new WorkspaceExistsError(input.slug);
const query = {
teams: {
$: {
where: {
slug,
deletedAt: { $isNull: true }
}
},
}
}
await afterTx(() =>
bus.publish(Resource.Bus, Events.Created, {
teamID: id,
}),
);
return id;
})
})
const res = await db.query(query)
export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => {
const account = assertActor("user");
const row = await tx
.select({
teamID: memberTable.teamID,
})
.from(memberTable)
.where(
and(
eq(memberTable.teamID, input),
eq(memberTable.email, account.properties.email),
),
)
.execute()
.then((rows) => rows.at(0));
if (!row) return;
await tx
.update(teamTable)
.set({
timeDeleted: sql`now()`,
})
.where(eq(teamTable.id, row.teamID));
}),
);
const teams = res.teams
if (!teams || teams.length === 0) {
return null
}
export const list = fn(z.void(), () =>
useTransaction((tx) =>
tx
.select()
.from(teamTable)
.execute()
.then((rows) => rows.map(serialize)),
),
);
const result = pipe(
teams,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
slug: group[0].slug,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
// owner: group[0].owner === user.id
}))
)
export const fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) => {
return tx
.select()
.from(teamTable)
.where(eq(teamTable.id, id))
.execute()
.then((rows) => rows.map(serialize))
.then((rows) => rows.at(0));
}),
);
return result[0]
})
export const fromSlug = fn(z.string().min(1), async (input) =>
useTransaction(async (tx) => {
return tx
.select()
.from(teamTable)
.where(eq(teamTable.slug, input))
.execute()
.then((rows) => rows.map(serialize))
.then((rows) => rows.at(0));
}),
);
export const create = fn(Info.pick({ name: true, slug: true }), async (input) => {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
export function serialize(
input: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
slug: input.slug,
createdAt: now,
updatedAt: now,
}).link({ owner: user.id, members: user.id }))
return id
})
export const remove = fn(z.string(), async (id) => {
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.teams[id]!.update({
deletedAt: now
}))
return "ok"
})
export const invite = fn(z.object({email:z.string(), id: z.string()}), async (input) => {
//TODO:
// const db = databaseClient()
// const now = new Date().toISOString()
// await db.transact(db.tx.teams[id]!.update({
// deletedAt: now
// }))
return "ok"
})
};
}
}