mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
feat: better type narrowing for list and array types
This commit is contained in:
@@ -3119,13 +3119,20 @@ components:
|
||||
typ:
|
||||
oneOf:
|
||||
- type: string
|
||||
enum: ["str", "float", "int", "bool", "unknown"]
|
||||
enum: ["str", "float", "int", "bool", "email", "unknown"]
|
||||
- type: object
|
||||
properties:
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: string
|
||||
enum: ["str", "float", "int", "email"]
|
||||
required:
|
||||
- list
|
||||
has_default:
|
||||
type: boolean
|
||||
default: {}
|
||||
|
||||
+39
-5
@@ -25,6 +25,16 @@ pub struct MainArgSignature {
|
||||
pub args: Vec<Arg>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub enum InnerTyp {
|
||||
Str,
|
||||
Int,
|
||||
Float,
|
||||
Bytes,
|
||||
Email,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub enum Typ {
|
||||
@@ -33,10 +43,11 @@ pub enum Typ {
|
||||
Float,
|
||||
Bool,
|
||||
Dict,
|
||||
List,
|
||||
List(InnerTyp),
|
||||
Bytes,
|
||||
Datetime,
|
||||
Resource(String),
|
||||
Email,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -95,7 +106,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
|
||||
"int" => Typ::Int,
|
||||
"bool" => Typ::Bool,
|
||||
"dict" => Typ::Dict,
|
||||
"list" => Typ::List,
|
||||
"list" => Typ::List(InnerTyp::Str),
|
||||
"bytes" => Typ::Bytes,
|
||||
"datetime" => Typ::Datetime,
|
||||
"datetime.datetime" => Typ::Datetime,
|
||||
@@ -120,7 +131,7 @@ use swc_common::sync::Lrc;
|
||||
use swc_common::{FileName, SourceMap};
|
||||
use swc_ecma_ast::{
|
||||
AssignPat, BindingIdent, Decl, ExportDecl, FnDecl, Ident, ModuleDecl, ModuleItem, Pat,
|
||||
TsEntityName, TsKeywordTypeKind, TsType, TsTypeRef,
|
||||
TsArrayType, TsEntityName, TsKeywordTypeKind, TsType, TsTypeRef,
|
||||
};
|
||||
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
|
||||
|
||||
@@ -239,7 +250,28 @@ fn binding_ident_to_arg(
|
||||
_ => Typ::Unknown,
|
||||
},
|
||||
// TODO: we can do better here and extract the inner type of array
|
||||
TsType::TsArrayType(_) => Typ::List,
|
||||
TsType::TsArrayType(TsArrayType { span: _, elem_type }) => {
|
||||
match &**elem_type {
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name:
|
||||
TsEntityName::Ident(Ident {
|
||||
span: _,
|
||||
sym,
|
||||
optional: _,
|
||||
}),
|
||||
type_params: _,
|
||||
}) => match sym.to_string().as_str() {
|
||||
"Base64" => Typ::List(InnerTyp::Bytes),
|
||||
"Email" => Typ::List(InnerTyp::Email),
|
||||
"bigint" => Typ::List(InnerTyp::Int),
|
||||
"number" => Typ::List(InnerTyp::Float),
|
||||
_ => Typ::List(InnerTyp::Str),
|
||||
},
|
||||
//TsType::TsKeywordType(())
|
||||
_ => Typ::List(InnerTyp::Str),
|
||||
}
|
||||
}
|
||||
TsType::TsTypeRef(TsTypeRef {
|
||||
span: _,
|
||||
type_name,
|
||||
@@ -266,6 +298,8 @@ fn binding_ident_to_arg(
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
),
|
||||
"Base64" => Typ::Bytes,
|
||||
"Email" => Typ::Email,
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
}
|
||||
@@ -755,7 +789,7 @@ def main():
|
||||
let code = "
|
||||
|
||||
export function main(test1: string, test2: string = \"burkina\",
|
||||
test3: wmill.Resource<'postgres'>, email: email_string) {
|
||||
test3: wmill.Resource<'postgres'>, b64: Base64, ls: Base64[], email: Email) {
|
||||
console.log(42)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ export {
|
||||
UserApi, WorkspaceApi
|
||||
} from './windmill-api/index.ts'
|
||||
|
||||
export type string_regex<S extends string> = String
|
||||
export type string_email = String
|
||||
|
||||
export type Email = string
|
||||
export type Base64 = string
|
||||
export type Resource<S extends string> = {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface SchemaProperty {
|
||||
enum?: string[]
|
||||
contentEncoding?: 'base64' | 'binary'
|
||||
format?: string
|
||||
items?: { type?: 'string' | 'number' }
|
||||
items?: { type?: 'string' | 'number' | 'bytes', contentEncoding?: 'base64' },
|
||||
}
|
||||
|
||||
export type Schema = {
|
||||
|
||||
+11
-6
@@ -44,7 +44,7 @@ export async function inferArgs(
|
||||
}
|
||||
}
|
||||
|
||||
function argSigToJsonSchemaType(t: string | { resource: string }, s: SchemaProperty): void {
|
||||
function argSigToJsonSchemaType(t: string | { resource: string } | { list: string }, s: SchemaProperty): void {
|
||||
if (t === 'int') {
|
||||
s.type = 'integer'
|
||||
} else if (t === 'float') {
|
||||
@@ -55,18 +55,23 @@ function argSigToJsonSchemaType(t: string | { resource: string }, s: SchemaPrope
|
||||
s.type = 'string'
|
||||
} else if (t === 'dict') {
|
||||
s.type = 'object'
|
||||
} else if (t === 'list') {
|
||||
s.type = 'array'
|
||||
} else if (t === 'bytes') {
|
||||
s.type = 'string'
|
||||
s.contentEncoding = 'base64'
|
||||
} else if (t === 'datetime') {
|
||||
s.type = 'string'
|
||||
s.format = 'date-time'
|
||||
} else if (typeof t !== 'string' && t.resource != undefined) {
|
||||
} else if (typeof t !== 'string' && `resource` in t) {
|
||||
s.type = 'object'
|
||||
s.format = `resource-${t.resource}`
|
||||
} else {
|
||||
s.type = undefined
|
||||
} else if (typeof t !== 'string' && `list` in t) {
|
||||
s.type = 'array'
|
||||
if (t.list === 'int' || t.list === 'float') {
|
||||
s.items = { type: 'number' }
|
||||
} else if (t.list === 'bytes') {
|
||||
s.items = { type: 'string', contentEncoding: 'base64' }
|
||||
} else {
|
||||
s.items = { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
export let enum_: string[] | undefined = undefined
|
||||
export let disabled = false
|
||||
export let editableSchema = false
|
||||
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined
|
||||
export let itemsType: { type?: 'string' | 'number'; contentEncoding?: string } | undefined =
|
||||
undefined
|
||||
export let displayHeader = true
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
@@ -70,16 +71,16 @@
|
||||
rawValue = JSON.stringify(value, null, 4)
|
||||
}
|
||||
|
||||
function fileChanged(e: any) {
|
||||
function fileChanged(e: any, cb: (v: string | undefined) => void) {
|
||||
let t = e.target
|
||||
if (t && 'files' in t && t.files.length > 0) {
|
||||
let reader = new FileReader()
|
||||
reader.onload = (e: any) => {
|
||||
value = e.target.result.split('base64,')[1]
|
||||
cb(e.target.result.split('base64,')[1])
|
||||
}
|
||||
reader.readAsDataURL(t.files[0])
|
||||
} else {
|
||||
value = undefined
|
||||
cb(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +148,9 @@
|
||||
<option value={undefined}>No specific item type</option>
|
||||
<option value={{ type: 'string' }}> Items are strings</option>
|
||||
<option value={{ type: 'number' }}>Items are numbers</option>
|
||||
<option value={{ type: 'string', contentEncoding: 'base64' }}
|
||||
>Items are bytes</option
|
||||
>
|
||||
</select>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -192,6 +196,13 @@
|
||||
<div class="flex flex-row max-w-md">
|
||||
{#if itemsType.type == 'number'}
|
||||
<input type="number" bind:value={v} />
|
||||
{:else if itemsType.type == 'string' && itemsType.contentEncoding == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (v = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else}
|
||||
<input type="text" bind:value={v} />
|
||||
{/if}
|
||||
@@ -236,7 +247,12 @@
|
||||
{:else if type == 'string' && format == 'date-time'}
|
||||
<input class="inline-block" type="datetime-local" bind:value />
|
||||
{:else if type == 'string' && contentEncoding == 'base64'}
|
||||
<input type="file" class="my-6" on:change={fileChanged} multiple={false} />
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else if type == 'string' && format?.startsWith('resource')}
|
||||
<ResourcePicker
|
||||
bind:value
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { Schema } from '../../common'
|
||||
import { emptySchema } from '../../utils'
|
||||
import { emptySchema, loadSchema } from '../../utils'
|
||||
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
@@ -22,16 +22,13 @@
|
||||
await Promise.all(
|
||||
flow.value.modules.map(async (x, i) => {
|
||||
if (x.value.path) {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: x.value.path ?? ''
|
||||
})
|
||||
const schema = await loadSchema(x.value.path ?? '')
|
||||
if (
|
||||
JSON.stringify(Object.keys(script.schema?.properties ?? {}).sort()) !=
|
||||
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
|
||||
JSON.stringify(Object.keys(x.input_transform).sort())
|
||||
) {
|
||||
let it = {}
|
||||
Object.keys(script.schema?.properties ?? {}).map(
|
||||
Object.keys(schema?.properties ?? {}).map(
|
||||
(x) =>
|
||||
(it[x] = {
|
||||
type: 'static',
|
||||
@@ -40,7 +37,7 @@
|
||||
)
|
||||
schemaForms[i]?.setArgs(it)
|
||||
}
|
||||
schemas[i] = script.schema ?? emptySchema()
|
||||
schemas[i] = schema ?? emptySchema()
|
||||
} else {
|
||||
schemaForms[i]?.setArgs({})
|
||||
schemas[i] = emptySchema()
|
||||
@@ -84,11 +81,7 @@
|
||||
disabled={flow.value.modules.length == 0 ||
|
||||
flow.value.modules[0].value.path == undefined}
|
||||
on:click={async () => {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: flow.value.modules[0].value.path ?? ''
|
||||
})
|
||||
flow.schema = script.schema
|
||||
flow.schema = await loadSchema(flow.value.modules[0].value.path ?? '')
|
||||
}}
|
||||
>Copy from step 1's schema
|
||||
</button>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '../../stores'
|
||||
|
||||
import type { Schema } from '../../common'
|
||||
import { ScriptService, type Flow, type FlowModule } from '../../gen'
|
||||
import type { Flow, FlowModule } from '../../gen'
|
||||
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import ScriptPicker from './ScriptPicker.svelte'
|
||||
import { emptySchema } from '../../utils'
|
||||
import { emptySchema, loadSchema as UloadSchema } from '../../utils'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import { inferArgs } from '../../infer'
|
||||
|
||||
export let flow: Flow
|
||||
export let i: number
|
||||
@@ -20,18 +17,7 @@
|
||||
|
||||
export async function loadSchema() {
|
||||
if (mod.value.path) {
|
||||
let schema
|
||||
if (mod.value.path.startsWith('hub/')) {
|
||||
const code = await ScriptService.getHubScriptContentByPath({ path: mod.value.path })
|
||||
schema = emptySchema()
|
||||
await inferArgs('deno', code, schema)
|
||||
} else {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: mod.value.path ?? ''
|
||||
})
|
||||
schema = script.schema
|
||||
}
|
||||
let schema = await UloadSchema(mod.value.path)
|
||||
if (
|
||||
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
|
||||
JSON.stringify(Object.keys(mod.input_transform).sort())
|
||||
|
||||
+18
-2
@@ -2,8 +2,9 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import { get } from 'svelte/store'
|
||||
import { CancelablePromise, UserService, type User } from './gen'
|
||||
import { clearStores, superadmin, userStore, workspaceStore, type UserExt } from './stores'
|
||||
import { ScriptService, UserService, type User } from './gen'
|
||||
import { inferArgs } from './infer'
|
||||
import { clearStores, superadmin, workspaceStore, type UserExt } from './stores'
|
||||
|
||||
export function isToday(someDate: Date): boolean {
|
||||
const today = new Date()
|
||||
@@ -138,6 +139,21 @@ export function clickOutside(node: any): any {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSchema(path: string) {
|
||||
|
||||
if (path.startsWith('hub/')) {
|
||||
const code = await ScriptService.getHubScriptContentByPath({ path })
|
||||
const schema = emptySchema()
|
||||
await inferArgs('deno', code, schema)
|
||||
return schema
|
||||
} else {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
path: path ?? ''
|
||||
})
|
||||
return script.schema
|
||||
}
|
||||
}
|
||||
export type DropdownType = 'action' | 'delete'
|
||||
|
||||
export interface DropdownItem {
|
||||
|
||||
Reference in New Issue
Block a user