mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-13 01:05:37 +02:00
Compare commits
29 Commits
feat/play
...
feat/usage
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09bfbdce03 | ||
|
|
126839e6ae | ||
|
|
47e61599bb | ||
|
|
76d27e4708 | ||
|
|
896832b89c | ||
|
|
492013d610 | ||
|
|
e93099784c | ||
|
|
9a6826b069 | ||
|
|
f408ec56cb | ||
|
|
8394bb4259 | ||
|
|
0305a14fdd | ||
|
|
6b1521d7d4 | ||
|
|
39e187832a | ||
|
|
de80f3e6ab | ||
|
|
6990494b34 | ||
|
|
5a3fdf25ff | ||
|
|
18b14a4261 | ||
|
|
f4aa2ca4a4 | ||
|
|
6092c4e4f8 | ||
|
|
f3d7ea2663 | ||
|
|
7ff4ff8c90 | ||
|
|
7ecc068466 | ||
|
|
633b332700 | ||
|
|
cacdae79c0 | ||
|
|
ca4432bcde | ||
|
|
261a1276f5 | ||
|
|
a45b2bf9b7 | ||
|
|
f62fc1fb4b | ||
|
|
957eca7794 |
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,28 +1,2 @@
|
|||||||
## 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 -->
|
|
||||||
40
.github/workflows/docs.yml
vendored
Normal file
40
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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 }}
|
||||||
6
.github/workflows/runner.yml
vendored
6
.github/workflows/runner.yml
vendored
@@ -27,6 +27,7 @@ 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:
|
||||||
@@ -55,7 +56,7 @@ jobs:
|
|||||||
swap-size-gb: 20
|
swap-size-gb: 20
|
||||||
-
|
-
|
||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: containers/runner.Containerfile
|
file: containers/runner.Containerfile
|
||||||
context: ./
|
context: ./
|
||||||
@@ -107,7 +108,7 @@ jobs:
|
|||||||
swap-size-gb: 20
|
swap-size-gb: 20
|
||||||
-
|
-
|
||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: containers/runner.Containerfile
|
file: containers/runner.Containerfile
|
||||||
context: ./
|
context: ./
|
||||||
@@ -116,3 +117,4 @@ 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
|
||||||
|
|||||||
40
.github/workflows/www.yml
vendored
Normal file
40
.github/workflows/www.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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 }}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
3843
apps/docs/package-lock.json
generated
3843
apps/docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,19 +4,19 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"nestri.dev": "nuxi dev",
|
"nestri.dev": "nuxi dev",
|
||||||
"build": "nuxi build",
|
"build": "nuxi build --preset=cloudflare_pages",
|
||||||
"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": "^1.4.1",
|
"@nuxt/devtools": "^2.3.2",
|
||||||
"@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.15.4"
|
"nuxt": "^3.16.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "^0.2.6",
|
"@openauthjs/openauth": "*",
|
||||||
"@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": "5.4.12",
|
"vite": "6.0.15",
|
||||||
"vite-tsconfig-paths": "^4.2.1",
|
"vite-tsconfig-paths": "^4.2.1",
|
||||||
"wrangler": "^3.0.0"
|
"wrangler": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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="/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" >
|
<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" >
|
||||||
Get early access
|
Join our Discord
|
||||||
</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="/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" >
|
<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" >
|
||||||
Get early access
|
Join our Discord
|
||||||
</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">
|
||||||
|
|||||||
@@ -521,8 +521,8 @@ export default component$(() => {
|
|||||||
</MotionComponent>
|
</MotionComponent>
|
||||||
<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="/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" >
|
<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" >
|
||||||
Get early access
|
Join our Discord
|
||||||
</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">
|
||||||
|
|||||||
12
containers/maitred.Containerfile
Normal file
12
containers/maitred.Containerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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"]
|
||||||
@@ -13,6 +13,7 @@ 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"
|
||||||
@@ -23,6 +24,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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 with proper directory structure
|
# Clone repository
|
||||||
RUN git clone -b dev-dmabuf https://github.com/games-on-whales/gst-wayland-display.git
|
RUN git clone -b dev-dmabuf https://github.com/DatCaptainHorse/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 \
|
steam steam-native-runtime gtk3 lib32-gtk3 \
|
||||||
sudo xorg-xwayland seatd libinput labwc wlr-randr mangohud \
|
sudo xorg-xwayland seatd libinput labwc wlr-randr gamescope 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,6 +144,9 @@ 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}/*
|
||||||
@@ -185,6 +188,30 @@ 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/
|
||||||
|
|||||||
105
infra/api.ts
105
infra/api.ts
@@ -1,82 +1,63 @@
|
|||||||
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 { database } from "./database";
|
import { cluster } from "./cluster";
|
||||||
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
sst.Linkable.wrap(random.RandomString, (resource) => ({
|
export const api = new sst.aws.Service("Api", {
|
||||||
properties: {
|
cluster,
|
||||||
value: resource.result,
|
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
|
||||||
|
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: [
|
|
||||||
{
|
{
|
||||||
actions: ["ses:SendEmail"],
|
listen: "80/http",
|
||||||
resources: ["*"],
|
forward: "3001/http",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
domain: {
|
dev: {
|
||||||
name: "auth." + domain,
|
command: "bun dev:api",
|
||||||
dns: sst.cloudflare.dns(),
|
directory: "packages/functions",
|
||||||
|
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 api = new sst.aws.Router("Api", {
|
export const apiRoute = new sst.aws.Router("ApiRoute", {
|
||||||
routes: {
|
routes: {
|
||||||
"/*": apiFunction.url
|
// I think api.url should work all the same
|
||||||
|
"/*": 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,
|
|
||||||
};
|
|
||||||
66
infra/auth.ts
Normal file
66
infra/auth.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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(),
|
||||||
|
},
|
||||||
|
})
|
||||||
13
infra/bus.ts
13
infra/bus.ts
@@ -1,15 +1,18 @@
|
|||||||
import { email } from "./email";
|
import { vpc } from "./vpc";
|
||||||
|
// import { email } from "./email";
|
||||||
import { allSecrets } from "./secret";
|
import { allSecrets } from "./secret";
|
||||||
import { database } from "./database";
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
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: [
|
||||||
database,
|
// email,
|
||||||
email,
|
postgres,
|
||||||
...allSecrets],
|
...allSecrets
|
||||||
|
],
|
||||||
timeout: "5 minutes",
|
timeout: "5 minutes",
|
||||||
permissions: [
|
permissions: [
|
||||||
{
|
{
|
||||||
|
|||||||
6
infra/cluster.ts
Normal file
6
infra/cluster.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { vpc } from "./vpc";
|
||||||
|
|
||||||
|
export const cluster = new sst.aws.Cluster("Cluster", {
|
||||||
|
vpc,
|
||||||
|
forceUpgrade: "v2"
|
||||||
|
});
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
//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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
|
|
||||||
export const email = new sst.aws.Email("Mail",{
|
export const email = new sst.aws.Email("Email",{
|
||||||
sender: domain,
|
sender: domain,
|
||||||
dns: sst.cloudflare.dns(),
|
dns: sst.cloudflare.dns(),
|
||||||
})
|
})
|
||||||
68
infra/postgres.ts
Normal file
68
infra/postgres.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
9
infra/realtime.ts
Normal file
9
infra/realtime.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
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);
|
||||||
2
infra/stage.ts
Normal file
2
infra/stage.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const isPermanentStage =
|
||||||
|
$app.stage === "production" || $app.stage === "dev";
|
||||||
7
infra/steam.ts
Normal file
7
infra/steam.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
new sst.x.DevCommand("Steam", {
|
||||||
|
dev: {
|
||||||
|
command: "bun dev",
|
||||||
|
directory: "packages/steam",
|
||||||
|
autostart: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
1
infra/storage.ts
Normal file
1
infra/storage.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const storage = new sst.aws.Bucket("Storage");
|
||||||
11
infra/vpc.ts
Normal file
11
infra/vpc.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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");
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// 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",
|
||||||
@@ -14,7 +16,8 @@ new sst.aws.StaticSite("Web", {
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
VITE_API_URL: api.url,
|
VITE_API_URL: api.url,
|
||||||
VITE_AUTH_URL: auth.url,
|
|
||||||
VITE_STAGE: $app.stage,
|
VITE_STAGE: $app.stage,
|
||||||
|
VITE_AUTH_URL: auth.url,
|
||||||
|
VITE_ZERO_URL: zero.url,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
196
infra/zero.ts
Normal file
196
infra/zero.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
29
nestri.sln
Normal file
29
nestri.sln
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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
|
||||||
12
package.json
12
package.json
@@ -16,9 +16,19 @@
|
|||||||
"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": [
|
||||||
@@ -26,6 +36,6 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sst": "3.9.1"
|
"sst": "^3.11.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
function addPoolerSuffix(original: string): string {
|
const connection = {
|
||||||
const firstDotIndex = original.indexOf('.');
|
user: Resource.Database.username,
|
||||||
if (firstDotIndex === -1) return original + '-pooler';
|
password: Resource.Database.password,
|
||||||
return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
|
host: Resource.Database.host,
|
||||||
}
|
};
|
||||||
|
|
||||||
const dbHost = addPoolerSuffix(Resource.Database.host)
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/**/*.sql.ts",
|
verbose: true,
|
||||||
|
strict: true,
|
||||||
out: "./migrations",
|
out: "./migrations",
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
verbose: true,
|
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: `postgresql://${Resource.Database.user}:${Resource.Database.password}@${dbHost}/${Resource.Database.name}?sslmode=require`,
|
url: `postgres://${connection.user}:${connection.password}@${connection.host}/nestri`,
|
||||||
},
|
},
|
||||||
|
schema: "./src/**/*.sql.ts",
|
||||||
});
|
});
|
||||||
@@ -14,8 +14,9 @@ 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,
|
||||||
"name" varchar(255) NOT NULL
|
"plan_type" text NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "user" (
|
CREATE TABLE "user" (
|
||||||
@@ -24,14 +25,15 @@ 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,
|
||||||
"polar_customer_id" varchar(255) NOT NULL,
|
"email" 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 UNIQUE INDEX "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint
|
|
||||||
CREATE INDEX "email_global" ON "member" USING btree ("email");--> 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 "member_email" ON "member" USING btree ("team_id","email");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "team_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");
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user" ALTER COLUMN "polar_customer_id" DROP NOT NULL;
|
|
||||||
2
packages/core/migrations/0001_nifty_sauron.sql
Normal file
2
packages/core/migrations/0001_nifty_sauron.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX "team_slug";--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "slug" ON "team" USING btree ("slug");
|
||||||
17
packages/core/migrations/0002_simple_outlaw_kid.sql
Normal file
17
packages/core/migrations/0002_simple_outlaw_kid.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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");
|
||||||
13
packages/core/migrations/0002_tiny_toad_men.sql
Normal file
13
packages/core/migrations/0002_tiny_toad_men.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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");
|
||||||
22
packages/core/migrations/0003_first_big_bertha.sql
Normal file
22
packages/core/migrations/0003_first_big_bertha.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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";
|
||||||
8
packages/core/migrations/0004_amused_mattie_franklin.sql
Normal file
8
packages/core/migrations/0004_amused_mattie_franklin.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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";
|
||||||
2
packages/core/migrations/0005_aspiring_stature.sql
Normal file
2
packages/core/migrations/0005_aspiring_stature.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "machine" DROP CONSTRAINT "machine_user_id_id_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "machine" DROP COLUMN "user_id";
|
||||||
2
packages/core/migrations/0006_worthless_dreadnoughts.sql
Normal file
2
packages/core/migrations/0006_worthless_dreadnoughts.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "team" DROP COLUMN "plan_type";
|
||||||
15
packages/core/migrations/0007_warm_secret_warriors.sql
Normal file
15
packages/core/migrations/0007_warm_secret_warriors.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
3
packages/core/migrations/0008_third_mindworm.sql
Normal file
3
packages/core/migrations/0008_third_mindworm.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
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");
|
||||||
2
packages/core/migrations/0009_luxuriant_wraith.sql
Normal file
2
packages/core/migrations/0009_luxuriant_wraith.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE UNIQUE INDEX "steam_id" ON "steam" USING btree ("steam_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "steam_user_id" ON "steam" USING btree ("user_id");
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
|
"id": "f09034df-208a-42b3-b61f-f842921c6e24",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -54,6 +54,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": [
|
||||||
@@ -74,21 +89,6 @@
|
|||||||
"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,22 +136,28 @@
|
|||||||
"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
|
||||||
},
|
},
|
||||||
"name": {
|
"plan_type": {
|
||||||
"name": "name",
|
"name": "plan_type",
|
||||||
"type": "varchar(255)",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"slug": {
|
"team_slug": {
|
||||||
"name": "slug",
|
"name": "team_slug",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "slug",
|
"expression": "slug",
|
||||||
@@ -209,12 +215,6 @@
|
|||||||
"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,11 +227,24 @@
|
|||||||
"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": true
|
"notNull": false
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"name": "flags",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'{}'::json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "c09359df-19fe-4246-9a41-43b3a429c12f",
|
"id": "6f428226-b5d8-4182-a676-d04f842f9ded",
|
||||||
"prevId": "08ba0262-ce0a-4d87-b4e2-0d17dc0ee28c",
|
"prevId": "f09034df-208a-42b3-b61f-f842921c6e24",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -54,6 +54,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": [
|
||||||
@@ -74,21 +89,6 @@
|
|||||||
"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,15 +136,21 @@
|
|||||||
"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
|
||||||
},
|
},
|
||||||
"name": {
|
"plan_type": {
|
||||||
"name": "name",
|
"name": "plan_type",
|
||||||
"type": "varchar(255)",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
@@ -209,12 +215,6 @@
|
|||||||
"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,11 +227,24 @@
|
|||||||
"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": {
|
||||||
|
|||||||
420
packages/core/migrations/meta/0002_snapshot.json
Normal file
420
packages/core/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
507
packages/core/migrations/meta/0003_snapshot.json
Normal file
507
packages/core/migrations/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
499
packages/core/migrations/meta/0004_snapshot.json
Normal file
499
packages/core/migrations/meta/0004_snapshot.json
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
485
packages/core/migrations/meta/0005_snapshot.json
Normal file
485
packages/core/migrations/meta/0005_snapshot.json
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
485
packages/core/migrations/meta/0006_snapshot.json
Normal file
485
packages/core/migrations/meta/0006_snapshot.json
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
580
packages/core/migrations/meta/0007_snapshot.json
Normal file
580
packages/core/migrations/meta/0007_snapshot.json
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
619
packages/core/migrations/meta/0008_snapshot.json
Normal file
619
packages/core/migrations/meta/0008_snapshot.json
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
650
packages/core/migrations/meta/0009_snapshot.json
Normal file
650
packages/core/migrations/meta/0009_snapshot.json
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,15 +5,71 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1740345380808,
|
"when": 1741759978256,
|
||||||
"tag": "0000_wise_black_widow",
|
"tag": "0000_flaky_matthew_murdock",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1740487217291,
|
"when": 1741955636085,
|
||||||
"tag": "0001_flaky_tomorrow_man",
|
"tag": "0001_nifty_sauron",
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
"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:push": "sst shell drizzle-kit push",
|
"db:exec": "sst shell ../scripts/src/psql.sh",
|
||||||
"db:migrate": "sst shell drizzle-kit migrate",
|
"db:reset": "sst shell ../scripts/src/db-reset.sh"
|
||||||
"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"
|
||||||
@@ -18,23 +17,25 @@
|
|||||||
"@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.19.0",
|
"remeda": "^2.21.2",
|
||||||
"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",
|
||||||
"@neondatabase/serverless": "^0.10.4",
|
"@openauthjs/openauth": "*",
|
||||||
"@openauthjs/openauth": "0.4.3",
|
|
||||||
"@openauthjs/openevent": "^0.0.27",
|
"@openauthjs/openevent": "^0.0.27",
|
||||||
"@polar-sh/sdk": "^0.26.1",
|
"@polar-sh/ingestion": "^0.2.2",
|
||||||
"drizzle-orm": "^0.39.3",
|
"@polar-sh/sdk": "^0.32.10",
|
||||||
"ws": "^8.18.1"
|
"drizzle-kit": "^0.30.5",
|
||||||
|
"drizzle-orm": "^0.40.0",
|
||||||
|
"postgres": "^3.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { eq } from "./drizzle";
|
import { eq } from "./drizzle";
|
||||||
import { VisibleError } from "./error";
|
import { ErrorCodes, 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,24 +37,65 @@ 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>;
|
||||||
|
|
||||||
const ActorContext = createContext<Actor>("actor");
|
export 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(
|
||||||
"unauthorized",
|
"authentication",
|
||||||
|
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`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,25 +109,34 @@ 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 Error(`Expected actor to have teamID`);
|
throw new VisibleError(
|
||||||
}
|
"authentication",
|
||||||
|
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||||
export async function assertUserFlag(flag: keyof UserFlags) {
|
`Expected actor to have teamID`
|
||||||
return useTransaction((tx) =>
|
);
|
||||||
tx
|
}
|
||||||
.select({ flags: userTable.flags })
|
|
||||||
.from(userTable)
|
/**
|
||||||
.where(eq(userTable.id, useUserID()))
|
* Returns the fingerprint of the current actor if the actor has a machine identity.
|
||||||
.then((rows) => {
|
*
|
||||||
const flags = rows[0]?.flags;
|
* @returns The fingerprint of the current machine actor.
|
||||||
if (!flags)
|
* @throws {VisibleError} If the current actor does not have a machine identity.
|
||||||
throw new VisibleError(
|
*/
|
||||||
"user.flags",
|
export function useMachine() {
|
||||||
"Actor does not have " + flag + " flag",
|
const actor = useActor();
|
||||||
);
|
if ("machineID" in actor.properties) return actor.properties.fingerprint;
|
||||||
}),
|
throw new VisibleError(
|
||||||
|
"authentication",
|
||||||
|
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||||
|
`Expected actor to have fingerprint`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
15
packages/core/src/billing/billing.sql.ts
Normal file
15
packages/core/src/billing/billing.sql.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { bigint, pgEnum, pgTable } from "drizzle-orm/pg-core";
|
||||||
|
import { teamID, timestamps } from "../drizzle/types";
|
||||||
|
|
||||||
|
export const CreditsType = ["gpu", "bandwidth", "storage"] as const;
|
||||||
|
export const creditsEnum = pgEnum('credits_type', CreditsType);
|
||||||
|
|
||||||
|
export const usage = pgTable(
|
||||||
|
"usage",
|
||||||
|
{
|
||||||
|
...teamID,
|
||||||
|
...timestamps,
|
||||||
|
type: creditsEnum("type").notNull(),
|
||||||
|
creditsUsed: bigint("credits_used", { mode: "number" }).notNull(),
|
||||||
|
}
|
||||||
|
)
|
||||||
27
packages/core/src/billing/index.ts
Normal file
27
packages/core/src/billing/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Common } from "../common";
|
||||||
|
import { Examples } from "../examples";
|
||||||
|
import { CreditsType } from "./billing.sql";
|
||||||
|
|
||||||
|
export namespace Billing {
|
||||||
|
export const Info = z.object({
|
||||||
|
id: z.string().openapi({
|
||||||
|
description: Common.IdDescription,
|
||||||
|
example: Examples.Usage.id,
|
||||||
|
}),
|
||||||
|
creditsUsed: z.number().openapi({
|
||||||
|
description: "The credits used",
|
||||||
|
example: Examples.Usage.creditsUsed
|
||||||
|
}),
|
||||||
|
type: z.enum(CreditsType).openapi({
|
||||||
|
description: "The type of credits this was billed on"
|
||||||
|
}),
|
||||||
|
// game:
|
||||||
|
// session:
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "Billing",
|
||||||
|
description: "Represents a usage billing",
|
||||||
|
example: Examples.Usage,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import "zod-openapi/extend";
|
import "zod-openapi/extend";
|
||||||
|
|
||||||
export module Common {
|
export namespace 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'`;
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,17 @@
|
|||||||
export * from "drizzle-orm";
|
export * from "drizzle-orm";
|
||||||
import ws from 'ws';
|
|
||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { drizzle as neonDrizzle, NeonDatabase } from "drizzle-orm/neon-serverless";
|
import postgres from "postgres";
|
||||||
// import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { Pool, neonConfig } from "@neondatabase/serverless";
|
|
||||||
|
|
||||||
neonConfig.webSocketConstructor = ws;
|
const client = postgres({
|
||||||
|
idle_timeout: 30000,
|
||||||
function addPoolerSuffix(original: string): string {
|
connect_timeout: 30000,
|
||||||
const firstDotIndex = original.indexOf('.');
|
host: Resource.Database.host,
|
||||||
if (firstDotIndex === -1) return original + '-pooler';
|
database: Resource.Database.database,
|
||||||
return original.slice(0, firstDotIndex) + '-pooler' + original.slice(firstDotIndex);
|
user: Resource.Database.username,
|
||||||
}
|
password: Resource.Database.password,
|
||||||
|
port: Resource.Database.port,
|
||||||
const dbHost = addPoolerSuffix(Resource.Database.host)
|
max: parseInt(process.env.POSTGRES_POOL_MAX || "1"),
|
||||||
|
|
||||||
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, {});
|
||||||
@@ -4,14 +4,13 @@ import {
|
|||||||
PgTransactionConfig
|
PgTransactionConfig
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import {
|
import {
|
||||||
NeonQueryResultHKT
|
PostgresJsQueryResultHKT
|
||||||
// NeonHttpQueryResultHKT
|
} from "drizzle-orm/postgres-js";
|
||||||
} 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<
|
||||||
NeonQueryResultHKT,
|
PostgresJsQueryResultHKT,
|
||||||
Record<string, never>,
|
Record<string, never>,
|
||||||
ExtractTablesWithRelations<Record<string, never>>
|
ExtractTablesWithRelations<Record<string, never>>
|
||||||
>;
|
>;
|
||||||
@@ -59,7 +58,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
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 });
|
||||||
|
|
||||||
@@ -17,6 +18,15 @@ 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,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export namespace Email {
|
|||||||
subject: string,
|
subject: string,
|
||||||
body: string,
|
body: string,
|
||||||
) {
|
) {
|
||||||
from = from + "@" + Resource.Mail.sender;
|
from = from + "@" + Resource.Email.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({
|
||||||
|
|||||||
@@ -1,8 +1,145 @@
|
|||||||
|
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 code: string,
|
public type: ErrorResponseType["type"],
|
||||||
public message: string,
|
public code: string,
|
||||||
) {
|
public message: string,
|
||||||
super(message);
|
public param?: string,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
import { prefixes } from "./utils";
|
import { prefixes } from "./utils";
|
||||||
export module Examples {
|
export namespace 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",
|
||||||
@@ -10,24 +31,67 @@ export module 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 Team = {
|
export const game = {
|
||||||
id: Id("team"),
|
id: Id("game")
|
||||||
name: "John Does' Team",
|
}
|
||||||
slug: "john_doe",
|
|
||||||
|
export const session = {
|
||||||
|
id: Id("session")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Usage = {
|
||||||
|
id: Id("usage"),
|
||||||
|
creditsUsed: 20,
|
||||||
|
type: "gpu" as const, //or bandwidth, storage
|
||||||
|
game: [game],
|
||||||
|
session: [session]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Product = {
|
||||||
|
id: Id("product"),
|
||||||
|
name: "RTX 4090",
|
||||||
|
description: "Ideal for dedicated gamers who crave more flexibility and social gaming experiences.",
|
||||||
|
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 Polar = {
|
export const Team = {
|
||||||
teamID: Id("team"),
|
id: Id("team"),
|
||||||
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
name: "John Does' Team",
|
||||||
|
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",
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
155
packages/core/src/machine/index.ts
Normal file
155
packages/core/src/machine/index.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/core/src/machine/machine.sql.ts
Normal file
40
packages/core/src/machine/machine.sql.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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], }),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -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 } from "./member.sql";
|
import { memberTable, role } 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 module Member {
|
export namespace 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().or(z.null()).openapi({
|
timeSeen: z.date().nullable().or(z.undefined()).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,6 +25,10 @@ export module 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
|
||||||
@@ -66,15 +70,12 @@ export module 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,
|
||||||
email: input.email,
|
|
||||||
teamID: useTeam(),
|
teamID: useTeam(),
|
||||||
timeSeen: input.first ? sql`CURRENT_TIMESTAMP()` : null,
|
email: input.email,
|
||||||
}).onConflictDoUpdate({
|
role: input.first ? "owner" : "member",
|
||||||
target: memberTable.id,
|
timeSeen: input.first ? sql`now()` : null,
|
||||||
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 }),
|
||||||
);
|
);
|
||||||
@@ -82,16 +83,16 @@ export module Member {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const remove = fn(Info.shape.id, (input) =>
|
export const remove = fn(Info.shape.id, (id) =>
|
||||||
useTransaction(async (tx) => {
|
useTransaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
.update(memberTable)
|
.update(memberTable)
|
||||||
.set({
|
.set({
|
||||||
timeDeleted: sql`CURRENT_TIMESTAMP()`,
|
timeDeleted: sql`now()`,
|
||||||
})
|
})
|
||||||
.where(and(eq(memberTable.id, input), eq(memberTable.teamID, useTeam())))
|
.where(and(eq(memberTable.id, id), eq(memberTable.teamID, useTeam())))
|
||||||
.execute();
|
.execute();
|
||||||
return input;
|
return id;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,9 +103,8 @@ export module 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))
|
.then((rows) => rows.map(serialize).at(0))
|
||||||
.then((rows) => rows.at(0))
|
)
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const fromID = fn(z.string(), async (id) =>
|
export const fromID = fn(z.string(), async (id) =>
|
||||||
@@ -114,16 +114,22 @@ export module 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))
|
.then((rows) => rows.map(serialize).at(0))
|
||||||
.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
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
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, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
import { index, pgEnum, pgTable, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const role = ["admin", "member", "owner"] as const;
|
||||||
|
const pgRole = pgEnum("role", role)
|
||||||
|
|
||||||
export const memberTable = pgTable(
|
export const memberTable = pgTable(
|
||||||
"member",
|
"member",
|
||||||
{
|
{
|
||||||
...teamID,
|
...teamID,
|
||||||
...timestamps,
|
...timestamps,
|
||||||
|
role: pgRole().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),
|
||||||
uniqueIndex("member_email").on(table.teamID, table.email),
|
|
||||||
index("email_global").on(table.email),
|
index("email_global").on(table.email),
|
||||||
|
uniqueIndex("member_email").on(table.teamID, table.email),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1,69 +1,16 @@
|
|||||||
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 { eq, and } from "../drizzle";
|
import { useTeam, useUserID } from "../actor";
|
||||||
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 { useTransaction } from "../drizzle/transaction";
|
import { validateEvent } from "@polar-sh/sdk/webhooks";
|
||||||
|
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 module Polar {
|
export namespace 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 })
|
||||||
@@ -81,89 +28,69 @@ export module Polar {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
|
const getProductIDs = (plan: z.infer<typeof planType>) => {
|
||||||
useTransaction(async (tx) =>
|
switch (plan) {
|
||||||
tx
|
case "free":
|
||||||
.insert(polarTable)
|
return [Resource.NestriFreeMonthly.value]
|
||||||
.values({
|
case "pro":
|
||||||
teamID: useTeam(),
|
return [Resource.NestriProYearly.value, Resource.NestriProMonthly.value]
|
||||||
customerID,
|
case "family":
|
||||||
standing: "new",
|
return [Resource.NestriFamilyYearly.value, Resource.NestriFamilyMonthly.value]
|
||||||
})
|
default:
|
||||||
.execute(),
|
return [Resource.NestriFreeMonthly.value]
|
||||||
),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return checkoutUrl.url
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
24
packages/core/src/realtime/index.ts
Normal file
24
packages/core/src/realtime/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
packages/core/src/steam/index.ts
Normal file
137
packages/core/src/steam/index.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
packages/core/src/steam/steam.sql.ts
Normal file
45
packages/core/src/steam/steam.sql.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
192
packages/core/src/subscription/index.ts
Normal file
192
packages/core/src/subscription/index.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
31
packages/core/src/subscription/subscription.sql.ts
Normal file
31
packages/core/src/subscription/subscription.sql.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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]
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
20
packages/core/src/task/task.sql.todo
Normal file
20
packages/core/src/task/task.sql.todo
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -1,32 +1,43 @@
|
|||||||
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 { createID, fn } from "../utils";
|
import { Member } from "../member";
|
||||||
import { Examples } from "../examples";
|
|
||||||
import { teamTable } from "./team.sql";
|
import { teamTable } from "./team.sql";
|
||||||
|
import { Examples } from "../examples";
|
||||||
|
import { assertActor } from "../actor";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { assertActor, withActor } from "../actor";
|
import { createID, fn } from "../utils";
|
||||||
import { and, eq, sql } from "../drizzle";
|
import { Subscription } from "../subscription";
|
||||||
|
import { and, eq, sql, isNull } from "../drizzle";
|
||||||
import { memberTable } from "../member/member.sql";
|
import { memberTable } from "../member/member.sql";
|
||||||
import { HTTPException } from 'hono/http-exception';
|
import { ErrorCodes, VisibleError } from "../error";
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { groupBy, map, pipe, values } from "remeda";
|
||||||
|
import { subscriptionTable } from "../subscription/subscription.sql";
|
||||||
|
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
export module Team {
|
export namespace 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,
|
||||||
}),
|
}),
|
||||||
slug: z.string().openapi({
|
// Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
|
||||||
|
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",
|
||||||
@@ -45,41 +56,36 @@ export module Team {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TeamExistsError extends HTTPException {
|
export class TeamExistsError extends VisibleError {
|
||||||
constructor(slug: string) {
|
constructor(slug: string) {
|
||||||
super(
|
super(
|
||||||
400,
|
"already_exists",
|
||||||
{ message: `There is already a team named "${slug}"`, }
|
ErrorCodes.Validation.TEAM_ALREADY_EXISTS,
|
||||||
|
`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");
|
||||||
@@ -106,48 +112,107 @@ export module Team {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const list = fn(z.void(), () =>
|
export const list = fn(z.void(), () => {
|
||||||
useTransaction((tx) =>
|
const actor = assertActor("user");
|
||||||
|
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) => rows.map(serialize)),
|
.then((rows) => serialize(rows))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fromID = fn(z.string().min(1), async (id) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(teamTable)
|
||||||
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(teamTable.id, id),
|
||||||
|
isNull(memberTable.timeDeleted),
|
||||||
|
isNull(teamTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
.then((rows) => serialize(rows).at(0))
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fromID = fn(z.string().min(1), async (id) =>
|
export const fromSlug = fn(z.string().min(1), async (slug) =>
|
||||||
useTransaction(async (tx) => {
|
useTransaction(async (tx) =>
|
||||||
return tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(teamTable)
|
.from(teamTable)
|
||||||
.where(eq(teamTable.id, id))
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(teamTable.slug, slug),
|
||||||
|
isNull(memberTable.timeDeleted),
|
||||||
|
isNull(teamTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
.execute()
|
.execute()
|
||||||
.then((rows) => rows.map(serialize))
|
.then((rows) => serialize(rows).at(0))
|
||||||
.then((rows) => rows.at(0));
|
),
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const fromSlug = fn(z.string().min(1), async (input) =>
|
|
||||||
useTransaction(async (tx) => {
|
|
||||||
return tx
|
|
||||||
.select()
|
|
||||||
.from(teamTable)
|
|
||||||
.where(eq(teamTable.slug, input))
|
|
||||||
.execute()
|
|
||||||
.then((rows) => rows.map(serialize))
|
|
||||||
.then((rows) => rows.at(0));
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: typeof teamTable.$inferSelect,
|
input: { team: typeof teamTable.$inferSelect, subscription: typeof subscriptionTable.$inferInsert | null, member: typeof memberTable.$inferInsert | null }[],
|
||||||
): z.infer<typeof Info> {
|
): z.infer<typeof Info>[] {
|
||||||
return {
|
console.log("serialize", input)
|
||||||
id: input.id,
|
return pipe(
|
||||||
name: input.name,
|
input,
|
||||||
slug: input.slug,
|
groupBy((row) => row.team.id),
|
||||||
};
|
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,
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
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(
|
||||||
@@ -12,10 +11,12 @@ export const teamTable = pgTable(
|
|||||||
{
|
{
|
||||||
...id,
|
...id,
|
||||||
...timestamps,
|
...timestamps,
|
||||||
slug: varchar("slug", { length: 255 }).notNull(),
|
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
slug: varchar("slug", { length: 255 }).notNull(),
|
||||||
},
|
},
|
||||||
(table) => [uniqueIndex("slug").on(table.slug)],
|
(table) => [
|
||||||
|
uniqueIndex("slug").on(table.slug)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export function teamIndexes(table: any) {
|
export function teamIndexes(table: any) {
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
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 { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
|
import { pipe, groupBy, values, map } from "remeda";
|
||||||
|
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 module User {
|
export namespace User {
|
||||||
const MAX_ATTEMPTS = 50;
|
const MAX_ATTEMPTS = 50;
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -44,6 +48,10 @@ export module 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",
|
||||||
@@ -102,7 +110,7 @@ export module User {
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
|
||||||
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true }).partial({ avatarUrl: true, id: true }), async (input) => {
|
export const create = fn(Info.omit({ polarCustomerID: true, discriminator: true, steamAccounts: 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
|
||||||
@@ -151,57 +159,86 @@ export module 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) => rows.map(serialize))
|
.then((rows => serialize(rows).at(0)))
|
||||||
.then((rows) => rows.at(0))
|
)
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const fromID = fn(z.string(), async (id) =>
|
export const fromID = fn(z.string(), (id) =>
|
||||||
useTransaction(async (tx) =>
|
useTransaction(async (tx) =>
|
||||||
tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(userTable)
|
.from(userTable)
|
||||||
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted)))
|
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
|
||||||
|
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted), isNull(steamTable.timeDeleted)))
|
||||||
.orderBy(asc(userTable.timeCreated))
|
.orderBy(asc(userTable.timeCreated))
|
||||||
.then((rows) => rows.map(serialize))
|
.then((rows) => serialize(rows).at(0))
|
||||||
.then((rows) => rows.at(0))
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function serialize(
|
export const remove = fn(Info.shape.id, (id) =>
|
||||||
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`CURRENT_TIMESTAMP()`,
|
timeDeleted: sql`now()`,
|
||||||
})
|
})
|
||||||
.where(and(eq(userTable.id, input)))
|
.where(and(eq(userTable.id, id)))
|
||||||
.execute();
|
.execute();
|
||||||
return input;
|
return id;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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((tx) =>
|
return useTransaction(async (tx) =>
|
||||||
tx
|
tx
|
||||||
.select(getTableColumns(teamTable))
|
.select()
|
||||||
.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(
|
||||||
@@ -211,7 +248,7 @@ export module User {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.execute()
|
.execute()
|
||||||
.then((rows) => rows.map(Team.serialize))
|
.then((rows) => Team.serialize(rows))
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
@@ -2,10 +2,29 @@ import { ulid } from "ulid";
|
|||||||
|
|
||||||
export const prefixes = {
|
export const prefixes = {
|
||||||
user: "usr",
|
user: "usr",
|
||||||
team: "tea",
|
team: "tem",
|
||||||
member: "mbr"
|
task: "tsk",
|
||||||
|
machine: "mch",
|
||||||
|
member: "mbr",
|
||||||
|
steam: "stm",
|
||||||
|
subscription: "sub",
|
||||||
|
invite: "inv",
|
||||||
|
product: "prd",
|
||||||
|
usage: "usg",
|
||||||
|
game: "gme",
|
||||||
|
session: "ssn"
|
||||||
} 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("_");
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
packages/functions/Containerfile
Normal file
17
packages/functions/Containerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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"]
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@@ -14,9 +20,12 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openauthjs/openauth": "0.4.3",
|
"@actor-core/bun": "^0.7.9",
|
||||||
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
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 module AccountApi {
|
export namespace AccountApi {
|
||||||
export const route = new Hono()
|
export const route = new Hono()
|
||||||
.use(notPublic)
|
.use(notPublic)
|
||||||
.get("/",
|
.get("/",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
tags: ["Account"],
|
tags: ["Account"],
|
||||||
summary: "Retrieve the current user's details",
|
summary: "Get user account",
|
||||||
description: "Returns the user's account details, plus the teams they have joined",
|
description: "Get the current user's account details",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@@ -24,39 +25,34 @@ export module 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: "Successfully retrieved account details"
|
description: "User 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) return c.json({ error: "This account does not exist; it may have been deleted" }, 404)
|
if (!currentUser)
|
||||||
|
throw new VisibleError(
|
||||||
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
|
"not_found",
|
||||||
|
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||||
|
"User not found",
|
||||||
|
);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
data: {
|
data: {
|
||||||
id,
|
...currentUser,
|
||||||
email,
|
|
||||||
name,
|
|
||||||
teams,
|
teams,
|
||||||
avatarUrl,
|
|
||||||
discriminator,
|
|
||||||
polarCustomerID,
|
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,47 +1,51 @@
|
|||||||
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.Urls.auth,
|
issuer: Resource.Auth.url,
|
||||||
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 HTTPException(401, { message: "Unauthorized" });
|
throw new VisibleError(
|
||||||
|
"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 next();
|
if (!authHeader) return withActor({ type: "public", properties: {} }, next);
|
||||||
const match = authHeader.match(/^Bearer (.+)$/);
|
const match = authHeader.match(/^Bearer (.+)$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new VisibleError(
|
throw new VisibleError(
|
||||||
"auth.token",
|
"authentication",
|
||||||
"Bearer token not found or improperly formatted",
|
ErrorCodes.Authentication.INVALID_TOKEN,
|
||||||
|
"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 HTTPException(401, {
|
throw new VisibleError(
|
||||||
message: "Unauthorized",
|
"authentication",
|
||||||
});
|
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") //|| c.req.query("teamID");
|
const teamID = c.req.header("x-nestri-team");
|
||||||
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",
|
||||||
@@ -49,21 +53,11 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
|||||||
teamID,
|
teamID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
next
|
async () =>
|
||||||
// async () => {
|
withActor(
|
||||||
// const user = await User.fromEmail(email);
|
result.subject,
|
||||||
// if (!user || user.length === 0) {
|
next,
|
||||||
// c.status(401);
|
)
|
||||||
// return c.text("Unauthorized");
|
|
||||||
// }
|
|
||||||
// return withActor(
|
|
||||||
// {
|
|
||||||
// type: "member",
|
|
||||||
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
|
|
||||||
// },
|
|
||||||
// next,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
246
packages/functions/src/api/common.ts
Normal file
246
packages/functions/src/api/common.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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.",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
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 { ZodError } from "zod";
|
import { cors } from "hono/cors";
|
||||||
|
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 { VisibleError } from "@nestri/core/error";
|
import { patchLogger } from "../log-polyfill";
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from "hono/http-exception";
|
||||||
import { handle, streamHandle } from "hono/aws-lambda";
|
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||||
|
|
||||||
|
export const app = new Hono();
|
||||||
const app = new Hono();
|
|
||||||
app
|
app
|
||||||
.use(logger(), async (c, next) => {
|
.use(logger())
|
||||||
|
.use(cors())
|
||||||
|
.use(async (c, next) => {
|
||||||
c.header("Cache-Control", "no-store");
|
c.header("Cache-Control", "no-store");
|
||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
@@ -20,42 +25,34 @@ 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) {
|
||||||
return c.json(
|
console.error("api error:", error);
|
||||||
{
|
// @ts-expect-error
|
||||||
code: error.code,
|
return c.json(error.toResponse(), error.statusCode());
|
||||||
message: error.message,
|
|
||||||
},
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error instanceof ZodError) {
|
|
||||||
const e = error.errors[0];
|
|
||||||
if (e) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
code: e?.code,
|
|
||||||
message: e?.message,
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Handle HTTP exceptions
|
||||||
if (error instanceof HTTPException) {
|
if (error instanceof HTTPException) {
|
||||||
|
console.error("http error:", error);
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
code: "request",
|
type: "validation",
|
||||||
|
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
||||||
message: "Invalid request",
|
message: "Invalid request",
|
||||||
},
|
},
|
||||||
400,
|
error.status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.error("unhandled error:", error);
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
code: "internal",
|
type: "internal",
|
||||||
|
code: ErrorCodes.Server.INTERNAL_ERROR,
|
||||||
message: "Internal server error",
|
message: "Internal server error",
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
@@ -68,9 +65,8 @@ app.get(
|
|||||||
documentation: {
|
documentation: {
|
||||||
info: {
|
info: {
|
||||||
title: "Nestri API",
|
title: "Nestri API",
|
||||||
description:
|
description: "The Nestri API gives you the power to run your own customized cloud gaming platform.",
|
||||||
"The Nestri API gives you the power to run your own customized cloud gaming platform.",
|
version: "0.0.1",
|
||||||
version: "0.3.0",
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
@@ -81,19 +77,30 @@ 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" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type Routes = typeof routes;
|
patchLogger();
|
||||||
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: () => { },
|
||||||
|
}),
|
||||||
|
};
|
||||||
292
packages/functions/src/api/machine.ts
Normal file
292
packages/functions/src/api/machine.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
54
packages/functions/src/api/messages.ts
Normal file
54
packages/functions/src/api/messages.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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]);
|
||||||
174
packages/functions/src/api/polar.ts
Normal file
174
packages/functions/src/api/polar.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
28
packages/functions/src/api/realtime/actor-core.ts
Normal file
28
packages/functions/src/api/realtime/actor-core.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
15
packages/functions/src/api/realtime/index.ts
Normal file
15
packages/functions/src/api/realtime/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
46
packages/functions/src/api/subscription.ts
Normal file
46
packages/functions/src/api/subscription.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
124
packages/functions/src/api/team.ts
Normal file
124
packages/functions/src/api/team.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
20
packages/functions/src/api/types/hook.ts
Normal file
20
packages/functions/src/api/types/hook.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 };
|
||||||
@@ -2,16 +2,18 @@ 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: {
|
||||||
@@ -22,13 +24,20 @@ 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: {
|
||||||
device: {
|
machine: {
|
||||||
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",
|
||||||
@@ -44,9 +53,7 @@ const app = issuer({
|
|||||||
font: {
|
font: {
|
||||||
family: "Geist, sans-serif",
|
family: "Geist, sans-serif",
|
||||||
},
|
},
|
||||||
css: `
|
css: `@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');`,
|
||||||
@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: {
|
||||||
@@ -73,29 +80,25 @@ const app = issuer({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
device: {
|
machine: {
|
||||||
type: "device",
|
type: "machine",
|
||||||
async client(input) {
|
async client(input) {
|
||||||
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
// FIXME: Do we really need this?
|
||||||
throw new Error("Invalid authorization token");
|
// if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
||||||
}
|
// throw new Error("Invalid authorization token");
|
||||||
const teamSlug = input.params.team;
|
// }
|
||||||
if (!teamSlug) {
|
|
||||||
throw new Error("Team slug is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostname = input.params.hostname;
|
const fingerprint = input.params.fingerprint;
|
||||||
if (!hostname) {
|
if (!fingerprint) {
|
||||||
throw new Error("Hostname is required");
|
throw new Error("Hostname is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname,
|
fingerprint,
|
||||||
teamSlug
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
init() { }
|
init() { }
|
||||||
} as Provider<{ teamSlug: string; hostname: string; }>,
|
} as Provider<{ fingerprint: string; }>,
|
||||||
},
|
},
|
||||||
allow: async (input) => {
|
allow: async (input) => {
|
||||||
const url = new URL(input.redirectURI);
|
const url = new URL(input.redirectURI);
|
||||||
@@ -104,20 +107,47 @@ const app = issuer({
|
|||||||
if (hostname === "localhost") return true;
|
if (hostname === "localhost") return true;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
success: async (ctx, value) => {
|
success: async (ctx, value, req) => {
|
||||||
// if (value.provider === "device") {
|
if (value.provider === "machine") {
|
||||||
// const team = await Teams.fromSlug(value.teamSlug)
|
const countryCode = req.headers.get('CloudFront-Viewer-Country') || 'Unknown'
|
||||||
// console.log("team", team)
|
const country = req.headers.get('CloudFront-Viewer-Country-Name') || 'Unknown'
|
||||||
// console.log("teamSlug", value.teamSlug)
|
const latitude = Number(req.headers.get('CloudFront-Viewer-Latitude')) || 0
|
||||||
// if (team) {
|
const longitude = Number(req.headers.get('CloudFront-Viewer-Longitude')) || 0
|
||||||
// await Instances.create({ hostname: value.hostname, teamID: team.id })
|
const timezone = req.headers.get('CloudFront-Viewer-Time-Zone') || 'Unknown'
|
||||||
|
const fingerprint = value.fingerprint
|
||||||
|
|
||||||
// return await ctx.subject("device", {
|
const existing = await Machine.fromFingerprint(fingerprint)
|
||||||
// teamSlug: value.teamSlug,
|
if (!existing) {
|
||||||
// hostname: value.hostname,
|
const machineID = await Machine.create({
|
||||||
// })
|
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
|
||||||
@@ -136,12 +166,17 @@ 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,12 +210,16 @@ 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,4 +233,14 @@ const app = issuer({
|
|||||||
},
|
},
|
||||||
}).use(logger())
|
}).use(logger())
|
||||||
|
|
||||||
export const handler = handle(app)
|
patchLogger();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
port: 3002,
|
||||||
|
idleTimeout: 255,
|
||||||
|
fetch: (req: Request) =>
|
||||||
|
app.fetch(req, undefined, {
|
||||||
|
waitUntil: (fn) => fn,
|
||||||
|
passThroughOnException: () => { },
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user