mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 08:01:26 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;')
|
||||
})
|
||||
})
|
||||
@@ -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};`
|
||||
|
||||
Reference in New Issue
Block a user