16 Commits

Author SHA1 Message Date
Wanjohi
8aa983834c Merge branch 'main' into feat/play 2025-03-16 00:11:54 +03:00
AquaWolf
5189bf768a merge 2025-03-15 21:06:30 +01:00
AquaWolf
b734892c55 extracted modal component + showing modal on enter and mouspointer loss 2025-03-09 14:53:11 +01:00
AquaWolf
402e894224 added modal with correct styling. Open: need to wire the modals correctly and have the welcome modal 2025-03-03 22:28:53 +01:00
Wanjohi
fb0cb0b6ca fix: Portal mount 2025-03-03 23:47:00 +03:00
Wanjohi
4fd339b55f fix: Have a root component 2025-03-03 23:42:58 +03:00
Wanjohi
1e78238593 fix: Colors 2025-03-03 23:36:18 +03:00
AquaWolf
c994dc112c added new App changes and background for theme 2025-03-03 21:16:27 +01:00
AquaWolf
a727a9b710 some wip with styles 2025-03-03 21:07:13 +01:00
AquaWolf
805a8a6115 some changes to the play route 2025-03-03 17:39:38 +01:00
Wanjohi
05aa177681 Merge branch 'main' into feat/play 2025-03-03 15:00:31 +03:00
Wanjohi
421fcb067c Merge branch 'main' into feat/play 2025-03-02 14:58:11 +03:00
Wanjohi
7dee7e480b Merge branch 'main' into feat/play 2025-03-02 01:31:37 +03:00
Wanjohi
1a49c709f7 Merge branch 'main' into feat/play 2025-03-02 00:11:22 +03:00
AquaWolf
90e0533fdd right parameter 2025-02-28 21:47:46 +01:00
AquaWolf
058ac24954 added first draft of the play route 2025-02-28 21:30:23 +01:00
259 changed files with 5506 additions and 32736 deletions

View File

@@ -1,2 +1,28 @@
## Description ## Description
<!-- Briefly describe the purpose and scope of your changes --> <!-- Briefly describe the purpose and scope of your changes -->
## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->
## Type of Change
- [ ] Bug fix (non-breaking change)
- [ ] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing functionality)
- [ ] Documentation update
- [ ] Other (please describe):
## Checklist
- [ ] I have updated relevant documentation
- [ ] My code follows the project's coding style
- [ ] 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 -->
## 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 -->

View File

@@ -1,40 +0,0 @@
name: Build docs
on:
pull_request:
paths:
- "apps/docs/**"
- ".github/workflows/docs.yml"
push:
branches: [main]
paths:
- "apps/docs/**"
- ".github/workflows/docs.yml"
jobs:
deploy-docs:
name: Build and deploy docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: "apps/docs"
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build Project Artifacts
run: bun run build
- name: Deploy Project Artifacts to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
packageManager: bun
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "3.93.0"
workingDirectory: "apps/docs"
command: pages deploy ./dist --project-name=${{ vars.CF_DOCS_PAGES_PROJECT_NAME }} --commit-dirty=true
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@@ -27,7 +27,6 @@ env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: nestrilabs/nestri IMAGE_NAME: nestrilabs/nestri
BASE_TAG_PREFIX: runner BASE_TAG_PREFIX: runner
BASE_IMAGE: docker.io/cachyos/cachyos:latest
# This makes our release ci quit prematurely # This makes our release ci quit prematurely
# concurrency: # concurrency:
@@ -56,7 +55,7 @@ jobs:
swap-size-gb: 20 swap-size-gb: 20
- -
name: Build Docker image name: Build Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
file: containers/runner.Containerfile file: containers/runner.Containerfile
context: ./ context: ./
@@ -108,7 +107,7 @@ jobs:
swap-size-gb: 20 swap-size-gb: 20
- -
name: Build Docker image name: Build Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
file: containers/runner.Containerfile file: containers/runner.Containerfile
context: ./ context: ./
@@ -117,4 +116,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,mode=max cache-from: type=gha,mode=max
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
pull: ${{ github.event_name == 'schedule' }} # Pull base image for scheduled builds

View File

@@ -1,40 +0,0 @@
name: Build www
on:
pull_request:
paths:
- "apps/www/**"
- ".github/workflows/www.yml"
push:
branches: [main]
paths:
- "apps/www/**"
- ".github/workflows/www.yml"
jobs:
deploy-www:
name: Build and deploy www
runs-on: ubuntu-latest
defaults:
run:
working-directory: "apps/www"
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build Project Artifacts
run: bun run build
- name: Deploy Project Artifacts to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
packageManager: bun
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "3.93.0"
workingDirectory: "apps/www"
command: pages deploy ./dist --project-name=${{ vars.CF_WWW_PAGES_PROJECT_NAME }} --commit-dirty=true
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,19 @@
"private": true, "private": true,
"scripts": { "scripts": {
"nestri.dev": "nuxi dev", "nestri.dev": "nuxi dev",
"build": "nuxi build --preset=cloudflare_pages", "build": "nuxi build",
"generate": "nuxi generate", "generate": "nuxi generate",
"preview": "nuxi preview", "preview": "nuxi preview",
"lint": "eslint ." "lint": "eslint ."
}, },
"devDependencies": { "devDependencies": {
"@nuxt-themes/docus": "latest", "@nuxt-themes/docus": "latest",
"@nuxt/devtools": "^2.3.2", "@nuxt/devtools": "^1.4.1",
"@nuxt/eslint-config": "^0.5.6", "@nuxt/eslint-config": "^0.5.6",
"@nuxt/ui": "^2.19.2", "@nuxt/ui": "^2.19.2",
"@nuxtjs/plausible": "^1.0.2", "@nuxtjs/plausible": "^1.0.2",
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"eslint": "^9.10.0", "eslint": "^9.10.0",
"nuxt": "^3.16.1" "nuxt": "^3.15.4"
} }
} }

View File

@@ -44,7 +44,7 @@
"@nestri/libmoq": "*", "@nestri/libmoq": "*",
"@nestri/sdk": "0.1.0-alpha.14", "@nestri/sdk": "0.1.0-alpha.14",
"@nestri/ui": "*", "@nestri/ui": "*",
"@openauthjs/openauth": "*", "@openauthjs/openauth": "^0.2.6",
"@polar-sh/checkout": "^0.1.8", "@polar-sh/checkout": "^0.1.8",
"@polar-sh/sdk": "^0.21.1", "@polar-sh/sdk": "^0.21.1",
"@qwik-ui/headless": "^0.6.4", "@qwik-ui/headless": "^0.6.4",
@@ -67,7 +67,7 @@
"typescript": "5.4.5", "typescript": "5.4.5",
"undici": "*", "undici": "*",
"valibot": "^0.42.1", "valibot": "^0.42.1",
"vite": "6.0.15", "vite": "5.4.12",
"vite-tsconfig-paths": "^4.2.1", "vite-tsconfig-paths": "^4.2.1",
"wrangler": "^3.0.0" "wrangler": "^3.0.0"
}, },

View File

@@ -16,8 +16,8 @@ export default component$(() => {
<div class="w-screen relative"> <div class="w-screen relative">
<HeroSection client:load> <HeroSection client:load>
<div class="sm:w-full flex gap-3 justify-center pt-4 sm:flex-row flex-col w-auto items-center"> <div class="sm:w-full flex gap-3 justify-center pt-4 sm:flex-row flex-col w-auto items-center">
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="flex ring-2 ring-primary-500 font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" > <Link href="/auth/login" prefetch={false} class="flex ring-2 ring-primary-500 font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord Get early access
</Link> </Link>
<Link href="/links/github" prefetch={false} class="sm:flex text-sm sm:text-base hidden font-bricolage items-center gap-2 rounded-full font-semibold text-gray-900/70 dark:text-gray-100/70 bg-white dark:bg-black px-5 py-4 ring-2 ring-gray-300 dark:ring-gray-700 transition-all hover:scale-105 active:scale-95 sm:px-6" > <Link href="/links/github" prefetch={false} class="sm:flex text-sm sm:text-base hidden font-bricolage items-center gap-2 rounded-full font-semibold text-gray-900/70 dark:text-gray-100/70 bg-white dark:bg-black px-5 py-4 ring-2 ring-gray-300 dark:ring-gray-700 transition-all hover:scale-105 active:scale-95 sm:px-6" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 fill-content3-light"><path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5 fill-content3-light"><path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd"></path></svg>
@@ -570,8 +570,8 @@ export default component$(() => {
</section> </section>
<Footer client:load> <Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3"> <div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="ring-2 ring-primary-500 flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" > <Link href="/auth/login" prefetch={false} class="ring-2 ring-primary-500 flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord Get early access
</Link> </Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400"> <div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200"> <span class="hover:text-primary-500 transition-colors duration-200">

View File

@@ -116,30 +116,8 @@ export default component$(() => {
return ( return (
<div class="w-screen relative"> <div class="w-screen relative">
<TitleSection client:load title="Pricing" description={"The biggest bang, binge, and blast for your buck"} /> <TitleSection client:load title="Pricing" description={"We're growing at the speed of trust. Choose a price that feels right for you and help support Nestri"} />
<MotionComponent
<Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="https://discord.gg/6um5K6jrYj" prefetch={false} class="flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Join our Discord
</Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">
<Link rel="noreferrer" href="/terms" >Terms of Service</Link></span>
<span class="text-gray-400 dark:text-gray-600"></span>
<span class="hover:text-primary-500 transition-colors duration-200" >
<Link href="/privacy">Privacy Policy</Link>
</span>
</div>
</div>
</Footer>
</div>
)
})
/**
* <MotionComponent
initial={{ opacity: 0, y: 100 }} initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
@@ -166,6 +144,7 @@ export default component$(() => {
</div> </div>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<p class="text-[4rem] leading-[1] font-medium font-title"> Free </p> <p class="text-[4rem] leading-[1] font-medium font-title"> Free </p>
{/**FIXME: Add the link to the docs here */}
<a href={CONSTANTS.githubLink} ref={v => bookRef.value = v} class="h-[154px] w-full flex items-start pt-4 justify-center overflow-hidden"> <a href={CONSTANTS.githubLink} ref={v => bookRef.value = v} class="h-[154px] w-full flex items-start pt-4 justify-center overflow-hidden">
<Book textColor="#FFF" <Book textColor="#FFF"
bgColor="#FF4F01" bgColor="#FF4F01"
@@ -540,4 +519,21 @@ export default component$(() => {
</section> </section>
</div> </div>
</MotionComponent> </MotionComponent>
*/ <Footer client:load>
<div class="w-full flex justify-center flex-col items-center gap-3">
<Link href="/auth/login" prefetch={false} class="flex font-bricolage text-sm sm:text-base rounded-full bg-primary-500 px-5 py-4 font-semibold text-white transition-all hover:scale-105 active:scale-95 sm:px-6" >
Get early access
</Link>
<div class="mt-6 flex w-full items-center justify-center gap-2 text-xs sm:text-sm font-medium text-neutral-600 dark:text-neutral-400">
<span class="hover:text-primary-500 transition-colors duration-200">
<Link rel="noreferrer" href="/terms" >Terms of Service</Link></span>
<span class="text-gray-400 dark:text-gray-600"></span>
<span class="hover:text-primary-500 transition-colors duration-200" >
<Link href="/privacy">Privacy Policy</Link>
</span>
</div>
</div>
</Footer>
</div>
)
})

2883
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
FROM docker.io/golang:1.24-bookworm AS go-build
WORKDIR /builder
COPY packages/maitred/ /builder/
RUN go build
FROM docker.io/golang:1.24-bookworm
COPY --from=go-build /builder/maitred /maitred/maitred
WORKDIR /maitred
RUN apt update && apt install -y --no-install-recommends pciutils
ENTRYPOINT ["/maitred/maitred"]

View File

@@ -13,7 +13,6 @@ WORKDIR /relay
ENV VERBOSE=false ENV VERBOSE=false
ENV DEBUG=false ENV DEBUG=false
ENV ENDPOINT_PORT=8088 ENV ENDPOINT_PORT=8088
ENV MESH_PORT=8089
ENV WEBRTC_UDP_START=10000 ENV WEBRTC_UDP_START=10000
ENV WEBRTC_UDP_END=20000 ENV WEBRTC_UDP_END=20000
ENV STUN_SERVER="stun.l.google.com:19302" ENV STUN_SERVER="stun.l.google.com:19302"
@@ -24,7 +23,6 @@ ENV TLS_CERT=""
ENV TLS_KEY="" ENV TLS_KEY=""
EXPOSE $ENDPOINT_PORT EXPOSE $ENDPOINT_PORT
EXPOSE $MESH_PORT
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
EXPOSE $WEBRTC_UDP_MUX/udp EXPOSE $WEBRTC_UDP_MUX/udp

View File

@@ -85,8 +85,8 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --noconfirm meson pkgconf cmake git gcc make \ pacman -Sy --noconfirm meson pkgconf cmake git gcc make \
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
# Clone repository # Clone repository with proper directory structure
RUN git clone -b dev-dmabuf https://github.com/DatCaptainHorse/gst-wayland-display.git RUN git clone -b dev-dmabuf https://github.com/games-on-whales/gst-wayland-display.git
#-------------------------------------------------------------------- #--------------------------------------------------------------------
FROM gst-wayland-deps AS gst-wayland-planner FROM gst-wayland-deps AS gst-wayland-planner
@@ -133,8 +133,8 @@ RUN sed -i \
RUN --mount=type=cache,target=/var/cache/pacman/pkg \ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
pacman -Sy --needed --noconfirm \ pacman -Sy --needed --noconfirm \
vulkan-intel lib32-vulkan-intel vpl-gpu-rt mesa \ vulkan-intel lib32-vulkan-intel vpl-gpu-rt mesa \
steam steam-native-runtime gtk3 lib32-gtk3 \ steam steam-native-runtime \
sudo xorg-xwayland seatd libinput labwc wlr-randr gamescope mangohud \ sudo xorg-xwayland seatd libinput labwc wlr-randr mangohud \
libssh2 curl wget \ libssh2 curl wget \
pipewire pipewire-pulse pipewire-alsa wireplumber \ pipewire pipewire-pulse pipewire-alsa wireplumber \
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib && \ noto-fonts-cjk supervisor jq chwd lshw pacman-contrib && \
@@ -144,9 +144,6 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
gst-plugins-bad gst-plugin-pipewire \ gst-plugins-bad gst-plugin-pipewire \
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \ gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
gst-plugin-va gst-plugin-qsv && \ gst-plugin-va gst-plugin-qsv && \
# lib32 GStreamer stack to fix some games with videos
pacman -Sy --needed --noconfirm \
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
# Cleanup # Cleanup
paccache -rk1 && \ paccache -rk1 && \
rm -rf /usr/share/{info,man,doc}/* rm -rf /usr/share/{info,man,doc}/*
@@ -188,30 +185,6 @@ RUN mkdir -p /run/dbus && \
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \ -e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
/usr/share/wireplumber/wireplumber.conf /usr/share/wireplumber/wireplumber.conf
### PipeWire Latency Optimizations (1-5ms instead of 20ms) ###
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
echo "[audio]\
\n default.clock.rate = 48000\
\n default.clock.quantum = 128\
\n default.clock.min-quantum = 128\
\n default.clock.max-quantum = 256" > /etc/pipewire/pipewire.conf.d/low-latency.conf && \
mkdir -p /etc/wireplumber/main.lua.d && \
echo 'table.insert(default_nodes.rules, {\
\n matches = { { { "node.name", "matches", ".*" } } },\
\n apply_properties = {\
\n ["audio.format"] = "S16LE",\
\n ["audio.rate"] = 48000,\
\n ["audio.channels"] = 2,\
\n ["api.alsa.period-size"] = 128,\
\n ["api.alsa.headroom"] = 0,\
\n ["session.suspend-timeout-seconds"] = 0\
\n }\
\n})' > /etc/wireplumber/main.lua.d/50-low-latency.lua && \
echo "default-fragments = 2\
\ndefault-fragment-size-msec = 2" >> /etc/pulse/daemon.conf && \
echo "load-module module-loopback latency_msec=1" >> /etc/pipewire/pipewire.conf.d/loopback.conf
### Artifacts and Verification ### ### Artifacts and Verification ###
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/ COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/ COPY --from=gst-wayland-cached-builder /artifacts/lib/ /usr/lib/

View File

@@ -1,63 +1,82 @@
import { bus } from "./bus"; import { bus } from "./bus";
import { auth } from "./auth";
import { domain } from "./dns"; import { domain } from "./dns";
import { email } from "./email";
import { secret } from "./secret"; import { secret } from "./secret";
import { cluster } from "./cluster"; import { database } from "./database";
import { postgres } from "./postgres";
export const api = new sst.aws.Service("Api", { sst.Linkable.wrap(random.RandomString, (resource) => ({
cluster, properties: {
cpu: $app.stage === "production" ? "2 vCPU" : undefined, value: resource.result,
memory: $app.stage === "production" ? "4 GB" : undefined,
command: ["bun", "run", "./src/api/index.ts"],
link: [
bus,
auth,
postgres,
secret.PolarSecret,
secret.PolarWebhookSecret,
secret.NestriFamilyMonthly,
secret.NestriFamilyYearly,
secret.NestriFreeMonthly,
secret.NestriProMonthly,
secret.NestriProYearly,
],
image: {
dockerfile: "packages/functions/Containerfile",
}, },
environment: { }));
NO_COLOR: "1",
export const urls = new sst.Linkable("Urls", {
properties: {
api: "https://api." + domain,
auth: "https://auth." + domain,
site: $dev ? "http://localhost:4321" : "https://" + domain,
}, },
loadBalancer: { });
rules: [
export const authFingerprintKey = new random.RandomString(
"AuthFingerprintKey",
{
length: 32,
},
);
export const auth = new sst.aws.Auth("Auth", {
issuer: {
timeout: "3 minutes",
handler: "./packages/functions/src/auth.handler",
link: [
bus,
email,
database,
authFingerprintKey,
secret.PolarSecret,
secret.GithubClientID,
secret.DiscordClientID,
secret.GithubClientSecret,
secret.DiscordClientSecret,
],
permissions: [
{ {
listen: "80/http", actions: ["ses:SendEmail"],
forward: "3001/http", resources: ["*"],
}, },
], ],
}, },
dev: { domain: {
command: "bun dev:api", name: "auth." + domain,
directory: "packages/functions", dns: sst.cloudflare.dns(),
url: "http://localhost:3001",
}, },
scaling: })
$app.stage === "production"
? {
min: 2,
max: 10,
}
: undefined,
});
export const apiFunction = new sst.aws.Function("ApiFn", {
handler: "packages/functions/src/api/index.handler",
link: [
bus,
urls,
database,
secret.PolarSecret,
],
timeout: "3 minutes",
streaming: !$dev,
url: true
})
export const apiRoute = new sst.aws.Router("ApiRoute", { export const api = new sst.aws.Router("Api", {
routes: { routes: {
// I think api.url should work all the same "/*": apiFunction.url
"/*": api.nodes.loadBalancer.dnsName,
}, },
domain: { domain: {
name: "api." + domain, name: "api." + domain,
dns: sst.cloudflare.dns(), dns: sst.cloudflare.dns(),
}, },
}) })
export const outputs = {
auth: auth.url,
api: api.url,
};

View File

@@ -1,66 +0,0 @@
import { bus } from "./bus";
import { domain } from "./dns";
import { secret } from "./secret";
import { cluster } from "./cluster";
import { postgres } from "./postgres";
//FIXME: Use a shared /tmp folder
export const auth = new sst.aws.Service("Auth", {
cluster,
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
memory: $app.stage === "production" ? "2 GB" : undefined,
command: ["bun", "run", "./src/auth.ts"],
link: [
bus,
postgres,
secret.PolarSecret,
secret.GithubClientID,
secret.DiscordClientID,
secret.GithubClientSecret,
secret.DiscordClientSecret,
],
image: {
dockerfile: "packages/functions/Containerfile",
},
environment: {
NO_COLOR: "1",
STORAGE: $dev ? "/tmp/persist.json" : "/mnt/efs/persist.json"
},
loadBalancer: {
rules: [
{
listen: "80/http",
forward: "3002/http",
},
],
},
permissions: [
{
actions: ["ses:SendEmail"],
resources: ["*"],
},
],
dev: {
command: "bun dev:auth",
directory: "packages/functions",
url: "http://localhost:3002",
},
scaling:
$app.stage === "production"
? {
min: 2,
max: 10,
}
: undefined,
});
export const authRoute = new sst.aws.Router("AuthRoute", {
routes: {
// I think auth.url should work all the same
"/*": auth.nodes.loadBalancer.dnsName,
},
domain: {
name: "auth." + domain,
dns: sst.cloudflare.dns(),
},
})

View File

@@ -1,18 +1,15 @@
import { vpc } from "./vpc"; import { email } from "./email";
// import { email } from "./email";
import { allSecrets } from "./secret"; import { allSecrets } from "./secret";
import { postgres } from "./postgres"; import { database } from "./database";
export const bus = new sst.aws.Bus("Bus"); export const bus = new sst.aws.Bus("Bus");
bus.subscribe("Event", { bus.subscribe("Event", {
vpc,
handler: "./packages/functions/src/event/event.handler", handler: "./packages/functions/src/event/event.handler",
link: [ link: [
// email, database,
postgres, email,
...allSecrets ...allSecrets],
],
timeout: "5 minutes", timeout: "5 minutes",
permissions: [ permissions: [
{ {

View File

@@ -1,6 +0,0 @@
import { vpc } from "./vpc";
export const cluster = new sst.aws.Cluster("Cluster", {
vpc,
forceUpgrade: "v2"
});

40
infra/database.ts Normal file
View File

@@ -0,0 +1,40 @@
//Created manually from the dashboard and shared with the whole team/org
const dbProject = neon.getProjectOutput({
id: "black-sky-26872933"
})
const dbBranchId = $app.stage !== "production" ?
new neon.Branch("NeonBranch", {
parentId: dbProject.defaultBranchId,
projectId: dbProject.id,
name: $app.stage,
}).id : dbProject.defaultBranchId
const dbEndpoint = new neon.Endpoint("NeonEndpoint", {
projectId: dbProject.id,
branchId: dbBranchId,
poolerEnabled: true,
type: "read_write",
})
const dbRole = new neon.Role("NeonRole", {
name: "admin",
branchId: dbBranchId,
projectId: dbProject.id,
})
const db = new neon.Database("NeonDatabase", {
branchId: dbBranchId,
projectId: dbProject.id,
ownerName: dbRole.name,
name: `nestri-${$app.stage}`,
})
export const database = new sst.Linkable("Database", {
properties: {
name: db.name,
user: dbRole.name,
host: dbEndpoint.host,
password: dbRole.password,
},
});

View File

@@ -1,6 +1,6 @@
import { domain } from "./dns"; import { domain } from "./dns";
export const email = new sst.aws.Email("Email",{ export const email = new sst.aws.Email("Mail",{
sender: domain, sender: domain,
dns: sst.cloudflare.dns(), dns: sst.cloudflare.dns(),
}) })

View File

@@ -1,68 +0,0 @@
import { vpc } from "./vpc";
import { isPermanentStage } from "./stage";
// TODO: Add a dev db to use, this will help with running zero locally... and testing it
export const postgres = new sst.aws.Aurora("Database", {
vpc,
engine: "postgres",
scaling: isPermanentStage
? undefined
: {
min: "0 ACU",
max: "1 ACU",
},
transform: {
clusterParameterGroup: {
parameters: [
{
name: "rds.logical_replication",
value: "1",
applyMethod: "pending-reboot",
},
{
name: "max_slot_wal_keep_size",
value: "10240",
applyMethod: "pending-reboot",
},
{
name: "rds.force_ssl",
value: "0",
applyMethod: "pending-reboot",
},
{
name: "max_connections",
value: "1000",
applyMethod: "pending-reboot",
},
],
},
},
});
new sst.x.DevCommand("Studio", {
link: [postgres],
dev: {
command: "bun db:dev studio",
directory: "packages/core",
autostart: true,
},
});
const migrator = new sst.aws.Function("DatabaseMigrator", {
handler: "packages/functions/src/migrator.handler",
link: [postgres],
copyFiles: [
{
from: "packages/core/migrations",
to: "./migrations",
},
],
});
if (!$dev) {
new aws.lambda.Invocation("DatabaseMigratorInvocation", {
input: Date.now().toString(),
functionName: migrator.name,
});
}

View File

@@ -1,9 +0,0 @@
import { auth } from "./auth";
import { postgres } from "./postgres";
export const device = new sst.aws.Realtime("Realtime", {
authorizer: {
link: [auth, postgres],
handler: "packages/functions/src/realtime/authorizer.handler"
}
})

View File

@@ -1,17 +1,11 @@
export const secret = { export const secret = {
// InstantAppId: new sst.Secret("InstantAppId"),
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY), PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
GithubClientID: new sst.Secret("GithubClientID"), GithubClientID: new sst.Secret("GithubClientID"),
DiscordClientID: new sst.Secret("DiscordClientID"), DiscordClientID: new sst.Secret("DiscordClientID"),
PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"),
GithubClientSecret: new sst.Secret("GithubClientSecret"), GithubClientSecret: new sst.Secret("GithubClientSecret"),
// InstantAdminToken: new sst.Secret("InstantAdminToken"),
DiscordClientSecret: new sst.Secret("DiscordClientSecret"), DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
// Pricing
NestriFreeMonthly: new sst.Secret("NestriFreeMonthly"),
NestriProMonthly: new sst.Secret("NestriProMonthly"),
NestriProYearly: new sst.Secret("NestriProYearly"),
NestriFamilyMonthly: new sst.Secret("NestriFamilyMonthly"),
NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"),
}; };
export const allSecrets = Object.values(secret); export const allSecrets = Object.values(secret);

View File

@@ -1,2 +0,0 @@
export const isPermanentStage =
$app.stage === "production" || $app.stage === "dev";

View File

@@ -1,7 +0,0 @@
new sst.x.DevCommand("Steam", {
dev: {
command: "bun dev",
directory: "packages/steam",
autostart: true,
},
});

View File

@@ -1 +0,0 @@
export const storage = new sst.aws.Bucket("Storage");

View File

@@ -1,11 +0,0 @@
import { isPermanentStage } from "./stage";
export const vpc = isPermanentStage
? new sst.aws.Vpc("VPC", {
az: 2,
// For lambdas to work in this VPC
nat: "ec2",
// For SST tunnel to work
bastion: true,
})
: sst.aws.Vpc.get("VPC", "vpc-0beb1cdc21a725748");

View File

@@ -1,11 +1,9 @@
// This is the website part where people play and connect // This is the website part where people play and connect
import { api } from "./api";
import { auth } from "./auth";
import { zero } from "./zero";
import { domain } from "./dns"; import { domain } from "./dns";
import { auth, api } from "./api";
new sst.aws.StaticSite("Web", { new sst.aws.StaticSite("Web", {
path: "packages/www", path: "./packages/www",
build: { build: {
output: "./dist", output: "./dist",
command: "bun run build", command: "bun run build",
@@ -16,8 +14,7 @@ new sst.aws.StaticSite("Web", {
}, },
environment: { environment: {
VITE_API_URL: api.url, VITE_API_URL: api.url,
VITE_STAGE: $app.stage,
VITE_AUTH_URL: auth.url, VITE_AUTH_URL: auth.url,
VITE_ZERO_URL: zero.url, VITE_STAGE: $app.stage,
}, },
}) })

View File

@@ -1,196 +0,0 @@
import { vpc } from "./vpc";
import { auth } from "./auth";
import { domain } from "./dns";
import { readFileSync } from "fs";
import { cluster } from "./cluster";
import { storage } from "./storage";
import { postgres } from "./postgres";
// const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}/${postgres.database}`
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
const tag = $dev
? `latest`
: JSON.parse(
readFileSync("./node_modules/@rocicorp/zero/package.json").toString(),
).version.replace("+", "-");
const zeroEnv = {
FORCE: "1",
NO_COLOR: "1",
ZERO_LOG_LEVEL: "info",
ZERO_LITESTREAM_LOG_LEVEL: "info",
ZERO_UPSTREAM_DB: connectionString,
ZERO_IMAGE_URL: `rocicorp/zero:${tag}`,
ZERO_CVR_DB: connectionString,
ZERO_CHANGE_DB: connectionString,
ZERO_REPLICA_FILE: "/tmp/nestri.db",
ZERO_LITESTREAM_RESTORE_PARALLELISM: "64",
ZERO_SHARD_ID: $app.stage,
ZERO_AUTH_JWKS_URL: $interpolate`${auth.url}/.well-known/jwks.json`,
...($dev
? {
}
: {
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${storage.name}/zero`,
}),
};
// Replication Manager Service
const replicationManager = !$dev
? new sst.aws.Service(`ZeroReplication`, {
cluster,
wait: true,
...($app.stage === "production"
? {
cpu: "2 vCPU",
memory: "4 GB",
}
: {}),
architecture: "arm64",
image: zeroEnv.ZERO_IMAGE_URL,
link: [storage, postgres],
health: {
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
interval: "5 seconds",
retries: 3,
startPeriod: "300 seconds",
},
environment: {
...zeroEnv,
ZERO_CHANGE_MAX_CONNS: "3",
ZERO_NUM_SYNC_WORKERS: "0",
},
logging: {
retention: "1 month",
},
loadBalancer: {
public: false,
ports: [
{
listen: "80/http",
forward: "4849/http",
},
],
},
transform: {
loadBalancer: {
idleTimeout: 3600,
},
service: {
healthCheckGracePeriodSeconds: 900,
},
},
}) : undefined;
// Permissions deployment
const permissions = new sst.aws.Function(
"ZeroPermissions",
{
vpc,
link: [postgres],
handler: "packages/functions/src/zero.handler",
// environment: { ["ZERO_UPSTREAM_DB"]: connectionString },
copyFiles: [{
from: "packages/zero/.permissions.sql",
to: "./.permissions.sql"
}],
}
);
if (replicationManager) {
new aws.lambda.Invocation(
"ZeroPermissionsInvocation",
{
input: Date.now().toString(),
functionName: permissions.name,
},
{ dependsOn: replicationManager }
);
// new command.local.Command(
// "ZeroPermission",
// {
// dir: process.cwd() + "/packages/zero",
// environment: {
// ZERO_UPSTREAM_DB: connectionString,
// },
// create: "bun run zero-deploy-permissions",
// triggers: [Date.now()],
// },
// {
// dependsOn: [replicationManager],
// },
// );
}
export const zero = new sst.aws.Service("Zero", {
cluster,
image: zeroEnv.ZERO_IMAGE_URL,
link: [storage, postgres],
architecture: "arm64",
...($app.stage === "production"
? {
cpu: "2 vCPU",
memory: "4 GB",
capacity: "spot"
}
: {
capacity: "spot"
}),
environment: {
...zeroEnv,
...($dev
? {
ZERO_NUM_SYNC_WORKERS: "1",
}
: {
ZERO_CHANGE_STREAMER_URI: replicationManager.url.apply((val) =>
val.replace("http://", "ws://"),
),
ZERO_UPSTREAM_MAX_CONNS: "15",
ZERO_CVR_MAX_CONNS: "160",
}),
},
wait: true,
health: {
retries: 3,
command: ["CMD-SHELL", "curl -f http://localhost:4848/ || exit 1"],
interval: "5 seconds",
startPeriod: "300 seconds",
},
loadBalancer: {
domain: {
name: "zero." + domain,
dns: sst.cloudflare.dns()
},
rules: [
{ listen: "443/https", forward: "4848/http" },
{ listen: "80/http", forward: "4848/http" },
],
},
scaling: {
min: 1,
max: 4,
},
logging: {
retention: "1 month",
},
transform: {
service: {
healthCheckGracePeriodSeconds: 900,
},
// taskDefinition: {
// ephemeralStorage: {
// sizeInGib: 200,
// },
// },
loadBalancer: {
idleTimeout: 3600,
},
},
dev: {
command: "bun dev",
directory: "packages/zero",
url: "http://localhost:4848",
},
});

View File

@@ -1,29 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "steam", "packages\steam\steam.csproj", "{96118F95-BF02-0ED3-9042-36FA1B740D67}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96118F95-BF02-0ED3-9042-36FA1B740D67}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{96118F95-BF02-0ED3-9042-36FA1B740D67} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {526AD703-4D15-43CF-B7C0-83F10D3158DB}
EndGlobalSection
EndGlobal

View File

@@ -16,19 +16,9 @@
"format": "prettier --write \"**/*.{ts,tsx,md}\"", "format": "prettier --write \"**/*.{ts,tsx,md}\"",
"sso": "aws sso login --sso-session=nestri --no-browser --use-device-code" "sso": "aws sso login --sso-session=nestri --no-browser --use-device-code"
}, },
"overrides": {
"@openauthjs/openauth": "0.4.3",
"@rocicorp/zero": "0.16.2025022000"
},
"patchedDependencies": {
"@macaron-css/solid@1.5.3": "patches/@macaron-css%2Fsolid@1.5.3.patch",
"drizzle-orm@0.36.1": "patches/drizzle-orm@0.36.1.patch"
},
"trustedDependencies": [ "trustedDependencies": [
"core-js-pure", "core-js-pure",
"esbuild", "esbuild",
"protobufjs",
"@rocicorp/zero-sqlite3",
"workerd" "workerd"
], ],
"workspaces": [ "workspaces": [
@@ -36,6 +26,6 @@
"packages/*" "packages/*"
], ],
"dependencies": { "dependencies": {
"sst": "^3.11.21" "sst": "3.9.1"
} }
} }

View File

@@ -1,19 +1,20 @@
import { Resource } from "sst"; import { Resource } from "sst";
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
const connection = { function addPoolerSuffix(original: string): string {
user: Resource.Database.username, const firstDotIndex = original.indexOf('.');
password: Resource.Database.password, if (firstDotIndex === -1) return original + '-pooler';
host: Resource.Database.host, return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
}; }
const dbHost = addPoolerSuffix(Resource.Database.host)
export default defineConfig({ export default defineConfig({
verbose: true, schema: "./src/**/*.sql.ts",
strict: true,
out: "./migrations", out: "./migrations",
dialect: "postgresql", dialect: "postgresql",
verbose: true,
dbCredentials: { dbCredentials: {
url: `postgres://${connection.user}:${connection.password}@${connection.host}/nestri`, url: `postgresql://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require`,
}, },
schema: "./src/**/*.sql.ts",
}); });

View File

@@ -14,9 +14,8 @@ CREATE TABLE "team" (
"time_created" timestamp with time zone DEFAULT now() NOT NULL, "time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL, "time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone, "time_deleted" timestamp with time zone,
"name" varchar(255) NOT NULL,
"slug" varchar(255) NOT NULL, "slug" varchar(255) NOT NULL,
"plan_type" text NOT NULL "name" varchar(255) NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE "user" ( CREATE TABLE "user" (
@@ -25,15 +24,14 @@ CREATE TABLE "user" (
"time_updated" timestamp with time zone DEFAULT now() NOT NULL, "time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone, "time_deleted" timestamp with time zone,
"avatar_url" text, "avatar_url" text,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL, "name" varchar(255) NOT NULL,
"discriminator" integer NOT NULL, "discriminator" integer NOT NULL,
"email" varchar(255) NOT NULL, "polar_customer_id" varchar(255) NOT NULL,
"polar_customer_id" varchar(255),
"flags" json DEFAULT '{}'::json,
CONSTRAINT "user_polar_customer_id_unique" UNIQUE("polar_customer_id") CONSTRAINT "user_polar_customer_id_unique" UNIQUE("polar_customer_id")
); );
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX "email_global" ON "member" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint CREATE UNIQUE INDEX "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint
CREATE UNIQUE INDEX "team_slug" ON "team" USING btree ("slug");--> statement-breakpoint CREATE INDEX "email_global" ON "member" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "team" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("email"); CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("email");

View File

@@ -0,0 +1 @@
ALTER TABLE "user" ALTER COLUMN "polar_customer_id" DROP NOT NULL;

View File

@@ -1,2 +0,0 @@
DROP INDEX "team_slug";--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "team" USING btree ("slug");

View File

@@ -1,17 +0,0 @@
CREATE TABLE "steam" (
"id" char(30) NOT NULL,
"user_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"avatar_url" text NOT NULL,
"access_token" text NOT NULL,
"email" varchar(255) NOT NULL,
"country" varchar(255) NOT NULL,
"username" varchar(255) NOT NULL,
"persona_name" varchar(255) NOT NULL,
CONSTRAINT "steam_user_id_id_pk" PRIMARY KEY("user_id","id")
);
--> statement-breakpoint
CREATE INDEX "global_steam_email" ON "steam" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "steam_email" ON "steam" USING btree ("user_id","email");

View File

@@ -1,13 +0,0 @@
CREATE TABLE "machine" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"country" text NOT NULL,
"timezone" text NOT NULL,
"location" "point" NOT NULL,
"fingerprint" varchar(32) NOT NULL,
"country_code" varchar(2) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "machine_fingerprint" ON "machine" USING btree ("fingerprint");

View File

@@ -1,22 +0,0 @@
CREATE TABLE "machine" (
"id" char(30) PRIMARY KEY NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"country" text NOT NULL,
"timezone" text NOT NULL,
"location" "point" NOT NULL,
"fingerprint" varchar(32) NOT NULL,
"country_code" varchar(2) NOT NULL
);
--> statement-breakpoint
ALTER TABLE "steam" RENAME COLUMN "country" TO "country_code";--> statement-breakpoint
DROP INDEX "global_steam_email";--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "time_seen" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "steam_id" integer NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "last_game" json NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "steam_email" varchar(255) NOT NULL;--> statement-breakpoint
ALTER TABLE "steam" ADD COLUMN "limitation" json NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX "machine_fingerprint" ON "machine" USING btree ("fingerprint");--> statement-breakpoint
ALTER TABLE "steam" DROP COLUMN "access_token";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "flags";

View File

@@ -1,8 +0,0 @@
ALTER TABLE "steam" RENAME COLUMN "time_seen" TO "last_seen";--> statement-breakpoint
DROP INDEX "steam_email";--> statement-breakpoint
ALTER TABLE "steam" DROP CONSTRAINT "steam_user_id_id_pk";--> statement-breakpoint
ALTER TABLE "steam" ADD PRIMARY KEY ("id");--> statement-breakpoint
ALTER TABLE "machine" ADD CONSTRAINT "machine_user_id_id_pk" PRIMARY KEY("user_id","id");--> statement-breakpoint
ALTER TABLE "machine" ADD COLUMN "user_id" char(30);--> statement-breakpoint
ALTER TABLE "steam" ADD CONSTRAINT "steam_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "steam" DROP COLUMN "email";

View File

@@ -1,2 +0,0 @@
ALTER TABLE "machine" DROP CONSTRAINT "machine_user_id_id_pk";--> statement-breakpoint
ALTER TABLE "machine" DROP COLUMN "user_id";

View File

@@ -1,2 +0,0 @@
ALTER TABLE "member" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint
ALTER TABLE "team" DROP COLUMN "plan_type";

View File

@@ -1,15 +0,0 @@
CREATE TABLE "subscription" (
"id" char(30) NOT NULL,
"user_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"team_id" char(30) NOT NULL,
"standing" text NOT NULL,
"plan_type" text NOT NULL,
"tokens" integer NOT NULL,
"product_id" varchar(255),
"subscription_id" varchar(255)
);
--> statement-breakpoint
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_id_team_id_pk" PRIMARY KEY("id","team_id");--> statement-breakpoint
CREATE UNIQUE INDEX "subscription_id" ON "subscription" USING btree ("id");--> statement-breakpoint
CREATE INDEX "subscription_user_id" ON "subscription" USING btree ("user_id");

View File

@@ -1,2 +0,0 @@
CREATE UNIQUE INDEX "steam_id" ON "steam" USING btree ("steam_id");--> statement-breakpoint
CREATE INDEX "steam_user_id" ON "steam" USING btree ("user_id");

View File

@@ -1,5 +1,5 @@
{ {
"id": "f09034df-208a-42b3-b61f-f842921c6e24", "id": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@@ -54,21 +54,6 @@
} }
}, },
"indexes": { "indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": { "member_email": {
"name": "member_email", "name": "member_email",
"columns": [ "columns": [
@@ -89,6 +74,21 @@
"concurrently": false, "concurrently": false,
"method": "btree", "method": "btree",
"with": {} "with": {}
},
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@@ -136,28 +136,22 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": { "slug": {
"name": "slug", "name": "slug",
"type": "varchar(255)", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"plan_type": { "name": {
"name": "plan_type", "name": "name",
"type": "text", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
} }
}, },
"indexes": { "indexes": {
"team_slug": { "slug": {
"name": "team_slug", "name": "slug",
"columns": [ "columns": [
{ {
"expression": "slug", "expression": "slug",
@@ -215,6 +209,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": { "name": {
"name": "name", "name": "name",
"type": "varchar(255)", "type": "varchar(255)",
@@ -227,24 +227,11 @@
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": { "polar_customer_id": {
"name": "polar_customer_id", "name": "polar_customer_id",
"type": "varchar(255)", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": true
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
} }
}, },
"indexes": { "indexes": {

View File

@@ -1,6 +1,6 @@
{ {
"id": "6f428226-b5d8-4182-a676-d04f842f9ded", "id": "c09359df-19fe-4246-9a41-43b3a429c12f",
"prevId": "f09034df-208a-42b3-b61f-f842921c6e24", "prevId": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"tables": { "tables": {
@@ -54,21 +54,6 @@
} }
}, },
"indexes": { "indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": { "member_email": {
"name": "member_email", "name": "member_email",
"columns": [ "columns": [
@@ -89,6 +74,21 @@
"concurrently": false, "concurrently": false,
"method": "btree", "method": "btree",
"with": {} "with": {}
},
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@@ -136,21 +136,15 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": { "slug": {
"name": "slug", "name": "slug",
"type": "varchar(255)", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"plan_type": { "name": {
"name": "plan_type", "name": "name",
"type": "text", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
} }
@@ -215,6 +209,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": { "name": {
"name": "name", "name": "name",
"type": "varchar(255)", "type": "varchar(255)",
@@ -227,24 +227,11 @@
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": { "polar_customer_id": {
"name": "polar_customer_id", "name": "polar_customer_id",
"type": "varchar(255)", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
} }
}, },
"indexes": { "indexes": {

View File

@@ -1,420 +0,0 @@
{
"id": "227c54d2-b643-48d5-964b-af6fe004369a",
"prevId": "6f428226-b5d8-4182-a676-d04f842f9ded",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"global_steam_email": {
"name": "global_steam_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"steam_email": {
"name": "steam_email",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"steam_user_id_id_pk": {
"name": "steam_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"flags": {
"name": "flags",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'{}'::json"
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,507 +0,0 @@
{
"id": "eb5d41aa-5f85-4b2d-8633-fc021b211241",
"prevId": "227c54d2-b643-48d5-964b-af6fe004369a",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"steam_email": {
"name": "steam_email",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"steam_user_id_id_pk": {
"name": "steam_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,499 +0,0 @@
{
"id": "65574f71-e0d3-4363-9449-394e7c376a30",
"prevId": "eb5d41aa-5f85-4b2d-8633-fc021b211241",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"machine_user_id_id_pk": {
"name": "machine_user_id_id_pk",
"columns": [
"user_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,485 +0,0 @@
{
"id": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
"prevId": "65574f71-e0d3-4363-9449-394e7c376a30",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,485 +0,0 @@
{
"id": "69827225-1351-4709-a9b2-facb0f569215",
"prevId": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,580 +0,0 @@
{
"id": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"prevId": "69827225-1351-4709-a9b2-facb0f569215",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,619 +0,0 @@
{
"id": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"prevId": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,650 +0,0 @@
{
"id": "1717c769-cee0-4242-bcbb-9538c80d985c",
"prevId": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"steam_id": {
"name": "steam_id",
"columns": [
{
"expression": "steam_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"steam_user_id": {
"name": "steam_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -5,71 +5,15 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1741759978256, "when": 1740345380808,
"tag": "0000_flaky_matthew_murdock", "tag": "0000_wise_black_widow",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "7", "version": "7",
"when": 1741955636085, "when": 1740487217291,
"tag": "0001_nifty_sauron", "tag": "0001_flaky_tomorrow_man",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1743794969007,
"tag": "0002_simple_outlaw_kid",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1744287542918,
"tag": "0003_first_big_bertha",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1744614629788,
"tag": "0004_amused_mattie_franklin",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1744614896792,
"tag": "0005_aspiring_stature",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1744634229644,
"tag": "0006_worthless_dreadnoughts",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1744634322996,
"tag": "0007_warm_secret_warriors",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1744651530530,
"tag": "0008_third_mindworm",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1744651817581,
"tag": "0009_luxuriant_wraith",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -4,11 +4,12 @@
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
"scripts": { "scripts": {
"db:dev": "drizzle-kit",
"typecheck": "tsc --noEmit",
"db": "sst shell drizzle-kit", "db": "sst shell drizzle-kit",
"db:exec": "sst shell ../scripts/src/psql.sh", "db:push": "sst shell drizzle-kit push",
"db:reset": "sst shell ../scripts/src/db-reset.sh" "db:migrate": "sst shell drizzle-kit migrate",
"db:generate": "sst shell drizzle-kit generate",
"db:connect": "sst shell ../scripts/src/psql.ts",
"db:move": "sst shell drizzle-kit generate && sst shell drizzle-kit migrate && sst shell drizzle-kit push"
}, },
"exports": { "exports": {
"./*": "./src/*.ts" "./*": "./src/*.ts"
@@ -17,24 +18,23 @@
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"aws-iot-device-sdk-v2": "^1.21.1", "aws-iot-device-sdk-v2": "^1.21.1",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"drizzle-kit": "^0.30.4",
"loops": "^3.4.1", "loops": "^3.4.1",
"mqtt": "^5.10.3", "mqtt": "^5.10.3",
"remeda": "^2.21.2", "remeda": "^2.19.0",
"ulid": "^2.3.0", "ulid": "^2.3.0",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"zod": "^3.24.1", "zod": "^3.24.1",
"zod-openapi": "^4.2.2" "zod-openapi": "^4.2.2"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-iot-data-plane": "^3.758.0",
"@aws-sdk/client-rds-data": "^3.758.0",
"@aws-sdk/client-sesv2": "^3.753.0", "@aws-sdk/client-sesv2": "^3.753.0",
"@instantdb/admin": "^0.17.7", "@instantdb/admin": "^0.17.7",
"@openauthjs/openauth": "*", "@neondatabase/serverless": "^0.10.4",
"@openauthjs/openauth": "0.4.3",
"@openauthjs/openevent": "^0.0.27", "@openauthjs/openevent": "^0.0.27",
"@polar-sh/sdk": "^0.26.1", "@polar-sh/sdk": "^0.26.1",
"drizzle-kit": "^0.30.5", "drizzle-orm": "^0.39.3",
"drizzle-orm": "^0.40.0", "ws": "^8.18.1"
"postgres": "^3.4.5"
} }
} }

View File

@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { eq } from "./drizzle"; import { eq } from "./drizzle";
import { ErrorCodes, VisibleError } from "./error"; import { VisibleError } from "./error";
import { createContext } from "./context"; import { createContext } from "./context";
import { UserFlags, userTable } from "./user/user.sql"; import { UserFlags, userTable } from "./user/user.sql";
import { useTransaction } from "./drizzle/transaction"; import { useTransaction } from "./drizzle/transaction";
@@ -37,65 +37,24 @@ export const SystemActor = z.object({
}); });
export type SystemActor = z.infer<typeof SystemActor>; export type SystemActor = z.infer<typeof SystemActor>;
export const MachineActor = z.object({
type: z.literal("machine"),
properties: z.object({
fingerprint: z.string(),
machineID: z.string(),
}),
});
export type MachineActor = z.infer<typeof MachineActor>;
export const Actor = z.discriminatedUnion("type", [ export const Actor = z.discriminatedUnion("type", [
MemberActor, MemberActor,
UserActor, UserActor,
PublicActor, PublicActor,
SystemActor, SystemActor,
MachineActor
]); ]);
export type Actor = z.infer<typeof Actor>; export type Actor = z.infer<typeof Actor>;
export const ActorContext = createContext<Actor>("actor"); const ActorContext = createContext<Actor>("actor");
export const useActor = ActorContext.use; export const useActor = ActorContext.use;
export const withActor = ActorContext.with; export const withActor = ActorContext.with;
/**
* Retrieves the user ID of the current actor.
*
* This function accesses the actor context and returns the `userID` if the current
* actor is of type "user". If the actor is not a user, it throws a `VisibleError`
* with an authentication error code, indicating that the caller is not authorized
* to access user-specific resources.
*
* @throws {VisibleError} When the current actor is not of type "user".
*/
export function useUserID() { export function useUserID() {
const actor = ActorContext.use(); const actor = ActorContext.use();
if (actor.type === "user") return actor.properties.userID; if (actor.type === "user") return actor.properties.userID;
throw new VisibleError( throw new VisibleError(
"authentication", "unauthorized",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
/**
* Retrieves the properties of the current user actor.
*
* This function obtains the current actor from the context and returns its properties if the actor is identified as a user.
* If the actor is not of type "user", it throws a {@link VisibleError} with an authentication error code,
* indicating that the user is not authorized to access user-specific resources.
*
* @returns The properties of the current user actor, typically including user-specific details such as userID and email.
* @throws {VisibleError} If the current actor is not a user.
*/
export function useUser() {
const actor = ActorContext.use();
if (actor.type === "user") return actor.properties;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`, `You don't have permission to access this resource`,
); );
} }
@@ -109,34 +68,25 @@ export function assertActor<T extends Actor["type"]>(type: T) {
return actor as Extract<Actor, { type: T }>; return actor as Extract<Actor, { type: T }>;
} }
/**
* Returns the current actor's team ID.
*
* @returns The team ID associated with the current actor.
* @throws {VisibleError} If the current actor does not have a {@link teamID} property.
*/
export function useTeam() { export function useTeam() {
const actor = useActor(); const actor = useActor();
if ("teamID" in actor.properties) return actor.properties.teamID; if ("teamID" in actor.properties) return actor.properties.teamID;
throw new VisibleError( throw new Error(`Expected actor to have teamID`);
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have teamID`
);
} }
/** export async function assertUserFlag(flag: keyof UserFlags) {
* Returns the fingerprint of the current actor if the actor has a machine identity. return useTransaction((tx) =>
* tx
* @returns The fingerprint of the current machine actor. .select({ flags: userTable.flags })
* @throws {VisibleError} If the current actor does not have a machine identity. .from(userTable)
*/ .where(eq(userTable.id, useUserID()))
export function useMachine() { .then((rows) => {
const actor = useActor(); const flags = rows[0]?.flags;
if ("machineID" in actor.properties) return actor.properties.fingerprint; if (!flags)
throw new VisibleError( throw new VisibleError(
"authentication", "user.flags",
ErrorCodes.Authentication.UNAUTHORIZED, "Actor does not have " + flag + " flag",
`Expected actor to have fingerprint` );
}),
); );
} }

View File

@@ -1,11 +1,7 @@
import { sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import "zod-openapi/extend"; import "zod-openapi/extend";
export namespace Common { export module Common {
export const IdDescription = `Unique object identifier. export const IdDescription = `Unique object identifier.
The format and length of IDs may change over time.`; The format and length of IDs may change over time.`;
export const now = () => sql`now()`;
export const utc = () => sql`now() at time zone 'utc'`;
} }

View File

@@ -1,17 +1,30 @@
export * from "drizzle-orm"; export * from "drizzle-orm";
import ws from 'ws';
import { Resource } from "sst"; import { Resource } from "sst";
import postgres from "postgres"; import { drizzle as neonDrizzle, NeonDatabase } from "drizzle-orm/neon-serverless";
import { drizzle } from "drizzle-orm/postgres-js"; // import { drizzle } from 'drizzle-orm/postgres-js';
import { Pool, neonConfig } from "@neondatabase/serverless";
const client = postgres({ neonConfig.webSocketConstructor = ws;
idle_timeout: 30000,
connect_timeout: 30000, function addPoolerSuffix(original: string): string {
host: Resource.Database.host, const firstDotIndex = original.indexOf('.');
database: Resource.Database.database, if (firstDotIndex === -1) return original + '-pooler';
user: Resource.Database.username, return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
password: Resource.Database.password, }
port: Resource.Database.port,
max: parseInt(process.env.POSTGRES_POOL_MAX || "1"), const dbHost = addPoolerSuffix(Resource.Database.host)
const client = new Pool({ connectionString: `postgres://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require` })
export const db = neonDrizzle(client, {
logger:
process.env.DRIZZLE_LOG === "true"
? {
logQuery(query, params) {
console.log("query", query);
console.log("params", params);
},
}
: undefined,
}); });
export const db = drizzle(client, {});

View File

@@ -4,13 +4,14 @@ import {
PgTransactionConfig PgTransactionConfig
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { import {
PostgresJsQueryResultHKT NeonQueryResultHKT
} from "drizzle-orm/postgres-js"; // NeonHttpQueryResultHKT
} from "drizzle-orm/neon-serverless";
import { ExtractTablesWithRelations } from "drizzle-orm"; import { ExtractTablesWithRelations } from "drizzle-orm";
import { createContext } from "../context"; import { createContext } from "../context";
export type Transaction = PgTransaction< export type Transaction = PgTransaction<
PostgresJsQueryResultHKT, NeonQueryResultHKT,
Record<string, never>, Record<string, never>,
ExtractTablesWithRelations<Record<string, never>> ExtractTablesWithRelations<Record<string, never>>
>; >;
@@ -58,6 +59,7 @@ export async function createTransaction<T>(
}, },
); );
await Promise.all(effects.map((x) => x())); await Promise.all(effects.map((x) => x()));
// await db.$client.end()
return result as T; return result as T;
} }
} }

View File

@@ -1,5 +1,4 @@
import { char, timestamp as rawTs } from "drizzle-orm/pg-core"; import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
import { teamTable } from "../team/team.sql";
export const ulid = (name: string) => char(name, { length: 26 + 4 }); export const ulid = (name: string) => char(name, { length: 26 + 4 });
@@ -18,15 +17,6 @@ export const teamID = {
}, },
}; };
export const userID = {
get id() {
return ulid("id").notNull();
},
get userID() {
return ulid("user_id").notNull();
},
};
export const utc = (name: string) => export const utc = (name: string) =>
rawTs(name, { rawTs(name, {
withTimezone: true, withTimezone: true,

View File

@@ -10,7 +10,7 @@ export namespace Email {
subject: string, subject: string,
body: string, body: string,
) { ) {
from = from + "@" + Resource.Email.sender; from = from + "@" + Resource.Mail.sender;
console.log("sending email", subject, from, to); console.log("sending email", subject, from, to);
await Client.send( await Client.send(
new SendEmailCommand({ new SendEmailCommand({

View File

@@ -1,145 +1,8 @@
import { z } from "zod"
/**
* Standard error response schema used for OpenAPI documentation
*/
export const ErrorResponse = z
.object({
type: z
.enum([
"validation",
"authentication",
"forbidden",
"not_found",
"already_exists",
"rate_limit",
"internal",
])
.openapi({
description: "The error type category",
examples: ["validation", "authentication"],
}),
code: z.string().openapi({
description: "Machine-readable error code identifier",
examples: ["invalid_parameter", "missing_required_field", "unauthorized"],
}),
message: z.string().openapi({
description: "Human-readable error message",
examples: ["The request was invalid", "Authentication required"],
}),
param: z
.string()
.optional()
.openapi({
description: "The parameter that caused the error (if applicable)",
examples: ["email", "user_id", "team_id"],
}),
details: z.any().optional().openapi({
description: "Additional error context information",
}),
})
.openapi({ ref: "ErrorResponse" });
export type ErrorResponseType = z.infer<typeof ErrorResponse>;
/**
* Standardized error codes for the API
*/
export const ErrorCodes = {
// Validation errors (400)
Validation: {
MISSING_REQUIRED_FIELD: "missing_required_field",
ALREADY_EXISTS: "resource_already_exists",
TEAM_ALREADY_EXISTS: "team_already_exists",
INVALID_PARAMETER: "invalid_parameter",
INVALID_FORMAT: "invalid_format",
INVALID_STATE: "invalid_state",
IN_USE: "resource_in_use",
},
// Authentication errors (401)
Authentication: {
UNAUTHORIZED: "unauthorized",
INVALID_TOKEN: "invalid_token",
EXPIRED_TOKEN: "expired_token",
INVALID_CREDENTIALS: "invalid_credentials",
},
// Permission errors (403)
Permission: {
FORBIDDEN: "forbidden",
INSUFFICIENT_PERMISSIONS: "insufficient_permissions",
ACCOUNT_RESTRICTED: "account_restricted",
},
// Resource not found errors (404)
NotFound: {
RESOURCE_NOT_FOUND: "resource_not_found",
},
// Rate limit errors (429)
RateLimit: {
TOO_MANY_REQUESTS: "too_many_requests",
QUOTA_EXCEEDED: "quota_exceeded",
},
// Server errors (500)
Server: {
INTERNAL_ERROR: "internal_error",
SERVICE_UNAVAILABLE: "service_unavailable",
DEPENDENCY_FAILURE: "dependency_failure",
},
};
/**
* Standard error that will be exposed to clients through API responses
*/
export class VisibleError extends Error { export class VisibleError extends Error {
constructor( constructor(
public type: ErrorResponseType["type"], public code: string,
public code: string, public message: string,
public message: string, ) {
public param?: string, super(message);
public details?: any,
) {
super(message);
}
/**
* Convert this error to an HTTP status code
*/
public statusCode(): number {
switch (this.type) {
case "validation":
return 400;
case "authentication":
return 401;
case "forbidden":
return 403;
case "not_found":
return 404;
case "already_exists":
return 409;
case "rate_limit":
return 429;
case "internal":
return 500;
} }
} }
/**
* Convert this error to a standard response object
*/
public toResponse(): ErrorResponseType {
const response: ErrorResponseType = {
type: this.type,
code: this.code,
message: this.message,
};
if (this.param) response.param = this.param;
if (this.details) response.details = this.details;
return response;
}
}

View File

@@ -1,29 +1,8 @@
import { prefixes } from "./utils"; import { prefixes } from "./utils";
export namespace Examples { export module Examples {
export const Id = (prefix: keyof typeof prefixes) => export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`; `${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const Steam = {
id: Id("steam"),
userID: Id("user"),
countryCode: "KE",
steamID: 74839300282033,
limitation: {
isLimited: false,
isBanned: false,
isLocked: false,
isAllowedToInviteFriends: false,
},
lastGame: {
gameID: 2531310,
gameName: "The Last of Us™ Part II Remastered",
},
personaName: "John",
username: "johnsteamaccount",
steamEmail: "john@example.com",
avatarUrl: "https://avatars.akamai.steamstatic.com/XXXXXXXXXXXX_full.jpg",
}
export const User = { export const User = {
id: Id("user"), id: Id("user"),
name: "John Doe", name: "John Doe",
@@ -31,51 +10,24 @@ export namespace Examples {
discriminator: 47, discriminator: 47,
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png", avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4", polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
steamAccounts: [Steam]
}; };
export const Product = { export const Team = {
id: Id("product"), id: Id("team"),
name: "RTX 4090", name: "John Does' Team",
description: "Ideal for dedicated gamers who crave more flexibility and social gaming experiences.", slug: "john_doe",
tokensPerHour: 20,
}
export const Subscription = {
tokens: 100,
id: Id("subscription"),
userID: Id("user"),
teamID: Id("team"),
planType: "pro" as const, // free, pro, family, enterprise
standing: "new" as const, // new, good, overdue, cancelled
polarProductID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
polarSubscriptionID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
} }
export const Member = { export const Member = {
id: Id("member"), id: Id("member"),
email: "john@example.com", email: "john@example.com",
teamID: Id("team"), teamID: Id("team"),
role: "admin" as const,
timeSeen: new Date("2025-02-23T13:39:52.249Z"), timeSeen: new Date("2025-02-23T13:39:52.249Z"),
} }
export const Team = { export const Polar = {
id: Id("team"), teamID: Id("team"),
name: "John Does' Team", timeSeen: new Date("2025-02-23T13:39:52.249Z"),
slug: "john_doe",
subscriptions: [Subscription],
members: [Member]
}
export const Machine = {
id: Id("machine"),
userID: Id("user"),
country: "Kenya",
countryCode: "KE",
timezone: "Africa/Nairobi",
location: { latitude: 36.81550, longitude: -1.28410 },
fingerprint: "fc27f428f9ca47d4b41b707ae0c62090",
} }
} }

View File

@@ -1,155 +0,0 @@
import { z } from "zod";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { Examples } from "../examples";
import { machineTable } from "./machine.sql";
import { getTableColumns, eq, sql, and, isNull } from "../drizzle";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Machine {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
// userID: z.string().nullable().openapi({
// description: "The userID of the user who owns this machine, in the case of BYOG",
// example: Examples.Machine.userID
// }),
country: z.string().openapi({
description: "The fullname of the country this machine is running in",
example: Examples.Machine.country
}),
fingerprint: z.string().openapi({
description: "The fingerprint of this machine, deduced from the host machine's machine id - /etc/machine-id",
example: Examples.Machine.fingerprint
}),
location: z.object({ longitude: z.number(), latitude: z.number() }).openapi({
description: "This is the 2d location of this machine, they might not be accurate",
example: Examples.Machine.location
}),
countryCode: z.string().openapi({
description: "This is the 2 character country code of the country this machine [ISO 3166-1 alpha-2] ",
example: Examples.Machine.countryCode
}),
timezone: z.string().openapi({
description: "The IANA timezone formatted string of the timezone of the location where the machine is running",
example: Examples.Machine.timezone
})
})
.openapi({
ref: "Machine",
description: "Represents a hosted or BYOG machine connected to Nestri",
example: Examples.Machine,
});
export type Info = z.infer<typeof Info>;
export const create = fn(Info.partial({ id: true }), async (input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("machine");
await tx.insert(machineTable).values({
id,
country: input.country,
timezone: input.timezone,
fingerprint: input.fingerprint,
countryCode: input.countryCode,
// userID: input.userID,
location: { x: input.location.longitude, y: input.location.latitude },
})
// await afterTx(() =>
// bus.publish(Resource.Bus, Events.Created, {
// teamID: id,
// }),
// );
return id;
})
)
// export const fromUserID = fn(z.string(), async (userID) =>
// useTransaction(async (tx) =>
// tx
// .select()
// .from(machineTable)
// .where(and(eq(machineTable.userID, userID), isNull(machineTable.timeDeleted)))
// .then((rows) => rows.map(serialize))
// )
// )
// export const list = fn(z.void(), async () =>
// useTransaction(async (tx) =>
// tx
// .select()
// .from(machineTable)
// // Show only hosted machines, not BYOG machines
// .where(and(isNull(machineTable.userID), isNull(machineTable.timeDeleted)))
// .then((rows) => rows.map(serialize))
// )
// )
export const fromID = fn(Info.shape.id, async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(machineTable)
.where(and(eq(machineTable.id, id), isNull(machineTable.timeDeleted)))
.then((rows) => rows.map(serialize).at(0))
)
)
export const fromFingerprint = fn(Info.shape.fingerprint, async (fingerprint) =>
useTransaction(async (tx) =>
tx
.select()
.from(machineTable)
.where(and(eq(machineTable.fingerprint, fingerprint), isNull(machineTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize).at(0))
)
)
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) => {
await tx
.update(machineTable)
.set({
timeDeleted: sql`now()`,
})
.where(and(eq(machineTable.id, id)))
.execute();
return id;
}),
);
export const fromLocation = fn(Info.shape.location, async (location) =>
useTransaction(async (tx) => {
const sqlDistance = sql`location <-> point(${location.longitude}, ${location.latitude})`;
return tx
.select({
...getTableColumns(machineTable),
distance: sql`round((${sqlDistance})::numeric, 2)`
})
.from(machineTable)
.where(isNull(machineTable.timeDeleted))
.orderBy(sqlDistance)
.limit(3)
.then((rows) => rows.map(serialize))
})
)
export function serialize(
input: typeof machineTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
// userID: input.userID,
country: input.country,
timezone: input.timezone,
fingerprint: input.fingerprint,
countryCode: input.countryCode,
location: { latitude: input.location.y, longitude: input.location.x },
};
}
}

View File

@@ -1,40 +0,0 @@
import { } from "drizzle-orm/postgres-js";
import { timestamps, id, ulid } from "../drizzle/types";
import {
text,
varchar,
pgTable,
uniqueIndex,
point,
primaryKey,
} from "drizzle-orm/pg-core";
export const machineTable = pgTable(
"machine",
{
...id,
...timestamps,
// userID: ulid("user_id"),
country: text('country').notNull(),
timezone: text('timezone').notNull(),
location: point('location', { mode: 'xy' }).notNull(),
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
countryCode: varchar('country_code', { length: 2 }).notNull(),
// provider: text("provider").notNull(),
// gpuType: text("gpu_type").notNull(),
// storage: numeric("storage").notNull(),
// ipaddress: text("ipaddress").notNull(),
// gpuNumber: integer("gpu_number").notNull(),
// computePrice: numeric("compute_price").notNull(),
// driverVersion: integer("driver_version").notNull(),
// operatingSystem: text("operating_system").notNull(),
// fingerprint: varchar("fingerprint", { length: 32 }).notNull(),
// externalID: varchar("external_id", { length: 255 }).notNull(),
// cudaVersion: numeric("cuda_version", { precision: 4, scale: 2 }).notNull(),
},
(table) => [
// uniqueIndex("external_id").on(table.externalID),
uniqueIndex("machine_fingerprint").on(table.fingerprint),
// primaryKey({ columns: [table.userID, table.id], }),
],
);

View File

@@ -6,18 +6,18 @@ import { Common } from "../common";
import { createID, fn } from "../utils"; import { createID, fn } from "../utils";
import { createEvent } from "../event"; import { createEvent } from "../event";
import { Examples } from "../examples"; import { Examples } from "../examples";
import { memberTable, role } from "./member.sql"; import { memberTable } from "./member.sql";
import { and, eq, sql, asc, isNull } from "../drizzle"; import { and, eq, sql, asc, isNull } from "../drizzle";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Member { export module Member {
export const Info = z export const Info = z
.object({ .object({
id: z.string().openapi({ id: z.string().openapi({
description: Common.IdDescription, description: Common.IdDescription,
example: Examples.Member.id, example: Examples.Member.id,
}), }),
timeSeen: z.date().nullable().or(z.undefined()).openapi({ timeSeen: z.date().or(z.null()).openapi({
description: "The last time this team member was active", description: "The last time this team member was active",
example: Examples.Member.timeSeen example: Examples.Member.timeSeen
}), }),
@@ -25,10 +25,6 @@ export namespace Member {
description: "The unique id of the team this member is on", description: "The unique id of the team this member is on",
example: Examples.Member.teamID example: Examples.Member.teamID
}), }),
role: z.enum(role).openapi({
description: "The role of this team member",
example: Examples.Member.role
}),
email: z.string().openapi({ email: z.string().openapi({
description: "The email of this team member", description: "The email of this team member",
example: Examples.Member.email example: Examples.Member.email
@@ -70,12 +66,15 @@ export namespace Member {
const id = input.id ?? createID("member"); const id = input.id ?? createID("member");
await tx.insert(memberTable).values({ await tx.insert(memberTable).values({
id, id,
teamID: useTeam(),
email: input.email, email: input.email,
role: input.first ? "owner" : "member", teamID: useTeam(),
timeSeen: input.first ? sql`now()` : null, timeSeen: input.first ? sql`CURRENT_TIMESTAMP()` : null,
}).onConflictDoUpdate({
target: memberTable.id,
set: {
timeDeleted: null,
}
}) })
await afterTx(() => await afterTx(() =>
async () => bus.publish(Resource.Bus, Events.Created, { memberID: id }), async () => bus.publish(Resource.Bus, Events.Created, { memberID: id }),
); );
@@ -83,16 +82,16 @@ export namespace Member {
}), }),
); );
export const remove = fn(Info.shape.id, (id) => export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => { useTransaction(async (tx) => {
await tx await tx
.update(memberTable) .update(memberTable)
.set({ .set({
timeDeleted: sql`now()`, timeDeleted: sql`CURRENT_TIMESTAMP()`,
}) })
.where(and(eq(memberTable.id, id), eq(memberTable.teamID, useTeam()))) .where(and(eq(memberTable.id, input), eq(memberTable.teamID, useTeam())))
.execute(); .execute();
return id; return input;
}), }),
); );
@@ -103,8 +102,9 @@ export namespace Member {
.from(memberTable) .from(memberTable)
.where(and(eq(memberTable.email, email), isNull(memberTable.timeDeleted))) .where(and(eq(memberTable.email, email), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated)) .orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize).at(0)) .then((rows) => rows.map(serialize))
) .then((rows) => rows.at(0))
),
) )
export const fromID = fn(z.string(), async (id) => export const fromID = fn(z.string(), async (id) =>
@@ -114,22 +114,16 @@ export namespace Member {
.from(memberTable) .from(memberTable)
.where(and(eq(memberTable.id, id), isNull(memberTable.timeDeleted))) .where(and(eq(memberTable.id, id), isNull(memberTable.timeDeleted)))
.orderBy(asc(memberTable.timeCreated)) .orderBy(asc(memberTable.timeCreated))
.then((rows) => rows.map(serialize).at(0)) .then((rows) => rows.map(serialize))
.then((rows) => rows.at(0))
), ),
) )
/**
* Converts a raw member database row into a standardized {@link Member.Info} object.
*
* @param input - The database row representing a member.
* @returns The member information formatted as a {@link Member.Info} object.
*/
export function serialize( export function serialize(
input: typeof memberTable.$inferSelect, input: typeof memberTable.$inferSelect,
): z.infer<typeof Info> { ): z.infer<typeof Info> {
return { return {
id: input.id, id: input.id,
role: input.role,
email: input.email, email: input.email,
teamID: input.teamID, teamID: input.teamID,
timeSeen: input.timeSeen timeSeen: input.timeSeen

View File

@@ -1,21 +1,18 @@
import { teamIndexes } from "../team/team.sql"; import { teamIndexes } from "../team/team.sql";
import { timestamps, utc, teamID } from "../drizzle/types"; import { timestamps, utc, teamID } from "../drizzle/types";
import { index, pgTable, text, uniqueIndex, varchar } from "drizzle-orm/pg-core"; import { index, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const role = ["admin", "member", "owner"] as const;
export const memberTable = pgTable( export const memberTable = pgTable(
"member", "member",
{ {
...teamID, ...teamID,
...timestamps, ...timestamps,
role: text("role", { enum: role }).notNull(),
timeSeen: utc("time_seen"), timeSeen: utc("time_seen"),
email: varchar("email", { length: 255 }).notNull(), email: varchar("email", { length: 255 }).notNull(),
}, },
(table) => [ (table) => [
...teamIndexes(table), ...teamIndexes(table),
index("email_global").on(table.email),
uniqueIndex("member_email").on(table.teamID, table.email), uniqueIndex("member_email").on(table.teamID, table.email),
index("email_global").on(table.email),
], ],
); );

View File

@@ -1,16 +1,69 @@
import { z } from "zod"; import { z } from "zod";
import { fn } from "../utils"; import { fn } from "../utils";
import { Resource } from "sst"; import { Resource } from "sst";
import { useTeam, useUserID } from "../actor"; import { eq, and } from "../drizzle";
import { useTeam } from "../actor";
import { createEvent } from "../event";
import { polarTable, Standing } from "./polar.sql";
import { Polar as PolarSdk } from "@polar-sh/sdk"; import { Polar as PolarSdk } from "@polar-sh/sdk";
import { validateEvent } from "@polar-sh/sdk/webhooks"; import { useTransaction } from "../drizzle/transaction";
import { PlanType } from "../subscription/subscription.sql";
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" }); const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
const planType = z.enum(PlanType)
export namespace Polar { export module Polar {
export const client = polar; export const client = polar;
export const Info = z.object({
teamID: z.string(),
customerID: z.string(),
subscriptionID: z.string().nullable(),
subscriptionItemID: z.string().nullable(),
standing: z.enum(Standing),
});
export type Info = z.infer<typeof Info>;
export const Checkout = z.object({
annual: z.boolean().optional(),
successUrl: z.string(),
cancelUrl: z.string(),
});
export const CheckoutSession = z.object({
url: z.string().nullable(),
});
export const CustomerSubscriptionEventType = [
"created",
"updated",
"deleted",
] as const;
export const Events = {
CustomerSubscriptionEvent: createEvent(
"polar.customer-subscription-event",
z.object({
type: z.enum(CustomerSubscriptionEventType),
status: z.string(),
teamID: z.string().min(1),
customerID: z.string().min(1),
subscriptionID: z.string().min(1),
subscriptionItemID: z.string().min(1),
}),
),
};
export function get() {
return useTransaction(async (tx) =>
tx
.select()
.from(polarTable)
.where(eq(polarTable.teamID, useTeam()))
.execute()
.then((rows) => rows.map(serialize).at(0)),
);
}
export const fromUserEmail = fn(z.string().min(1), async (email) => { export const fromUserEmail = fn(z.string().min(1), async (email) => {
try { try {
const customers = await client.customers.list({ email }) const customers = await client.customers.list({ email })
@@ -28,69 +81,89 @@ export namespace Polar {
} }
}) })
const getProductIDs = (plan: z.infer<typeof planType>) => { export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
switch (plan) { useTransaction(async (tx) =>
case "free": tx
return [Resource.NestriFreeMonthly.value] .insert(polarTable)
case "pro": .values({
return [Resource.NestriProYearly.value, Resource.NestriProMonthly.value] teamID: useTeam(),
case "family": customerID,
return [Resource.NestriFamilyYearly.value, Resource.NestriFamilyMonthly.value] standing: "new",
default:
return [Resource.NestriFreeMonthly.value]
}
}
export const createPortal = fn(
z.string(),
async (customerId) => {
const session = await client.customerSessions.create({
customerId
})
return session.customerPortalUrl
}
)
//TODO: Implement this
export const handleWebhook = async(payload: ReturnType<typeof validateEvent>) => {
switch (payload.type) {
case "subscription.created":
const teamID = payload.data.metadata.teamID
}
}
export const createCheckout = fn(
z
.object({
planType: z.enum(PlanType),
customerEmail: z.string(),
successUrl: z.string(),
customerID: z.string(),
allowDiscountCodes: z.boolean(),
teamID: z.string()
})
.partial({
customerEmail: true,
allowDiscountCodes: true,
customerID: true,
teamID: true
}),
async (input) => {
const productIDs = getProductIDs(input.planType)
const checkoutUrl =
await client.checkouts.create({
products: productIDs,
customerEmail: input.customerEmail ?? useUserID(),
successUrl: `${input.successUrl}?checkout={CHECKOUT_ID}`,
allowDiscountCodes: input.allowDiscountCodes ?? false,
customerId: input.customerID,
customerMetadata: {
teamID: input.teamID ?? useTeam()
}
}) })
.execute(),
),
);
return checkoutUrl.url export const setSubscription = fn(
}) Info.pick({
subscriptionID: true,
subscriptionItemID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx
.update(polarTable)
.set({
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
})
.where(eq(polarTable.teamID, useTeam()))
.returning()
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export const removeSubscription = fn(
z.string().min(1),
(stripeSubscriptionID) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({
subscriptionItemID: null,
subscriptionID: null,
})
.where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
.execute(),
),
);
export const setStanding = fn(
Info.pick({
subscriptionID: true,
standing: true,
}),
(input) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({ standing: input.standing })
.where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
.execute(),
),
);
export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
useTransaction((tx) =>
tx
.select()
.from(polarTable)
.where(and(eq(polarTable.customerID, customerID)))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
function serialize(
input: typeof polarTable.$inferSelect,
): z.infer<typeof Info> {
return {
teamID: input.teamID,
customerID: input.customerID,
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
standing: input.standing,
};
}
} }

View File

@@ -0,0 +1,22 @@
import { timestamps, teamID } from "../drizzle/types";
import { teamIndexes, teamTable } from "../team/team.sql";
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue"] as const;
export const polarTable = pgTable(
"polar",
{
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
...timestamps,
customerID: varchar("customer_id", { length: 255 }).notNull(),
subscriptionID: varchar("subscription_id", { length: 255 }),
subscriptionItemID: varchar("subscription_item_id", {
length: 255,
}),
standing: text("standing", { enum: Standing }).notNull(),
},
(table) => ({
...teamIndexes(table),
})
)

View File

@@ -1,24 +0,0 @@
import {
IoTDataPlaneClient,
PublishCommand,
} from "@aws-sdk/client-iot-data-plane";
import { useMachine } from "../actor";
import { Resource } from "sst";
export namespace Realtime {
const client = new IoTDataPlaneClient({});
export async function publish(message: any, subTopic?: string) {
const fingerprint = useMachine();
let topic = `${Resource.App.name}/${Resource.App.stage}/${fingerprint}/`;
if (subTopic)
topic = `${topic}${subTopic}`;
await client.send(
new PublishCommand({
payload: Buffer.from(JSON.stringify(message)),
topic: topic,
})
);
}
}

View File

@@ -1,137 +0,0 @@
import { z } from "zod";
import { Common } from "../common";
import { Examples } from "../examples";
import { createID, fn } from "../utils";
import { useUser, useUserID } from "../actor";
import { eq, and, isNull, sql } from "../drizzle";
import { steamTable, AccountLimitation, LastGame } from "./steam.sql";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Steam {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Steam.id,
}),
avatarUrl: z.string().openapi({
description: "The avatar url of this Steam account",
example: Examples.Steam.avatarUrl
}),
steamEmail: z.string().openapi({
description: "The email regisered with this Steam account",
example: Examples.Steam.steamEmail
}),
steamID: z.number().openapi({
description: "The Steam ID this Steam account",
example: Examples.Steam.steamID
}),
limitation: AccountLimitation.openapi({
description: " The limitations of this Steam account",
example: Examples.Steam.limitation
}),
lastGame: LastGame.openapi({
description: "The last game played on this Steam account",
example: Examples.Steam.lastGame
}),
userID: z.string().openapi({
description: "The unique id of the user who owns this steam account",
example: Examples.Steam.userID
}),
username: z.string().openapi({
description: "The unique username of this steam user",
example: Examples.Steam.username
}),
personaName: z.string().openapi({
description: "The last recorded persona name used by this account",
example: Examples.Steam.personaName
}),
countryCode: z.string().openapi({
description: "The country this account is connected from",
example: Examples.Steam.countryCode
})
})
.openapi({
ref: "Steam",
description: "Represents a steam user's information stored on Nestri",
example: Examples.Steam,
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info.partial({
id: true,
userID: true,
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("steam");
const user = useUser()
await tx.insert(steamTable).values({
id,
lastSeen: sql`now()`,
userID: input.userID ?? user.userID,
countryCode: input.countryCode,
username: input.username,
steamID: input.steamID,
lastGame: input.lastGame,
limitation: input.limitation,
steamEmail: input.steamEmail,
avatarUrl: input.avatarUrl,
personaName: input.personaName,
})
return id;
}),
);
export const fromUserID = fn(
z.string(),
(userID) =>
useTransaction((tx) =>
tx
.select()
.from(steamTable)
.where(and(eq(steamTable.userID, userID), isNull(steamTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
)
export const list = () =>
useTransaction((tx) =>
tx
.select()
.from(steamTable)
.where(and(eq(steamTable.userID, useUserID()), isNull(steamTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize)),
)
/**
* Serializes a raw Steam table record into a standardized Info object.
*
* This function maps the fields from a database record (retrieved from the Steam table) to the
* corresponding properties defined in the Info schema.
*
* @param input - A raw record from the Steam table containing user information.
* @returns An object conforming to the Info schema.
*/
export function serialize(
input: typeof steamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
userID: input.userID,
countryCode: input.countryCode,
username: input.username,
avatarUrl: input.avatarUrl,
personaName: input.personaName,
steamEmail: input.steamEmail,
steamID: input.steamID,
limitation: input.limitation,
lastGame: input.lastGame,
};
}
}

View File

@@ -1,45 +0,0 @@
import { z } from "zod";
import { userTable } from "../user/user.sql";
import { id, timestamps, ulid, utc } from "../drizzle/types";
import { index, pgTable, integer, uniqueIndex, varchar, text, json } from "drizzle-orm/pg-core";
export const LastGame = z.object({
gameID: z.number(),
gameName: z.string()
});
export const AccountLimitation = z.object({
isLimited: z.boolean().nullable(),
isBanned: z.boolean().nullable(),
isLocked: z.boolean().nullable(),
isAllowedToInviteFriends: z.boolean().nullable(),
});
export type LastGame = z.infer<typeof LastGame>;
export type AccountLimitation = z.infer<typeof AccountLimitation>;
export const steamTable = pgTable(
"steam",
{
...id,
...timestamps,
userID: ulid("user_id")
.notNull()
.references(() => userTable.id, {
onDelete: "cascade",
}),
lastSeen: utc("last_seen").notNull(),
steamID: integer("steam_id").notNull(),
avatarUrl: text("avatar_url").notNull(),
lastGame: json("last_game").$type<LastGame>().notNull(),
username: varchar("username", { length: 255 }).notNull(),
countryCode: varchar('country_code', { length: 2 }).notNull(),
steamEmail: varchar("steam_email", { length: 255 }).notNull(),
personaName: varchar("persona_name", { length: 255 }).notNull(),
limitation: json("limitation").$type<AccountLimitation>().notNull(),
},
(table) => [
uniqueIndex("steam_id").on(table.steamID),
index("steam_user_id").on(table.userID),
],
);

View File

@@ -1,192 +0,0 @@
import { z } from "zod";
import { Common } from "../common";
import { Examples } from "../examples";
import { createID, fn } from "../utils";
import { eq, and, isNull } from "../drizzle";
import { useTeam, useUserID } from "../actor";
import { createTransaction, useTransaction } from "../drizzle/transaction";
import { PlanType, Standing, subscriptionTable } from "./subscription.sql";
export namespace Subscription {
export const Info = z.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
polarSubscriptionID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the plan this subscription is on",
example: Examples.Subscription.polarSubscriptionID,
}),
teamID: z.string().openapi({
description: "The unique id of the team this subscription is for",
example: Examples.Subscription.teamID,
}),
userID: z.string().openapi({
description: "The unique id of the user who is paying this subscription",
example: Examples.Subscription.userID,
}),
polarProductID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the product this subscription is for",
example: Examples.Subscription.polarProductID,
}),
tokens: z.number().openapi({
description: "The number of tokens this subscription has left",
example: Examples.Subscription.tokens,
}),
planType: z.enum(PlanType).openapi({
description: "The type of plan this subscription is for",
example: Examples.Subscription.planType,
}),
standing: z.enum(Standing).openapi({
description: "The standing of this subscription",
example: Examples.Subscription.standing,
}),
}).openapi({
ref: "Subscription",
description: "Represents a subscription on Nestri",
example: Examples.Subscription
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info
.partial({
teamID: true,
userID: true,
id: true,
standing: true,
planType: true,
polarProductID: true,
polarSubscriptionID: true,
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("subscription");
await tx.insert(subscriptionTable).values({
id,
tokens: input.tokens,
polarProductID: input.polarProductID ?? null,
polarSubscriptionID: input.polarSubscriptionID ?? null,
standing: input.standing ?? "new",
planType: input.planType ?? "free",
userID: input.userID ?? useUserID(),
teamID: input.teamID ?? useTeam(),
});
return id;
})
)
export const setPolarProductID = fn(
Info.pick({
id: true,
polarProductID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarProductID: input.polarProductID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
export const setPolarSubscriptionID = fn(
Info.pick({
id: true,
polarSubscriptionID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarSubscriptionID: input.polarSubscriptionID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.id, id),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const fromTeamID = fn(z.string(), async (teamID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.teamID, teamID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const fromUserID = fn(z.string(), async (userID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.userID, userID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) =>
tx
.update(subscriptionTable)
.set({
timeDeleted: Common.now(),
})
.where(eq(subscriptionTable.id, id))
.execute()
)
)
/**
* Converts a raw subscription database record into a structured {@link Info} object.
*
* @param input - The subscription record retrieved from the database.
* @returns The subscription data formatted according to the {@link Info} schema.
*/
export function serialize(
input: typeof subscriptionTable.$inferSelect
): z.infer<typeof Info> {
return {
id: input.id,
userID: input.userID,
teamID: input.teamID,
standing: input.standing,
planType: input.planType,
tokens: input.tokens,
polarProductID: input.polarProductID,
polarSubscriptionID: input.polarSubscriptionID,
};
}
}

View File

@@ -1,31 +0,0 @@
import { teamTable } from "../team/team.sql";
import { ulid, userID, timestamps } from "../drizzle/types";
import { index, integer, pgTable, primaryKey, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue", "cancelled"] as const;
export const PlanType = ["free", "pro", "family", "enterprise"] as const;
export const subscriptionTable = pgTable(
"subscription",
{
...userID,
...timestamps,
teamID: ulid("team_id")
.references(() => teamTable.id, { onDelete: "cascade" })
.notNull(),
standing: text("standing", { enum: Standing })
.notNull(),
planType: text("plan_type", { enum: PlanType })
.notNull(),
tokens: integer("tokens").notNull(),
polarProductID: varchar("product_id", { length: 255 }),
polarSubscriptionID: varchar("subscription_id", { length: 255 }),
},
(table) => [
uniqueIndex("subscription_id").on(table.id),
index("subscription_user_id").on(table.userID),
primaryKey({
columns: [table.id, table.teamID]
}),
]
)

View File

@@ -1,20 +0,0 @@
import { id, timestamps } from "../drizzle/types";
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
//This represents a task created on a machine for running a game
//Add billing info here?
//Add who owns the task here
// Add the session ID here
//Add which machine owns this task
export const taskTable = pgTable(
"task",
{
...id,
...timestamps,
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
},
(table) => [
uniqueIndex("task_fingerprint").on(table.fingerprint),
],
);

View File

@@ -1,43 +1,32 @@
import { z } from "zod"; import { z } from "zod";
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Common } from "../common"; import { Common } from "../common";
import { Member } from "../member";
import { teamTable } from "./team.sql";
import { Examples } from "../examples";
import { assertActor } from "../actor";
import { createEvent } from "../event";
import { createID, fn } from "../utils"; import { createID, fn } from "../utils";
import { Subscription } from "../subscription"; import { Examples } from "../examples";
import { and, eq, sql, isNull } from "../drizzle"; import { teamTable } from "./team.sql";
import { createEvent } from "../event";
import { assertActor, withActor } from "../actor";
import { and, eq, sql } from "../drizzle";
import { memberTable } from "../member/member.sql"; import { memberTable } from "../member/member.sql";
import { ErrorCodes, VisibleError } from "../error"; import { HTTPException } from 'hono/http-exception';
import { groupBy, map, pipe, values } from "remeda"; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
import { subscriptionTable } from "../subscription/subscription.sql";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Team { export module Team {
export const Info = z export const Info = z
.object({ .object({
id: z.string().openapi({ id: z.string().openapi({
description: Common.IdDescription, description: Common.IdDescription,
example: Examples.Team.id, example: Examples.Team.id,
}), }),
// Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this) slug: z.string().openapi({
slug: z.string().regex(/^[a-z0-9\-]+$/, "Use a URL friendly name.").openapi({
description: "The unique and url-friendly slug of this team", description: "The unique and url-friendly slug of this team",
example: Examples.Team.slug example: Examples.Team.slug
}), }),
name: z.string().openapi({ name: z.string().openapi({
description: "The name of this team", description: "The name of this team",
example: Examples.Team.name example: Examples.Team.name
}), })
members: Member.Info.array().openapi({
description: "The members of this team",
example: Examples.Team.members
}),
subscriptions: Subscription.Info.array().openapi({
description: "The subscriptions of this team",
example: Examples.Team.subscriptions
}),
}) })
.openapi({ .openapi({
ref: "Team", ref: "Team",
@@ -56,36 +45,41 @@ export namespace Team {
), ),
}; };
export class TeamExistsError extends VisibleError { export class TeamExistsError extends HTTPException {
constructor(slug: string) { constructor(slug: string) {
super( super(
"already_exists", 400,
ErrorCodes.Validation.TEAM_ALREADY_EXISTS, { message: `There is already a team named "${slug}"`, }
`There is already a team named "${slug}"`
); );
} }
} }
export const create = fn( export const create = fn(
Info.pick({ slug: true, id: true, name: true, }).partial({ Info.pick({ slug: true, id: true, name: true }).partial({
id: true, id: true,
}), (input) => }), (input) => {
createTransaction(async (tx) => { createTransaction(async (tx) => {
const id = input.id ?? createID("team"); const id = input.id ?? createID("team");
const result = await tx.insert(teamTable).values({ const result = await tx.insert(teamTable).values({
id, id,
slug: input.slug, slug: input.slug,
name: input.name name: input.name
})
.onConflictDoNothing({ target: teamTable.slug })
if (!result.rowCount) throw new TeamExistsError(input.slug);
await afterTx(() =>
withActor({ type: "system", properties: { teamID: id } }, () =>
bus.publish(Resource.Bus, Events.Created, {
teamID: id,
})
),
);
return id;
}) })
.onConflictDoNothing({ target: teamTable.slug })
if (result.count === 0) throw new TeamExistsError(input.slug);
return id;
}) })
)
//TODO: "Delete" subscription and member(s) as well
export const remove = fn(Info.shape.id, (input) => export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => { useTransaction(async (tx) => {
const account = assertActor("user"); const account = assertActor("user");
@@ -112,107 +106,48 @@ export namespace Team {
}), }),
); );
export const list = fn(z.void(), () => { export const list = fn(z.void(), () =>
const actor = assertActor("user"); useTransaction((tx) =>
return useTransaction(async (tx) =>
tx tx
.select() .select()
.from(teamTable) .from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(memberTable.email, actor.properties.email),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute() .execute()
.then((rows) => serialize(rows)) .then((rows) => rows.map(serialize)),
) ),
}); );
export const fromID = fn(z.string().min(1), async (id) => export const fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) => useTransaction(async (tx) => {
tx return tx
.select() .select()
.from(teamTable) .from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id)) .where(eq(teamTable.id, id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.id, id),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute() .execute()
.then((rows) => serialize(rows).at(0)) .then((rows) => rows.map(serialize))
), .then((rows) => rows.at(0));
}),
); );
export const fromSlug = fn(z.string().min(1), async (slug) => export const fromSlug = fn(z.string().min(1), async (input) =>
useTransaction(async (tx) => useTransaction(async (tx) => {
tx return tx
.select() .select()
.from(teamTable) .from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id)) .where(eq(teamTable.slug, input))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.slug, slug),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute() .execute()
.then((rows) => serialize(rows).at(0)) .then((rows) => rows.map(serialize))
), .then((rows) => rows.at(0));
}),
); );
/**
* Transforms an array of team, subscription, and member records into structured team objects.
*
* Groups input rows by team ID and constructs an array of team objects, each including its associated members and subscriptions.
*
* @param input - Array of objects containing team, subscription, and member data.
* @returns An array of team objects with their members and subscriptions.
*/
export function serialize( export function serialize(
input: { team: typeof teamTable.$inferSelect, subscription: typeof subscriptionTable.$inferInsert | null, member: typeof memberTable.$inferInsert | null }[], input: typeof teamTable.$inferSelect,
): z.infer<typeof Info>[] { ): z.infer<typeof Info> {
console.log("serialize", input) return {
return pipe( id: input.id,
input, name: input.name,
groupBy((row) => row.team.id), slug: input.slug,
values(), };
map((group) => ({
name: group[0].team.name,
id: group[0].team.id,
slug: group[0].team.slug,
subscriptions: !group[0].subscription ?
[] :
group.map((row) => ({
planType: row.subscription!.planType,
polarProductID: row.subscription!.polarProductID,
polarSubscriptionID: row.subscription!.polarSubscriptionID,
standing: row.subscription!.standing,
tokens: row.subscription!.tokens,
teamID: row.subscription!.teamID,
userID: row.subscription!.userID,
id: row.subscription!.id,
})),
members:
!group[0].member ?
[] :
group.map((row) => ({
id: row.member!.id,
email: row.member!.email,
role: row.member!.role,
teamID: row.member!.teamID,
timeSeen: row.member!.timeSeen,
}))
})),
);
} }
} }

View File

@@ -1,9 +1,10 @@
import {} from "drizzle-orm/postgres-js";
import { timestamps, id } from "../drizzle/types"; import { timestamps, id } from "../drizzle/types";
import { import {
varchar,
pgTable, pgTable,
primaryKey, primaryKey,
uniqueIndex, uniqueIndex,
varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
export const teamTable = pgTable( export const teamTable = pgTable(
@@ -11,12 +12,10 @@ export const teamTable = pgTable(
{ {
...id, ...id,
...timestamps, ...timestamps,
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull(), slug: varchar("slug", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }).notNull(),
}, },
(table) => [ (table) => [uniqueIndex("slug").on(table.slug)],
uniqueIndex("slug").on(table.slug)
],
); );
export function teamIndexes(table: any) { export function teamIndexes(table: any) {

View File

@@ -1,25 +1,21 @@
import { z } from "zod"; import { z } from "zod";
import { Polar } from "../polar";
import { Team } from "../team"; import { Team } from "../team";
import { bus } from "sst/aws/bus"; import { bus } from "sst/aws/bus";
import { Steam } from "../steam";
import { Common } from "../common"; import { Common } from "../common";
import { Polar } from "../polar/index";
import { createID, fn } from "../utils"; import { createID, fn } from "../utils";
import { userTable } from "./user.sql"; import { userTable } from "./user.sql";
import { createEvent } from "../event"; import { createEvent } from "../event";
import { Examples } from "../examples"; import { Examples } from "../examples";
import { Resource } from "sst/resource"; import { Resource } from "sst/resource";
import { teamTable } from "../team/team.sql"; import { teamTable } from "../team/team.sql";
import { steamTable } from "../steam/steam.sql";
import { assertActor, withActor } from "../actor"; import { assertActor, withActor } from "../actor";
import { memberTable } from "../member/member.sql"; import { memberTable } from "../member/member.sql";
import { pipe, groupBy, values, map } from "remeda"; import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
import { and, eq, isNull, asc, sql } from "../drizzle";
import { subscriptionTable } from "../subscription/subscription.sql";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export namespace User { export module User {
const MAX_ATTEMPTS = 50; const MAX_ATTEMPTS = 50;
export const Info = z export const Info = z
@@ -48,10 +44,6 @@ export namespace User {
description: "The (number) discriminator for this user", description: "The (number) discriminator for this user",
example: Examples.User.discriminator, example: Examples.User.discriminator,
}), }),
steamAccounts: Steam.Info.array().openapi({
description: "The steam accounts for this user",
example: Examples.User.steamAccounts,
}),
}) })
.openapi({ .openapi({
ref: "User", ref: "User",
@@ -110,7 +102,7 @@ export namespace User {
return null; return null;
}) })
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true, steamAccounts: true }).partial({ avatarUrl: true, id: true }), async (input) => { export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true }).partial({ avatarUrl: true, id: true }), async (input) => {
const userID = createID("user") const userID = createID("user")
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake //FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
@@ -159,86 +151,57 @@ export namespace User {
tx tx
.select() .select()
.from(userTable) .from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
.where(and(eq(userTable.email, email), isNull(userTable.timeDeleted))) .where(and(eq(userTable.email, email), isNull(userTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated)) .orderBy(asc(userTable.timeCreated))
.then((rows => serialize(rows).at(0))) .then((rows) => rows.map(serialize))
) .then((rows) => rows.at(0))
),
) )
export const fromID = fn(z.string(), (id) => export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) => useTransaction(async (tx) =>
tx tx
.select() .select()
.from(userTable) .from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID)) .where(and(eq(userTable.id, id), isNull(userTable.timeDeleted)))
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted), isNull(steamTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated)) .orderBy(asc(userTable.timeCreated))
.then((rows) => serialize(rows).at(0)) .then((rows) => rows.map(serialize))
.then((rows) => rows.at(0))
), ),
) )
export const remove = fn(Info.shape.id, (id) => export function serialize(
input: typeof userTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
email: input.email,
avatarUrl: input.avatarUrl,
discriminator: input.discriminator,
polarCustomerID: input.polarCustomerID,
};
}
export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => { useTransaction(async (tx) => {
await tx await tx
.update(userTable) .update(userTable)
.set({ .set({
timeDeleted: sql`now()`, timeDeleted: sql`CURRENT_TIMESTAMP()`,
}) })
.where(and(eq(userTable.id, id))) .where(and(eq(userTable.id, input)))
.execute(); .execute();
return id; return input;
}), }),
); );
/**
* Converts an array of user and Steam account records into structured user objects with associated Steam accounts.
*
* @param input - An array of objects containing user data and optional Steam account data.
* @returns An array of user objects, each including a list of their associated Steam accounts.
*/
export function serialize(
input: { user: typeof userTable.$inferSelect; steam: typeof steamTable.$inferSelect | null }[],
): z.infer<typeof Info>[] {
return pipe(
input,
groupBy((row) => row.user.id),
values(),
map((group) => ({
...group[0].user,
steamAccounts: !group[0].steam ?
[] :
group.map((row) => ({
id: row.steam!.id,
lastSeen: row.steam!.lastSeen,
countryCode: row.steam!.countryCode,
username: row.steam!.username,
steamID: row.steam!.steamID,
lastGame: row.steam!.lastGame,
limitation: row.steam!.limitation,
steamEmail: row.steam!.steamEmail,
userID: row.steam!.userID,
personaName: row.steam!.personaName,
avatarUrl: row.steam!.avatarUrl,
})),
})),
)
}
/**
* Retrieves the list of teams that the current user belongs to.
*
* @returns An array of team information objects representing the user's active team memberships.
*
* @remark Only teams and memberships that have not been deleted are included in the result.
*/
export function teams() { export function teams() {
const actor = assertActor("user"); const actor = assertActor("user");
return useTransaction(async (tx) => return useTransaction((tx) =>
tx tx
.select() .select(getTableColumns(teamTable))
.from(teamTable) .from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id)) .innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where( .where(
and( and(
@@ -248,7 +211,7 @@ export namespace User {
), ),
) )
.execute() .execute()
.then((rows) => Team.serialize(rows)) .then((rows) => rows.map(Team.serialize))
) );
} }
} }

View File

@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { id, timestamps } from "../drizzle/types"; import { id, timestamps } from "../drizzle/types";
import { integer, pgTable, text, uniqueIndex, varchar, json } from "drizzle-orm/pg-core"; import { integer, pgTable, text, uniqueIndex, varchar,json } from "drizzle-orm/pg-core";
// Whether this user is part of the Nestri Team, comes with privileges // Whether this user is part of the Nestri Team, comes with privileges
export const UserFlags = z.object({ export const UserFlags = z.object({
@@ -15,13 +15,13 @@ export const userTable = pgTable(
...id, ...id,
...timestamps, ...timestamps,
avatarUrl: text("avatar_url"), avatarUrl: text("avatar_url"),
email: varchar("email", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
discriminator: integer("discriminator").notNull(), discriminator: integer("discriminator").notNull(),
email: varchar("email", { length: 255 }).notNull(),
polarCustomerID: varchar("polar_customer_id", { length: 255 }).unique(), polarCustomerID: varchar("polar_customer_id", { length: 255 }).unique(),
// flags: json("flags").$type<UserFlags>().default({}), flags: json("flags").$type<UserFlags>().default({}),
}, },
(user) => [ (user) => [
uniqueIndex("user_email").on(user.email), uniqueIndex("user_email").on(user.email),
] ],
); );

View File

@@ -2,26 +2,10 @@ import { ulid } from "ulid";
export const prefixes = { export const prefixes = {
user: "usr", user: "usr",
team: "tem", team: "tea",
task: "tsk", member: "mbr"
machine: "mch",
member: "mbr",
steam: "stm",
subscription: "sub",
invite: "inv",
product: "prd",
} as const; } as const;
/**
* Generates a unique identifier by concatenating a predefined prefix with a ULID.
*
* Given a key from the predefined prefixes mapping (e.g., "user", "team", "member", "steam"),
* this function retrieves the corresponding prefix and combines it with a ULID using an underscore
* as a separator. The resulting identifier is formatted as "prefix_ulid".
*
* @param prefix - A key from the prefixes mapping.
* @returns A unique identifier string.
*/
export function createID(prefix: keyof typeof prefixes): string { export function createID(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], ulid()].join("_"); return [prefixes[prefix], ulid()].join("_");
} }

View File

@@ -1,8 +1,9 @@
{ {
"extends": "@tsconfig/node20/tsconfig.json", "extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"strict": true,
"module": "esnext", "module": "esnext",
"jsx": "react-jsx",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
} }
} }

View File

@@ -1,17 +0,0 @@
FROM mirror.gcr.io/oven/bun:1.2
# TODO: Add a way to build C# Steam.exe and start it to run in the container before the API
ADD ./package.json .
ADD ./bun.lock .
ADD ./packages/core/package.json ./packages/core/package.json
ADD ./packages/functions/package.json ./packages/functions/package.json
ADD ./patches ./patches
RUN bun install --ignore-scripts
ADD ./packages/functions ./packages/functions
ADD ./packages/core ./packages/core
WORKDIR ./packages/functions
CMD ["bun", "run", "./src/api/index.ts"]

View File

@@ -1,13 +1,7 @@
{ {
"name": "@nestri/functions", "name": "@nestri/functions",
"module": "index.ts",
"type": "module", "type": "module",
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"dev:auth": "bun run --watch ./src/auth.ts",
"dev:api": "bun run --watch ./src/api/index.ts"
},
"devDependencies": { "devDependencies": {
"@aws-sdk/client-ecs": "^3.738.0", "@aws-sdk/client-ecs": "^3.738.0",
"@aws-sdk/client-sqs": "^3.734.0", "@aws-sdk/client-sqs": "^3.734.0",
@@ -20,12 +14,9 @@
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@actor-core/bun": "^0.7.9", "@openauthjs/openauth": "0.4.3",
"@openauthjs/openauth": "*",
"actor-core": "^0.7.9",
"hono": "^4.6.15", "hono": "^4.6.15",
"hono-openapi": "^0.3.1", "hono-openapi": "^0.3.1",
"partysocket": "1.0.3", "partysocket": "1.0.3"
"postgres": "^3.4.5"
} }
} }

View File

@@ -1,22 +1,21 @@
import { z } from "zod"; import { z } from "zod";
import { Hono } from "hono"; import { Hono } from "hono";
import { notPublic } from "./auth"; import { notPublic } from "./auth";
import { Result } from "../common";
import { resolver } from "hono-openapi/zod";
import { describeRoute } from "hono-openapi"; import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index"; import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index"; import { Team } from "@nestri/core/team/index";
import { assertActor } from "@nestri/core/actor"; import { assertActor } from "@nestri/core/actor";
import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
export namespace AccountApi { export module AccountApi {
export const route = new Hono() export const route = new Hono()
.use(notPublic) .use(notPublic)
.get("/", .get("/",
describeRoute({ describeRoute({
tags: ["Account"], tags: ["Account"],
summary: "Get user account", summary: "Retrieve the current user's details",
description: "Get the current user's account details", description: "Returns the user's account details, plus the teams they have joined",
responses: { responses: {
200: { 200: {
content: { content: {
@@ -25,34 +24,39 @@ export namespace AccountApi {
z.object({ z.object({
...User.Info.shape, ...User.Info.shape,
teams: Team.Info.array(), teams: Team.Info.array(),
}).openapi({
description: "User account information",
example: { ...Examples.User, teams: [Examples.Team] }
}) })
), ),
}, },
}, },
description: "User account details" description: "Successfully retrieved account details"
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "This account does not exist",
}, },
404: ErrorResponses[404],
429: ErrorResponses[429]
} }
}), }),
async (c) => { async (c) => {
const actor = assertActor("user"); const actor = assertActor("user");
const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()]) const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()])
if (!currentUser) if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404)
throw new VisibleError(
"not_found", const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
return c.json({ return c.json({
data: { data: {
...currentUser, id,
email,
name,
teams, teams,
avatarUrl,
discriminator,
polarCustomerID,
} }
}, 200); }, 200);
}, },

View File

@@ -1,51 +1,47 @@
import { Resource } from "sst"; import { Resource } from "sst";
import { subjects } from "../subjects"; import { subjects } from "../subjects";
import { type MiddlewareHandler } from "hono"; import { type MiddlewareHandler } from "hono";
// import { User } from "@nestri/core/user/index";
import { VisibleError } from "@nestri/core/error";
import { HTTPException } from "hono/http-exception";
import { useActor, withActor } from "@nestri/core/actor"; import { useActor, withActor } from "@nestri/core/actor";
import { createClient } from "@openauthjs/openauth/client"; import { createClient } from "@openauthjs/openauth/client";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
const client = createClient({ const client = createClient({
issuer: Resource.Auth.url, issuer: Resource.Urls.auth,
clientID: "api", clientID: "api",
}); });
export const notPublic: MiddlewareHandler = async (c, next) => { export const notPublic: MiddlewareHandler = async (c, next) => {
const actor = useActor(); const actor = useActor();
if (actor.type === "public") if (actor.type === "public")
throw new VisibleError( throw new HTTPException(401, { message: "Unauthorized" });
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
"Missing authorization header",
);
return next(); return next();
}; };
export const auth: MiddlewareHandler = async (c, next) => { export const auth: MiddlewareHandler = async (c, next) => {
const authHeader = const authHeader =
c.req.query("authorization") ?? c.req.header("authorization"); c.req.query("authorization") ?? c.req.header("authorization");
if (!authHeader) return withActor({ type: "public", properties: {} }, next); if (!authHeader) return next();
const match = authHeader.match(/^Bearer (.+)$/); const match = authHeader.match(/^Bearer (.+)$/);
if (!match) { if (!match) {
throw new VisibleError( throw new VisibleError(
"authentication", "auth.token",
ErrorCodes.Authentication.INVALID_TOKEN, "Bearer token not found or improperly formatted",
"Invalid personal access token",
); );
} }
const bearerToken = match[1]; const bearerToken = match[1];
let result = await client.verify(subjects, bearerToken!); let result = await client.verify(subjects, bearerToken!);
if (result.err) { if (result.err) {
throw new VisibleError( throw new HTTPException(401, {
"authentication", message: "Unauthorized",
ErrorCodes.Authentication.INVALID_TOKEN, });
"Invalid bearer token",
);
} }
if (result.subject.type === "user") { if (result.subject.type === "user") {
const teamID = c.req.header("x-nestri-team"); const teamID = c.req.header("x-nestri-team") //|| c.req.query("teamID");
if (!teamID) return withActor(result.subject, next); if (!teamID) return withActor(result.subject, next);
// const email = result.subject.properties.email;
return withActor( return withActor(
{ {
type: "system", type: "system",
@@ -53,11 +49,21 @@ export const auth: MiddlewareHandler = async (c, next) => {
teamID, teamID,
}, },
}, },
async () => next
withActor( // async () => {
result.subject, // const user = await User.fromEmail(email);
next, // if (!user || user.length === 0) {
) // c.status(401);
// return c.text("Unauthorized");
// }
// return withActor(
// {
// type: "member",
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
// },
// next,
// );
// },
); );
} }
}; };

View File

@@ -1,246 +0,0 @@
import { z, ZodSchema } from "zod";
import {type Hook } from "./types/hook";
import { ErrorCodes, ErrorResponse } from "@nestri/core/error";
import type { MiddlewareHandler, ValidationTargets } from "hono";
import { resolver, validator as zodValidator } from "hono-openapi/zod";
export function Result<T extends z.ZodTypeAny>(schema: T) {
return resolver(
z.object({
data: schema,
}),
);
}
/**
* Custom validator wrapper around hono-openapi/zod validator that formats errors
* according to our standard API error format
*/
export const validator = <
T extends ZodSchema,
Target extends keyof ValidationTargets
>(
target: Target,
schema: T
): MiddlewareHandler<
any,
string,
{
in: {
[K in Target]: z.input<T>;
};
out: {
[K in Target]: z.output<T>;
};
}
> => {
// Create a custom error handler that formats errors according to our standards
// const standardErrorHandler: Parameters<typeof zodValidator>[2] = (
const standardErrorHandler: Hook<z.infer<T>, any, any, Target> = (
result,
c,
) => {
if (!result.success) {
// Get the validation issues
const issues = result.error.issues || result.error.errors || [];
if (issues.length === 0) {
// If there are no issues, return a generic error
return c.json(
{
type: "validation",
code: ErrorCodes.Validation.INVALID_PARAMETER,
message: "Invalid request data",
},
400,
);
}
// Get the first error for the main response
const firstIssue = issues[0]!;
const fieldPath = firstIssue.path
? Array.isArray(firstIssue.path)
? firstIssue.path.join(".")
: firstIssue.path
: undefined;
// Map Zod error codes to our standard error codes
let errorCode = ErrorCodes.Validation.INVALID_PARAMETER;
if (
firstIssue.code === "invalid_type" &&
firstIssue.received === "undefined"
) {
errorCode = ErrorCodes.Validation.MISSING_REQUIRED_FIELD;
} else if (
["invalid_string", "invalid_date", "invalid_regex"].includes(
firstIssue.code,
)
) {
errorCode = ErrorCodes.Validation.INVALID_FORMAT;
}
// Create our standardized error response
const response = {
type: "validation",
code: errorCode,
message: firstIssue.message,
param: fieldPath,
details: undefined as any,
};
// Add details if we have multiple issues
if (issues.length > 0) {
response.details = {
issues: issues.map((issue) => ({
path: issue.path
? Array.isArray(issue.path)
? issue.path.join(".")
: issue.path
: undefined,
code: issue.code,
message: issue.message,
// @ts-expect-error
expected: issue.expected,
// @ts-expect-error
received: issue.received,
})),
};
}
console.log("Validation error in validator:", response);
return c.json(response, 400);
}
};
// Use the original validator with our custom error handler
return zodValidator(target, schema, standardErrorHandler);
};
/**
* Standard error responses for OpenAPI documentation
*/
export const ErrorResponses = {
400: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Validation error",
example: {
type: "validation",
code: "invalid_parameter",
message: "The request was invalid",
param: "email",
},
}),
),
},
},
description:
"Bad Request - The request could not be understood or was missing required parameters.",
},
401: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Authentication error",
example: {
type: "authentication",
code: "unauthorized",
message: "Authentication required",
},
}),
),
},
},
description:
"Unauthorized - Authentication is required and has failed or has not been provided.",
},
403: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Permission error",
example: {
type: "forbidden",
code: "permission_denied",
message: "You do not have permission to access this resource",
},
}),
),
},
},
description:
"Forbidden - You do not have permission to access this resource.",
},
404: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Not found error",
example: {
type: "not_found",
code: "resource_not_found",
message: "The requested resource could not be found",
},
}),
),
},
},
description: "Not Found - The requested resource does not exist.",
},
409: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Conflict Error",
example: {
type: "already_exists",
code: "resource_already_exists",
message: "The resource could not be created because it already exists",
},
}),
),
},
},
description: "Conflict - The resource could not be created because it already exists.",
},
429: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Rate limit error",
example: {
type: "rate_limit",
code: "too_many_requests",
message: "Rate limit exceeded",
},
}),
),
},
},
description:
"Too Many Requests - You have made too many requests in a short period of time.",
},
500: {
content: {
"application/json": {
schema: resolver(
ErrorResponse.openapi({
description: "Server error",
example: {
type: "internal",
code: "internal_error",
message: "Internal server error",
},
}),
),
},
},
description: "Internal Server Error - Something went wrong on our end.",
},
};

View File

@@ -1,23 +1,18 @@
import "zod-openapi/extend"; import "zod-openapi/extend";
import { Hono } from "hono"; import { Hono } from "hono";
import { auth } from "./auth"; import { auth } from "./auth";
import { cors } from "hono/cors"; import { ZodError } from "zod";
import { TeamApi } from "./team";
import { PolarApi } from "./polar";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { Realtime } from "./realtime";
import { AccountApi } from "./account"; import { AccountApi } from "./account";
import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi"; import { openAPISpecs } from "hono-openapi";
import { patchLogger } from "../log-polyfill"; import { VisibleError } from "@nestri/core/error";
import { HTTPException } from "hono/http-exception"; import { HTTPException } from "hono/http-exception";
import { ErrorCodes, VisibleError } from "@nestri/core/error"; import { handle, streamHandle } from "hono/aws-lambda";
export const app = new Hono();
const app = new Hono();
app app
.use(logger()) .use(logger(), async (c, next) => {
.use(cors())
.use(async (c, next) => {
c.header("Cache-Control", "no-store"); c.header("Cache-Control", "no-store");
return next(); return next();
}) })
@@ -25,34 +20,42 @@ app
const routes = app const routes = app
.get("/", (c) => c.text("Hello World!")) .get("/", (c) => c.text("Hello World!"))
.route("/realtime", Realtime.route)
.route("/team", TeamApi.route)
.route("/polar", PolarApi.route)
.route("/account", AccountApi.route) .route("/account", AccountApi.route)
.route("/machine", MachineApi.route)
.onError((error, c) => { .onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) { if (error instanceof VisibleError) {
console.error("api error:", error);
// @ts-expect-error
return c.json(error.toResponse(), error.statusCode());
}
// Handle HTTP exceptions
if (error instanceof HTTPException) {
console.error("http error:", error);
return c.json( return c.json(
{ {
type: "validation", code: error.code,
code: ErrorCodes.Validation.INVALID_PARAMETER, message: error.message,
message: "Invalid request",
}, },
error.status, 400
);
}
if (error instanceof ZodError) {
const e = error.errors[0];
if (e) {
return c.json(
{
code: e?.code,
message: e?.message,
},
400,
);
}
}
if (error instanceof HTTPException) {
return c.json(
{
code: "request",
message: "Invalid request",
},
400,
); );
} }
console.error("unhandled error:", error);
return c.json( return c.json(
{ {
type: "internal", code: "internal",
code: ErrorCodes.Server.INTERNAL_ERROR,
message: "Internal server error", message: "Internal server error",
}, },
500, 500,
@@ -65,8 +68,9 @@ app.get(
documentation: { documentation: {
info: { info: {
title: "Nestri API", title: "Nestri API",
description: "The Nestri API gives you the power to run your own customized cloud gaming platform.", description:
version: "0.0.1", "The Nestri API gives you the power to run your own customized cloud gaming platform.",
version: "0.3.0",
}, },
components: { components: {
securitySchemes: { securitySchemes: {
@@ -77,30 +81,19 @@ app.get(
}, },
TeamID: { TeamID: {
type: "apiKey", type: "apiKey",
description: "The team ID to use for this query", description:"The team ID to use for this query",
in: "header", in: "header",
name: "x-nestri-team" name: "x-nestri-team"
}, },
}, },
}, },
security: [{ Bearer: [], TeamID: [] }], security: [{ Bearer: [], TeamID:[] }],
servers: [ servers: [
{ description: "Production", url: "https://api.nestri.io" }, { description: "Production", url: "https://api.nestri.io" },
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
], ],
}, },
}), }),
); );
patchLogger(); export type Routes = typeof routes;
export const handler = process.env.SST_DEV ? handle(app) : streamHandle(app);
export default {
port: 3001,
idleTimeout: 255,
webSocketHandler: Realtime.webSocketHandler,
fetch: (req: Request) =>
app.fetch(req, undefined, {
waitUntil: (fn) => fn,
passThroughOnException: () => { },
}),
};

View File

@@ -1,292 +0,0 @@
import { z } from "zod"
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Machine } from "@nestri/core/machine/index";
import { Realtime } from "@nestri/core/realtime/index";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { CreateMessageSchema, StartMessageSchema, StopMessageSchema } from "./messages.ts";
export namespace MachineApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Machine"],
summary: "Get all BYOG machines",
description: "All the BYOG machines owned by this user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the user's BYOG machines",
example: [Examples.Machine],
}),
),
},
},
description: "Successfully retrieved all the user's machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const user = assertActor("user");
const machineInfo = await Machine.fromUserID(user.properties.userID);
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.get("/hosted",
describeRoute({
tags: ["Machine"],
summary: "Get all cloud machines",
description: "All the machines that are connected to Nestri",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the machines connected to Nestri",
example: [{ ...Examples.Machine, userID: null }],
}),
),
},
},
description: "Successfully retrieved all the hosted machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const machineInfo = await Machine.list();
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.post("/",
describeRoute({
tags: ["Machine"],
summary: "Send messages to the machine",
description: "Send messages directly to the machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.literal("ok")
),
},
},
description: "Successfully sent the message to Maitred"
},
}
}),
validator(
"json",
z.any()
),
async (c) => {
const actor = assertActor("machine");
console.log("actor.id", actor.properties.machineID)
await Realtime.publish(c.req.valid("json"))
return c.json({
data: "ok"
}, 200);
},
)
.post("/:machineID/create",
describeRoute({
tags: ["Machine"],
summary: "Request to create a container for a specific machine",
description: "Publishes a message to create a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("create request sent"),
})
),
},
},
description: "Create request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish create request",
},
},
}),
validator("json", CreateMessageSchema.shape.payload.optional()), // No payload required for create
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "create" as const,
payload: body || {}, // Empty payload if none provided
};
try {
await Realtime.publish(message, "create");
console.log("Published create request to");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send create request" }, 400);
}
return c.json({
data: {
message: "create request sent",
},
}, 200);
}
)
.post("/:machineID/start",
describeRoute({
tags: ["Machine"],
summary: "Request to start a container for a specific machine",
description: "Publishes a message to start a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("start request sent"),
})
),
},
},
description: "Start request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish start request",
},
},
}),
validator("json", StartMessageSchema.shape.payload), // Use the payload schema
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "start" as const,
payload: {
container_id: body.container_id,
},
};
try {
await Realtime.publish(message, "start");
console.log("Published start request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send start request" }, 400);
}
return c.json({
data: {
message: "start request sent",
},
}, 200);
}
)
.post("/:machineID/stop",
describeRoute({
tags: ["Machine"],
summary: "Request to stop a container for a specific machine",
description: "Publishes a message to stop a container via MQTT for the given machine ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
message: z.literal("stop request sent"),
})
),
},
},
description: "Stop request successfully sent to MQTT",
},
400: {
content: {
"application/json": {
schema: Result(
z.object({ error: z.string() })
),
},
},
description: "Failed to publish start request",
},
},
}),
validator("json", StopMessageSchema.shape.payload), // Use the payload schema
async (c) => {
const actor = assertActor("machine");
const body = c.req.valid("json");
const message = {
type: "stop" as const,
payload: {
container_id: body.container_id,
},
};
try {
await Realtime.publish(message, "stop");
console.log("Published stop request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({ error: "Failed to send stop request" }, 400);
}
return c.json({
data: {
message: "stop request sent",
},
}, 200);
}
)
}

View File

@@ -1,54 +0,0 @@
import { z } from "zod"
// Base message interface
export interface BaseMessage {
type: string; // e.g., "start", "stop", "status"
payload: Record<string, any>; // Generic payload, refined by specific types
}
// Specific message types
export interface StartMessage extends BaseMessage {
type: "start";
payload: {
container_id: string;
[key: string]: any; // Allow additional fields for future expansion
};
}
// Example future message type
export interface StopMessage extends BaseMessage {
type: "stop";
payload: {
container_id: string;
[key: string]: any;
};
}
// Union type for all possible messages (expandable)
export type MachineMessage = StartMessage | StopMessage; // Add more types as needed
// Zod schema for validation
export const BaseMessageSchema = z.object({
type: z.string(),
payload: z.record(z.any()),
});
export const CreateMessageSchema = BaseMessageSchema.extend({
type: z.literal("create"),
});
export const StartMessageSchema = BaseMessageSchema.extend({
type: z.literal("start"),
payload: z.object({
container_id: z.string(),
}).passthrough(),
});
export const StopMessageSchema = BaseMessageSchema.extend({
type: z.literal("stop"),
payload: z.object({
container_id: z.string(),
}).passthrough(),
});
export const MachineMessageSchema = z.union([StartMessageSchema, StopMessageSchema]);

View File

@@ -1,174 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { Resource } from "sst";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { assertActor } from "@nestri/core/actor";
import { Polar } from "@nestri/core/polar/index";
import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result, validator } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
import { WebhookVerificationError, validateEvent } from "@polar-sh/sdk/webhooks";
export namespace PolarApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Polar"],
summary: "Create a Polar.sh customer portal",
description: "Creates Polar.sh's customer portal url where the user can manage their payments",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
portalUrl: z.string()
}).openapi({
description: "The customer portal url",
example: { portalUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "customer portal url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const portalUrl = await Polar.createPortal(user.polarCustomerID)
return c.json({
data: {
portalUrl
}
})
}
)
.post("/checkout",
describeRoute({
tags: ["Polar"],
summary: "Create a checkout url",
description: "Creates a Polar.sh's checkout url for the user to pay a subscription for this team",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
checkoutUrl: z.string()
}).openapi({
description: "The checkout url",
example: { checkoutUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "checkout url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
validator(
"json",
z
.object({
planType: z.enum(PlanType),
successUrl: z.string().url("Success url must be a valid url")
})
.openapi({
description: "Details of the team to create",
example: {
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json");
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const checkoutUrl = await Polar.createCheckout({ customerID: user.polarCustomerID, planType: body.planType, successUrl: body.successUrl })
return c.json({
data: {
checkoutUrl,
}
})
}
)
.post("/webhook",
async (c) => {
const requestBody = await c.req.text();
const webhookSecret = Resource.PolarWebhookSecret.value
const webhookHeaders = {
"webhook-id": c.req.header("webhook-id") ?? "",
"webhook-timestamp": c.req.header("webhook-timestamp") ?? "",
"webhook-signature": c.req.header("webhook-signature") ?? "",
};
let webhookPayload: ReturnType<typeof validateEvent>;
try {
webhookPayload = validateEvent(
requestBody,
webhookHeaders,
webhookSecret,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return c.json({ received: false }, { status: 403 });
}
throw error;
}
await Polar.handleWebhook(webhookPayload)
return c.json({ received: true });
}
)
}

View File

@@ -1,28 +0,0 @@
import { actor } from "actor-core";
// Define a chat room actor
const chatRoom = actor({
// Initialize state when the actor is first created
createState: () => ({
messages: [] as any[],
}),
// Define actions clients can call
actions: {
// Action to send a message
sendMessage: (c, sender, text) => {
// Update state
c.state.messages.push({ sender, text });
// Broadcast to all connected clients
c.broadcast("newMessage", { sender, text });
},
// Action to get chat history
getHistory: (c) => {
return c.state.messages;
}
}
});
export default chatRoom;

View File

@@ -1,15 +0,0 @@
import { setup } from "actor-core";
import chatRoom from "./actor-core";
import { createRouter } from "@actor-core/bun";
export namespace Realtime {
const app = setup({
actors: { chatRoom },
basePath: "/realtime"
});
const realtimeRouter = createRouter(app);
export const route = realtimeRouter.router;
export const webSocketHandler = realtimeRouter.webSocketHandler;
}

View File

@@ -1,46 +0,0 @@
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
export namespace SubscriptionApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Subscription"],
summary: "Get user subscriptions",
description: "Get all user subscriptions",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Subscription.Info.array().openapi({
description: "All the subscriptions this user has",
example: [Examples.Subscription]
})
),
},
},
description: "All user subscriptions"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user")
const subscriptions = await Subscription.fromUserID(actor.properties.userID)
return c.json({
data: subscriptions
})
}
)
}

View File

@@ -1,124 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index";
import { Examples } from "@nestri/core/examples";
import { Polar } from "@nestri/core/polar/index";
import { Member } from "@nestri/core/member/index";
import { assertActor, withActor } from "@nestri/core/actor";
import { ErrorResponses, Result, validator } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
export namespace TeamApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Team"],
summary: "List teams",
description: "List the teams associated with the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Team.Info.array().openapi({
description: "List of teams",
example: [Examples.Team]
})
),
},
},
description: "List of teams"
},
}
}),
async (c) => {
return c.json({
data: await User.teams()
}, 200);
},
)
.post("/",
describeRoute({
tags: ["Team"],
summary: "Create a team",
description: "Create a team for the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
checkoutUrl: z.string().openapi({
description: "The checkout url to confirm subscription for this team",
example: "https://polar.sh/checkout/2903038439320298377"
})
})
)
}
},
description: "Team created succesfully"
},
400: ErrorResponses[400],
409: ErrorResponses[409],
429: ErrorResponses[429],
500: ErrorResponses[500],
}
}),
validator(
"json",
Team.create.schema
.pick({ slug: true, name: true })
.extend({ planType: z.enum(PlanType), successUrl: z.string().url("Success url must be a valid url") })
.openapi({
description: "Details of the team to create",
example: {
slug: Examples.Team.slug,
name: Examples.Team.name,
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json")
const actor = assertActor("user");
const teamID = await Team.create({ name: body.name, slug: body.slug });
await withActor(
{
type: "system",
properties: {
teamID,
},
},
async () => {
await Member.create({
first: true,
email: actor.properties.email,
});
await Subscription.create({
planType: body.planType,
userID: actor.properties.userID,
// FIXME: Make this make sense
tokens: body.planType === "free" ? 100 : body.planType === "pro" ? 1000 : body.planType === "family" ? 10000 : 0,
});
}
);
const checkoutUrl = await Polar.createCheckout({ planType: body.planType, successUrl: body.successUrl, teamID })
return c.json({
data: {
checkoutUrl,
}
})
}
)
}

View File

@@ -1,20 +0,0 @@
import { ZodError, ZodSchema, z } from 'zod';
import type { Env, ValidationTargets, Context, TypedResponse, Input, MiddlewareHandler } from 'hono';
type Hook<T, E extends Env, P extends string, Target extends keyof ValidationTargets = keyof ValidationTargets, O = {}> = (result: ({
success: true;
data: T;
} | {
success: false;
error: ZodError;
data: T;
}) & {
target: Target;
}, c: Context<E, P>) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>;
type HasUndefined<T> = undefined extends T ? true : false;
declare const zValidator: <T extends ZodSchema<any, z.ZodTypeDef, any>, Target extends keyof ValidationTargets, E extends Env, P extends string, In = z.input<T>, Out = z.output<T>, I extends Input = {
in: HasUndefined<In> extends true ? { [K in Target]?: (In extends ValidationTargets[K] ? In : { [K2 in keyof In]?: ValidationTargets[K][K2] | undefined; }) | undefined; } : { [K_1 in Target]: In extends ValidationTargets[K_1] ? In : { [K2_1 in keyof In]: ValidationTargets[K_1][K2_1]; }; };
out: { [K_2 in Target]: Out; };
}, V extends I = I>(target: Target, schema: T, hook?: Hook<z.TypeOf<T>, E, P, Target, {}> | undefined) => MiddlewareHandler<E, P, V>;
export { type Hook, zValidator };

View File

@@ -2,18 +2,16 @@ import { Resource } from "sst"
import { Select } from "./ui/select"; import { Select } from "./ui/select";
import { subjects } from "./subjects" import { subjects } from "./subjects"
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { handle } from "hono/aws-lambda";
import { PasswordUI } from "./ui/password" import { PasswordUI } from "./ui/password"
import { patchLogger } from "./log-polyfill";
import { issuer } from "@openauthjs/openauth"; import { issuer } from "@openauthjs/openauth";
import { User } from "@nestri/core/user/index" import { User } from "@nestri/core/user/index"
import { Email } from "@nestri/core/email/index"; import { Email } from "@nestri/core/email/index";
import { handleDiscord, handleGithub } from "./utils"; import { handleDiscord, handleGithub } from "./utils";
import { GithubAdapter } from "./ui/adapters/github"; import { GithubAdapter } from "./ui/adapters/github";
import { Machine } from "@nestri/core/machine/index"
import { DiscordAdapter } from "./ui/adapters/discord"; import { DiscordAdapter } from "./ui/adapters/discord";
import { PasswordAdapter } from "./ui/adapters/password"; import { PasswordAdapter } from "./ui/adapters/password"
import { type Provider } from "@openauthjs/openauth/provider/provider" import { type Provider } from "@openauthjs/openauth/provider/provider"
import { MemoryStorage } from "@openauthjs/openauth/storage/memory";
type OauthUser = { type OauthUser = {
primary: { primary: {
@@ -24,20 +22,13 @@ type OauthUser = {
avatar: any; avatar: any;
username: any; username: any;
} }
console.log("STORAGE", process.env.STORAGE)
const app = issuer({ const app = issuer({
select: Select({ select: Select({
providers: { providers: {
machine: { device: {
hide: true hide: true,
} },
} },
}),
//TODO: Create our own Storage
storage: MemoryStorage({
persist: process.env.STORAGE //"/tmp/persist.json",
}), }),
theme: { theme: {
title: "Nestri | Auth", title: "Nestri | Auth",
@@ -53,7 +44,9 @@ const app = issuer({
font: { font: {
family: "Geist, sans-serif", family: "Geist, sans-serif",
}, },
css: `@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');`, css: `
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');
`,
}, },
subjects, subjects,
providers: { providers: {
@@ -80,25 +73,29 @@ const app = issuer({
}, },
}), }),
), ),
machine: { device: {
type: "machine", type: "device",
async client(input) { async client(input) {
// FIXME: Do we really need this? if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
// if (input.clientSecret !== Resource.AuthFingerprintKey.value) { throw new Error("Invalid authorization token");
// throw new Error("Invalid authorization token"); }
// } const teamSlug = input.params.team;
if (!teamSlug) {
throw new Error("Team slug is required");
}
const fingerprint = input.params.fingerprint; const hostname = input.params.hostname;
if (!fingerprint) { if (!hostname) {
throw new Error("Hostname is required"); throw new Error("Hostname is required");
} }
return { return {
fingerprint, hostname,
teamSlug
}; };
}, },
init() { } init() { }
} as Provider<{ fingerprint: string; }>, } as Provider<{ teamSlug: string; hostname: string; }>,
}, },
allow: async (input) => { allow: async (input) => {
const url = new URL(input.redirectURI); const url = new URL(input.redirectURI);
@@ -107,47 +104,20 @@ const app = issuer({
if (hostname === "localhost") return true; if (hostname === "localhost") return true;
return false; return false;
}, },
success: async (ctx, value, req) => { success: async (ctx, value) => {
if (value.provider === "machine") { // if (value.provider === "device") {
const countryCode = req.headers.get('CloudFront-Viewer-Country') || 'Unknown' // const team = await Teams.fromSlug(value.teamSlug)
const country = req.headers.get('CloudFront-Viewer-Country-Name') || 'Unknown' // console.log("team", team)
const latitude = Number(req.headers.get('CloudFront-Viewer-Latitude')) || 0 // console.log("teamSlug", value.teamSlug)
const longitude = Number(req.headers.get('CloudFront-Viewer-Longitude')) || 0 // if (team) {
const timezone = req.headers.get('CloudFront-Viewer-Time-Zone') || 'Unknown' // await Instances.create({ hostname: value.hostname, teamID: team.id })
const fingerprint = value.fingerprint
const existing = await Machine.fromFingerprint(fingerprint) // return await ctx.subject("device", {
if (!existing) { // teamSlug: value.teamSlug,
const machineID = await Machine.create({ // hostname: value.hostname,
countryCode, // })
country, // }
fingerprint, // }
timezone,
location: {
latitude,
longitude
},
//FIXME: Make this better
// userID: null
})
return ctx.subject("machine", {
machineID,
fingerprint
});
}
return ctx.subject("machine", {
machineID: existing.id,
fingerprint
});
}
// TODO: This works, so use this while registering the task
// console.log("country_code", req.headers.get('CloudFront-Viewer-Country'))
// console.log("country_name", req.headers.get('CloudFront-Viewer-Country-Name'))
// console.log("latitude", req.headers.get('CloudFront-Viewer-Latitude'))
// console.log("longitude", req.headers.get('CloudFront-Viewer-Longitude'))
// console.log("timezone", req.headers.get('CloudFront-Viewer-Time-Zone'))
if (value.provider === "password") { if (value.provider === "password") {
const email = value.email const email = value.email
@@ -166,17 +136,12 @@ const app = issuer({
return ctx.subject("user", { return ctx.subject("user", {
userID, userID,
email email
}, {
subject: email
}); });
} else if (matching) { } else if (matching) {
//Sign In //Sign In
return ctx.subject("user", { return ctx.subject("user", {
userID: matching.id, userID: matching.id,
email email
}, {
subject: email
}); });
} }
} }
@@ -210,16 +175,12 @@ const app = issuer({
return ctx.subject("user", { return ctx.subject("user", {
userID, userID,
email: user.primary.email email: user.primary.email
}, {
subject: user.primary.email
}); });
} else { } else {
//Sign In //Sign In
return await ctx.subject("user", { return await ctx.subject("user", {
userID: matching.id, userID: matching.id,
email: user.primary.email email: user.primary.email
}, {
subject: user.primary.email
}); });
} }
@@ -233,14 +194,4 @@ const app = issuer({
}, },
}).use(logger()) }).use(logger())
patchLogger(); export const handler = handle(app)
export default {
port: 3002,
idleTimeout: 255,
fetch: (req: Request) =>
app.fetch(req, undefined, {
waitUntil: (fn) => fn,
passThroughOnException: () => { },
}),
};

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
import { resolver } from "hono-openapi/zod";
export function Result<T extends z.ZodTypeAny>(schema: T) {
return resolver(
z.object({
data: schema,
}),
);
}

View File

@@ -0,0 +1,9 @@
export class VisibleError extends Error {
constructor(
public kind: "input" | "auth",
public code: string,
public message: string,
) {
super(message);
}
}

View File

@@ -1,27 +0,0 @@
import { format } from "util";
/**
* Overrides the default Node.js console logging methods with a custom logger.
*
* This function patches console.log, console.warn, console.error, console.trace, and console.debug so that each logs
* messages prefixed with a log level. The messages are formatted using Node.js formatting conventions, with newline
* characters replaced by carriage returns, and are written directly to standard output.
*
* @example
* patchLogger();
* console.info("Server started on port %d", 3000);
*/
export function patchLogger() {
const log =
(level: "INFO" | "WARN" | "TRACE" | "DEBUG" | "ERROR") =>
(msg: string, ...rest: any[]) => {
let line = `${level}\t${format(msg, ...rest)}`;
line = line.replace(/\n/g, "\r");
process.stdout.write(line + "\n");
};
console.log = log("INFO");
console.warn = log("WARN");
console.error = log("ERROR");
console.trace = log("TRACE");
console.debug = log("DEBUG");
}

Some files were not shown because too many files have changed in this diff Show More