Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"body": [
{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
"name": "test-branch",
"project_ref": "branchrefaaaaaaaaa",
"parent_project_ref": "__PROJECT_REF__",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("renderBranchesListTable", () => {
it("renders all 8 columns in declared order", () => {
const out = renderBranchesListTable([
{
id: "11111111-2222-3333-4444-555555555555",
id: "11111111-2222-4333-8444-555555555555",
name: "feat-1",
project_ref: "abcdefghijklmnopqrst",
parent_project_ref: "parent-ref-aaaaaaaaa",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { legacyBranchesCreate } from "./create.handler.ts";
type CreatedBranch = typeof V1CreateABranchOutput.Type;

const CREATED: CreatedBranch = {
id: "11111111-2222-3333-4444-555555555555",
id: "11111111-2222-4333-8444-555555555555",
name: "feat-x",
project_ref: "aaaaaaaaaaaaaaaaaaaa",
parent_project_ref: "bbbbbbbbbbbbbbbbbbbb",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ const FIND: FindResponse = {
is_default: false,
persistent: false,
status: "MIGRATIONS_PASSED",
created_at: "",
updated_at: "",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
with_data: false,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { legacyBranchesList } from "./list.handler.ts";
type Branches = typeof V1ListAllBranchesOutput.Type;

const SAMPLE_BRANCH: Branches[number] = {
id: "11111111-2222-3333-4444-555555555555",
id: "11111111-2222-4333-8444-555555555555",
name: "feat-1",
project_ref: "aaaaaaaaaaaaaaaaaaaa",
parent_project_ref: "bbbbbbbbbbbbbbbbbbbb",
Expand Down Expand Up @@ -171,7 +171,7 @@ describe("legacy branches list integration", () => {
expect(out.stdoutText).toBe(`- createdat: 2026-05-27T01:02:03Z
deletionscheduledat: null
gitbranch: feat-1
id: 11111111-2222-3333-4444-555555555555
id: 11111111-2222-4333-8444-555555555555
isdefault: false
latestcheckrunid: null
name: feat-1
Expand Down Expand Up @@ -226,7 +226,7 @@ describe("legacy branches list integration", () => {
expect(out.stdoutText).toBe(`[[branches]]
CreatedAt = 2026-05-27T01:02:03Z
GitBranch = "feat-1"
Id = "11111111-2222-3333-4444-555555555555"
Id = "11111111-2222-4333-8444-555555555555"
IsDefault = false
Name = "feat-1"
ParentProjectRef = "bbbbbbbbbbbbbbbbbbbb"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { legacySnippetsList } from "./list.handler.ts";

type SnippetsResponse = typeof V1ListAllSnippetsOutput.Type;

const SNIPPET_ID = "00000000-0000-0000-0000-000000000001";
const SNIPPET_ID = "00000000-0000-4000-8000-000000000001";

const SNIPPET_BASE = {
id: SNIPPET_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("renderSnippetsTable", () => {
it("preserves literal `|` characters in name, visibility, and owner (Glamour decodes Go's escape back)", () => {
const out = renderSnippetsTable([
{
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "name|here",
visibility: "user|public",
owner: { username: "user|name" },
Expand All @@ -35,7 +35,7 @@ describe("renderSnippetsTable", () => {
it("formats RFC3339 timestamps as UTC YYYY-MM-DD HH:MM:SS", () => {
const out = renderSnippetsTable([
{
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "n",
visibility: "user",
owner: { username: "u" },
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/next/auth/platform-api.layer.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe("platformApiLayer", () => {
return Effect.succeed(
jsonResponse(request, 200, [
{
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "main",
project_ref: "mainrefghijklmnopqrst",
parent_project_ref: "parentrefabcdefghijk",
Expand Down Expand Up @@ -219,7 +219,7 @@ describe("platformApiLayer", () => {
return Effect.succeed(
jsonResponse(request, 200, [
{
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "main",
project_ref: "mainrefghijklmnopqrst",
parent_project_ref: "parentrefabcdefghijk",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function makeCreatedBranch(
overrides: Partial<typeof V1CreateABranchOutput.Type> = {},
): typeof V1CreateABranchOutput.Type {
return {
id: "00000000-0000-0000-0000-000000000002",
id: "00000000-0000-4000-8000-000000000002",
name: "feature-branch",
project_ref: "branchrefabcdefghijk",
parent_project_ref: "parentrefabcdefghijk",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function makeBranch(
overrides: Partial<typeof BranchResponse.Type> = {},
): typeof BranchResponse.Type {
return {
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "main",
project_ref: "mainrefghijklmnopqrst",
parent_project_ref: "parentrefabcdefghijk",
Expand Down Expand Up @@ -142,7 +142,7 @@ describe("branches list handler", () => {
() =>
Effect.gen(function* () {
const devBranch = makeBranch({
id: "00000000-0000-0000-0000-000000000002",
id: "00000000-0000-4000-8000-000000000002",
name: "dev",
project_ref: "devrefghijklmnopqrst",
is_default: false,
Expand Down Expand Up @@ -206,7 +206,7 @@ describe("branches list handler", () => {
it.live("sets active:false for non-active branches in JSON mode", () =>
Effect.gen(function* () {
const devBranch = makeBranch({
id: "00000000-0000-0000-0000-000000000002",
id: "00000000-0000-4000-8000-000000000002",
name: "dev",
project_ref: "devrefghijklmnopqrst",
is_default: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function makeBranch(
overrides: Partial<typeof BranchResponse.Type> = {},
): typeof BranchResponse.Type {
return {
id: "00000000-0000-0000-0000-000000000001",
id: "00000000-0000-4000-8000-000000000001",
name: "main",
project_ref: "mainrefghijklmnopqrst",
parent_project_ref: "parentref1234567890",
Expand All @@ -36,7 +36,7 @@ function makeBranch(

const MAIN_BRANCH = makeBranch();
const DEV_BRANCH = makeBranch({
id: "00000000-0000-0000-0000-000000000002",
id: "00000000-0000-4000-8000-000000000002",
name: "dev",
project_ref: "devrefghijklmnopqrst",
is_default: false,
Expand Down
16 changes: 15 additions & 1 deletion apps/cli/src/next/commands/platform/platform-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,26 @@ function buildMultipartBodyExample(schema: PlatformSchemaNode | undefined): Json
}

if (schema.kind === "object") {
return objectExampleForNode(schema, false, false);
return objectExampleForNode(
{
...schema,
properties: schema.properties?.filter((property) => !isBinaryProperty(property)),
},
false,
false,
);
}

return exampleValueForNode(schema, false, false);
}

function isBinaryProperty(property: PlatformSchemaNode): boolean {
return (
property.format === "binary" ||
(property.kind === "array" && property.items?.format === "binary")
);
}

function multipartUploadSegments(schema: PlatformSchemaNode | undefined): ReadonlyArray<string> {
return (
schema?.properties?.flatMap((property) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,16 @@ function toPlatformSchemaNode(options: {
readonly required: boolean;
readonly sensitive: boolean;
readonly description?: string;
readonly ancestors?: ReadonlySet<PlatformOpenApiSchema>;
}): PlatformSchemaNode {
const resolved = resolvePlatformOpenApiSchema(options.schema);
const objectShape = getPlatformOpenApiObjectShape(options.schema);
const kind = classifyPlatformSchemaKind(options.schema);
const isRecursive = options.ancestors?.has(resolved) === true;
const ancestors = new Set(options.ancestors);
ancestors.add(resolved);
const objectShape = isRecursive ? undefined : getPlatformOpenApiObjectShape(options.schema);
const kind = isRecursive ? "unknown" : classifyPlatformSchemaKind(options.schema);
const enumValues = enumValuesForNode(options.schema);
const unionVariants = unionVariantsFor(options.schema);
const unionVariants = isRecursive ? [] : unionVariantsFor(options.schema);

const properties =
objectShape && Object.keys(objectShape.properties).length > 0
Expand All @@ -170,17 +174,19 @@ function toPlatformSchemaNode(options: {
label: humanizeFieldName(name),
required: objectShape.required.has(name),
sensitive: isSensitiveField(name),
ancestors,
}),
)
: undefined;

const items =
resolved.type === "array" && resolved.items !== undefined
!isRecursive && resolved.type === "array" && resolved.items !== undefined
? toPlatformSchemaNode({
schema: resolved.items,
label: options.label ? `${options.label} Item` : "Item",
required: true,
sensitive: options.sensitive,
ancestors,
})
: undefined;

Expand All @@ -192,6 +198,7 @@ function toPlatformSchemaNode(options: {
label: options.label ? `${options.label} Variant ${index + 1}` : `Variant ${index + 1}`,
required: options.required,
sensitive: options.sensitive,
ancestors,
}),
)
: undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,17 @@ describe("platform schema text rendering", () => {
expect(rendered).not.toContain("id: string (optional)");
});

it("bounds recursive JSON response schemas", () => {
const payload = buildPlatformSchemaPayload(
findPlatformOperationDescriptor("v1GetHostnameConfig"),
);
const rendered = renderPlatformSchemaPayload(payload);

expect(rendered).toContain("GET /v1/projects/{ref}/custom-hostname");
expect(rendered).toContain("errors?:");
expect(rendered.length).toBeLessThan(10_000);
});

it("renders multipart guidance without dumping nested renderer state", () => {
const payload = buildPlatformSchemaPayload(
findPlatformOperationDescriptor("v1DeployAFunction"),
Expand All @@ -208,7 +219,7 @@ describe("platform schema text rendering", () => {
expect(rendered).toContain(" --json");
expect(rendered).toContain(" --upload");
expect(rendered).toContain("metadata: object");
expect(rendered).toContain("file?: binary[]");
expect(rendered).toContain("file: binary[]");
expect(rendered).toContain(
"note: Use repeated `--upload field=path` flags for binary multipart fields, including array-valued fields.",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,37 @@ describe("api schema payload", () => {
});
});

it("uses unknown placeholders for recursive response schemas", () => {
const payload = buildPlatformSchemaPayload(
findPlatformOperationDescriptor("v1GetHostnameConfig"),
);

expect(payload).toMatchObject({
response: {
properties: {
data: {
properties: {
errors: {
items: {
oneOf: expect.arrayContaining([
{
type: "array",
items: {
type: "unknown",
description: "Any JSON-serializable value",
},
},
]),
},
},
},
},
},
},
});
expect(JSON.stringify(payload)).not.toContain('"oneOf":[]');
});

it("splits multipart routes into structured and upload channels", () => {
const payload = buildPlatformSchemaPayload(
findPlatformOperationDescriptor("v1DeployAFunction"),
Expand Down Expand Up @@ -177,7 +208,7 @@ describe("api schema payload", () => {
},
upload: {
flag: "--upload",
optional: {
required: {
file: {
type: "array",
items: {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/shared/functions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,7 +1630,7 @@ const uploadFunctionSource = Effect.fnUntraced(function* (
...(bundleOnly ? { bundleOnly: true } : {}),
body: {
metadata,
...(files.length > 0 ? { file: files } : {}),
file: files,
},
})
.pipe(
Expand Down
19 changes: 17 additions & 2 deletions packages/api/scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,19 @@ function resolveSchema(document: OpenApiDocument, schema: OpenApiSchema): OpenAp
return sanitizeOpenApiSchema(schema);
}

export function normalizeQueryParameterSchema(
parameter: OpenApiParameter,
schema: OpenApiSchema,
): OpenApiSchema {
const acceptsBoolean =
schema.type === "string" &&
(typeof parameter.schema?.example === "boolean" ||
/\bboolean string\b/iu.test(parameter.description ?? "") ||
/\bif (?:true|false)\b/iu.test(parameter.description ?? ""));

return acceptsBoolean ? { anyOf: [schema, { type: "boolean" }] } : schema;
}

function getObjectShape(document: OpenApiDocument, schema: OpenApiSchema): ObjectShape | undefined {
const resolved = resolveSchema(document, schema);
if (resolved.type === "object" || resolved.properties !== undefined) {
Expand Down Expand Up @@ -535,7 +548,9 @@ function buildCombinedInputSchema(
if (parameter.in === "cookie" || parameter.schema === undefined) {
continue;
}
properties[parameter.name] = resolveSchema(document, parameter.schema);
const schema = resolveSchema(document, parameter.schema);
properties[parameter.name] =
parameter.in === "query" ? normalizeQueryParameterSchema(parameter, schema) : schema;
if (parameter.required === true) {
required.add(parameter.name);
}
Expand Down Expand Up @@ -689,7 +704,7 @@ function renderSchemaSource(
parts.push("// recursive definitions");
for (const [name, code] of recursiveEntries) {
parts.push(
`export const ${name} = ${hasBinaryInputs ? replaceBinarySchemaCode(code.runtime) : code.runtime}`,
`export type ${name} = ${code.Type}\nexport const ${name} = ${hasBinaryInputs ? replaceBinarySchemaCode(code.runtime) : code.runtime}`,
);
}
}
Expand Down
Loading
Loading