diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 8c5b0361..eaab137c 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -25,7 +25,7 @@ on: permissions: actions: write - contents: read + contents: write concurrency: group: beta-${{ inputs.git_ref }} @@ -34,11 +34,12 @@ concurrency: jobs: beta: runs-on: ubuntu-latest + environment: beta env: GH_TOKEN: ${{ github.token }} - BETA_APP_DOMAIN: beta-app.getdory.dev - BETA_VERCEL_PROJECT_NAME: dory-beta + APP_DOMAIN: ${{ vars.APP_DOMAIN || 'beta-app.getdory.dev' }} + VERCEL_PROJECT_NAME: ${{ vars.VERCEL_PROJECT_NAME || 'dory-beta' }} steps: - name: Checkout @@ -67,13 +68,13 @@ jobs: id: beta_meta shell: bash env: - INPUT_CLOUD_API_URL: ${{ secrets.BETA_CLOUD_API_URL }} - INPUT_AUTH_URL: ${{ secrets.BETA_AUTH_URL }} - INPUT_TRUSTED_ORIGINS: ${{ secrets.BETA_TRUSTED_ORIGINS }} + INPUT_CLOUD_API_URL: ${{ secrets.NEXT_PUBLIC_DORY_CLOUD_API_URL }} + INPUT_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} + INPUT_TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} run: | COMMIT_SHA="$(git rev-parse HEAD)" SHORT_SHA="$(git rev-parse --short=12 HEAD)" - AUTH_URL="${INPUT_AUTH_URL:-https://${BETA_APP_DOMAIN}}" + AUTH_URL="${INPUT_AUTH_URL:-https://${APP_DOMAIN}}" AUTH_URL="${AUTH_URL%/}" TRUSTED_ORIGINS="${INPUT_TRUSTED_ORIGINS:-$AUTH_URL}" CLOUD_API_URL="${INPUT_CLOUD_API_URL:-${AUTH_URL}/api}" @@ -86,6 +87,78 @@ jobs: echo "cloud_api_url=$CLOUD_API_URL" >> "$GITHUB_OUTPUT" echo "neon_branch=beta" >> "$GITHUB_OUTPUT" + - name: Resolve beta release tag + if: ${{ inputs.build_desktop }} + id: release_meta + shell: bash + run: | + BASE_TAG="$(git describe --tags --abbrev=0 --match 'v*' HEAD 2>/dev/null || true)" + PACKAGE_VERSION="$(node -p "require('./package.json').version")" + RELEASE_VERSION="$( + BASE_TAG="$BASE_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" node <<'NODE' + const source = (process.env.BASE_TAG || '').trim() || `v${(process.env.PACKAGE_VERSION || '').trim()}`; + + const match = source.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/); + if (!match) { + console.error(`Unsupported version source: ${source}`); + process.exit(1); + } + + let major = Number(match[1]); + let minor = Number(match[2]); + let patch = Number(match[3]); + const prerelease = match[4] || ''; + + const betaMatch = prerelease.match(/^beta\.(\d+)$/); + if (betaMatch) { + process.stdout.write(`${major}.${minor}.${patch}-beta.${Number(betaMatch[1]) + 1}`); + process.exit(0); + } + + patch += 1; + process.stdout.write(`${major}.${minor}.${patch}-beta.0`); + NODE + )" + RELEASE_TAG="v${RELEASE_VERSION}" + + echo "base_tag=$BASE_TAG" >> "$GITHUB_OUTPUT" + echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" + + - name: Generate beta release notes + if: ${{ inputs.build_desktop }} + id: release_notes + uses: ./.github/actions/release-notes + env: + RELEASE_NOTES_AI_PROVIDER: ${{ secrets.RELEASE_NOTES_AI_PROVIDER || 'openai' }} + RELEASE_NOTES_AI_MODEL: ${{ secrets.RELEASE_NOTES_AI_MODEL || 'gpt-4o-mini' }} + RELEASE_NOTES_AI_API_KEY: ${{ secrets.RELEASE_NOTES_AI_API_KEY }} + RELEASE_NOTES_AI_URL: ${{ secrets.RELEASE_NOTES_AI_URL }} + with: + tag: ${{ steps.release_meta.outputs.tag }} + to_ref: HEAD + + - name: Create or update beta GitHub release + if: ${{ inputs.build_desktop }} + shell: bash + env: + RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} + NOTES_FILE: ${{ steps.release_notes.outputs.notes_file }} + TARGET_SHA: ${{ steps.beta_meta.outputs.commit_sha }} + run: | + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release edit "$RELEASE_TAG" \ + --title "$RELEASE_TAG" \ + --notes-file "$NOTES_FILE" \ + --prerelease + else + gh release create "$RELEASE_TAG" \ + --target "$TARGET_SHA" \ + --title "$RELEASE_TAG" \ + --notes-file "$NOTES_FILE" \ + --prerelease + fi + - name: Prepare beta web env if: ${{ inputs.deploy_web }} shell: bash @@ -93,9 +166,9 @@ jobs: AUTH_URL: ${{ steps.beta_meta.outputs.auth_url }} TRUSTED_ORIGINS: ${{ steps.beta_meta.outputs.trusted_origins }} CLOUD_API_URL: ${{ steps.beta_meta.outputs.cloud_api_url }} - BETTER_AUTH_SECRET: ${{ secrets.BETA_BETTER_AUTH_SECRET }} - DS_SECRET_KEY: ${{ secrets.BETA_DS_SECRET_KEY }} - DATABASE_URL: ${{ secrets.NEON_BETA_DATABASE_URL }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} + DS_SECRET_KEY: ${{ secrets.DS_SECRET_KEY }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} run: | cat > apps/web/.env <> "$GITHUB_OUTPUT" - echo "beta_url=https://${BETA_APP_DOMAIN}" >> "$GITHUB_OUTPUT" + echo "beta_url=https://${APP_DOMAIN}" >> "$GITHUB_OUTPUT" - name: Trigger beta desktop packaging workflows if: ${{ inputs.build_desktop }} @@ -278,11 +351,13 @@ jobs: TARGET_REF: ${{ inputs.git_ref }} TARGET_SHA: ${{ steps.beta_meta.outputs.commit_sha }} AUTH_URL: ${{ steps.beta_meta.outputs.auth_url }} + RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} run: | dispatch_workflow() { local workflow_path="$1" gh workflow run "$workflow_path" \ --ref "$TARGET_REF" \ + -f release_tag="$RELEASE_TAG" \ -f distribution=beta \ -f app_base_url="$AUTH_URL" \ -f auth_base_url="$AUTH_URL" @@ -320,6 +395,7 @@ jobs: BUILD_DESKTOP: ${{ inputs.build_desktop }} COMMIT_SHA: ${{ steps.beta_meta.outputs.commit_sha }} BETA_URL: ${{ steps.deploy_web.outputs.beta_url }} + RELEASE_TAG: ${{ steps.release_meta.outputs.tag }} VERCEL_PROJECT_ID: ${{ steps.vercel_project.outputs.project_id }} MACOS_RUN_URL: ${{ steps.desktop_runs.outputs.macos_run_url }} WINDOWS_RUN_URL: ${{ steps.desktop_runs.outputs.windows_run_url }} @@ -337,6 +413,8 @@ jobs: echo "- Beta URL: skipped" fi if [ "$BUILD_DESKTOP" = "true" ]; then + echo "- Release tag: \`$RELEASE_TAG\`" + echo "- Release URL: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG" echo "- macOS packaging run: ${MACOS_RUN_URL:-pending}" echo "- Windows packaging run: ${WINDOWS_RUN_URL:-pending}" else diff --git a/.github/workflows/macos-package.yml b/.github/workflows/macos-package.yml index bd2e5ee1..9f4ee7d5 100644 --- a/.github/workflows/macos-package.yml +++ b/.github/workflows/macos-package.yml @@ -12,9 +12,8 @@ on: required: false type: string distribution: - description: 'Build distribution.' + description: 'Build distribution. Leave empty to infer from release_tag.' required: false - default: stable type: choice options: - stable @@ -36,8 +35,62 @@ concurrency: cancel-in-progress: false jobs: + resolve_context: + runs-on: ubuntu-latest + + env: + GH_TOKEN: ${{ github.token }} + + outputs: + tag: ${{ steps.tag.outputs.tag }} + distribution: ${{ steps.distribution.outputs.distribution }} + environment_name: ${{ steps.distribution.outputs.environment_name }} + + steps: + - name: Resolve release tag + id: tag + shell: bash + run: | + TAG="" + if [ "${{ github.event_name }}" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + elif [ -n "${{ inputs.release_tag }}" ]; then + TAG="${{ inputs.release_tag }}" + fi + echo "Resolved release tag: ${TAG:-}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Resolve distribution + id: distribution + shell: bash + env: + INPUT_DISTRIBUTION: ${{ inputs.distribution }} + RESOLVED_TAG: ${{ steps.tag.outputs.tag }} + run: | + DISTRIBUTION="stable" + + if [ "${{ github.event_name }}" = "release" ]; then + if [ "${{ github.event.release.prerelease }}" = "true" ]; then + DISTRIBUTION="beta" + fi + elif [ -n "$INPUT_DISTRIBUTION" ]; then + DISTRIBUTION="$INPUT_DISTRIBUTION" + elif [ -n "$RESOLVED_TAG" ]; then + IS_PRERELEASE="$(gh release view "$RESOLVED_TAG" --json isPrerelease -q '.isPrerelease')" + echo "Resolved prerelease flag: ${IS_PRERELEASE}" + if [ "$IS_PRERELEASE" = "true" ]; then + DISTRIBUTION="beta" + fi + fi + + echo "Resolved distribution: $DISTRIBUTION" + echo "distribution=$DISTRIBUTION" >> "$GITHUB_OUTPUT" + echo "environment_name=$DISTRIBUTION" >> "$GITHUB_OUTPUT" + package: + needs: resolve_context runs-on: macos-latest + environment: ${{ needs.resolve_context.outputs.environment_name }} env: GH_TOKEN: ${{ github.token }} @@ -46,7 +99,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref }} + ref: ${{ needs.resolve_context.outputs.tag != '' && needs.resolve_context.outputs.tag || github.ref }} fetch-depth: 0 - name: Setup Node @@ -65,70 +118,33 @@ jobs: - name: Install dependencies run: yarn install --immutable - - name: Resolve release tag - id: tag - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - TAG="" - if [ "${{ github.event_name }}" = "release" ]; then - TAG="${{ github.event.release.tag_name }}" - elif [ -n "${{ inputs.release_tag }}" ]; then - TAG="${{ inputs.release_tag }}" - fi - echo "Resolved release tag: ${TAG:-}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Resolve release kind - id: release_kind - if: steps.tag.outputs.tag != '' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - if [ "${{ github.event_name }}" = "release" ]; then - IS_PRERELEASE="${{ github.event.release.prerelease }}" - else - IS_PRERELEASE="$(gh release view "${{ steps.tag.outputs.tag }}" --json isPrerelease -q '.isPrerelease')" - fi - echo "Resolved prerelease flag: ${IS_PRERELEASE}" - echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" - - name: Resolve build config id: build_config shell: bash env: - INPUT_DISTRIBUTION: ${{ inputs.distribution }} + DISTRIBUTION: ${{ needs.resolve_context.outputs.distribution }} INPUT_APP_BASE_URL: ${{ inputs.app_base_url }} INPUT_AUTH_BASE_URL: ${{ inputs.auth_base_url }} - STABLE_NEXT_PUBLIC_DORY_RUNTIME: ${{ secrets.NEXT_PUBLIC_DORY_RUNTIME }} - STABLE_NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ secrets.NEXT_PUBLIC_DORY_CLOUD_API_URL }} - STABLE_BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} + DEFAULT_NEXT_PUBLIC_DORY_RUNTIME: ${{ secrets.NEXT_PUBLIC_DORY_RUNTIME }} + DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ secrets.NEXT_PUBLIC_DORY_CLOUD_API_URL }} + DEFAULT_BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} run: | - DISTRIBUTION="stable" - if [ "${{ github.event_name }}" = "release" ]; then - if [ "${{ github.event.release.prerelease }}" = "true" ]; then - DISTRIBUTION="beta" - fi - elif [ -n "$INPUT_DISTRIBUTION" ]; then - DISTRIBUTION="$INPUT_DISTRIBUTION" - fi - if [ "$DISTRIBUTION" = "beta" ]; then APP_BASE_URL="${INPUT_APP_BASE_URL:-https://beta-app.getdory.dev}" APP_BASE_URL="${APP_BASE_URL%/}" - AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$APP_BASE_URL}" - NEXT_PUBLIC_DORY_RUNTIME="desktop" - NEXT_PUBLIC_DORY_CLOUD_API_URL="${APP_BASE_URL}/api" + AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-${DEFAULT_BETTER_AUTH_URL:-$APP_BASE_URL}}" + AUTH_BASE_URL="${AUTH_BASE_URL%/}" + NEXT_PUBLIC_DORY_RUNTIME="${DEFAULT_NEXT_PUBLIC_DORY_RUNTIME:-desktop}" + NEXT_PUBLIC_DORY_CLOUD_API_URL="${DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL:-${APP_BASE_URL}/api}" + NEXT_PUBLIC_DORY_CLOUD_API_URL="${NEXT_PUBLIC_DORY_CLOUD_API_URL%/}" DORY_UPDATE_CHANNEL="beta" DORY_ELECTRON_APP_ID="com.dory.app.beta" DORY_PROTOCOL_SCHEME="dory-beta" else APP_BASE_URL="${INPUT_APP_BASE_URL:-}" - AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$STABLE_BETTER_AUTH_URL}" - NEXT_PUBLIC_DORY_RUNTIME="$STABLE_NEXT_PUBLIC_DORY_RUNTIME" - NEXT_PUBLIC_DORY_CLOUD_API_URL="$STABLE_NEXT_PUBLIC_DORY_CLOUD_API_URL" + AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$DEFAULT_BETTER_AUTH_URL}" + NEXT_PUBLIC_DORY_RUNTIME="$DEFAULT_NEXT_PUBLIC_DORY_RUNTIME" + NEXT_PUBLIC_DORY_CLOUD_API_URL="$DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL" DORY_UPDATE_CHANNEL="latest" DORY_ELECTRON_APP_ID="" DORY_PROTOCOL_SCHEME="" @@ -154,41 +170,22 @@ jobs: - name: Prepare web env shell: bash env: - DISTRIBUTION: ${{ steps.build_config.outputs.distribution }} + DISTRIBUTION: ${{ needs.resolve_context.outputs.distribution }} AUTH_BASE_URL: ${{ steps.build_config.outputs.auth_base_url }} NEXT_PUBLIC_DORY_RUNTIME: ${{ steps.build_config.outputs.next_public_dory_runtime }} NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ steps.build_config.outputs.next_public_dory_cloud_api_url }} - STABLE_BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} - BETA_BETTER_AUTH_SECRET: ${{ secrets.BETA_BETTER_AUTH_SECRET }} - STABLE_DS_SECRET_KEY: ${{ secrets.DS_SECRET_KEY }} - BETA_DS_SECRET_KEY: ${{ secrets.BETA_DS_SECRET_KEY }} - STABLE_DB_TYPE: ${{ secrets.DB_TYPE }} - STABLE_DATABASE_URL: ${{ secrets.DATABASE_URL }} - BETA_DATABASE_URL: ${{ secrets.NEON_BETA_DATABASE_URL }} - STABLE_TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} - BETA_TRUSTED_ORIGINS: ${{ secrets.BETA_TRUSTED_ORIGINS }} - STABLE_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} - STABLE_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} - STABLE_DORY_AI_CLOUD_URL: ${{ secrets.DORY_AI_CLOUD_URL }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} + DS_SECRET_KEY: ${{ secrets.DS_SECRET_KEY }} + DB_TYPE: ${{ secrets.DB_TYPE }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} + NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + DORY_AI_CLOUD_URL: ${{ secrets.DORY_AI_CLOUD_URL }} run: | - if [ "$DISTRIBUTION" = "beta" ]; then - BETTER_AUTH_SECRET="$BETA_BETTER_AUTH_SECRET" - DS_SECRET_KEY="$BETA_DS_SECRET_KEY" - DB_TYPE="postgres" - DATABASE_URL="$BETA_DATABASE_URL" - TRUSTED_ORIGINS="$BETA_TRUSTED_ORIGINS" - NEXT_PUBLIC_POSTHOG_KEY="" - NEXT_PUBLIC_POSTHOG_HOST="" - DORY_AI_CLOUD_URL="" - else - BETTER_AUTH_SECRET="$STABLE_BETTER_AUTH_SECRET" - DS_SECRET_KEY="$STABLE_DS_SECRET_KEY" - DB_TYPE="$STABLE_DB_TYPE" - DATABASE_URL="$STABLE_DATABASE_URL" - TRUSTED_ORIGINS="$STABLE_TRUSTED_ORIGINS" - NEXT_PUBLIC_POSTHOG_KEY="$STABLE_POSTHOG_KEY" - NEXT_PUBLIC_POSTHOG_HOST="$STABLE_POSTHOG_HOST" - DORY_AI_CLOUD_URL="$STABLE_DORY_AI_CLOUD_URL" + EFFECTIVE_DB_TYPE="${DB_TYPE:-}" + if [ "$DISTRIBUTION" = "beta" ] && [ -z "$EFFECTIVE_DB_TYPE" ]; then + EFFECTIVE_DB_TYPE="postgres" fi cat > apps/web/.env <}" >&2 + exit 1 + fi + yarn workspace web run postgres:check + + - name: Run stable Postgres migrations + if: ${{ inputs.run_migrations }} + env: + DB_TYPE: ${{ secrets.DB_TYPE }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: | + if [ "$DB_TYPE" != "postgres" ]; then + echo "Stable schema migrations require DB_TYPE=postgres, got: ${DB_TYPE:-}" >&2 + exit 1 + fi + yarn workspace web run drizzle:migrate + - name: Compute release version id: version run: | - case "${{ inputs.channel }}:${{ inputs.bump }}" in - stable:patch|stable:minor|stable:major) + case "${{ inputs.bump }}" in + patch|minor|major) npm version "${{ inputs.bump }}" --no-git-tag-version ;; - stable:promote) + promote) NEW_VERSION="$(node -e "const version=require('./package.json').version; if (!version.includes('-')) { process.stderr.write('Current version is not a prerelease\\n'); process.exit(1); } process.stdout.write(version.split('-')[0]);")" npm version "$NEW_VERSION" --no-git-tag-version ;; - beta:patch|beta:minor|beta:major) - npm version "pre${{ inputs.bump }}" --preid beta --no-git-tag-version - ;; - beta:prerelease) - npm version prerelease --preid beta --no-git-tag-version - ;; *) - echo "Unsupported combination: channel=${{ inputs.channel }}, bump=${{ inputs.bump }}" + echo "Unsupported bump: ${{ inputs.bump }}" exit 1 ;; esac @@ -118,14 +133,9 @@ jobs: VERSION="${{ steps.version.outputs.version }}" TAG="v$VERSION" NOTES_FILE="${{ steps.release_notes.outputs.notes_file }}" - PRERELEASE_FLAG="" - if [ "${{ inputs.channel }}" = "beta" ]; then - PRERELEASE_FLAG="--prerelease" - fi gh release create "$TAG" \ --title "$TAG" \ - --notes-file "$NOTES_FILE" \ - $PRERELEASE_FLAG + --notes-file "$NOTES_FILE" - name: Dispatch packaging workflows run: | diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index 72a93045..4155028b 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -12,9 +12,8 @@ on: required: false type: string distribution: - description: 'Build distribution.' + description: 'Build distribution. Leave empty to infer from release_tag.' required: false - default: stable type: choice options: - stable @@ -36,8 +35,62 @@ concurrency: cancel-in-progress: false jobs: + resolve_context: + runs-on: ubuntu-latest + + env: + GH_TOKEN: ${{ github.token }} + + outputs: + tag: ${{ steps.tag.outputs.tag }} + distribution: ${{ steps.distribution.outputs.distribution }} + environment_name: ${{ steps.distribution.outputs.environment_name }} + + steps: + - name: Resolve release tag + id: tag + shell: bash + run: | + TAG="" + if [ "${{ github.event_name }}" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + elif [ -n "${{ inputs.release_tag }}" ]; then + TAG="${{ inputs.release_tag }}" + fi + echo "Resolved release tag: ${TAG:-}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Resolve distribution + id: distribution + shell: bash + env: + INPUT_DISTRIBUTION: ${{ inputs.distribution }} + RESOLVED_TAG: ${{ steps.tag.outputs.tag }} + run: | + DISTRIBUTION="stable" + + if [ "${{ github.event_name }}" = "release" ]; then + if [ "${{ github.event.release.prerelease }}" = "true" ]; then + DISTRIBUTION="beta" + fi + elif [ -n "$INPUT_DISTRIBUTION" ]; then + DISTRIBUTION="$INPUT_DISTRIBUTION" + elif [ -n "$RESOLVED_TAG" ]; then + IS_PRERELEASE="$(gh release view "$RESOLVED_TAG" --json isPrerelease -q '.isPrerelease')" + echo "Resolved prerelease flag: ${IS_PRERELEASE}" + if [ "$IS_PRERELEASE" = "true" ]; then + DISTRIBUTION="beta" + fi + fi + + echo "Resolved distribution: $DISTRIBUTION" + echo "distribution=$DISTRIBUTION" >> "$GITHUB_OUTPUT" + echo "environment_name=$DISTRIBUTION" >> "$GITHUB_OUTPUT" + package: + needs: resolve_context runs-on: windows-latest + environment: ${{ needs.resolve_context.outputs.environment_name }} env: GH_TOKEN: ${{ github.token }} @@ -46,7 +99,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref }} + ref: ${{ needs.resolve_context.outputs.tag != '' && needs.resolve_context.outputs.tag || github.ref }} fetch-depth: 0 - name: Setup Node @@ -86,70 +139,33 @@ jobs: Write-Error "yarn install failed after $maxAttempts attempts" exit 1 - - name: Resolve release tag - id: tag - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - TAG="" - if [ "${{ github.event_name }}" = "release" ]; then - TAG="${{ github.event.release.tag_name }}" - elif [ -n "${{ inputs.release_tag }}" ]; then - TAG="${{ inputs.release_tag }}" - fi - echo "Resolved release tag: ${TAG:-}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Resolve release kind - id: release_kind - if: steps.tag.outputs.tag != '' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - if [ "${{ github.event_name }}" = "release" ]; then - IS_PRERELEASE="${{ github.event.release.prerelease }}" - else - IS_PRERELEASE="$(gh release view "${{ steps.tag.outputs.tag }}" --json isPrerelease -q '.isPrerelease')" - fi - echo "Resolved prerelease flag: ${IS_PRERELEASE}" - echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" - - name: Resolve build config id: build_config shell: bash env: - INPUT_DISTRIBUTION: ${{ inputs.distribution }} + DISTRIBUTION: ${{ needs.resolve_context.outputs.distribution }} INPUT_APP_BASE_URL: ${{ inputs.app_base_url }} INPUT_AUTH_BASE_URL: ${{ inputs.auth_base_url }} - STABLE_NEXT_PUBLIC_DORY_RUNTIME: ${{ secrets.NEXT_PUBLIC_DORY_RUNTIME }} - STABLE_NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ secrets.NEXT_PUBLIC_DORY_CLOUD_API_URL }} - STABLE_BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} + DEFAULT_NEXT_PUBLIC_DORY_RUNTIME: ${{ secrets.NEXT_PUBLIC_DORY_RUNTIME }} + DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ secrets.NEXT_PUBLIC_DORY_CLOUD_API_URL }} + DEFAULT_BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} run: | - DISTRIBUTION="stable" - if [ "${{ github.event_name }}" = "release" ]; then - if [ "${{ github.event.release.prerelease }}" = "true" ]; then - DISTRIBUTION="beta" - fi - elif [ -n "$INPUT_DISTRIBUTION" ]; then - DISTRIBUTION="$INPUT_DISTRIBUTION" - fi - if [ "$DISTRIBUTION" = "beta" ]; then APP_BASE_URL="${INPUT_APP_BASE_URL:-https://beta-app.getdory.dev}" APP_BASE_URL="${APP_BASE_URL%/}" - AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$APP_BASE_URL}" - NEXT_PUBLIC_DORY_RUNTIME="desktop" - NEXT_PUBLIC_DORY_CLOUD_API_URL="${APP_BASE_URL}/api" + AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-${DEFAULT_BETTER_AUTH_URL:-$APP_BASE_URL}}" + AUTH_BASE_URL="${AUTH_BASE_URL%/}" + NEXT_PUBLIC_DORY_RUNTIME="${DEFAULT_NEXT_PUBLIC_DORY_RUNTIME:-desktop}" + NEXT_PUBLIC_DORY_CLOUD_API_URL="${DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL:-${APP_BASE_URL}/api}" + NEXT_PUBLIC_DORY_CLOUD_API_URL="${NEXT_PUBLIC_DORY_CLOUD_API_URL%/}" DORY_UPDATE_CHANNEL="beta" DORY_ELECTRON_APP_ID="com.dory.app.beta" DORY_PROTOCOL_SCHEME="dory-beta" else APP_BASE_URL="${INPUT_APP_BASE_URL:-}" - AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$STABLE_BETTER_AUTH_URL}" - NEXT_PUBLIC_DORY_RUNTIME="$STABLE_NEXT_PUBLIC_DORY_RUNTIME" - NEXT_PUBLIC_DORY_CLOUD_API_URL="$STABLE_NEXT_PUBLIC_DORY_CLOUD_API_URL" + AUTH_BASE_URL="${INPUT_AUTH_BASE_URL:-$DEFAULT_BETTER_AUTH_URL}" + NEXT_PUBLIC_DORY_RUNTIME="$DEFAULT_NEXT_PUBLIC_DORY_RUNTIME" + NEXT_PUBLIC_DORY_CLOUD_API_URL="$DEFAULT_NEXT_PUBLIC_DORY_CLOUD_API_URL" DORY_UPDATE_CHANNEL="latest" DORY_ELECTRON_APP_ID="" DORY_PROTOCOL_SCHEME="" @@ -167,41 +183,22 @@ jobs: - name: Prepare web env shell: bash env: - DISTRIBUTION: ${{ steps.build_config.outputs.distribution }} + DISTRIBUTION: ${{ needs.resolve_context.outputs.distribution }} AUTH_BASE_URL: ${{ steps.build_config.outputs.auth_base_url }} NEXT_PUBLIC_DORY_RUNTIME: ${{ steps.build_config.outputs.next_public_dory_runtime }} NEXT_PUBLIC_DORY_CLOUD_API_URL: ${{ steps.build_config.outputs.next_public_dory_cloud_api_url }} - STABLE_BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} - BETA_BETTER_AUTH_SECRET: ${{ secrets.BETA_BETTER_AUTH_SECRET }} - STABLE_DS_SECRET_KEY: ${{ secrets.DS_SECRET_KEY }} - BETA_DS_SECRET_KEY: ${{ secrets.BETA_DS_SECRET_KEY }} - STABLE_DB_TYPE: ${{ secrets.DB_TYPE }} - STABLE_DATABASE_URL: ${{ secrets.DATABASE_URL }} - BETA_DATABASE_URL: ${{ secrets.NEON_BETA_DATABASE_URL }} - STABLE_TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} - BETA_TRUSTED_ORIGINS: ${{ secrets.BETA_TRUSTED_ORIGINS }} - STABLE_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} - STABLE_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} - STABLE_DORY_AI_CLOUD_URL: ${{ secrets.DORY_AI_CLOUD_URL }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} + DS_SECRET_KEY: ${{ secrets.DS_SECRET_KEY }} + DB_TYPE: ${{ secrets.DB_TYPE }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} + NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + DORY_AI_CLOUD_URL: ${{ secrets.DORY_AI_CLOUD_URL }} run: | - if [ "$DISTRIBUTION" = "beta" ]; then - BETTER_AUTH_SECRET="$BETA_BETTER_AUTH_SECRET" - DS_SECRET_KEY="$BETA_DS_SECRET_KEY" - DB_TYPE="postgres" - DATABASE_URL="$BETA_DATABASE_URL" - TRUSTED_ORIGINS="$BETA_TRUSTED_ORIGINS" - NEXT_PUBLIC_POSTHOG_KEY="" - NEXT_PUBLIC_POSTHOG_HOST="" - DORY_AI_CLOUD_URL="" - else - BETTER_AUTH_SECRET="$STABLE_BETTER_AUTH_SECRET" - DS_SECRET_KEY="$STABLE_DS_SECRET_KEY" - DB_TYPE="$STABLE_DB_TYPE" - DATABASE_URL="$STABLE_DATABASE_URL" - TRUSTED_ORIGINS="$STABLE_TRUSTED_ORIGINS" - NEXT_PUBLIC_POSTHOG_KEY="$STABLE_POSTHOG_KEY" - NEXT_PUBLIC_POSTHOG_HOST="$STABLE_POSTHOG_HOST" - DORY_AI_CLOUD_URL="$STABLE_DORY_AI_CLOUD_URL" + EFFECTIVE_DB_TYPE="${DB_TYPE:-}" + if [ "$DISTRIBUTION" = "beta" ] && [ -z "$EFFECTIVE_DB_TYPE" ]; then + EFFECTIVE_DB_TYPE="postgres" fi cat > apps/web/.env <(); const router = useRouter(); const collapsed = state === 'collapsed'; + const organizationSlug = params.organization; const renderMenuContent = () => ( @@ -30,6 +32,15 @@ export function NavUser({ user }: { user: User | null }) { + {/* { + if (!organizationSlug) return; + router.push(`/${organizationSlug}/settings/organization`); + }} + > + + My Project + */} { e.preventDefault(); diff --git a/apps/web/app/(app)/[organization]/settings/billing/page.client.tsx b/apps/web/app/(app)/[organization]/settings/billing/page.client.tsx new file mode 100644 index 00000000..2a46630a --- /dev/null +++ b/apps/web/app/(app)/[organization]/settings/billing/page.client.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useMemo } from 'react'; +import { useParams } from 'next/navigation'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { Button } from '@/registry/new-york-v4/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/registry/new-york-v4/ui/card'; +import { getOrganizationBillingStatus, openOrganizationBillingPortal, upgradeOrganizationToPro } from '@/lib/billing/api'; +import { getOrganizationAccess, getFullOrganization } from '@/lib/organization/api'; + +function formatDate(value: string | null) { + if (!value) { + return 'N/A'; + } + + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + + return date.toLocaleString(); +} + +function getStatusDescription(status: string | null, cancelAtPeriodEnd: boolean, periodEnd: string | null) { + if (cancelAtPeriodEnd) { + return `Pro remains active until ${formatDate(periodEnd)}. Cancellation is scheduled at the end of the current billing period.`; + } + + if (!status) { + return 'No paid subscription is active for this organization.'; + } + + if (status === 'canceled') { + return 'The paid subscription has been canceled. The organization remains on Hobby.'; + } + + if (status === 'incomplete' || status === 'incomplete_expired' || status === 'past_due' || status === 'unpaid') { + return 'The last paid subscription is not active. The organization remains on Hobby until Stripe reports an active or trialing Pro subscription.'; + } + + return `Stripe reports the latest subscription status as ${status}.`; +} + +export default function BillingSettingsPageClient() { + const params = useParams<{ organization: string }>(); + const organizationSlug = params.organization; + + const organizationQuery = useQuery({ + queryKey: ['organization-full', organizationSlug], + queryFn: () => getFullOrganization({ organizationSlug }), + retry: false, + }); + const accessQuery = useQuery({ + queryKey: ['organization-access', organizationSlug, organizationQuery.data?.id], + queryFn: () => getOrganizationAccess(organizationQuery.data!.id), + enabled: Boolean(organizationQuery.data?.id), + retry: false, + }); + const billingStatusQuery = useQuery({ + queryKey: ['organization-billing', organizationSlug, organizationQuery.data?.id], + queryFn: () => getOrganizationBillingStatus(organizationQuery.data!.id), + enabled: Boolean(organizationQuery.data?.id), + retry: false, + }); + + const upgradeMutation = useMutation({ + mutationFn: async () => { + if (!organizationQuery.data?.id) { + throw new Error('Organization not found'); + } + + await upgradeOrganizationToPro(organizationQuery.data.id, organizationSlug); + }, + onError: error => { + toast.error(error instanceof Error ? error.message : 'Failed to start checkout'); + }, + }); + + const portalMutation = useMutation({ + mutationFn: async () => { + if (!organizationQuery.data?.id || !billingStatusQuery.data?.subscriptionId) { + throw new Error('No manageable subscription found'); + } + + await openOrganizationBillingPortal(organizationQuery.data.id, organizationSlug, billingStatusQuery.data.subscriptionId); + }, + onError: error => { + toast.error(error instanceof Error ? error.message : 'Failed to open billing portal'); + }, + }); + + const canManageBilling = accessQuery.data?.role === 'owner'; + const billingStatus = billingStatusQuery.data; + const organization = organizationQuery.data; + const isOrganizationLoading = organizationQuery.isLoading; + const isBillingLoading = organizationQuery.isSuccess && billingStatusQuery.isLoading; + const isLoading = isOrganizationLoading || isBillingLoading; + const currentPlanLabel = billingStatus?.plan === 'pro' ? 'Pro' : 'Hobby'; + const billingDescription = billingStatusQuery.isError + ? billingStatusQuery.error instanceof Error + ? billingStatusQuery.error.message + : 'Failed to load billing status.' + : isLoading + ? 'Loading billing status...' + : getStatusDescription( + billingStatus?.subscriptionStatus ?? null, + billingStatus?.cancelAtPeriodEnd ?? false, + billingStatus?.periodEnd ?? null, + ); + + const detailRows = useMemo( + () => [ + { label: 'Plan', value: currentPlanLabel }, + { label: 'Subscription status', value: billingStatus?.subscriptionStatus ?? 'No subscription' }, + { label: 'Stripe subscription ID', value: billingStatus?.stripeSubscriptionId ?? 'N/A' }, + { label: 'Current period end', value: formatDate(billingStatus?.periodEnd ?? null) }, + ], + [billingStatus, currentPlanLabel], + ); + + if (organizationQuery.isError) { + return ( + + + Billing + Unable to load organization billing. + + +

+ {organizationQuery.error instanceof Error ? organizationQuery.error.message : 'Failed to load organization details.'} +

+
+
+ ); + } + + if (!isLoading && !organization) { + return ( + + + Billing + Unable to load organization billing. + + +

The current organization could not be resolved from this URL.

+
+
+ ); + } + + return ( + + + Billing + Review the current organization plan, upgrade to Pro, or manage billing in Stripe. + + +
+
Current plan
+
{isLoading ? 'Loading...' : currentPlanLabel}
+

{billingDescription}

+
+ +
+ {detailRows.map(row => ( +
+ {row.label} + {row.value} +
+ ))} +
+ + {billingStatus?.cancelAtPeriodEnd ? ( +
+ Cancellation is scheduled for {formatDate(billingStatus.cancelAt || billingStatus.periodEnd)}. +
+ ) : null} + +
+ {billingStatus?.plan !== 'pro' && canManageBilling ? ( + + ) : null} + + {billingStatus?.isManageable && canManageBilling ? ( + + ) : null} +
+ + {!canManageBilling ? ( +

Only organization owners can upgrade, cancel, or manage billing. Your current role is read-only here.

+ ) : null} +
+
+ ); +} diff --git a/apps/web/app/(app)/[organization]/settings/billing/page.tsx b/apps/web/app/(app)/[organization]/settings/billing/page.tsx new file mode 100644 index 00000000..98085786 --- /dev/null +++ b/apps/web/app/(app)/[organization]/settings/billing/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from 'next/navigation'; +import { isBillingEnabledForServer } from '@/lib/runtime/runtime'; +import BillingSettingsPageClient from './page.client'; + +export default async function OrganizationBillingSettingsPage({ params }: { params: Promise<{ organization: string }> }) { + const { organization } = await params; + + if (!isBillingEnabledForServer()) { + redirect(`/${organization}/settings/organization`); + } + + return ; +} diff --git a/apps/web/app/(app)/[organization]/settings/layout.tsx b/apps/web/app/(app)/[organization]/settings/layout.tsx index d2128f44..fbcb7588 100644 --- a/apps/web/app/(app)/[organization]/settings/layout.tsx +++ b/apps/web/app/(app)/[organization]/settings/layout.tsx @@ -1,8 +1,9 @@ import Link from 'next/link'; import type React from 'react'; +import { isBillingEnabledForServer } from '@/lib/runtime/runtime'; import { cn } from '@/lib/utils'; -const NAV_ITEMS = [{ slug: 'organization', label: 'Organization' }]; +const NAV_ITEMS = [{ slug: 'organization', label: 'Organization' }, ...(isBillingEnabledForServer() ? [{ slug: 'billing', label: 'Billing' }] : [])]; export default async function OrganizationSettingsLayout({ children, params }: { children: React.ReactNode; params: Promise<{ organization: string }> }) { const { organization } = await params; diff --git a/apps/web/app/(app)/[organization]/settings/organization/page.tsx b/apps/web/app/(app)/[organization]/settings/organization/page.tsx index 098ba4a2..88014987 100644 --- a/apps/web/app/(app)/[organization]/settings/organization/page.tsx +++ b/apps/web/app/(app)/[organization]/settings/organization/page.tsx @@ -33,12 +33,22 @@ export default function OrganizationSettingsPage() { }, [organizationQuery.data]); const updateMutation = useMutation({ - mutationFn: () => - updateOrganization({ + mutationFn: () => { + const normalizedSlug = slugifyOrganizationName(slug); + if (!slug.trim()) { + throw new Error('Slug is required'); + } + + if (normalizedSlug !== slug.trim()) { + throw new Error('Slug can only contain lowercase letters, numbers, and hyphens'); + } + + return updateOrganization({ organizationId: organizationQuery.data!.id, name: name.trim(), slug: slug.trim(), - }), + }); + }, onSuccess: async updated => { toast.success('Organization updated'); await queryClient.invalidateQueries({ queryKey: ['organization-full'] }); @@ -70,13 +80,7 @@ export default function OrganizationSettingsPage() { { - const nextName = event.target.value; - setName(nextName); - if (!slug.trim() || slug === slugifyOrganizationName(name)) { - setSlug(slugifyOrganizationName(nextName)); - } - }} + onChange={event => setName(event.target.value)} disabled={!organization || !canUpdate || updateMutation.isPending} /> @@ -85,7 +89,7 @@ export default function OrganizationSettingsPage() { setSlug(slugifyOrganizationName(event.target.value))} + onChange={event => setSlug(event.target.value)} disabled={!organization || !canUpdate || updateMutation.isPending} /> @@ -110,4 +114,3 @@ export default function OrganizationSettingsPage() { ); } - diff --git a/apps/web/app/api/organization/billing/route.ts b/apps/web/app/api/organization/billing/route.ts new file mode 100644 index 00000000..79d06759 --- /dev/null +++ b/apps/web/app/api/organization/billing/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server'; +import { withUserHandler } from '@/app/api/utils/with-organization-handler'; +import { getApiLocale, translateApi } from '@/app/api/utils/i18n'; +import { getOrganizationBillingStatus } from '@/lib/billing/server'; +import { canManageOrganizationBilling } from '@/lib/billing/authz'; +import { ErrorCodes } from '@/lib/errors'; +import { ResponseUtil } from '@/lib/result'; +import { resolveOrganizationAccess } from '@/lib/server/authz'; + +export const runtime = 'nodejs'; + +export const GET = withUserHandler(async ({ req, userId }) => { + const locale = await getApiLocale(); + const organizationId = req.nextUrl.searchParams.get('organizationId'); + + if (!organizationId) { + return NextResponse.json( + ResponseUtil.error({ + code: ErrorCodes.INVALID_PARAMS, + message: translateApi('Api.Errors.MissingOrganizationContext', undefined, locale), + }), + { status: 400 }, + ); + } + + const access = await resolveOrganizationAccess(organizationId, userId); + if (!access?.isMember) { + return NextResponse.json( + ResponseUtil.error({ + code: ErrorCodes.FORBIDDEN, + message: translateApi('Api.Errors.Unauthorized', undefined, locale), + }), + { status: 403 }, + ); + } + + const billingStatus = await getOrganizationBillingStatus(organizationId, canManageOrganizationBilling(access.role)); + + return NextResponse.json( + ResponseUtil.success({ + billingStatus, + }), + ); +}); diff --git a/apps/web/lib/auth-client.ts b/apps/web/lib/auth-client.ts index 59093267..7f63c0f0 100644 --- a/apps/web/lib/auth-client.ts +++ b/apps/web/lib/auth-client.ts @@ -1,6 +1,7 @@ // lib/auth/client.ts — only import in client components 'use client'; import { dashClient, sentinelClient } from '@better-auth/infra/client'; +import { stripeClient } from '@better-auth/stripe/client'; import { createAuthClient } from 'better-auth/react'; import { inferOrgAdditionalFields, organizationClient } from 'better-auth/client/plugins'; import { translate } from '@/lib/i18n/i18n'; @@ -25,10 +26,12 @@ export const authClient = createAuthClient({ roles: organizationRoles, schema: inferOrgAdditionalFields>>(), }), + stripeClient({ + subscription: true, + }), ], }); - // ==== Wrapper: social login ==== export function signInViaGithub(redirectTo = '/') { // Usually triggers a redirect; return value is not used diff --git a/apps/web/lib/auth.ts b/apps/web/lib/auth.ts index 03944d18..fa48a7a6 100644 --- a/apps/web/lib/auth.ts +++ b/apps/web/lib/auth.ts @@ -1,8 +1,10 @@ import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { jwt, organization } from 'better-auth/plugins'; -import { eq } from 'drizzle-orm'; +import { stripe as stripePlugin } from '@better-auth/stripe'; import { dash, sentinel } from '@better-auth/infra'; +import Stripe from 'stripe'; +import { and, eq, isNull, or } from 'drizzle-orm'; import { createCachedAsyncFactory } from '@dory/auth-core'; import type { PostgresDBClient } from '../types'; import { getClient } from './database/postgres/client'; @@ -12,8 +14,9 @@ import { sendEmail } from './email'; import { resolveOrganizationIdForSession, shouldCreateDefaultOrganization } from './auth/migration-state'; import { translate } from './i18n/i18n'; import { getServerLocale } from './i18n/server-locale'; -import { isDesktopRuntime } from './runtime/runtime'; +import { isBillingEnabledForServer, isDesktopRuntime } from './runtime/runtime'; import { organizationAc, organizationRoles } from './auth/organization-ac'; +import { canManageOrganizationBilling } from './billing/authz'; type AuthUser = { id: string; @@ -47,11 +50,63 @@ function createAuth() { const betterAuthApiKey = process.env.BETTER_AUTH_API_KEY?.trim() || undefined; const betterAuthApiUrl = process.env.BETTER_AUTH_API_URL?.trim() || undefined; const betterAuthKvUrl = process.env.BETTER_AUTH_KV_URL?.trim() || undefined; + const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim() || ''; + const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET?.trim() || ''; + const stripeProMonthlyPriceId = process.env.STRIPE_PRO_MONTHLY_PRICE_ID?.trim() || ''; + const stripeBillingEnabled = isBillingEnabledForServer(); + const stripeClient = stripeBillingEnabled ? new Stripe(stripeSecretKey) : null; const betterAuthInfraOptions = { ...(betterAuthApiKey ? { apiKey: betterAuthApiKey } : {}), ...(betterAuthApiUrl ? { apiUrl: betterAuthApiUrl } : {}), ...(betterAuthKvUrl ? { kvUrl: betterAuthKvUrl } : {}), }; + const authPlugins = [ + jwt(), + dash({ + ...betterAuthInfraOptions, + activityTracking: { + enabled: true, + updateInterval: 300000, + }, + }), + sentinel({ + ...betterAuthInfraOptions, + security: { + credentialStuffing: { + enabled: true, + thresholds: { + challenge: 3, + block: 5, + }, + windowSeconds: 3600, + cooldownSeconds: 900, + }, + impossibleTravel: { + enabled: true, + action: 'log', + }, + botBlocking: { + action: 'challenge', + }, + suspiciousIpBlocking: { + action: 'challenge', + }, + velocity: { + enabled: true, + thresholds: { + challenge: 10, + block: 20, + }, + maxSignupsPerVisitor: 5, + maxPasswordResetsPerIp: 10, + maxSignInsPerIp: 50, + windowSeconds: 3600, + action: 'challenge', + }, + challengeDifficulty: 18, + }, + }), + ]; console.log('[auth] TRUSTED_ORIGINS =', process.env.TRUSTED_ORIGINS); @@ -77,6 +132,22 @@ function createAuth() { return Boolean(await findInitialOrganizationId(userId)); } + async function getOrganizationMemberRole(organizationId: string, userId: string) { + const [membership] = await db + .select({ role: schema.organizationMembers.role }) + .from(schema.organizationMembers) + .where( + and( + eq(schema.organizationMembers.organizationId, organizationId), + eq(schema.organizationMembers.userId, userId), + or(eq(schema.organizationMembers.status, 'active'), isNull(schema.organizationMembers.status)), + ), + ) + .limit(1); + + return membership?.role ?? null; + } + /** * Shared helper: create a default organization + member relation through better-auth. * The plugin owns the organization/member writes. @@ -119,51 +190,7 @@ function createAuth() { // }, database: drizzleAdapter(db, { provider, schema }), plugins: [ - jwt(), - dash({ - ...betterAuthInfraOptions, - activityTracking: { - enabled: true, - updateInterval: 300000, - }, - }), - sentinel({ - ...betterAuthInfraOptions, - security: { - credentialStuffing: { - enabled: true, - thresholds: { - challenge: 3, - block: 5, - }, - windowSeconds: 3600, - cooldownSeconds: 900, - }, - impossibleTravel: { - enabled: true, - action: 'log', - }, - botBlocking: { - action: 'challenge', - }, - suspiciousIpBlocking: { - action: 'challenge', - }, - velocity: { - enabled: true, - thresholds: { - challenge: 10, - block: 20, - }, - maxSignupsPerVisitor: 5, - maxPasswordResetsPerIp: 10, - maxSignInsPerIp: 50, - windowSeconds: 3600, - action: 'challenge', - }, - challengeDifficulty: 18, - }, - }), + ...authPlugins, organization({ ac: organizationAc, roles: organizationRoles, @@ -282,6 +309,99 @@ function createAuth() { }); }, }), + ...(stripeBillingEnabled + ? [ + stripePlugin({ + stripeClient: stripeClient!, + stripeWebhookSecret, + createCustomerOnSignUp: false, + organization: { + enabled: true, + getCustomerCreateParams: async (_organization, ctx) => ({ + email: ctx.context.session?.user.email ?? undefined, + }), + }, + subscription: { + enabled: true, + plans: [ + { + name: 'pro', + priceId: stripeProMonthlyPriceId, + }, + ], + getCheckoutSessionParams: async ({ user, session, subscription }) => { + if (!stripeClient || !subscription.stripeCustomerId) { + return {}; + } + + const isOrganizationSubscription = session.activeOrganizationId === subscription.referenceId; + const ownerEmail = user.email?.trim() || ''; + + if (!isOrganizationSubscription || !ownerEmail) { + return {}; + } + + try { + const stripeCustomer = await stripeClient.customers.retrieve(subscription.stripeCustomerId); + if (!stripeCustomer.deleted && stripeCustomer.email !== ownerEmail) { + await stripeClient.customers.update(subscription.stripeCustomerId, { + email: ownerEmail, + }); + } + } catch (error) { + console.warn('[auth] failed to sync organization Stripe customer email before checkout', { + customerId: subscription.stripeCustomerId, + referenceId: subscription.referenceId, + error, + }); + } + + return {}; + }, + authorizeReference: async ({ user, referenceId }) => { + const role = await getOrganizationMemberRole(referenceId, user.id); + return canManageOrganizationBilling(role); + }, + }, + schema: { + user: { + fields: { + stripeCustomerId: 'stripeCustomerId', + }, + }, + organization: { + modelName: 'organizations', + fields: { + stripeCustomerId: 'stripeCustomerId', + }, + }, + subscription: { + modelName: 'subscription', + fields: { + plan: 'plan', + referenceId: 'referenceId', + stripeCustomerId: 'stripeCustomerId', + stripeSubscriptionId: 'stripeSubscriptionId', + status: 'status', + periodStart: 'periodStart', + periodEnd: 'periodEnd', + trialStart: 'trialStart', + trialEnd: 'trialEnd', + cancelAtPeriodEnd: 'cancelAtPeriodEnd', + cancelAt: 'cancelAt', + canceledAt: 'canceledAt', + endedAt: 'endedAt', + seats: 'seats', + billingInterval: 'billingInterval', + stripeScheduleId: 'stripeScheduleId', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, + }, + }, + }), + ] + : []), ], baseURL: isDesktop && desktopOrigin ? desktopOrigin : undefined, advanced: isDesktop ? { useSecureCookies: false } : undefined, diff --git a/apps/web/lib/billing/api.ts b/apps/web/lib/billing/api.ts new file mode 100644 index 00000000..7717e284 --- /dev/null +++ b/apps/web/lib/billing/api.ts @@ -0,0 +1,132 @@ +'use client'; + +import { authClient } from '@/lib/auth-client'; +import type { OrganizationBillingStatus } from './types'; + +type FetchMethod = 'GET' | 'POST'; +const REQUEST_TIMEOUT_MS = 10000; + +type StripeRedirectResponse = { + url?: string | null; + redirect?: boolean; +}; + +async function parseResponse(response: Response): Promise { + const payload = await response.json().catch(() => null); + if (!response.ok) { + const message = + (payload && typeof payload === 'object' && 'message' in payload && typeof payload.message === 'string' && payload.message) || + (payload && typeof payload === 'object' && 'error' in payload && typeof payload.error === 'string' && payload.error) || + 'Request failed'; + throw new Error(message); + } + + return payload as T; +} + +function createRequestSignal(timeoutMs: number): AbortSignal | undefined { + if (typeof AbortSignal === 'undefined') { + return undefined; + } + + if (typeof AbortSignal.timeout === 'function') { + return AbortSignal.timeout(timeoutMs); + } + + return undefined; +} + +async function appApiRequest( + path: string, + options?: { + method?: FetchMethod; + query?: Record; + }, +): Promise { + const method = options?.method ?? 'GET'; + const url = new URL(path, window.location.origin); + + for (const [key, value] of Object.entries(options?.query ?? {})) { + if (value === null || value === undefined || value === '') continue; + url.searchParams.set(key, String(value)); + } + + const response = await fetch(url.toString(), { + method, + credentials: 'include', + signal: createRequestSignal(REQUEST_TIMEOUT_MS), + }); + + return parseResponse(response); +} + +function getBillingReturnUrl(organizationSlug: string) { + return `/${organizationSlug}/settings/billing`; +} + +function resolveStripeError(error: unknown): Error { + if (error instanceof Error) { + return error; + } + + return new Error('Billing request failed'); +} + +function assertRedirectUrl(result: { data?: StripeRedirectResponse | null; error?: { message?: string | null } | null }) { + if (result.error) { + throw new Error(result.error.message || 'Billing request failed'); + } + + const url = result.data?.url; + if (!url) { + throw new Error('Stripe did not return a redirect URL'); + } + + window.location.assign(url); +} + +export async function getOrganizationBillingStatus(organizationId: string): Promise { + const response = await appApiRequest<{ code: number; data?: { billingStatus?: OrganizationBillingStatus } }>('/api/organization/billing', { + query: { organizationId }, + }); + + if (response.code !== 0 || !response.data?.billingStatus) { + throw new Error('Failed to load billing status'); + } + + return response.data.billingStatus; +} + +export async function upgradeOrganizationToPro(organizationId: string, organizationSlug: string) { + try { + const returnUrl = getBillingReturnUrl(organizationSlug); + const result = await authClient.subscription.upgrade({ + plan: 'pro', + referenceId: organizationId, + customerType: 'organization', + successUrl: returnUrl, + cancelUrl: returnUrl, + returnUrl, + disableRedirect: true, + }); + + assertRedirectUrl(result); + } catch (error) { + throw resolveStripeError(error); + } +} + +export async function openOrganizationBillingPortal(organizationId: string, organizationSlug: string, _subscriptionId: string) { + try { + const result = await authClient.subscription.billingPortal({ + referenceId: organizationId, + customerType: 'organization', + returnUrl: getBillingReturnUrl(organizationSlug), + disableRedirect: true, + }); + + assertRedirectUrl(result); + } catch (error) { + throw resolveStripeError(error); + } +} diff --git a/apps/web/lib/billing/authz.ts b/apps/web/lib/billing/authz.ts new file mode 100644 index 00000000..5d1a450b --- /dev/null +++ b/apps/web/lib/billing/authz.ts @@ -0,0 +1,5 @@ +import type { OrganizationRole } from '@/types/organization'; + +export function canManageOrganizationBilling(role: OrganizationRole | null | undefined): boolean { + return role === 'owner'; +} diff --git a/apps/web/lib/billing/normalize.ts b/apps/web/lib/billing/normalize.ts new file mode 100644 index 00000000..1f5c19f0 --- /dev/null +++ b/apps/web/lib/billing/normalize.ts @@ -0,0 +1,53 @@ +import type { BillingSubscriptionRecord, OrganizationBillingStatus } from './types'; + +function isActiveOrTrialingSubscription(record: BillingSubscriptionRecord): boolean { + return (record.status === 'active' || record.status === 'trialing') && record.plan === 'pro'; +} + +function toIsoString(value: Date | string | null | undefined): string | null { + if (!value) { + return null; + } + + if (value instanceof Date) { + return value.toISOString(); + } + + return value; +} + +function getSortTimestamp(record: BillingSubscriptionRecord): number { + const updatedAt = new Date(record.updatedAt).getTime(); + if (!Number.isNaN(updatedAt)) { + return updatedAt; + } + + return new Date(record.createdAt).getTime(); +} + +function selectRelevantSubscription(records: BillingSubscriptionRecord[]): BillingSubscriptionRecord | null { + const activeOrTrialing = records.find(isActiveOrTrialingSubscription); + if (activeOrTrialing) { + return activeOrTrialing; + } + + return [...records].sort((left, right) => getSortTimestamp(right) - getSortTimestamp(left))[0] ?? null; +} + +export function normalizeOrganizationBillingStatus(records: BillingSubscriptionRecord[], canManageBilling: boolean): OrganizationBillingStatus { + const selected = selectRelevantSubscription(records); + const activeOrTrialing = selected ? isActiveOrTrialingSubscription(selected) : false; + + return { + plan: activeOrTrialing ? 'pro' : 'hobby', + subscriptionStatus: selected?.status ?? null, + subscriptionId: selected?.id ?? null, + stripeSubscriptionId: selected?.stripeSubscriptionId ?? null, + cancelAtPeriodEnd: selected?.cancelAtPeriodEnd ?? false, + cancelAt: toIsoString(selected?.cancelAt), + canceledAt: toIsoString(selected?.canceledAt), + endedAt: toIsoString(selected?.endedAt), + periodEnd: toIsoString(selected?.periodEnd), + isManageable: Boolean(canManageBilling && selected && (selected.stripeCustomerId || selected.stripeSubscriptionId || selected.id)), + }; +} diff --git a/apps/web/lib/billing/server.ts b/apps/web/lib/billing/server.ts new file mode 100644 index 00000000..63991d70 --- /dev/null +++ b/apps/web/lib/billing/server.ts @@ -0,0 +1,10 @@ +import 'server-only'; + +import { getDBService } from '@/lib/database'; +import { normalizeOrganizationBillingStatus } from './normalize'; + +export async function getOrganizationBillingStatus(referenceId: string, canManageBilling: boolean) { + const db = await getDBService(); + const records = await db.billing.listByReferenceId(referenceId); + return normalizeOrganizationBillingStatus(records, canManageBilling); +} diff --git a/apps/web/lib/billing/types.ts b/apps/web/lib/billing/types.ts new file mode 100644 index 00000000..8bffb592 --- /dev/null +++ b/apps/web/lib/billing/types.ts @@ -0,0 +1,40 @@ +export type OrganizationPlan = 'hobby' | 'pro'; + +export type BillingSubscriptionStatus = + | 'active' + | 'trialing' + | 'incomplete' + | 'incomplete_expired' + | 'past_due' + | 'canceled' + | 'unpaid' + | 'paused' + | string; + +export type BillingSubscriptionRecord = { + id: string; + plan: string; + stripeCustomerId: string | null; + stripeSubscriptionId: string | null; + status: BillingSubscriptionStatus; + periodEnd: Date | string | null; + cancelAtPeriodEnd: boolean | null; + cancelAt: Date | string | null; + canceledAt: Date | string | null; + endedAt: Date | string | null; + createdAt: Date | string; + updatedAt: Date | string; +}; + +export type OrganizationBillingStatus = { + plan: OrganizationPlan; + subscriptionStatus: BillingSubscriptionStatus | null; + subscriptionId: string | null; + stripeSubscriptionId: string | null; + cancelAtPeriodEnd: boolean; + cancelAt: string | null; + canceledAt: string | null; + endedAt: string | null; + periodEnd: string | null; + isManageable: boolean; +}; diff --git a/apps/web/lib/database/index.ts b/apps/web/lib/database/index.ts index 79bfe3d2..63dbd373 100644 --- a/apps/web/lib/database/index.ts +++ b/apps/web/lib/database/index.ts @@ -9,6 +9,7 @@ import { PostgresSavedQueriesRepository } from './postgres/impl/sql-console/save import { PostgresSavedQueryFoldersRepository } from './postgres/impl/sql-console/saved-query-folders'; import { PostgresAiUsageRepository } from './postgres/impl/ai-usage'; import { PostgresSyncOperationsRepository } from './postgres/impl/sync-operations'; +import { PostgresBillingRepository } from './postgres/impl/billing'; import { translateDatabase } from './i18n'; import type { AiUsageRepository } from '@/types'; @@ -27,6 +28,7 @@ export type PostgresDBService = { savedQueryFolders: PostgresSavedQueryFoldersRepository; aiUsage: AiUsageRepository; syncOperations: PostgresSyncOperationsRepository; + billing: PostgresBillingRepository; }; /** @@ -75,6 +77,9 @@ export async function getDBService(): Promise { const syncOperationsRepo = new PostgresSyncOperationsRepository(); await syncOperationsRepo.init(); + const billingRepo = new PostgresBillingRepository(); + await billingRepo.init(); + instance = { tabState: tabStateRepo, chat: chatRepo, @@ -86,6 +91,7 @@ export async function getDBService(): Promise { savedQueryFolders: savedQueryFoldersRepo, aiUsage: aiUsageRepo, syncOperations: syncOperationsRepo, + billing: billingRepo, }; break; } diff --git a/apps/web/lib/database/pglite/migrations.json b/apps/web/lib/database/pglite/migrations.json index 0c29b280..d66bda02 100644 --- a/apps/web/lib/database/pglite/migrations.json +++ b/apps/web/lib/database/pglite/migrations.json @@ -106,5 +106,18 @@ "bps": true, "folderMillis": 1774112951843, "hash": "39f6a2a2536b4a36ea2fd566e1a5cebc699e8427f88a82bae2a8253aafcb69ac" + }, + { + "sql": [ + "CREATE TABLE \"subscription\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"plan\" text NOT NULL,\n\t\"reference_id\" text NOT NULL,\n\t\"stripe_customer_id\" text,\n\t\"stripe_subscription_id\" text,\n\t\"status\" text DEFAULT 'incomplete' NOT NULL,\n\t\"period_start\" timestamp with time zone,\n\t\"period_end\" timestamp with time zone,\n\t\"trial_start\" timestamp with time zone,\n\t\"trial_end\" timestamp with time zone,\n\t\"cancel_at_period_end\" boolean DEFAULT false,\n\t\"cancel_at\" timestamp with time zone,\n\t\"canceled_at\" timestamp with time zone,\n\t\"ended_at\" timestamp with time zone,\n\t\"seats\" integer,\n\t\"billing_interval\" text,\n\t\"stripe_schedule_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"user\" ADD COLUMN \"stripe_customer_id\" text;", + "\nALTER TABLE \"organizations\" ADD COLUMN \"stripe_customer_id\" text;", + "\nCREATE INDEX \"idx_subscription_reference_id\" ON \"subscription\" USING btree (\"reference_id\");", + "\nCREATE INDEX \"idx_subscription_stripe_subscription_id\" ON \"subscription\" USING btree (\"stripe_subscription_id\");", + "\nCREATE INDEX \"idx_subscription_stripe_customer_id\" ON \"subscription\" USING btree (\"stripe_customer_id\");" + ], + "bps": true, + "folderMillis": 1774155701241, + "hash": "d5413333c9f8ca8a0c05a95db4e1361d026eb06f17ec7269919e047fbcfc30ee" } ] \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/0003_mute_human_fly.sql b/apps/web/lib/database/pglite/migrations/0003_mute_human_fly.sql new file mode 100644 index 00000000..c37a2ee0 --- /dev/null +++ b/apps/web/lib/database/pglite/migrations/0003_mute_human_fly.sql @@ -0,0 +1,27 @@ +CREATE TABLE "subscription" ( + "id" text PRIMARY KEY NOT NULL, + "plan" text NOT NULL, + "reference_id" text NOT NULL, + "stripe_customer_id" text, + "stripe_subscription_id" text, + "status" text DEFAULT 'incomplete' NOT NULL, + "period_start" timestamp with time zone, + "period_end" timestamp with time zone, + "trial_start" timestamp with time zone, + "trial_end" timestamp with time zone, + "cancel_at_period_end" boolean DEFAULT false, + "cancel_at" timestamp with time zone, + "canceled_at" timestamp with time zone, + "ended_at" timestamp with time zone, + "seats" integer, + "billing_interval" text, + "stripe_schedule_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "stripe_customer_id" text;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "stripe_customer_id" text;--> statement-breakpoint +CREATE INDEX "idx_subscription_reference_id" ON "subscription" USING btree ("reference_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_stripe_subscription_id" ON "subscription" USING btree ("stripe_subscription_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_stripe_customer_id" ON "subscription" USING btree ("stripe_customer_id"); \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/meta/0003_snapshot.json b/apps/web/lib/database/pglite/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..21aaeb70 --- /dev/null +++ b/apps/web/lib/database/pglite/migrations/meta/0003_snapshot.json @@ -0,0 +1,3627 @@ +{ + "id": "1bce950a-57e2-4591-a832-57f11348d99d", + "prevId": "a9350472-7e6c-4f4b-a361-ba63d9d8fe4a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.tabs": { + "name": "tabs", + "schema": "", + "columns": { + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sql'" + }, + "tab_name": { + "name": "tab_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New Query'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_sub_tab": { + "name": "active_sub_tab", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'data'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_meta": { + "name": "result_meta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_invitation_organization_id": { + "name": "idx_invitation_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_email": { + "name": "idx_invitation_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_status": { + "name": "idx_invitation_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_reference_id": { + "name": "idx_subscription_reference_id", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_subscription_id": { + "name": "idx_subscription_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_customer_id": { + "name": "idx_subscription_stripe_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_chat_messages_session_time": { + "name": "idx_chat_messages_session_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_session_id": { + "name": "idx_chat_messages_session_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_organization_conn_time": { + "name": "idx_chat_messages_organization_conn_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_session_state": { + "name": "chat_session_state", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_tab_id": { + "name": "active_tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editor_context": { + "name": "editor_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_run_summary": { + "name": "last_run_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stable_context": { + "name": "stable_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_chat_state_organization_conn": { + "name": "idx_chat_state_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_tab": { + "name": "idx_chat_state_organization_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_updated": { + "name": "idx_chat_state_organization_updated", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_chat_sessions_list": { + "name": "idx_chat_sessions_list", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_user_type": { + "name": "idx_chat_sessions_organization_user_type", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_conn": { + "name": "idx_chat_sessions_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_db": { + "name": "idx_chat_sessions_organization_db", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_database", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uidx_chat_sessions_copilot_tab": { + "name": "uidx_chat_sessions_copilot_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_sessions\".\"type\" = 'copilot'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_chat_sessions_id_organization": { + "name": "uq_chat_sessions_id_organization", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_chat_sessions_type_tab": { + "name": "ck_chat_sessions_type_tab", + "value": "((\"chat_sessions\".\"type\" = 'copilot' AND \"chat_sessions\".\"tab_id\" IS NOT NULL) OR (\"chat_sessions\".\"type\" <> 'copilot' AND \"chat_sessions\".\"tab_id\" IS NULL))" + } + }, + "isRLSEnabled": false + }, + "public.query_audit": { + "name": "query_audit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_name": { + "name": "connection_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_id": { + "name": "query_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_read": { + "name": "rows_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bytes_read": { + "name": "bytes_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_written": { + "name": "rows_written", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "extra_json": { + "name": "extra_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organization_created": { + "name": "idx_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_source_created": { + "name": "idx_source_created", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_query_id": { + "name": "idx_query_id", + "columns": [ + { + "expression": "query_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "members_organization_id_user_id_unique": { + "name": "members_organization_id_user_id_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_organization": { + "name": "idx_members_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_user": { + "name": "idx_members_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_user_id_fk": { + "name": "members_user_id_user_id_fk", + "tableFrom": "members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_owner_user_id_user_id_fk": { + "name": "organizations_owner_user_id_user_id_fk", + "tableFrom": "organizations", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identities": { + "name": "connection_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_conn_identity_connection_name": { + "name": "uniq_conn_identity_connection_name", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_cloud_id": { + "name": "uniq_conn_identity_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_default_per_connection": { + "name": "uniq_conn_identity_default_per_connection", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"is_default\" = true AND \"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_connection_id": { + "name": "idx_conn_identity_connection_id", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_created_by_user_id": { + "name": "idx_conn_identity_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_id": { + "name": "idx_conn_identity_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_enabled": { + "name": "idx_conn_identity_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_sync_status": { + "name": "idx_conn_identity_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_cloud_id": { + "name": "idx_conn_identity_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identity_secrets": { + "name": "connection_identity_secrets", + "schema": "", + "columns": { + "identity_id": { + "name": "identity_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_ref": { + "name": "vault_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_ref": { + "name": "secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_ssh": { + "name": "connection_ssh", + "schema": "", + "columns": { + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_method": { + "name": "auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "passphrase_encrypted": { + "name": "passphrase_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connection_ssh_port": { + "name": "chk_connection_ssh_port", + "value": "\"connection_ssh\".\"port\" IS NULL OR (\"connection_ssh\".\"port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled connection'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "http_port": { + "name": "http_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "validation_errors": { + "name": "validation_errors", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_status": { + "name": "last_check_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "last_check_at": { + "name": "last_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_latency_ms": { + "name": "last_check_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_check_error": { + "name": "last_check_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "uniq_connections_organization_name": { + "name": "uniq_connections_organization_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_connections_cloud_id": { + "name": "uniq_connections_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_id_status": { + "name": "idx_connections_organization_id_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_created_by_user_id": { + "name": "idx_connections_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_sync_status": { + "name": "idx_connections_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_cloud_id": { + "name": "idx_connections_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_env": { + "name": "idx_connections_organization_env", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connections_port": { + "name": "chk_connections_port", + "value": "\"connections\".\"port\" IS NULL OR (\"connections\".\"port\" BETWEEN 1 AND 65535)" + }, + "chk_connections_http_port": { + "name": "chk_connections_http_port", + "value": "\"connections\".\"http_port\" IS NULL OR (\"connections\".\"http_port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.ai_schema_cache": { + "name": "ai_schema_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog": { + "name": "catalog", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "db_type": { + "name": "db_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt": { + "name": "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "prompt_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_organization_conn": { + "name": "idx_ai_cache_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_catalog_db_table": { + "name": "idx_ai_cache_catalog_db_table", + "columns": [ + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_schema_hash": { + "name": "idx_ai_cache_schema_hash", + "columns": [ + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_queries": { + "name": "saved_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "work_id": { + "name": "work_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_saved_queries_organization_user": { + "name": "idx_saved_queries_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_updated_at": { + "name": "idx_saved_queries_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_folder_id": { + "name": "idx_saved_queries_folder_id", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_query_folders": { + "name": "saved_query_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_saved_query_folders_organization_user": { + "name": "idx_saved_query_folders_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_events": { + "name": "ai_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "algo_version": { + "name": "algo_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gateway": { + "name": "gateway", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "from_cache": { + "name": "from_cache", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uidx_ai_usage_events_request_id": { + "name": "uidx_ai_usage_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created": { + "name": "idx_ai_usage_events_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created_total": { + "name": "idx_ai_usage_events_organization_created_total", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_tokens", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_user_created": { + "name": "idx_ai_usage_events_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_feature_created": { + "name": "idx_ai_usage_events_feature_created", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_feature_created": { + "name": "idx_ai_usage_events_organization_feature_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_ai_usage_events_status": { + "name": "ck_ai_usage_events_status", + "value": "\"ai_usage_events\".\"status\" in ('ok', 'error', 'aborted')" + } + }, + "isRLSEnabled": false + }, + "public.ai_usage_traces": { + "name": "ai_usage_traces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_text": { + "name": "input_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_text": { + "name": "output_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_json": { + "name": "input_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_json": { + "name": "output_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redacted": { + "name": "redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now() + interval '180 days'" + } + }, + "indexes": { + "uidx_ai_usage_traces_request_id": { + "name": "uidx_ai_usage_traces_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_created": { + "name": "idx_ai_usage_traces_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_user_created": { + "name": "idx_ai_usage_traces_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_expires_at": { + "name": "idx_ai_usage_traces_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_operations": { + "name": "sync_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_operations_organization_status": { + "name": "idx_sync_operations_organization_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_entity": { + "name": "idx_sync_operations_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_created_at": { + "name": "idx_sync_operations_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/lib/database/pglite/migrations/meta/_journal.json b/apps/web/lib/database/pglite/migrations/meta/_journal.json index 13729b6d..92c0a8f9 100644 --- a/apps/web/lib/database/pglite/migrations/meta/_journal.json +++ b/apps/web/lib/database/pglite/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1774112951843, "tag": "0002_yummy_pyro", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1774155701241, + "tag": "0003_mute_human_fly", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/web/lib/database/postgres/impl/billing/index.ts b/apps/web/lib/database/postgres/impl/billing/index.ts new file mode 100644 index 00000000..57f83a41 --- /dev/null +++ b/apps/web/lib/database/postgres/impl/billing/index.ts @@ -0,0 +1,26 @@ +import { desc, eq } from 'drizzle-orm'; +import { getClient } from '@/lib/database/postgres/client'; +import { translateDatabase } from '@/lib/database/i18n'; +import { subscription } from '@/lib/database/schema'; +import { DatabaseError } from '@/lib/errors/DatabaseError'; +import type { PostgresDBClient } from '@/types'; + +export class PostgresBillingRepository { + private db!: PostgresDBClient; + + async init() { + try { + this.db = (await getClient()) as PostgresDBClient; + if (!this.db) { + throw new DatabaseError(translateDatabase('Database.Errors.ConnectionFailed'), 500); + } + } catch (error) { + console.error(translateDatabase('Database.Logs.InitFailed'), error); + throw new DatabaseError(translateDatabase('Database.Errors.InitFailed'), 500); + } + } + + async listByReferenceId(referenceId: string) { + return this.db.select().from(subscription).where(eq(subscription.referenceId, referenceId)).orderBy(desc(subscription.updatedAt), desc(subscription.createdAt)); + } +} diff --git a/apps/web/lib/database/postgres/migrations/0003_nifty_quasimodo.sql b/apps/web/lib/database/postgres/migrations/0003_nifty_quasimodo.sql new file mode 100644 index 00000000..c37a2ee0 --- /dev/null +++ b/apps/web/lib/database/postgres/migrations/0003_nifty_quasimodo.sql @@ -0,0 +1,27 @@ +CREATE TABLE "subscription" ( + "id" text PRIMARY KEY NOT NULL, + "plan" text NOT NULL, + "reference_id" text NOT NULL, + "stripe_customer_id" text, + "stripe_subscription_id" text, + "status" text DEFAULT 'incomplete' NOT NULL, + "period_start" timestamp with time zone, + "period_end" timestamp with time zone, + "trial_start" timestamp with time zone, + "trial_end" timestamp with time zone, + "cancel_at_period_end" boolean DEFAULT false, + "cancel_at" timestamp with time zone, + "canceled_at" timestamp with time zone, + "ended_at" timestamp with time zone, + "seats" integer, + "billing_interval" text, + "stripe_schedule_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "stripe_customer_id" text;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "stripe_customer_id" text;--> statement-breakpoint +CREATE INDEX "idx_subscription_reference_id" ON "subscription" USING btree ("reference_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_stripe_subscription_id" ON "subscription" USING btree ("stripe_subscription_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_stripe_customer_id" ON "subscription" USING btree ("stripe_customer_id"); \ No newline at end of file diff --git a/apps/web/lib/database/postgres/migrations/meta/0003_snapshot.json b/apps/web/lib/database/postgres/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..131ff6bf --- /dev/null +++ b/apps/web/lib/database/postgres/migrations/meta/0003_snapshot.json @@ -0,0 +1,3627 @@ +{ + "id": "8e108864-56cd-440e-9bd9-39128f6e3aa6", + "prevId": "029f0282-cef0-40b1-8cdf-5f2b990ec8f4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.tabs": { + "name": "tabs", + "schema": "", + "columns": { + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sql'" + }, + "tab_name": { + "name": "tab_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New Query'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_sub_tab": { + "name": "active_sub_tab", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'data'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_meta": { + "name": "result_meta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_invitation_organization_id": { + "name": "idx_invitation_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_email": { + "name": "idx_invitation_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_status": { + "name": "idx_invitation_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_reference_id": { + "name": "idx_subscription_reference_id", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_subscription_id": { + "name": "idx_subscription_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_stripe_customer_id": { + "name": "idx_subscription_stripe_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_chat_messages_session_time": { + "name": "idx_chat_messages_session_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_session_id": { + "name": "idx_chat_messages_session_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_messages_organization_conn_time": { + "name": "idx_chat_messages_organization_conn_time", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_session_state": { + "name": "chat_session_state", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_tab_id": { + "name": "active_tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editor_context": { + "name": "editor_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_run_summary": { + "name": "last_run_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stable_context": { + "name": "stable_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_chat_state_organization_conn": { + "name": "idx_chat_state_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_tab": { + "name": "idx_chat_state_organization_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_state_organization_updated": { + "name": "idx_chat_state_organization_updated", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_database": { + "name": "active_database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_schema": { + "name": "active_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_chat_sessions_list": { + "name": "idx_chat_sessions_list", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_user_type": { + "name": "idx_chat_sessions_organization_user_type", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_conn": { + "name": "idx_chat_sessions_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_sessions_organization_db": { + "name": "idx_chat_sessions_organization_db", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_database", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uidx_chat_sessions_copilot_tab": { + "name": "uidx_chat_sessions_copilot_tab", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tab_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_sessions\".\"type\" = 'copilot'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_chat_sessions_id_organization": { + "name": "uq_chat_sessions_id_organization", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_chat_sessions_type_tab": { + "name": "ck_chat_sessions_type_tab", + "value": "((\"chat_sessions\".\"type\" = 'copilot' AND \"chat_sessions\".\"tab_id\" IS NOT NULL) OR (\"chat_sessions\".\"type\" <> 'copilot' AND \"chat_sessions\".\"tab_id\" IS NULL))" + } + }, + "isRLSEnabled": false + }, + "public.query_audit": { + "name": "query_audit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_id": { + "name": "tab_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_name": { + "name": "connection_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_id": { + "name": "query_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_read": { + "name": "rows_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bytes_read": { + "name": "bytes_read", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rows_written": { + "name": "rows_written", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "extra_json": { + "name": "extra_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organization_created": { + "name": "idx_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_source_created": { + "name": "idx_source_created", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_query_id": { + "name": "idx_query_id", + "columns": [ + { + "expression": "query_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "members_organization_id_user_id_unique": { + "name": "members_organization_id_user_id_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_organization": { + "name": "idx_members_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_user": { + "name": "idx_members_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_user_id_fk": { + "name": "members_user_id_user_id_fk", + "tableFrom": "members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_owner_user_id_user_id_fk": { + "name": "organizations_owner_user_id_user_id_fk", + "tableFrom": "organizations", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identities": { + "name": "connection_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uniq_conn_identity_connection_name": { + "name": "uniq_conn_identity_connection_name", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_cloud_id": { + "name": "uniq_conn_identity_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_conn_identity_default_per_connection": { + "name": "uniq_conn_identity_default_per_connection", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_identities\".\"is_default\" = true AND \"connection_identities\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_connection_id": { + "name": "idx_conn_identity_connection_id", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_created_by_user_id": { + "name": "idx_conn_identity_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_id": { + "name": "idx_conn_identity_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_enabled": { + "name": "idx_conn_identity_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_sync_status": { + "name": "idx_conn_identity_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_conn_identity_organization_cloud_id": { + "name": "idx_conn_identity_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_identity_secrets": { + "name": "connection_identity_secrets", + "schema": "", + "columns": { + "identity_id": { + "name": "identity_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_ref": { + "name": "vault_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_ref": { + "name": "secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_ssh": { + "name": "connection_ssh", + "schema": "", + "columns": { + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_method": { + "name": "auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "passphrase_encrypted": { + "name": "passphrase_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connection_ssh_port": { + "name": "chk_connection_ssh_port", + "value": "\"connection_ssh\".\"port\" IS NULL OR (\"connection_ssh\".\"port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "cloud_id": { + "name": "cloud_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remote_updated_at": { + "name": "remote_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled connection'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "http_port": { + "name": "http_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "validation_errors": { + "name": "validation_errors", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_status": { + "name": "last_check_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "last_check_at": { + "name": "last_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_latency_ms": { + "name": "last_check_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_check_error": { + "name": "last_check_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "uniq_connections_organization_name": { + "name": "uniq_connections_organization_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uniq_connections_cloud_id": { + "name": "uniq_connections_cloud_id", + "columns": [ + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"cloud_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_id_status": { + "name": "idx_connections_organization_id_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_created_by_user_id": { + "name": "idx_connections_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_sync_status": { + "name": "idx_connections_sync_status", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_cloud_id": { + "name": "idx_connections_organization_cloud_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connections_organization_env": { + "name": "idx_connections_organization_env", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chk_connections_port": { + "name": "chk_connections_port", + "value": "\"connections\".\"port\" IS NULL OR (\"connections\".\"port\" BETWEEN 1 AND 65535)" + }, + "chk_connections_http_port": { + "name": "chk_connections_http_port", + "value": "\"connections\".\"http_port\" IS NULL OR (\"connections\".\"http_port\" BETWEEN 1 AND 65535)" + } + }, + "isRLSEnabled": false + }, + "public.ai_schema_cache": { + "name": "ai_schema_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog": { + "name": "catalog", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "db_type": { + "name": "db_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt": { + "name": "uniq_ai_cache_organization_conn_catalog_feature_schema_model_prompt", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "prompt_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_organization_conn": { + "name": "idx_ai_cache_organization_conn", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_catalog_db_table": { + "name": "idx_ai_cache_catalog_db_table", + "columns": [ + { + "expression": "catalog", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_cache_schema_hash": { + "name": "idx_ai_cache_schema_hash", + "columns": [ + { + "expression": "schema_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_queries": { + "name": "saved_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_text": { + "name": "sql_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "work_id": { + "name": "work_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_saved_queries_organization_user": { + "name": "idx_saved_queries_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_updated_at": { + "name": "idx_saved_queries_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_saved_queries_folder_id": { + "name": "idx_saved_queries_folder_id", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_query_folders": { + "name": "saved_query_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_saved_query_folders_organization_user": { + "name": "idx_saved_query_folders_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_events": { + "name": "ai_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "algo_version": { + "name": "algo_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gateway": { + "name": "gateway", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "from_cache": { + "name": "from_cache", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uidx_ai_usage_events_request_id": { + "name": "uidx_ai_usage_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created": { + "name": "idx_ai_usage_events_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_created_total": { + "name": "idx_ai_usage_events_organization_created_total", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_tokens", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_user_created": { + "name": "idx_ai_usage_events_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_feature_created": { + "name": "idx_ai_usage_events_feature_created", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_events_organization_feature_created": { + "name": "idx_ai_usage_events_organization_feature_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_ai_usage_events_status": { + "name": "ck_ai_usage_events_status", + "value": "\"ai_usage_events\".\"status\" in ('ok', 'error', 'aborted')" + } + }, + "isRLSEnabled": false + }, + "public.ai_usage_traces": { + "name": "ai_usage_traces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_text": { + "name": "input_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_text": { + "name": "output_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_json": { + "name": "input_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_json": { + "name": "output_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redacted": { + "name": "redacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now() + interval '180 days'" + } + }, + "indexes": { + "uidx_ai_usage_traces_request_id": { + "name": "uidx_ai_usage_traces_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_created": { + "name": "idx_ai_usage_traces_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_organization_user_created": { + "name": "idx_ai_usage_traces_organization_user_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_traces_expires_at": { + "name": "idx_ai_usage_traces_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_operations": { + "name": "sync_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_operations_organization_status": { + "name": "idx_sync_operations_organization_status", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_entity": { + "name": "idx_sync_operations_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_operations_created_at": { + "name": "idx_sync_operations_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/lib/database/postgres/migrations/meta/_journal.json b/apps/web/lib/database/postgres/migrations/meta/_journal.json index a71ad19e..f0275549 100644 --- a/apps/web/lib/database/postgres/migrations/meta/_journal.json +++ b/apps/web/lib/database/postgres/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1774112944646, "tag": "0002_silly_thunderbolt", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1774155699725, + "tag": "0003_nifty_quasimodo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/web/lib/database/postgres/schemas/auth-schema.ts b/apps/web/lib/database/postgres/schemas/auth-schema.ts index 295ddf51..603af011 100644 --- a/apps/web/lib/database/postgres/schemas/auth-schema.ts +++ b/apps/web/lib/database/postgres/schemas/auth-schema.ts @@ -1,4 +1,4 @@ -import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core'; +import { pgTable, text, timestamp, boolean, index, integer } from 'drizzle-orm/pg-core'; import { newEntityId } from '@/lib/id'; /** @@ -15,6 +15,7 @@ export const user = pgTable('user', { .$defaultFn(() => false) .notNull(), image: text('image'), + stripeCustomerId: text('stripe_customer_id'), lastActiveAt: timestamp('last_active_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), @@ -118,3 +119,35 @@ export const jwks = pgTable('jwks', { createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), expiresAt: timestamp('expires_at', { withTimezone: true }), }); + +export const subscription = pgTable( + 'subscription', + { + id: text('id') + .primaryKey() + .$defaultFn(() => newEntityId()), + plan: text('plan').notNull(), + referenceId: text('reference_id').notNull(), + stripeCustomerId: text('stripe_customer_id'), + stripeSubscriptionId: text('stripe_subscription_id'), + status: text('status').notNull().default('incomplete'), + periodStart: timestamp('period_start', { withTimezone: true }), + periodEnd: timestamp('period_end', { withTimezone: true }), + trialStart: timestamp('trial_start', { withTimezone: true }), + trialEnd: timestamp('trial_end', { withTimezone: true }), + cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false), + cancelAt: timestamp('cancel_at', { withTimezone: true }), + canceledAt: timestamp('canceled_at', { withTimezone: true }), + endedAt: timestamp('ended_at', { withTimezone: true }), + seats: integer('seats'), + billingInterval: text('billing_interval'), + stripeScheduleId: text('stripe_schedule_id'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + table => ({ + referenceIdIdx: index('idx_subscription_reference_id').on(table.referenceId), + stripeSubscriptionIdIdx: index('idx_subscription_stripe_subscription_id').on(table.stripeSubscriptionId), + stripeCustomerIdIdx: index('idx_subscription_stripe_customer_id').on(table.stripeCustomerId), + }), +); diff --git a/apps/web/lib/database/postgres/schemas/organizations/organizations.ts b/apps/web/lib/database/postgres/schemas/organizations/organizations.ts index 47b5419c..448e65fd 100644 --- a/apps/web/lib/database/postgres/schemas/organizations/organizations.ts +++ b/apps/web/lib/database/postgres/schemas/organizations/organizations.ts @@ -20,6 +20,7 @@ export const organizations = pgTable( slug: text('slug'), logo: text('logo'), metadata: text('metadata'), + stripeCustomerId: text('stripe_customer_id'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), diff --git a/apps/web/lib/database/schema.ts b/apps/web/lib/database/schema.ts index 30e6aed7..cb4a02d4 100644 --- a/apps/web/lib/database/schema.ts +++ b/apps/web/lib/database/schema.ts @@ -14,6 +14,7 @@ export const session = activeSchemas.session; export const account = activeSchemas.account; export const verification = activeSchemas.verification; export const invitation = activeSchemas.invitation; +export const subscription = activeSchemas.subscription; export const organizations = activeSchemas?.organizations; export const ai_schema_cache = activeSchemas?.aiSchemaCache; diff --git a/apps/web/lib/organization/api.ts b/apps/web/lib/organization/api.ts index 9712c5e4..7167ac5a 100644 --- a/apps/web/lib/organization/api.ts +++ b/apps/web/lib/organization/api.ts @@ -3,6 +3,7 @@ import type { OrganizationRole } from '@/types/organization'; type FetchMethod = 'GET' | 'POST'; +const REQUEST_TIMEOUT_MS = 10000; async function parseResponse(response: Response): Promise { const payload = await response.json().catch(() => null); @@ -17,6 +18,18 @@ async function parseResponse(response: Response): Promise { return payload as T; } +function createRequestSignal(timeoutMs: number): AbortSignal | undefined { + if (typeof AbortSignal === 'undefined') { + return undefined; + } + + if (typeof AbortSignal.timeout === 'function') { + return AbortSignal.timeout(timeoutMs); + } + + return undefined; +} + async function authOrganizationRequest( path: string, options?: { @@ -38,6 +51,7 @@ async function authOrganizationRequest( headers: options?.body ? { 'Content-Type': 'application/json' } : undefined, body: options?.body ? JSON.stringify(options.body) : undefined, credentials: 'include', + signal: createRequestSignal(REQUEST_TIMEOUT_MS), }); return parseResponse(response); @@ -64,6 +78,7 @@ async function appApiRequest( headers: options?.body ? { 'Content-Type': 'application/json' } : undefined, body: options?.body ? JSON.stringify(options.body) : undefined, credentials: 'include', + signal: createRequestSignal(REQUEST_TIMEOUT_MS), }); return parseResponse(response); @@ -138,7 +153,7 @@ export function slugifyOrganizationName(name: string) { .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') || 'workspace'; + .replace(/^-+|-+$/g, ''); } export async function listOrganizations() { @@ -150,22 +165,41 @@ export async function createOrganization(input: { name: string; slug: string }) method: 'POST', body: { name: input.name, - slug: input.slug, + slug: input.slug || 'workspace', keepCurrentActiveOrganization: false, }, }); } +async function resolveOrganizationIdFromQuery(query?: { organizationId?: string; organizationSlug?: string }) { + if (query?.organizationId) { + return query.organizationId; + } + + if (!query?.organizationSlug) { + return undefined; + } + + const organizations = await listOrganizations(); + const match = organizations.find(organization => organization.slug === query.organizationSlug); + + return match?.id; +} + export async function setActiveOrganization(input: { organizationId?: string; organizationSlug?: string }) { + const organizationId = await resolveOrganizationIdFromQuery(input); + return authOrganizationRequest<{ organizationId: string | null }>('/organization/set-active', { method: 'POST', - body: input, + body: organizationId ? { organizationId } : input.organizationSlug ? {} : input, }); } export async function getFullOrganization(query?: { organizationId?: string; organizationSlug?: string }) { + const organizationId = await resolveOrganizationIdFromQuery(query); + return authOrganizationRequest('/organization/get-full-organization', { - query, + query: organizationId ? { organizationId } : {}, }); } @@ -183,8 +217,10 @@ export async function updateOrganization(input: { organizationId: string; name: } export async function listMembers(query?: { organizationId?: string; organizationSlug?: string }) { + const organizationId = await resolveOrganizationIdFromQuery(query); + return authOrganizationRequest<{ members: OrganizationMember[]; total: number }>('/organization/list-members', { - query, + query: organizationId ? { organizationId } : {}, }); } diff --git a/apps/web/lib/runtime/runtime.ts b/apps/web/lib/runtime/runtime.ts index 85935455..e479ee06 100644 --- a/apps/web/lib/runtime/runtime.ts +++ b/apps/web/lib/runtime/runtime.ts @@ -23,6 +23,27 @@ export function isDesktopRuntime(): boolean { return runtime === 'desktop'; } +export function isBillingAvailableRuntimeValue(value: DoryRuntime | null | undefined): boolean { + return value === 'web' || value === 'docker'; +} + +export function isBillingAvailableRuntime(): boolean { + return isBillingAvailableRuntimeValue(runtime); +} + +export function isBillingEnabledForServer(): boolean { + const resolvedRuntime = getRuntimeForServer() ?? 'web'; + + return ( + isBillingAvailableRuntimeValue(resolvedRuntime) && + Boolean( + process.env.STRIPE_SECRET_KEY?.trim() && + process.env.STRIPE_WEBHOOK_SECRET?.trim() && + process.env.STRIPE_PRO_MONTHLY_PRICE_ID?.trim(), + ) + ); +} + export function getRuntimeForServer(): DoryRuntime | null { const raw = process.env.DORY_RUNTIME?.trim() || process.env.NEXT_PUBLIC_DORY_RUNTIME?.trim() || ''; return normalizeRuntime(raw); diff --git a/apps/web/package.json b/apps/web/package.json index e7bb2ec1..5b487e6a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -46,6 +46,7 @@ "@ai-sdk/xai": "^3.0.23", "@better-auth/infra": "^0.1.12", "@better-auth/sso": "^1.5.5", + "@better-auth/stripe": "1.5.5", "@clickhouse/client": "^1.12.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -177,6 +178,7 @@ "ssh2": "^1.17.0", "ssh2-no-cpu-features": "^2.0.0", "streamdown": "^2.1.0", + "stripe": "^20", "swr": "^2.3.3", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.7", diff --git a/apps/web/scripts/tests/billing-authz.test.ts b/apps/web/scripts/tests/billing-authz.test.ts new file mode 100644 index 00000000..11e4f1c0 --- /dev/null +++ b/apps/web/scripts/tests/billing-authz.test.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { canManageOrganizationBilling } from '../../lib/billing/authz'; + +test('organization billing management is owner only', () => { + assert.equal(canManageOrganizationBilling('owner'), true); + assert.equal(canManageOrganizationBilling('admin'), false); + assert.equal(canManageOrganizationBilling('member'), false); + assert.equal(canManageOrganizationBilling('viewer'), false); + assert.equal(canManageOrganizationBilling(null), false); +}); diff --git a/apps/web/scripts/tests/billing-normalize.test.ts b/apps/web/scripts/tests/billing-normalize.test.ts new file mode 100644 index 00000000..db060f58 --- /dev/null +++ b/apps/web/scripts/tests/billing-normalize.test.ts @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { normalizeOrganizationBillingStatus } from '../../lib/billing/normalize'; +import type { BillingSubscriptionRecord } from '../../lib/billing/types'; + +function createSubscriptionRecord(overrides: Partial): BillingSubscriptionRecord { + return { + id: 'sub_internal_1', + plan: 'pro', + stripeCustomerId: 'cus_123', + stripeSubscriptionId: 'sub_123', + status: 'incomplete', + periodEnd: '2026-03-22T00:00:00.000Z', + cancelAtPeriodEnd: false, + cancelAt: null, + canceledAt: null, + endedAt: null, + createdAt: '2026-03-21T00:00:00.000Z', + updatedAt: '2026-03-22T00:00:00.000Z', + ...overrides, + }; +} + +test('normalizes no subscriptions to hobby', () => { + assert.deepEqual(normalizeOrganizationBillingStatus([], true), { + plan: 'hobby', + subscriptionStatus: null, + subscriptionId: null, + stripeSubscriptionId: null, + cancelAtPeriodEnd: false, + cancelAt: null, + canceledAt: null, + endedAt: null, + periodEnd: null, + isManageable: false, + }); +}); + +test('normalizes active pro subscription to pro plan', () => { + const status = normalizeOrganizationBillingStatus([createSubscriptionRecord({ status: 'active' })], true); + + assert.equal(status.plan, 'pro'); + assert.equal(status.subscriptionStatus, 'active'); + assert.equal(status.subscriptionId, 'sub_internal_1'); + assert.equal(status.stripeSubscriptionId, 'sub_123'); + assert.equal(status.isManageable, true); +}); + +test('normalizes trialing pro subscription to pro plan', () => { + const status = normalizeOrganizationBillingStatus([createSubscriptionRecord({ status: 'trialing' })], true); + + assert.equal(status.plan, 'pro'); + assert.equal(status.subscriptionStatus, 'trialing'); +}); + +test('keeps canceled subscriptions on hobby while exposing status', () => { + const status = normalizeOrganizationBillingStatus( + [ + createSubscriptionRecord({ + status: 'canceled', + canceledAt: '2026-03-22T01:00:00.000Z', + endedAt: '2026-03-22T02:00:00.000Z', + }), + ], + true, + ); + + assert.equal(status.plan, 'hobby'); + assert.equal(status.subscriptionStatus, 'canceled'); + assert.equal(status.canceledAt, '2026-03-22T01:00:00.000Z'); + assert.equal(status.endedAt, '2026-03-22T02:00:00.000Z'); +}); + +test('keeps incomplete subscriptions on hobby while exposing status', () => { + const status = normalizeOrganizationBillingStatus([createSubscriptionRecord({ status: 'incomplete' })], true); + + assert.equal(status.plan, 'hobby'); + assert.equal(status.subscriptionStatus, 'incomplete'); +}); + +test('prefers active subscription over newer canceled subscription', () => { + const status = normalizeOrganizationBillingStatus( + [ + createSubscriptionRecord({ + id: 'sub_active', + status: 'active', + updatedAt: '2026-03-21T00:00:00.000Z', + }), + createSubscriptionRecord({ + id: 'sub_canceled', + status: 'canceled', + updatedAt: '2026-03-22T00:00:00.000Z', + }), + ], + true, + ); + + assert.equal(status.plan, 'pro'); + assert.equal(status.subscriptionId, 'sub_active'); +}); diff --git a/apps/web/scripts/tests/runtime-billing.test.ts b/apps/web/scripts/tests/runtime-billing.test.ts new file mode 100644 index 00000000..a7d5a9f3 --- /dev/null +++ b/apps/web/scripts/tests/runtime-billing.test.ts @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { isBillingAvailableRuntimeValue, normalizeRuntime } from '../../lib/runtime/runtime'; + +test('normalizeRuntime resolves known runtime values', () => { + assert.equal(normalizeRuntime('desktop'), 'desktop'); + assert.equal(normalizeRuntime('web'), 'web'); + assert.equal(normalizeRuntime('docker'), 'docker'); +}); + +test('isBillingAvailableRuntimeValue only enables web and docker', () => { + assert.equal(isBillingAvailableRuntimeValue('desktop'), false); + assert.equal(isBillingAvailableRuntimeValue('web'), true); + assert.equal(isBillingAvailableRuntimeValue('docker'), true); + assert.equal(isBillingAvailableRuntimeValue(null), false); +}); + +test('normalizeRuntime rejects unsupported values', () => { + assert.equal(normalizeRuntime('mobile'), null); + assert.equal(normalizeRuntime(''), null); +}); diff --git a/package.json b/package.json index d8c2e3d2..9d5c3654 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "test:e2e:docker": "PLAYWRIGHT_INCLUDE_DOCKER=1 playwright test --grep @docker", "test:e2e:demo-flow": "playwright test tests/e2e/demo-connection-sql-console.spec.ts --project=chromium --no-deps", "test:e2e:demo-flow:record": "node ./scripts/run-demo-flow-recording.mjs", - "release-notes": "yarn workspace web run release-notes" + "release-notes": "yarn workspace web run release-notes", + "stripe": "stripe listen --forward-to localhost:3000/api/auth/stripe/webhook" }, "workspaces": [ "apps/*", diff --git a/yarn.lock b/yarn.lock index 56190296..537fa207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1951,6 +1951,21 @@ __metadata: languageName: node linkType: hard +"@better-auth/stripe@npm:1.5.5": + version: 1.5.5 + resolution: "@better-auth/stripe@npm:1.5.5" + dependencies: + defu: "npm:^6.1.4" + zod: "npm:^4.3.6" + peerDependencies: + "@better-auth/core": 1.5.5 + better-auth: 1.5.5 + better-call: 1.3.2 + stripe: ^18 || ^19 || ^20 + checksum: 10c0/a9065df5bac8fe25e39985034bbf2963b7ec2b07adbbb3c0265c8acdb7662c1e3e69a468d1a0eb9725980471a589ffdf49d42eec685b4485f0f9e99d7241bccb + languageName: node + linkType: hard + "@better-auth/telemetry@npm:1.5.5": version: 1.5.5 resolution: "@better-auth/telemetry@npm:1.5.5" @@ -21788,6 +21803,18 @@ __metadata: languageName: node linkType: hard +"stripe@npm:^20": + version: 20.4.1 + resolution: "stripe@npm:20.4.1" + peerDependencies: + "@types/node": ">=16" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/c4ca1099d70afd175b27ec1fc83f72230c46a95868b85d615a141a806e63dd6d1896fa239f593aca7ad110bb8a14a5fb136b8091453c1c5db663b07529e0f94b + languageName: node + linkType: hard + "strnum@npm:^2.1.2": version: 2.2.0 resolution: "strnum@npm:2.2.0" @@ -23172,6 +23199,7 @@ __metadata: "@ai-sdk/xai": "npm:^3.0.23" "@better-auth/infra": "npm:^0.1.12" "@better-auth/sso": "npm:^1.5.5" + "@better-auth/stripe": "npm:1.5.5" "@clickhouse/client": "npm:^1.12.0" "@dnd-kit/core": "npm:^6.3.1" "@dnd-kit/modifiers": "npm:^9.0.0" @@ -23317,6 +23345,7 @@ __metadata: ssh2: "npm:^1.17.0" ssh2-no-cpu-features: "npm:^2.0.0" streamdown: "npm:^2.1.0" + stripe: "npm:^20" swr: "npm:^2.3.3" tailwind-merge: "npm:^3.0.1" tailwindcss: "npm:^4.0.7"