fix: compile resource types with no properties instead of throwing (#10730)

* fix: compile resource types with no properties instead of throwing

* fix: keep property-less resource types in the editor RT namespace
This commit is contained in:
Ruben Fiszel
2026-08-17 22:14:07 +02:00
committed by GitHub
parent 6b5b9f72d4
commit b17fdab8ff
6 changed files with 123 additions and 56 deletions
+18 -7
View File
@@ -4,18 +4,29 @@ function quotePropName(name: string): string {
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
}
export function compileResourceTypeToTsType(schema: Schema) {
function rec(x: { [name: string]: SchemaProperty }, root = false) {
let res = "{\n";
function isPropertyMap(x: unknown): x is { [name: string]: SchemaProperty } {
return typeof x === "object" && x !== null && !Array.isArray(x);
}
// Schemas are free-form jsonb: the column is nullable and hub types such as
// `record` or `dbt_profile` carry `{}` / `{"type":"object"}` with no
// `properties`. Anything that is not a property map compiles to `any`, since a
// throw here aborts the whole rt.d.ts generation.
export function compileResourceTypeToTsType(schema: Schema | undefined | null) {
function rec(x: unknown): string {
if (!isPropertyMap(x)) {
return "any";
}
const entries = Object.entries(x);
if (entries.length == 0) {
return "any";
}
let res = "{\n";
let i = 0;
for (let [name, prop] of entries) {
if (prop.type == "object") {
res += ` ${quotePropName(name)}: ${rec(prop.properties ?? {})}`;
} else if (prop.type == "array") {
if (prop?.type == "object") {
res += ` ${quotePropName(name)}: ${rec(prop.properties)}`;
} else if (prop?.type == "array") {
res += ` ${quotePropName(name)}: ${prop?.items?.type ?? "any"}[]`;
} else {
let typ = prop?.type ?? "any";
@@ -33,5 +44,5 @@ export function compileResourceTypeToTsType(schema: Schema) {
return res;
}
return rec(schema.properties, true);
return rec(schema?.properties);
}
+20
View File
@@ -52,6 +52,26 @@ test("non-identifier property names are double-quoted", () => {
expect(out).toContain(' "3leading": boolean');
});
// WIN-2392: hub types like `record` (schema `{}`) or `dbt_profile`
// (`{"type":"object"}`) have no `properties`, and the schema column itself is
// nullable. A type whose property map is missing must still compile, otherwise
// it aborts the whole rt.d.ts generation.
test("schemas without a usable property map compile to any", () => {
expect(compileResourceTypeToTsType(undefined)).toBe("any");
expect(compileResourceTypeToTsType(null)).toBe("any");
expect(compileResourceTypeToTsType({ type: "object" } as any)).toBe("any");
expect(compileResourceTypeToTsType({ properties: null } as any)).toBe("any");
expect(compileResourceTypeToTsType(schema({}))).toBe("any");
});
test("a null property compiles to any instead of throwing", () => {
const out = compileResourceTypeToTsType(
schema({ host: null, port: { type: "integer" } } as any)
);
expect(out).toContain(" host: any");
expect(out).toContain(" port: number");
});
test("nested object and array property names are quoted too", () => {
const out = compileResourceTypeToTsType(
schema({
+24 -21
View File
@@ -59,7 +59,7 @@
Settings,
Users
} from 'lucide-svelte'
import { capitalize, formatS3Object, toCamel, type Item } from '$lib/utils'
import { capitalize, formatS3Object, isObject, toCamel, type Item } from '$lib/utils'
import DropdownV2 from './DropdownV2.svelte'
import type { Schema, SchemaProperty, SupportedLanguage } from '$lib/common'
import ScriptVersionHistory from './ScriptVersionHistory.svelte'
@@ -394,17 +394,17 @@
const dispatch = createEventDispatcher()
function compile(schema: Schema) {
function rec(x: { [name: string]: SchemaProperty }, root = false) {
function rec(x: { [name: string]: SchemaProperty } | undefined, root = false) {
let res = '{\n'
const entries = Object.entries(x)
const entries = Object.entries(isObject(x) ? x : {})
if (entries.length == 0) {
return 'any'
}
let i = 0
for (let [name, prop] of entries) {
if (prop.type == 'object') {
res += `${name}: ${rec(prop.properties ?? {})}`
} else if (prop.type == 'array') {
if (prop?.type == 'object') {
res += `${name}: ${rec(prop.properties)}`
} else if (prop?.type == 'array') {
res += `${name}: ${prop?.items?.type ?? 'any'}[]`
} else {
let typ = prop?.type ?? 'any'
@@ -423,7 +423,7 @@
}
return res
}
return rec(schema.properties, true)
return rec(schema?.properties, true)
}
async function quicktypeJSONSchema(targetLanguage, typeName, jsonSchemaString, rendererOptions) {
@@ -484,22 +484,23 @@
function phpCompile(schema: Schema) {
let res = ' '
const entries = Object.entries(schema.properties)
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
return 'array'
return ''
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'array'
if (prop.type === 'array') {
if (prop?.type === 'array') {
typ = 'array'
} else if (prop.type === 'string') {
} else if (prop?.type === 'string') {
typ = 'string'
} else if (prop.type === 'number') {
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop.type === 'integer') {
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop.type === 'boolean') {
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += `public ${typ} $${name};`
@@ -512,22 +513,24 @@
}
function pythonCompile(schema: Schema) {
let res = ''
const entries = Object.entries(schema.properties)
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
return 'dict'
// the result is inserted as a `class X(TypedDict):` body
return 'pass'
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'dict'
if (prop.type === 'array') {
if (prop?.type === 'array') {
typ = 'list'
} else if (prop.type === 'string') {
} else if (prop?.type === 'string') {
typ = 'str'
} else if (prop.type === 'number') {
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop.type === 'integer') {
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop.type === 'boolean') {
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += `${name}: ${typ}`
@@ -1,6 +1,6 @@
import { ResourceService, JobService } from '$lib/gen/services.gen'
import type { AIProvider, AIProviderModel, ResourceType, ScriptLang } from '$lib/gen/types.gen'
import { capitalize, isObject, toCamel } from '$lib/utils'
import { capitalize, toCamel } from '$lib/utils'
import { compile, phpCompile, pythonCompile } from '../../utils'
import type {
ChatCompletionSystemMessageParam,
@@ -41,16 +41,13 @@ export function formatResourceTypes(
allResourceTypes: ResourceType[],
lang: 'python3' | 'php' | 'bun' | 'deno' | 'nativets' | 'bunnative'
) {
const resourceTypes = allResourceTypes.filter(
(rt) => isObject(rt.schema) && 'properties' in rt.schema && isObject(rt.schema.properties)
)
if (lang === 'python3') {
const result = resourceTypes.map((resourceType) => {
const result = allResourceTypes.map((resourceType) => {
return `class ${resourceType.name}(TypedDict):\n${pythonCompile(resourceType.schema as any)}`
})
return '\n**Make sure to rename conflicting imported modules**\n' + result.join('\n\n')
} else if (lang === 'php') {
const result = resourceTypes.map((resourceType) => {
const result = allResourceTypes.map((resourceType) => {
return `class ${toCamel(capitalize(resourceType.name))} {\n${phpCompile(
resourceType.schema as any
)}\n}`
@@ -58,7 +55,7 @@ export function formatResourceTypes(
return '\n' + result.join('\n\n')
} else {
let resultStr = 'namespace RT {\n'
const result = resourceTypes.map((resourceType) => {
const result = allResourceTypes.map((resourceType) => {
return ` type ${toCamel(capitalize(resourceType.name))} = ${compile(
resourceType.schema as any
).replaceAll('\n', '\n ')}`
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest'
import { formatResourceTypes } from './utils'
// Hub resource types such as `record` (schema `{}`) or `dbt_profile`
// (`{"type":"object"}`) carry no `properties`, and the schema column is
// nullable, so any workspace can hold a type whose property map is missing.
const resourceTypes = [
{ name: 'record', schema: {} },
{ name: 'dbt_profile', schema: { type: 'object' } },
{ name: 'null_schema', schema: null },
{ name: 'null_properties', schema: { type: 'object', properties: null } },
{ name: 'ok', schema: { type: 'object', properties: { host: { type: 'string' } } } }
] as any
describe('formatResourceTypes tolerates resource types without a property map', () => {
it('emits `any` for typescript and keeps the valid types', () => {
const out = formatResourceTypes(resourceTypes, 'typescript')
expect(out).toContain('type Record = any')
expect(out).toContain('host: string')
})
it('emits an indented `pass` body for python', () => {
const out = formatResourceTypes(resourceTypes, 'python3')
expect(out).toContain('class record(TypedDict):\n pass')
expect(out).toContain('host: str')
})
it('emits an empty class body for php', () => {
const out = formatResourceTypes(resourceTypes, 'php')
expect(out).toContain('class Record {\n\n}')
expect(out).toContain('public string $host;')
})
})
+24 -21
View File
@@ -2,7 +2,7 @@ import type { Schema, SchemaProperty } from '../../common'
import type { ResourceType, ScriptLang } from '../../gen'
import { capitalize, toCamel } from '$lib/utils'
import { capitalize, isObject, toCamel } from '$lib/utils'
import YAML from 'yaml'
export const getCommentSymbol = (
@@ -40,17 +40,17 @@ export const getCommentSymbol = (
}
export function compile(schema: Schema) {
function rec(x: { [name: string]: SchemaProperty }, root = false) {
function rec(x: { [name: string]: SchemaProperty } | undefined, root = false) {
let res = '{\n'
const entries = Object.entries(x)
const entries = Object.entries(isObject(x) ? x : {})
if (entries.length == 0) {
return 'any'
}
let i = 0
for (let [name, prop] of entries) {
if (prop.type == 'object') {
res += ` ${name}: ${rec(prop.properties ?? {})}`
} else if (prop.type == 'array') {
if (prop?.type == 'object') {
res += ` ${name}: ${rec(prop.properties)}`
} else if (prop?.type == 'array') {
res += ` ${name}: ${prop?.items?.type ?? 'any'}[]`
} else {
let typ = prop?.type ?? 'any'
@@ -68,27 +68,29 @@ export function compile(schema: Schema) {
return res
}
return rec(schema.properties, true)
return rec(schema?.properties, true)
}
export function pythonCompile(schema: Schema) {
let res = ''
const entries = Object.entries(schema.properties)
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
return 'dict'
// callers embed the result as a `class X(TypedDict):` body
return ' pass'
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'dict'
if (prop.type === 'array') {
if (prop?.type === 'array') {
typ = 'list'
} else if (prop.type === 'string') {
} else if (prop?.type === 'string') {
typ = 'str'
} else if (prop.type === 'number') {
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop.type === 'integer') {
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop.type === 'boolean') {
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += ` ${name}: ${typ}`
@@ -102,22 +104,23 @@ export function pythonCompile(schema: Schema) {
export function phpCompile(schema: Schema) {
let res = ''
const entries = Object.entries(schema.properties)
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
return 'array'
return ''
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'array'
if (prop.type === 'array') {
if (prop?.type === 'array') {
typ = 'array'
} else if (prop.type === 'string') {
} else if (prop?.type === 'string') {
typ = 'string'
} else if (prop.type === 'number') {
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop.type === 'integer') {
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop.type === 'boolean') {
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += ` public ${typ} $${name};`