Files
windmill/frontend/src/lib/scriptEditorSchema.ts
T
Diego ImbertandClaude Fable 5.1 1be390aa87 fix(frontend): no phantom draft when opening a CLI-pushed script (#10997)
* fix(frontend): no phantom draft when opening a CLI-pushed script

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3

* fix(frontend): infer the dbt descriptor schema on mount like ScriptEditor

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3

* fix(frontend): retry the baseline schema inference once like the editors do

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 17:50:28 +02:00

91 lines
3.4 KiB
TypeScript

import type { Schema, SupportedLanguage } from '$lib/common'
import type { Script } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { emptySchema } from '$lib/utils'
import { parsePipelineAnnotations } from '$lib/components/assets/AssetGraph/parsePipelineAnnotations'
/**
* A `// partitioned` pipeline script is materialized one slice at a time and
* receives the slice as a runtime `partition` arg (the cascade injects it in
* production). It isn't a code parameter, so schema inference doesn't see it —
* surface it in the test form so a partitioned script can be run manually.
*/
export function injectPartitionArg(
s: any,
a: Record<string, any> | undefined,
l: string | undefined,
c: string
) {
try {
if (l !== 'duckdb' || !s?.properties) return
const part = parsePipelineAnnotations(c).partition
if (!part) return
// Date-based partition kinds render a date / datetime picker; a dynamic
// key is a free-form string.
const format =
part.kind === 'hourly'
? 'date-time'
: part.kind === 'daily' || part.kind === 'weekly' || part.kind === 'monthly'
? 'date'
: undefined
if (!s.properties['partition']) {
s.properties['partition'] = {
type: 'string',
...(format ? { format } : {}),
// ISO output so partition keys sort lexicographically (the date
// picker defaults to dd-MM-yyyy otherwise).
...(format === 'date' ? { dateFormat: 'yyyy-MM-dd' } : {}),
description:
part.kind === 'dynamic'
? 'Partition key value to materialize.'
: `Partition (${part.kind}) to materialize.`
}
if (Array.isArray(s.order) && !s.order.includes('partition')) {
s.order = ['partition', ...s.order]
}
}
// Pre-fill the *test* arg with the current slice for date kinds — a
// convenience default, kept on the args (not baked into the schema,
// where it would persist to the deployed script and go stale).
if (format && a && (a['partition'] == null || a['partition'] === '')) {
const now = new Date()
a['partition'] =
format === 'date' ? now.toISOString().slice(0, 10) : now.toISOString().slice(0, 16)
}
} catch (e) {}
}
/**
* The schema the script editor holds right after mounting on `content`, without
* any user input: `ScriptEditor.inferSchema` re-infers the main signature into
* the stored schema and injects the duckdb partition arg. A schema stored by
* another producer (a CLI push, an older parser) can lack keys the current
* parser emits, so the editor rewrites it on mount; a draft baseline taken from
* the raw stored schema would then never compare equal to the untouched editor.
* Returns a copy; a parse failure leaves it as far as inference got, which is
* also what the editor keeps.
*/
export async function schemaAsEditorMounts(
language: SupportedLanguage | undefined,
content: string,
schema: unknown,
kind: Script['kind'] | undefined
): Promise<Schema> {
const copy: Schema = schema ? structuredClone(schema as Schema) : emptySchema()
const infer = async () => {
await inferArgs(language, content, copy, kind === 'preprocessor' ? 'preprocessor' : undefined)
injectPartitionArg(copy, undefined, language, content)
}
try {
await infer()
} catch {
// The editors retry a failed mount inference once (transient wasm init);
// the baseline has to land where that retry leaves the editor.
if (!content || !language) return copy
try {
await infer()
} catch {}
}
return copy
}