From 56722fc54cce3210410ac47f82a454fcefdb5649 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 12 Feb 2026 13:02:19 -0800 Subject: [PATCH 1/8] refactor: build list to rpc --- bun.lock | 32 +- .../api/repositories/builds.repository.ts | 125 ++++--- src/types/database.types.ts | 313 ++++++++++-------- 3 files changed, 241 insertions(+), 229 deletions(-) diff --git a/bun.lock b/bun.lock index bac94f6bd..d40af47da 100644 --- a/bun.lock +++ b/bun.lock @@ -72,7 +72,7 @@ "e2b": "^2.7.0", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", - "fast-xml-parser": "^4.5.1", + "fast-xml-parser": "^5.3.5", "file-type": "^21.3.0", "geist": "^1.3.1", "immer": "^10.1.1", @@ -80,7 +80,7 @@ "micromatch": "^4.0.8", "motion": "^12.18.1", "nanoid": "^5.0.9", - "next": "16.0.11", + "next": "16.1.5", "next-safe-action": "^8.0.11", "next-themes": "^0.4.6", "nuqs": "^2.7.0", @@ -470,25 +470,25 @@ "@next-safe-action/adapter-react-hook-form": ["@next-safe-action/adapter-react-hook-form@2.0.0", "", { "peerDependencies": { "@hookform/resolvers": ">= 5.0.0", "next": ">= 14.0.0", "next-safe-action": ">= 8.0.0", "react": ">= 18.2.0", "react-dom": ">= 18.2.0", "react-hook-form": ">= 7.0.0" } }, "sha512-vp3KAd5NKnBBCF/3b8kDxEYj0+BUYVjlSZOWHikz/Y2O+yLheRmtj8Vcm379itlqGUKcLV5O2o/dwKHvH8pYog=="], - "@next/env": ["@next/env@16.0.11", "", {}, "sha512-hULMheQaOhFK1vAoFPigXca42LguwyLILtJKPRzpY1d+og6jk0YNAQVwLGNYYhWEMd2zj4gcIWSf1yC5PffqqA=="], + "@next/env": ["@next/env@16.1.5", "", {}, "sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3G7Rx6m6tgLqkc3Ce3QY/Yrsx7nJF4ithdHfx70Jmzel8m2xpjnGRC+oB4UcCHvQwN0ZP5YsLJakwx/M0vWbSQ=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-poUTsYKRwuG+eApDngouEiN6AGcAMq8TAQYP8Nou7iMS7x6+q3dFhhyhgodIzTF9acsEINl4cIzMaM9XJor8kw=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q9shvB+eLNrK/n8w+/ZTWSzbEIzJ56mP83ZVaqmHay6/Ulcn6THEId4gxfYCXmSwEG/xPAtv58FBWeZkp36XUA=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-rq+d/a0FZHVPEh3zismoQgfVkSIEzlTbNhD4Z8bToLMszUlggAh1D1syhJ4MHkYzXRszhjS2emy0PYXz7Uwttw=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-82Wroterii1p15O+ZF/DDsHPuxKptR1JGK+obgbAk13vrc3B/fTJ2qOOmdeoMwAQ15gb/9mN4LQl9+IzFje76Q=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YK9RoeZuHWBd+wHi5/7VLp6P5ZOldAjQfBjjtzcR4f14FNmwT0a3ozMMlG2txDxh53krAd5yOO601RbJxH0gCQ=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-pcDMpSckekV8xj2SSKO8PaqaJhrmDx84zUNip0kOWsT/ERhhDpnWkr6KXMqRXVp2y5CW9pp4LwOFdtpt3rhRgw=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.11", "", { "os": "win32", "cpu": "x64" }, "sha512-Zzo9NLLRzBSHw9zOGpER/gdc5rofZHLjR2OIUIfoBaN2Oo5zWRl43IF5rMSX2LX7MPLTx4Ww8+5lNHAhXgitnA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1762,7 +1762,7 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], + "fast-xml-parser": ["fast-xml-parser@5.3.5", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-JeaA2Vm9ffQKp9VjvfzObuMCjUYAp5WDYhRYL5LrBPY/jUDlUtOvDfot0vKSkB9tuX885BDHjtw4fZadD95wnA=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], @@ -2256,7 +2256,7 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "next": ["next@16.0.11", "", { "dependencies": { "@next/env": "16.0.11", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.11", "@next/swc-darwin-x64": "16.0.11", "@next/swc-linux-arm64-gnu": "16.0.11", "@next/swc-linux-arm64-musl": "16.0.11", "@next/swc-linux-x64-gnu": "16.0.11", "@next/swc-linux-x64-musl": "16.0.11", "@next/swc-win32-arm64-msvc": "16.0.11", "@next/swc-win32-x64-msvc": "16.0.11", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Xlo2aFWaoypPzXr4PFLSNmxrzNptlp+hgxnG9Y2THYvHrvmXIuHUyNAWO6Q+F4rm4/bmTOukprXEyF/j4qsC2A=="], + "next": ["next@16.1.5", "", { "dependencies": { "@next/env": "16.1.5", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.5", "@next/swc-darwin-x64": "16.1.5", "@next/swc-linux-arm64-gnu": "16.1.5", "@next/swc-linux-arm64-musl": "16.1.5", "@next/swc-linux-x64-gnu": "16.1.5", "@next/swc-linux-x64-musl": "16.1.5", "@next/swc-win32-arm64-msvc": "16.1.5", "@next/swc-win32-x64-msvc": "16.1.5", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w=="], "next-safe-action": ["next-safe-action@8.0.11", "", { "peerDependencies": { "next": ">= 14.0.0", "react": ">= 18.2.0", "react-dom": ">= 18.2.0" } }, "sha512-gqJLmnQLAoFCq1kRBopN46New+vx1n9J9Y/qDQLXpv/VqU40AWxDakvshwwnWAt8R0kLvlakNYNLX5PqlXWSMg=="], @@ -2660,7 +2660,7 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], @@ -2940,6 +2940,8 @@ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "@google-cloud/storage/fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], + "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], @@ -3472,6 +3474,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@google-cloud/storage/fast-xml-parser/strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw=="], "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts index 7dc0d2740..ff5072ad3 100644 --- a/src/server/api/repositories/builds.repository.ts +++ b/src/server/api/repositories/builds.repository.ts @@ -2,6 +2,7 @@ import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' import { supabaseAdmin } from '@/lib/clients/supabase/admin' +import type { Database } from '@/types/database.types' import { TRPCError } from '@trpc/server' import z from 'zod' import { apiError } from '../errors' @@ -9,7 +10,6 @@ import { ListedBuildDTO, mapDatabaseBuildReasonToListedBuildDTOStatusMessage, mapDatabaseBuildStatusToBuildStatusDTO, - mapDatabaseBuildToListedBuildDTO, type BuildStatusDB, type RunningBuildStatusDTO, } from '../models/builds.models' @@ -20,27 +20,31 @@ function isUUID(value: string): boolean { return z.uuid().safeParse(value).success } -async function resolveTemplateId( - templateIdOrAlias: string, - teamId: string -): Promise { - const { data: envById } = await supabaseAdmin - .from('envs') - .select('id') - .eq('id', templateIdOrAlias) - .eq('team_id', teamId) - .maybeSingle() +const CURSOR_SEPARATOR = '|' - if (envById) return envById.id +function decodeCursor(cursor?: string): { + cursorCreatedAt: string | null + cursorId: string | null +} { + if (!cursor) { + return { cursorCreatedAt: null, cursorId: null } + } - const { data: envByAlias } = await supabaseAdmin - .from('env_aliases') - .select('env_id, envs!inner(team_id)') - .eq('alias', templateIdOrAlias) - .eq('envs.team_id', teamId) - .maybeSingle() + // Backward-compatible with old cursor format (created_at only) + if (!cursor.includes(CURSOR_SEPARATOR)) { + return { cursorCreatedAt: cursor, cursorId: null } + } - return envByAlias?.env_id ?? null + const [cursorCreatedAtRaw, cursorIdRaw] = cursor.split(CURSOR_SEPARATOR, 2) + + return { + cursorCreatedAt: cursorCreatedAtRaw || null, + cursorId: cursorIdRaw && isUUID(cursorIdRaw) ? cursorIdRaw : null, + } +} + +function encodeCursor(createdAt: string, id: string): string { + return `${createdAt}${CURSOR_SEPARATOR}${id}` } // list builds @@ -55,6 +59,24 @@ interface ListBuildsResult { nextCursor: string | null } +type ListTeamBuildsRpcRow = + Database['public']['Functions']['list_team_builds_rpc']['Returns'][number] + +function mapRpcBuildToListedBuildDTO(build: ListTeamBuildsRpcRow): ListedBuildDTO { + return { + id: build.id, + template: build.template_alias ?? build.template_id, + templateId: build.template_id, + status: mapDatabaseBuildStatusToBuildStatusDTO(build.status as BuildStatusDB), + statusMessage: mapDatabaseBuildReasonToListedBuildDTOStatusMessage( + build.status, + build.reason + ), + createdAt: new Date(build.created_at).getTime(), + finishedAt: build.finished_at ? new Date(build.finished_at).getTime() : null, + } +} + async function listBuilds( teamId: string, buildIdOrTemplate?: string, @@ -62,55 +84,19 @@ async function listBuilds( options: ListBuildsOptions = {} ): Promise { const limit = options.limit ?? 50 + const { cursorCreatedAt, cursorId } = decodeCursor(options.cursor) - let query = supabaseAdmin - .from('env_builds') - .select( - ` - id, - env_id, - status, - reason, - created_at, - finished_at, - envs!inner( - id, - team_id, - env_aliases(alias) - ) - ` - ) - .eq('envs.team_id', teamId) - .in('status', statuses) - .order('created_at', { ascending: false }) - - if (buildIdOrTemplate) { - const resolvedEnvId = await resolveTemplateId(buildIdOrTemplate, teamId) - const isBuildUUID = isUUID(buildIdOrTemplate) - - if (!resolvedEnvId && !isBuildUUID) { - return { - data: [], - nextCursor: null, - } - } - - if (resolvedEnvId && isBuildUUID) { - query = query.or(`env_id.eq.${resolvedEnvId},id.eq.${buildIdOrTemplate}`) - } else if (resolvedEnvId) { - query = query.eq('env_id', resolvedEnvId) - } else if (isBuildUUID) { - query = query.eq('id', buildIdOrTemplate) + const { data: rawBuilds, error } = await supabaseAdmin.rpc( + 'list_team_builds_rpc', + { + p_team_id: teamId, + p_statuses: statuses, + p_limit: limit, + p_cursor_created_at: cursorCreatedAt ?? undefined, + p_cursor_id: cursorId ?? undefined, + p_build_id_or_template: buildIdOrTemplate?.trim() || undefined, } - } - - if (options.cursor) { - query = query.lt('created_at', options.cursor) - } - - query = query.limit(limit + 1) - - const { data: rawBuilds, error } = await query + ) if (error) { throw error @@ -127,9 +113,12 @@ async function listBuilds( const trimmedBuilds = hasMore ? rawBuilds.slice(0, limit) : rawBuilds return { - data: trimmedBuilds.map(mapDatabaseBuildToListedBuildDTO), + data: trimmedBuilds.map(mapRpcBuildToListedBuildDTO), nextCursor: hasMore - ? trimmedBuilds[trimmedBuilds.length - 1]!.created_at + ? encodeCursor( + trimmedBuilds[trimmedBuilds.length - 1]!.created_at, + trimmedBuilds[trimmedBuilds.length - 1]!.id + ) : null, } } diff --git a/src/types/database.types.ts b/src/types/database.types.ts index b0e743b42..628e34d47 100644 --- a/src/types/database.types.ts +++ b/src/types/database.types.ts @@ -10,7 +10,7 @@ export type Database = { // Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) __InternalSupabase: { - PostgrestVersion: "10.2.0 (e07807d)" + PostgrestVersion: '10.2.0 (e07807d)' } public: { Tables: { @@ -71,11 +71,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "access_tokens_users_access_tokens" - columns: ["user_id"] + foreignKeyName: 'access_tokens_users_access_tokens' + columns: ['user_id'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -127,25 +127,25 @@ export type Database = { } Relationships: [ { - foreignKeyName: "addons_teams_addons" - columns: ["team_id"] + foreignKeyName: 'addons_teams_addons' + columns: ['team_id'] isOneToOne: false - referencedRelation: "team_limits" - referencedColumns: ["id"] + referencedRelation: 'team_limits' + referencedColumns: ['id'] }, { - foreignKeyName: "addons_teams_addons" - columns: ["team_id"] + foreignKeyName: 'addons_teams_addons' + columns: ['team_id'] isOneToOne: false - referencedRelation: "teams" - referencedColumns: ["id"] + referencedRelation: 'teams' + referencedColumns: ['id'] }, { - foreignKeyName: "addons_users_addons" - columns: ["added_by"] + foreignKeyName: 'addons_users_addons' + columns: ['added_by'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -197,11 +197,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "env_aliases_envs_env_aliases" - columns: ["env_id"] + foreignKeyName: 'env_aliases_envs_env_aliases' + columns: ['env_id'] isOneToOne: false - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, ] } @@ -232,18 +232,18 @@ export type Database = { } Relationships: [ { - foreignKeyName: "fk_env_build_assignments_build" - columns: ["build_id"] + foreignKeyName: 'fk_env_build_assignments_build' + columns: ['build_id'] isOneToOne: false - referencedRelation: "env_builds" - referencedColumns: ["id"] + referencedRelation: 'env_builds' + referencedColumns: ['id'] }, { - foreignKeyName: "fk_env_build_assignments_env" - columns: ["env_id"] + foreignKeyName: 'fk_env_build_assignments_env' + columns: ['env_id'] isOneToOne: false - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, ] } @@ -328,11 +328,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "env_builds_envs_builds" - columns: ["env_id"] + foreignKeyName: 'env_builds_envs_builds' + columns: ['env_id'] isOneToOne: false - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, ] } @@ -351,11 +351,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "env_defaults_env_id_fkey" - columns: ["env_id"] + foreignKeyName: 'env_defaults_env_id_fkey' + columns: ['env_id'] isOneToOne: true - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, ] } @@ -398,32 +398,32 @@ export type Database = { } Relationships: [ { - foreignKeyName: "envs_cluster_id_fkey" - columns: ["cluster_id"] + foreignKeyName: 'envs_cluster_id_fkey' + columns: ['cluster_id'] isOneToOne: false - referencedRelation: "clusters" - referencedColumns: ["id"] + referencedRelation: 'clusters' + referencedColumns: ['id'] }, { - foreignKeyName: "envs_teams_envs" - columns: ["team_id"] + foreignKeyName: 'envs_teams_envs' + columns: ['team_id'] isOneToOne: false - referencedRelation: "team_limits" - referencedColumns: ["id"] + referencedRelation: 'team_limits' + referencedColumns: ['id'] }, { - foreignKeyName: "envs_teams_envs" - columns: ["team_id"] + foreignKeyName: 'envs_teams_envs' + columns: ['team_id'] isOneToOne: false - referencedRelation: "teams" - referencedColumns: ["id"] + referencedRelation: 'teams' + referencedColumns: ['id'] }, { - foreignKeyName: "envs_users_created_envs" - columns: ["created_by"] + foreignKeyName: 'envs_users_created_envs' + columns: ['created_by'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -451,11 +451,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "feedback_user_id_fkey" - columns: ["user_id"] + foreignKeyName: 'feedback_user_id_fkey' + columns: ['user_id'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -507,32 +507,32 @@ export type Database = { } Relationships: [ { - foreignKeyName: "fk_snapshots_team" - columns: ["team_id"] + foreignKeyName: 'fk_snapshots_team' + columns: ['team_id'] isOneToOne: false - referencedRelation: "team_limits" - referencedColumns: ["id"] + referencedRelation: 'team_limits' + referencedColumns: ['id'] }, { - foreignKeyName: "fk_snapshots_team" - columns: ["team_id"] + foreignKeyName: 'fk_snapshots_team' + columns: ['team_id'] isOneToOne: false - referencedRelation: "teams" - referencedColumns: ["id"] + referencedRelation: 'teams' + referencedColumns: ['id'] }, { - foreignKeyName: "snapshots_envs_base_env_id" - columns: ["base_env_id"] + foreignKeyName: 'snapshots_envs_base_env_id' + columns: ['base_env_id'] isOneToOne: false - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, { - foreignKeyName: "snapshots_envs_env_id" - columns: ["env_id"] + foreignKeyName: 'snapshots_envs_env_id' + columns: ['env_id'] isOneToOne: false - referencedRelation: "envs" - referencedColumns: ["id"] + referencedRelation: 'envs' + referencedColumns: ['id'] }, ] } @@ -581,25 +581,25 @@ export type Database = { } Relationships: [ { - foreignKeyName: "team_api_keys_teams_team_api_keys" - columns: ["team_id"] + foreignKeyName: 'team_api_keys_teams_team_api_keys' + columns: ['team_id'] isOneToOne: false - referencedRelation: "team_limits" - referencedColumns: ["id"] + referencedRelation: 'team_limits' + referencedColumns: ['id'] }, { - foreignKeyName: "team_api_keys_teams_team_api_keys" - columns: ["team_id"] + foreignKeyName: 'team_api_keys_teams_team_api_keys' + columns: ['team_id'] isOneToOne: false - referencedRelation: "teams" - referencedColumns: ["id"] + referencedRelation: 'teams' + referencedColumns: ['id'] }, { - foreignKeyName: "team_api_keys_users_created_api_keys" - columns: ["created_by"] + foreignKeyName: 'team_api_keys_users_created_api_keys' + columns: ['created_by'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -648,18 +648,18 @@ export type Database = { } Relationships: [ { - foreignKeyName: "teams_cluster_id_fkey" - columns: ["cluster_id"] + foreignKeyName: 'teams_cluster_id_fkey' + columns: ['cluster_id'] isOneToOne: false - referencedRelation: "clusters" - referencedColumns: ["id"] + referencedRelation: 'clusters' + referencedColumns: ['id'] }, { - foreignKeyName: "teams_tiers_teams" - columns: ["tier"] + foreignKeyName: 'teams_tiers_teams' + columns: ['tier'] isOneToOne: false - referencedRelation: "tiers" - referencedColumns: ["id"] + referencedRelation: 'tiers' + referencedColumns: ['id'] }, ] } @@ -720,11 +720,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "users_id_fkey" - columns: ["id"] + foreignKeyName: 'users_id_fkey' + columns: ['id'] isOneToOne: true - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -755,32 +755,32 @@ export type Database = { } Relationships: [ { - foreignKeyName: "users_teams_added_by_user" - columns: ["added_by"] + foreignKeyName: 'users_teams_added_by_user' + columns: ['added_by'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, { - foreignKeyName: "users_teams_teams_teams" - columns: ["team_id"] + foreignKeyName: 'users_teams_teams_teams' + columns: ['team_id'] isOneToOne: false - referencedRelation: "team_limits" - referencedColumns: ["id"] + referencedRelation: 'team_limits' + referencedColumns: ['id'] }, { - foreignKeyName: "users_teams_teams_teams" - columns: ["team_id"] + foreignKeyName: 'users_teams_teams_teams' + columns: ['team_id'] isOneToOne: false - referencedRelation: "teams" - referencedColumns: ["id"] + referencedRelation: 'teams' + referencedColumns: ['id'] }, { - foreignKeyName: "users_teams_users_users" - columns: ["user_id"] + foreignKeyName: 'users_teams_users_users' + columns: ['user_id'] isOneToOne: false - referencedRelation: "auth_users" - referencedColumns: ["id"] + referencedRelation: 'auth_users' + referencedColumns: ['id'] }, ] } @@ -823,6 +823,25 @@ export type Database = { Args: { team_id: string; user_id: string } Returns: undefined } + list_team_builds_rpc: { + Args: { + p_build_id_or_template?: string + p_cursor_created_at?: string + p_cursor_id?: string + p_limit?: number + p_statuses?: string[] + p_team_id: string + } + Returns: { + created_at: string + finished_at: string + id: string + reason: Json + status: string + template_alias: string + template_id: string + }[] + } generate_access_token: { Args: never; Returns: string } generate_sandbox_video_stream_token: { Args: never; Returns: string } generate_team_api_key: { Args: never; Returns: string } @@ -835,10 +854,10 @@ export type Database = { normalize_email: { Args: { email: string }; Returns: string } temp_create_access_token: { Args: never; Returns: string } try_cast_uuid: { Args: { p_value: string }; Returns: string } - unaccent: { Args: { "": string }; Returns: string } + unaccent: { Args: { '': string }; Returns: string } } Enums: { - deployment_state: "generating" | "deploying" | "finished" | "error" + deployment_state: 'generating' | 'deploying' | 'finished' | 'error' } CompositeTypes: { [_ in never]: never @@ -846,33 +865,33 @@ export type Database = { } } -type DatabaseWithoutInternals = Omit +type DatabaseWithoutInternals = Omit -type DefaultSchema = DatabaseWithoutInternals[Extract] +type DefaultSchema = DatabaseWithoutInternals[Extract] export type Tables< DefaultSchemaTableNameOrOptions extends - | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views']) : never = never, > = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { + ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & + DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { Row: infer R } ? R : never - : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & - DefaultSchema["Views"]) - ? (DefaultSchema["Tables"] & - DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & + DefaultSchema['Views']) + ? (DefaultSchema['Tables'] & + DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { Row: infer R } ? R @@ -881,23 +900,23 @@ export type Tables< export type TablesInsert< DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] + | keyof DefaultSchema['Tables'] | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, > = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { Insert: infer I } ? I : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { Insert: infer I } ? I @@ -906,23 +925,23 @@ export type TablesInsert< export type TablesUpdate< DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] + | keyof DefaultSchema['Tables'] | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] : never = never, > = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { Update: infer U } ? U : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] + ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { Update: infer U } ? U @@ -931,42 +950,42 @@ export type TablesUpdate< export type Enums< DefaultSchemaEnumNameOrOptions extends - | keyof DefaultSchema["Enums"] + | keyof DefaultSchema['Enums'] | { schema: keyof DatabaseWithoutInternals }, EnumName extends DefaultSchemaEnumNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] : never = never, > = DefaultSchemaEnumNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] - : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] - ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] + ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] + ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] : never export type CompositeTypes< PublicCompositeTypeNameOrOptions extends - | keyof DefaultSchema["CompositeTypes"] + | keyof DefaultSchema['CompositeTypes'] | { schema: keyof DatabaseWithoutInternals }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] : never = never, > = PublicCompositeTypeNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] - : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] - ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] + ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] : never export const Constants = { public: { Enums: { - deployment_state: ["generating", "deploying", "finished", "error"], + deployment_state: ['generating', 'deploying', 'finished', 'error'], }, }, } as const From 4793fa4a4e309f9c1b57e2f2175718cba8234369 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 12 Feb 2026 13:02:30 -0800 Subject: [PATCH 2/8] add: migration --- migrations/20260212120822.sql | 154 ++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 migrations/20260212120822.sql diff --git a/migrations/20260212120822.sql b/migrations/20260212120822.sql new file mode 100644 index 000000000..5cb71fc06 --- /dev/null +++ b/migrations/20260212120822.sql @@ -0,0 +1,154 @@ +-- Timestamp: 20260212120822 + +BEGIN; + +CREATE OR REPLACE FUNCTION public.list_team_builds_rpc( + p_team_id uuid, + p_statuses text[] DEFAULT ARRAY['waiting', 'building', 'uploaded', 'failed']::text[], + p_limit integer DEFAULT 50, + p_cursor_created_at timestamptz DEFAULT NULL, + p_cursor_id uuid DEFAULT NULL, + p_build_id_or_template text DEFAULT NULL +) +RETURNS TABLE ( + id uuid, + status text, + reason jsonb, + created_at timestamptz, + finished_at timestamptz, + template_id text, + template_alias text +) +LANGUAGE sql +STABLE +SECURITY INVOKER +SET search_path = pg_catalog, public +AS $function$ +WITH params AS ( + SELECT + p_team_id AS team_id, + CASE + WHEN p_statuses IS NULL OR CARDINALITY(p_statuses) = 0 + THEN ARRAY['waiting', 'building', 'uploaded', 'failed']::text[] + ELSE p_statuses + END AS statuses, + GREATEST(1, LEAST(COALESCE(p_limit, 50), 100)) AS requested_limit, + p_cursor_created_at AS cursor_created_at, + p_cursor_id AS cursor_id, + NULLIF(BTRIM(p_build_id_or_template), '') AS search_term +), +resolved AS ( + SELECT + p.*, + CASE + WHEN p.search_term ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + THEN p.search_term::uuid + ELSE NULL + END AS candidate_build_id, + ( + SELECT e.id + FROM public.envs e + WHERE e.team_id = p.team_id + AND e.id = p.search_term + LIMIT 1 + ) AS resolved_template_id_by_id + FROM params p +), +resolved_with_alias AS ( + SELECT + r.*, + COALESCE( + r.resolved_template_id_by_id, + ( + SELECT ea.env_id + FROM public.env_aliases ea + JOIN public.envs e ON e.id = ea.env_id + WHERE e.team_id = r.team_id + AND ea.alias = r.search_term + ORDER BY ea.id ASC + LIMIT 1 + ) + ) AS resolved_template_id + FROM resolved r +), +page_ids AS ( + SELECT DISTINCT ON (b.created_at, b.id) + b.id, + b.created_at, + a.env_id + FROM resolved_with_alias f + JOIN public.envs e + ON e.team_id = f.team_id + JOIN public.env_build_assignments a + ON a.env_id = e.id + JOIN public.env_builds b + ON b.id = a.build_id + WHERE b.status = ANY (f.statuses) + AND ( + f.cursor_created_at IS NULL + OR (f.cursor_id IS NULL AND b.created_at < f.cursor_created_at) + OR ( + f.cursor_id IS NOT NULL + AND (b.created_at, b.id) < (f.cursor_created_at, f.cursor_id) + ) + ) + AND ( + f.search_term IS NULL + OR ( + f.resolved_template_id IS NOT NULL + AND f.candidate_build_id IS NOT NULL + AND (a.env_id = f.resolved_template_id OR b.id = f.candidate_build_id) + ) + OR ( + f.resolved_template_id IS NOT NULL + AND f.candidate_build_id IS NULL + AND a.env_id = f.resolved_template_id + ) + OR ( + f.resolved_template_id IS NULL + AND f.candidate_build_id IS NOT NULL + AND b.id = f.candidate_build_id + ) + ) + ORDER BY + b.created_at DESC, + b.id DESC, + a.created_at DESC NULLS LAST, + a.id DESC + LIMIT (SELECT requested_limit + 1 FROM params) +), +page_data AS ( + SELECT + p.id, + b.status, + b.reason::jsonb AS reason, + p.created_at, + b.finished_at, + p.env_id AS template_id + FROM page_ids p + JOIN public.env_builds b + ON b.id = p.id +) +SELECT + d.id, + d.status, + d.reason, + d.created_at, + d.finished_at, + d.template_id, + ea.alias AS template_alias +FROM page_data d +LEFT JOIN LATERAL ( + SELECT x.alias + FROM public.env_aliases x + WHERE x.env_id = d.template_id + ORDER BY x.id ASC + LIMIT 1 +) ea ON TRUE +ORDER BY d.created_at DESC, d.id DESC; +$function$; + +REVOKE ALL ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) TO service_role; + +COMMIT; From bc719aa9a1e1eead84f87c218984433074a8e72f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 21:30:19 +0000 Subject: [PATCH 3/8] chore: remove unused listed build db mapper Co-authored-by: Ben Fornefeld --- src/server/api/models/builds.models.ts | 39 -------------------------- 1 file changed, 39 deletions(-) diff --git a/src/server/api/models/builds.models.ts b/src/server/api/models/builds.models.ts index 48023c77f..73212da92 100644 --- a/src/server/api/models/builds.models.ts +++ b/src/server/api/models/builds.models.ts @@ -46,22 +46,6 @@ export interface BuildDetailsDTO { hasRetainedLogs: boolean } -// database queries - -type RawListedBuildWithEnvAndAliasesDB = { - id: string - env_id: string - status: string - reason: unknown - created_at: string - finished_at: string | null - envs: { - id: string - team_id: string - env_aliases: Array<{ alias: string }> | null - } -} - // mappings export function checkIfBuildStillHasLogs(createdAt: number): boolean { @@ -79,29 +63,6 @@ export function mapDatabaseBuildReasonToListedBuildDTOStatusMessage( return reason.message } -export function mapDatabaseBuildToListedBuildDTO( - build: RawListedBuildWithEnvAndAliasesDB -): ListedBuildDTO { - const alias = build.envs.env_aliases?.[0]?.alias - - return { - id: build.id, - template: alias ?? build.env_id, - templateId: build.env_id, - status: mapDatabaseBuildStatusToBuildStatusDTO( - build.status as BuildStatusDB - ), - statusMessage: mapDatabaseBuildReasonToListedBuildDTOStatusMessage( - build.status, - build.reason - ), - createdAt: new Date(build.created_at).getTime(), - finishedAt: build.finished_at - ? new Date(build.finished_at).getTime() - : null, - } -} - export function mapBuildStatusDTOToDatabaseBuildStatus( buildStatusDTO: BuildStatusDTO ): BuildStatusDB[] { From 653b6e8d5d0b790bdb98b732dd70ebc484c6d713 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 21:30:38 +0000 Subject: [PATCH 4/8] fix list_team_builds_rpc nullable return fields Co-authored-by: Ben Fornefeld --- migrations/20260212120822.sql | 10 ++++++++-- src/types/database.types.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/migrations/20260212120822.sql b/migrations/20260212120822.sql index 5cb71fc06..f7d6e8a9e 100644 --- a/migrations/20260212120822.sql +++ b/migrations/20260212120822.sql @@ -134,9 +134,15 @@ SELECT d.status, d.reason, d.created_at, - d.finished_at, + CASE + WHEN d.finished_at IS NULL THEN NULL::timestamptz + ELSE d.finished_at + END AS finished_at, d.template_id, - ea.alias AS template_alias + CASE + WHEN ea.alias IS NULL THEN NULL::text + ELSE ea.alias + END AS template_alias FROM page_data d LEFT JOIN LATERAL ( SELECT x.alias diff --git a/src/types/database.types.ts b/src/types/database.types.ts index 628e34d47..bd0a2623c 100644 --- a/src/types/database.types.ts +++ b/src/types/database.types.ts @@ -834,11 +834,11 @@ export type Database = { } Returns: { created_at: string - finished_at: string + finished_at: string | null id: string reason: Json status: string - template_alias: string + template_alias: string | null template_id: string }[] } From f229ac881824871cfc88cb3b74ab7e5fe3b514ee Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 12 Feb 2026 14:04:14 -0800 Subject: [PATCH 5/8] add: running builds rpc and fix build info query --- migrations/20260212120822.sql | 46 +++++++++++ .../api/repositories/builds.repository.ts | 82 ++++++++++++------- src/types/database.types.ts | 13 ++- 3 files changed, 110 insertions(+), 31 deletions(-) diff --git a/migrations/20260212120822.sql b/migrations/20260212120822.sql index f7d6e8a9e..7962bcd09 100644 --- a/migrations/20260212120822.sql +++ b/migrations/20260212120822.sql @@ -157,4 +157,50 @@ $function$; REVOKE ALL ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.list_team_builds_rpc(uuid, text[], integer, timestamptz, uuid, text) TO service_role; +CREATE OR REPLACE FUNCTION public.list_team_running_build_statuses_rpc( + p_team_id uuid, + p_build_ids uuid[] +) +RETURNS TABLE ( + id uuid, + status text, + reason jsonb, + finished_at timestamptz +) +LANGUAGE sql +STABLE +SECURITY INVOKER +SET search_path = pg_catalog, public +AS $function$ +WITH requested_builds AS ( + SELECT DISTINCT requested.build_id + FROM UNNEST(COALESCE(p_build_ids, ARRAY[]::uuid[])) AS requested(build_id) +), +authorized_builds AS ( + SELECT DISTINCT ON (a.build_id) + a.build_id + FROM requested_builds r + JOIN public.env_build_assignments a + ON a.build_id = r.build_id + JOIN public.envs e + ON e.id = a.env_id + WHERE e.team_id = p_team_id + ORDER BY + a.build_id ASC, + a.created_at DESC NULLS LAST, + a.id DESC +) +SELECT + b.id, + b.status, + b.reason::jsonb AS reason, + b.finished_at +FROM authorized_builds ab +JOIN public.env_builds b + ON b.id = ab.build_id; +$function$; + +REVOKE ALL ON FUNCTION public.list_team_running_build_statuses_rpc(uuid, uuid[]) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.list_team_running_build_statuses_rpc(uuid, uuid[]) TO service_role; + COMMIT; diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts index ff5072ad3..21f9ffa79 100644 --- a/src/server/api/repositories/builds.repository.ts +++ b/src/server/api/repositories/builds.repository.ts @@ -61,19 +61,27 @@ interface ListBuildsResult { type ListTeamBuildsRpcRow = Database['public']['Functions']['list_team_builds_rpc']['Returns'][number] +type ListTeamRunningBuildStatusesRpcRow = + Database['public']['Functions']['list_team_running_build_statuses_rpc']['Returns'][number] -function mapRpcBuildToListedBuildDTO(build: ListTeamBuildsRpcRow): ListedBuildDTO { +function mapRpcBuildToListedBuildDTO( + build: ListTeamBuildsRpcRow +): ListedBuildDTO { return { id: build.id, template: build.template_alias ?? build.template_id, templateId: build.template_id, - status: mapDatabaseBuildStatusToBuildStatusDTO(build.status as BuildStatusDB), + status: mapDatabaseBuildStatusToBuildStatusDTO( + build.status as BuildStatusDB + ), statusMessage: mapDatabaseBuildReasonToListedBuildDTOStatusMessage( build.status, build.reason ), createdAt: new Date(build.created_at).getTime(), - finishedAt: build.finished_at ? new Date(build.finished_at).getTime() : null, + finishedAt: build.finished_at + ? new Date(build.finished_at).getTime() + : null, } } @@ -133,25 +141,23 @@ async function getRunningStatuses( return [] } - const { data, error } = await supabaseAdmin - .from('env_builds') - .select('id, status, reason, finished_at, envs!inner(team_id)') - .eq('envs.team_id', teamId) - .in('id', buildIds) + const { data, error } = await supabaseAdmin.rpc( + 'list_team_running_build_statuses_rpc', + { + p_team_id: teamId, + p_build_ids: buildIds, + } + ) if (error) throw error - return (data ?? []).map((build) => ({ - id: build.id, - status: mapDatabaseBuildStatusToBuildStatusDTO( - build.status as BuildStatusDB - ), - finishedAt: build.finished_at - ? new Date(build.finished_at).getTime() - : null, + return ((data ?? []) as ListTeamRunningBuildStatusesRpcRow[]).map((row) => ({ + id: row.id, + status: mapDatabaseBuildStatusToBuildStatusDTO(row.status as BuildStatusDB), + finishedAt: row.finished_at ? new Date(row.finished_at).getTime() : null, statusMessage: mapDatabaseBuildReasonToListedBuildDTOStatusMessage( - build.status, - build.reason + row.status, + row.reason ), })) } @@ -159,26 +165,25 @@ async function getRunningStatuses( // get build details export async function getBuildInfo(buildId: string, teamId: string) { - const { data, error } = await supabaseAdmin - .from('env_builds') - .select( - 'created_at, finished_at, status, reason, envs!inner(team_id, env_aliases(alias))' - ) - .eq('id', buildId) + const { data: assignment, error: assignmentError } = await supabaseAdmin + .from('env_build_assignments') + .select('env_id, envs!inner(team_id)') + .eq('build_id', buildId) .eq('envs.team_id', teamId) + .limit(1) .maybeSingle() - if (error) { + if (assignmentError) { l.error( { key: 'repositories:builds:get_build_info:supabase_error', - error: error, + error: assignmentError, team_id: teamId, context: { build_id: buildId, }, }, - `failed to query env_builds: ${error?.message || 'Unknown error'}` + `failed to query env_build_assignments: ${assignmentError?.message || 'Unknown error'}` ) throw new TRPCError({ @@ -187,14 +192,33 @@ export async function getBuildInfo(buildId: string, teamId: string) { }) } - if (!data) { + if (!assignment) { throw new TRPCError({ code: 'NOT_FOUND', message: "Build not found or you don't have access to it", }) } - const alias = data.envs.env_aliases?.[0]?.alias + const { data, error } = await supabaseAdmin + .from('env_builds') + .select('created_at, finished_at, status, reason') + .eq('id', buildId) + .maybeSingle() + + if (error || !data) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: "Build not found or you don't have access to it", + }) + } + + const { data: aliases } = await supabaseAdmin + .from('env_aliases') + .select('alias') + .eq('env_id', assignment.env_id) + .limit(1) + + const alias = aliases?.[0]?.alias return { alias, diff --git a/src/types/database.types.ts b/src/types/database.types.ts index bd0a2623c..6fbb66baf 100644 --- a/src/types/database.types.ts +++ b/src/types/database.types.ts @@ -834,14 +834,23 @@ export type Database = { } Returns: { created_at: string - finished_at: string | null + finished_at: string id: string reason: Json status: string - template_alias: string | null + template_alias: string template_id: string }[] } + list_team_running_build_statuses_rpc: { + Args: { p_build_ids: string[]; p_team_id: string } + Returns: { + finished_at: string + id: string + reason: Json + status: string + }[] + } generate_access_token: { Args: never; Returns: string } generate_sandbox_video_stream_token: { Args: never; Returns: string } generate_team_api_key: { Args: never; Returns: string } From 4bc06abde64d83e013d9efebf031ed431df696e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 22:52:49 +0000 Subject: [PATCH 6/8] Fix deterministic assignment selection in getBuildInfo Co-authored-by: Ben Fornefeld --- src/server/api/repositories/builds.repository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts index 21f9ffa79..a8ca788ea 100644 --- a/src/server/api/repositories/builds.repository.ts +++ b/src/server/api/repositories/builds.repository.ts @@ -170,6 +170,8 @@ export async function getBuildInfo(buildId: string, teamId: string) { .select('env_id, envs!inner(team_id)') .eq('build_id', buildId) .eq('envs.team_id', teamId) + .order('created_at', { ascending: false, nullsFirst: false }) + .order('id', { ascending: false }) .limit(1) .maybeSingle() From 0482dce13c67d9f55c7f1604d3676a7f8b6b8968 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 22:53:11 +0000 Subject: [PATCH 7/8] Fix build list limit normalization for pagination Co-authored-by: Ben Fornefeld --- src/server/api/repositories/builds.repository.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts index a8ca788ea..25d3d462a 100644 --- a/src/server/api/repositories/builds.repository.ts +++ b/src/server/api/repositories/builds.repository.ts @@ -21,6 +21,16 @@ function isUUID(value: string): boolean { } const CURSOR_SEPARATOR = '|' +const LIST_BUILDS_DEFAULT_LIMIT = 50 +const LIST_BUILDS_MIN_LIMIT = 1 +const LIST_BUILDS_MAX_LIMIT = 100 + +function normalizeListBuildsLimit(limit?: number): number { + return Math.max( + LIST_BUILDS_MIN_LIMIT, + Math.min(limit ?? LIST_BUILDS_DEFAULT_LIMIT, LIST_BUILDS_MAX_LIMIT) + ) +} function decodeCursor(cursor?: string): { cursorCreatedAt: string | null @@ -91,7 +101,7 @@ async function listBuilds( statuses: BuildStatusDB[] = ['waiting', 'building', 'uploaded', 'failed'], options: ListBuildsOptions = {} ): Promise { - const limit = options.limit ?? 50 + const limit = normalizeListBuildsLimit(options.limit) const { cursorCreatedAt, cursorId } = decodeCursor(options.cursor) const { data: rawBuilds, error } = await supabaseAdmin.rpc( From 637d540327246a9346564af4096c677ab6208457 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 23:01:56 +0000 Subject: [PATCH 8/8] fix types for nullable build rpc fields Co-authored-by: Ben Fornefeld --- src/types/database.types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/database.types.ts b/src/types/database.types.ts index 6fbb66baf..bdfcdd864 100644 --- a/src/types/database.types.ts +++ b/src/types/database.types.ts @@ -834,18 +834,18 @@ export type Database = { } Returns: { created_at: string - finished_at: string + finished_at: string | null id: string reason: Json status: string - template_alias: string + template_alias: string | null template_id: string }[] } list_team_running_build_statuses_rpc: { Args: { p_build_ids: string[]; p_team_id: string } Returns: { - finished_at: string + finished_at: string | null id: string reason: Json status: string