mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
Joshenlim/fe 4474 add command to generate types off api production (#50960)
## Context Adds a pnpm command `api:codegen:prod` to generate API types off prod to work with the `verify-production-types` GHA. Just uses the existing logic in `verify-production-types.mjs` to fetch the OpenAPI specs, and writes it into the local API types files <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * API types can now be generated from the production API specifications when a local API environment is unavailable. * **Bug Fixes** * Resending invitations and updating project-scoped roles now handle roles without a base role ID more reliably. * **Documentation** * Updated guidance clarifies that API or schema changes must be deployed before relying on updated types. * Production is the source of truth for merge checks. Type verification should pass after deployment, and production-generated types are expected to pass verification. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
ae7b9ee245
commit
54c2a2788f
6 files changed
+132
-44
No files matched your search
@@ -10,11 +10,13 @@ The generated API contract has three specs: API v1, API v2, and Platform. Their
|
||||
## Update types
|
||||
|
||||
1. Make the API/schema change and ensure it is deployed to production before relying on a type PR. Production is the merge-gate source of truth.
|
||||
2. Run `pnpm api:codegen` against a running local API environment. It fetches all three local OpenAPI specs and updates the committed files.
|
||||
2. Update the committed types, either:
|
||||
- Run `pnpm api:codegen` against a running local API environment. It fetches all three local OpenAPI specs and updates the committed files.
|
||||
- Or, if you don't have a local API environment running, run `pnpm api:codegen:prod`. It fetches the three production OpenAPI specs directly and overwrites the committed files with them (no diffing — it always writes).
|
||||
3. Inspect and commit only the intended generated type changes.
|
||||
4. Run `pnpm api:verify-types`. It fetches the three production OpenAPI specs, regenerates types with the repository tooling, and compares them with the committed files.
|
||||
|
||||
Complete the update only when `pnpm api:verify-types` passes after the production deployment is available.
|
||||
Complete the update only when `pnpm api:verify-types` passes after the production deployment is available. If you used `api:codegen:prod`, this should already pass since the committed files came straight from production.
|
||||
|
||||
## Interpret verification
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"setup:cli": "supabase start -x studio && supabase status --output json > keys.json && node scripts/generateLocalEnv.js",
|
||||
"generate:types": "supabase gen types typescript --local > ./supabase/functions/common/database-types.ts",
|
||||
"api:codegen": "cd packages/api-types && pnpm run codegen",
|
||||
"api:codegen:prod": "pnpm --filter=api-types run codegen:prod",
|
||||
"api:verify-types": "pnpm --filter=api-types run verify-production-types",
|
||||
"knip": "knip",
|
||||
"authorize-vercel-deploys": "tsx scripts/authorizeVercelDeploys.ts"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"clean": "rimraf .turbo tsconfig.tsbuildinfo",
|
||||
"codegen": "openapi-typescript --redocly ./redocly.yaml --alphabetize --default-non-nullable=false && prettier --cache --write types/*.d.ts",
|
||||
"codegen:prod": "node ./scripts/generate-production-types.mjs",
|
||||
"verify-production-types": "node ./scripts/verify-production-types.mjs",
|
||||
"test": "node --test ./scripts/*.test.mjs"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { generateTypes, specifications } from './verify-production-types.mjs'
|
||||
|
||||
const packageDirectory = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
const typesDirectory = process.env.API_TYPES_DIRECTORY ?? join(packageDirectory, 'types')
|
||||
|
||||
// Regenerates the committed API types directly from the production OpenAPI specifications,
|
||||
// bypassing the need for a running local API (unlike `codegen`, which reads from localhost).
|
||||
export async function generateProductionTypes() {
|
||||
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'api-types-prod-'))
|
||||
|
||||
try {
|
||||
await generateTypes(specifications, {
|
||||
temporaryDirectory,
|
||||
generatedTypesDirectory: typesDirectory,
|
||||
})
|
||||
} finally {
|
||||
await rm(temporaryDirectory, { force: true, recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
await generateProductionTypes()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
import { generateTypes } from './verify-production-types.mjs'
|
||||
|
||||
const specifications = [{ name: 'api-v1', url: 'https://example.com/api/v1-json' }]
|
||||
|
||||
test('generateTypes fetches specifications and generates types straight into the given directory', async (t) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'api-types-generate-'))
|
||||
t.after(() => rm(directory, { recursive: true, force: true }))
|
||||
const temporaryDirectory = join(directory, 'fetched')
|
||||
const generatedTypesDirectory = join(directory, 'output')
|
||||
await mkdir(temporaryDirectory)
|
||||
await mkdir(generatedTypesDirectory)
|
||||
const runCalls = []
|
||||
|
||||
await generateTypes(specifications, {
|
||||
temporaryDirectory,
|
||||
generatedTypesDirectory,
|
||||
fetchImpl: async (url) => ({ ok: true, text: async () => `spec from ${url}` }),
|
||||
runImpl: async (command, args) => {
|
||||
runCalls.push([command, args])
|
||||
if (args.includes('--find-config-path')) return { stdout: '/repo/.prettierrc\n' }
|
||||
return { stdout: '' }
|
||||
},
|
||||
})
|
||||
|
||||
const specification = await readFile(join(temporaryDirectory, 'api-v1.json'), 'utf8')
|
||||
assert.match(specification, /v1-json/)
|
||||
|
||||
const redocly = await readFile(join(temporaryDirectory, 'redocly.yaml'), 'utf8')
|
||||
assert.match(redocly, new RegExp(`output: ${generatedTypesDirectory}/api-v1.d.ts`))
|
||||
|
||||
assert.equal(runCalls.length, 3)
|
||||
assert.deepEqual(runCalls[0][0], 'pnpm')
|
||||
assert.ok(runCalls[0][1].includes('openapi-typescript'))
|
||||
assert.ok(runCalls[1][1].includes('--find-config-path'))
|
||||
assert.ok(runCalls[2][1].includes('--write'))
|
||||
assert.ok(runCalls[2][1].includes(join(generatedTypesDirectory, 'api-v1.d.ts')))
|
||||
})
|
||||
@@ -12,7 +12,7 @@ const platformApiUrl =
|
||||
process.env.PLATFORM_API_OPENAPI_URL ?? 'https://api.supabase.com/api/platform-json'
|
||||
const fetchTimeout = 30_000
|
||||
|
||||
const specifications = [
|
||||
export const specifications = [
|
||||
{ name: 'api-v1', url: 'https://api.supabase.com/api/v1-json' },
|
||||
{ name: 'api-v2', url: 'https://api.supabase.com/api/v2-json' },
|
||||
{ name: 'platform', url: platformApiUrl },
|
||||
@@ -116,53 +116,66 @@ export async function reportTypeDifferences(
|
||||
if (summaryPath) await appendFile(summaryPath, `${summary.join('\n')}\n`)
|
||||
}
|
||||
|
||||
// Fetches the given OpenAPI specifications and generates types for them into
|
||||
// `generatedTypesDirectory`, formatted with the repository's Prettier config. Shared by
|
||||
// `verifyProductionTypes` (which generates into a throwaway directory to diff against the
|
||||
// committed types) and `generate-production-types.mjs` (which generates directly into the
|
||||
// committed types directory).
|
||||
export async function generateTypes(
|
||||
specifications,
|
||||
{ temporaryDirectory, generatedTypesDirectory, runImpl = run, fetchImpl, writeFileImpl }
|
||||
) {
|
||||
const config = await fetchOpenApiSpecifications(specifications, {
|
||||
temporaryDirectory,
|
||||
generatedTypesDirectory,
|
||||
fetchImpl,
|
||||
writeFileImpl,
|
||||
})
|
||||
|
||||
await writeFile(join(temporaryDirectory, 'redocly.yaml'), `apis:\n${config.join('\n')}`)
|
||||
await runImpl(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'openapi-typescript',
|
||||
'--redocly',
|
||||
join(temporaryDirectory, 'redocly.yaml'),
|
||||
'--alphabetize',
|
||||
'--default-non-nullable=false',
|
||||
],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
|
||||
// Prettier resolves its config from the formatted file's location. The generated files live
|
||||
// in a temporary directory outside the repository, so pass the repository config explicitly
|
||||
// or they are formatted with Prettier's defaults and never match the committed files.
|
||||
const { stdout: prettierConfigPath } = await runImpl(
|
||||
'pnpm',
|
||||
['exec', 'prettier', '--find-config-path', join(packageDirectory, 'package.json')],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
|
||||
await runImpl(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'prettier',
|
||||
'--config',
|
||||
prettierConfigPath.trim(),
|
||||
'--write',
|
||||
...specifications.map(({ name }) => join(generatedTypesDirectory, `${name}.d.ts`)),
|
||||
],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
}
|
||||
|
||||
export async function verifyProductionTypes() {
|
||||
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'api-types-'))
|
||||
const generatedTypesDirectory = join(temporaryDirectory, 'types')
|
||||
|
||||
try {
|
||||
await mkdir(generatedTypesDirectory)
|
||||
|
||||
const config = await fetchOpenApiSpecifications(specifications, {
|
||||
temporaryDirectory,
|
||||
generatedTypesDirectory,
|
||||
})
|
||||
|
||||
await writeFile(join(temporaryDirectory, 'redocly.yaml'), `apis:\n${config.join('\n')}`)
|
||||
await run(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'openapi-typescript',
|
||||
'--redocly',
|
||||
join(temporaryDirectory, 'redocly.yaml'),
|
||||
'--alphabetize',
|
||||
'--default-non-nullable=false',
|
||||
],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
|
||||
// Prettier resolves its config from the formatted file's location. The generated files live
|
||||
// in a temporary directory outside the repository, so pass the repository config explicitly
|
||||
// or they are formatted with Prettier's defaults and never match the committed files.
|
||||
const { stdout: prettierConfigPath } = await run(
|
||||
'pnpm',
|
||||
['exec', 'prettier', '--find-config-path', join(packageDirectory, 'package.json')],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
|
||||
await run(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'prettier',
|
||||
'--config',
|
||||
prettierConfigPath.trim(),
|
||||
'--write',
|
||||
...specifications.map(({ name }) => join(generatedTypesDirectory, `${name}.d.ts`)),
|
||||
],
|
||||
{ cwd: packageDirectory }
|
||||
)
|
||||
await generateTypes(specifications, { temporaryDirectory, generatedTypesDirectory })
|
||||
|
||||
const changedTypes = await findMismatchedTypes(specifications, {
|
||||
generatedTypesDirectory,
|
||||
|
||||
Reference in new issue
Block a user