diff --git a/backend/parsers/windmill-parser-php/src/lib.rs b/backend/parsers/windmill-parser-php/src/lib.rs index 1ac2b9a63a..14f30f940c 100644 --- a/backend/parsers/windmill-parser-php/src/lib.rs +++ b/backend/parsers/windmill-parser-php/src/lib.rs @@ -44,23 +44,41 @@ fn parse_default_expr(e: Expression) -> Option { pub fn parse_php_signature( code: &str, - override_main: Option, + override_entrypoint: Option, ) -> anyhow::Result { - let main_name = override_main.unwrap_or("main".to_string()); + let entrypoint_fn_name = override_entrypoint.unwrap_or("main".to_string()); let ast = parser::parse(code) .map_err(|e| anyhow::anyhow!("Error parsing code: {}", e.to_string()))?; - let params = ast.into_iter().find_map(|x| match x { - Statement::Function(FunctionStatement { - name, - parameters: FunctionParameterList { parameters, .. }, - .. - }) if name.to_string() == main_name => Some(parameters), - _ => None, - }); + let mut entrypoint_params = None; + let mut has_preprocessor = None; + for node in ast.into_iter() { + match node { + Statement::Function(FunctionStatement { + name, + parameters: FunctionParameterList { parameters, .. }, + .. + }) => { + let fn_name = name.to_string(); - if let Some(params) = params { + if has_preprocessor.is_none() && fn_name == "preprocessor" { + has_preprocessor = Some(true); + } + + if entrypoint_params.is_none() && fn_name == entrypoint_fn_name { + entrypoint_params = Some(parameters); + } + + if has_preprocessor.is_some() && entrypoint_params.is_some() { + break; + } + } + _ => {} + }; + } + + if let Some(params) = entrypoint_params { let args = params .into_iter() .map(|x| { @@ -82,7 +100,7 @@ pub fn parse_php_signature( star_kwargs: false, args, no_main_func: Some(false), - has_preprocessor: None, + has_preprocessor, }) } else { Ok(MainArgSignature { @@ -90,7 +108,7 @@ pub fn parse_php_signature( star_kwargs: false, args: vec![], no_main_func: Some(true), - has_preprocessor: None, + has_preprocessor, }) } } diff --git a/backend/parsers/windmill-parser-wasm/src/lib.rs b/backend/parsers/windmill-parser-wasm/src/lib.rs index f3b7929d38..f6de0a7b05 100644 --- a/backend/parsers/windmill-parser-wasm/src/lib.rs +++ b/backend/parsers/windmill-parser-wasm/src/lib.rs @@ -134,8 +134,11 @@ pub fn parse_graphql(code: &str) -> String { #[cfg(feature = "php-parser")] #[wasm_bindgen] -pub fn parse_php(code: &str) -> String { - wrap_sig(windmill_parser_php::parse_php_signature(code, None)) +pub fn parse_php(code: &str, main_override: Option) -> String { + wrap_sig(windmill_parser_php::parse_php_signature( + code, + main_override, + )) } #[cfg(feature = "rust-parser")] diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index f9f8709f5c..f550bcd09d 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -2232,7 +2232,7 @@ async fn push_next_flow_job( } }); - // if this is an empty module without preprocessor of if the module has already been completed, successfully, update the parent flow + // if this is an empty module without preprocessor or if the module has already been completed, successfully, update the parent flow if (flow.modules.is_empty() && !step.is_preprocessor_step()) || matches!(status_module, FlowStatusModule::Success { .. }) { @@ -3717,11 +3717,13 @@ pub struct JobPayloadWithTag { pub timeout: Option, pub on_behalf_of: Option, } +#[derive(Debug)] enum ContinuePayload { SingleJob(JobPayloadWithTag), ParallelJobs(Vec), } +#[derive(Debug)] enum NextFlowTransform { EmptyInnerFlows { branch_chosen: Option }, Continue(ContinuePayload, NextStatus), diff --git a/cli/wasm/php/windmill_parser_wasm.d.ts b/cli/wasm/php/windmill_parser_wasm.d.ts index 7324082938..dcb548ab52 100644 --- a/cli/wasm/php/windmill_parser_wasm.d.ts +++ b/cli/wasm/php/windmill_parser_wasm.d.ts @@ -1,3 +1,3 @@ /* tslint:disable */ /* eslint-disable */ -export function parse_php(code: string): string; +export function parse_php(code: string, main_override?: string | null): string; diff --git a/cli/wasm/php/windmill_parser_wasm.js b/cli/wasm/php/windmill_parser_wasm.js index c0cdf9229d..e95e3a5126 100644 --- a/cli/wasm/php/windmill_parser_wasm.js +++ b/cli/wasm/php/windmill_parser_wasm.js @@ -56,30 +56,41 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + function getStringFromWasm0(ptr, len) { ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); + return decodeText(ptr, len); } /** * @param {string} code + * @param {string | null} [main_override] * @returns {string} */ -export function parse_php(code) { - let deferred2_0; - let deferred2_1; +export function parse_php(code, main_override) { + let deferred3_0; + let deferred3_1; try { const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_php(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; + var ptr1 = isLikeNone(main_override) ? 0 : passStringToWasm0(main_override, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + const ret = wasm.parse_php(ptr0, len0, ptr1, len1); + deferred3_0 = ret[0]; + deferred3_1 = ret[1]; return getStringFromWasm0(ret[0], ret[1]); } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); } } diff --git a/cli/wasm/php/windmill_parser_wasm_bg.wasm b/cli/wasm/php/windmill_parser_wasm_bg.wasm index 2d772cc834..c92e10ebac 100644 Binary files a/cli/wasm/php/windmill_parser_wasm_bg.wasm and b/cli/wasm/php/windmill_parser_wasm_bg.wasm differ diff --git a/cli/wasm/php/windmill_parser_wasm_bg.wasm.d.ts b/cli/wasm/php/windmill_parser_wasm_bg.wasm.d.ts index a771d8a427..30c3d0eff5 100644 --- a/cli/wasm/php/windmill_parser_wasm_bg.wasm.d.ts +++ b/cli/wasm/php/windmill_parser_wasm_bg.wasm.d.ts @@ -1,7 +1,7 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; -export const parse_php: (a: number, b: number) => [number, number]; +export const parse_php: (a: number, b: number, c: number, d: number) => [number, number]; export const __wbindgen_export_0: WebAssembly.Table; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2055f79b82..5ea8f6b9a5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -77,7 +77,7 @@ "windmill-parser-wasm-go": "1.510.1", "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", - "windmill-parser-wasm-php": "1.510.1", + "windmill-parser-wasm-php": "1.574.1", "windmill-parser-wasm-py": "1.538.0", "windmill-parser-wasm-regex": "1.565.0", "windmill-parser-wasm-ruby": "1.526.1", @@ -13749,9 +13749,9 @@ "integrity": "sha512-AJLFiUy6af+LpUe7CddDo4+JOmw3c0K/1iOWh8NdTwXcLDj90lL6089mdsVo1apyloLgrTbcuFDzZMXVGBgtCg==" }, "node_modules/windmill-parser-wasm-php": { - "version": "1.510.1", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-php/-/windmill-parser-wasm-php-1.510.1.tgz", - "integrity": "sha512-qM+yeaqPdMuAaPpqND31ZabpeHlPxxtmRWLs11cGHOCHU32FIaZ92/icNUoAxgBhdJjRhR4GXaeZG32nWit2cg==" + "version": "1.574.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-php/-/windmill-parser-wasm-php-1.574.1.tgz", + "integrity": "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA==" }, "node_modules/windmill-parser-wasm-py": { "version": "1.538.0", diff --git a/frontend/package.json b/frontend/package.json index 3f5a13c88e..53390939e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -142,7 +142,7 @@ "windmill-parser-wasm-go": "1.510.1", "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", - "windmill-parser-wasm-php": "1.510.1", + "windmill-parser-wasm-php": "1.574.1", "windmill-parser-wasm-py": "1.538.0", "windmill-parser-wasm-regex": "1.565.0", "windmill-parser-wasm-ruby": "1.526.1", diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index e1b5ded284..98efc71b86 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -92,7 +92,7 @@ import { aiChatManager } from './copilot/chat/AIChatManager.svelte' import type { Selection } from 'monaco-editor' import { getDbSchemas } from './apps/components/display/dbtable/utils' - import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers' + import { canHavePreprocessor, getPreprocessorModuleCode } from '$lib/script_helpers' import { setMonacoTypescriptOptions } from './monacoLanguagesOptions' import { copilotInfo } from '$lib/aiStore' // import EditorTheme from './EditorTheme.svelte' @@ -620,12 +620,18 @@ } let preprocessorCompletor: IDisposable | undefined = undefined - function addPreprocessorCompletions(lang: 'typescript' | 'python') { + function addPreprocessorCompletions(lang: string) { if (preprocessorCompletor) { preprocessorCompletor.dispose() } - const preprocessorCode = - lang === 'typescript' ? TS_PREPROCESSOR_MODULE_CODE : PYTHON_PREPROCESSOR_MODULE_CODE + + const windmillLang = lang === 'typescript' ? 'deno' : lang === 'python' ? 'python3' : lang + const preprocessorCode = getPreprocessorModuleCode(windmillLang as ScriptLang) + + if (!preprocessorCode) { + return + } + preprocessorCompletor = languages.registerCompletionItemProvider(lang, { provideCompletionItems: function (model, position) { const word = model.getWordUntilPosition(position) @@ -1653,8 +1659,8 @@ }) $effect(() => { - initialized && (lang === 'typescript' || lang === 'python') && enablePreprocessorSnippet - ? untrack(() => addPreprocessorCompletions(lang as 'typescript' | 'python')) + initialized && canHavePreprocessor(lang) && enablePreprocessorSnippet + ? untrack(() => addPreprocessorCompletions(lang)) : preprocessorCompletor?.dispose() }) diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index a34fcd388a..03bbe541c1 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -15,7 +15,12 @@ WorkerService } from '$lib/gen' import { inferArgs } from '$lib/infer' - import { initialCode } from '$lib/script_helpers' + import { + initialCode, + canHavePreprocessor, + getPreprocessorFullCode, + getMainFunctionPattern + } from '$lib/script_helpers' import AIFormSettings from './copilot/AIFormSettings.svelte' import { defaultScripts, @@ -80,12 +85,6 @@ import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte' import TriggersEditor from './triggers/TriggersEditor.svelte' import type { ScheduleTrigger, TriggerContext } from './triggers' - import { - TS_PREPROCESSOR_MODULE_CODE, - TS_PREPROCESSOR_SCRIPT_INTRO, - PYTHON_PREPROCESSOR_MODULE_CODE, - PYTHON_PREPROCESSOR_SCRIPT_INTRO - } from '$lib/script_helpers' import CaptureTable from './triggers/CaptureTable.svelte' import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes' import DeployButton from './DeployButton.svelte' @@ -313,7 +312,7 @@ { value: 'preprocessor', title: 'Preprocessor', - desc: 'Transform incoming requests before they are passed to the flow.', + desc: 'Transform incoming requests before they are passed to the main entrypoint.', documentationLink: 'https://www.windmill.dev/docs/core_concepts/preprocessors', Icon: Shuffle } @@ -853,13 +852,10 @@ function addPreprocessor() { const code = editor?.getCode() if (code) { - const preprocessorCode = - script.language === 'python3' - ? PYTHON_PREPROCESSOR_SCRIPT_INTRO + PYTHON_PREPROCESSOR_MODULE_CODE - : TS_PREPROCESSOR_SCRIPT_INTRO + TS_PREPROCESSOR_MODULE_CODE - const mainIndex = code.indexOf( - script.language === 'python3' ? 'def main' : 'export async function main' - ) + const preprocessorCode = getPreprocessorFullCode(script.language, false) + const mainPattern = getMainFunctionPattern(script.language) + const mainIndex = code.indexOf(mainPattern) + if (mainIndex === -1) { editor?.setCode(code + preprocessorCode) } else { @@ -1139,7 +1135,8 @@ btnClasses={isPicked ? '' : 'm-[1px]'} on:click={() => onScriptLanguageTrigger(lang)} disabled={lockedLanguage || - (enterpriseLangs.includes(lang) && !$enterpriseLicense)} + (enterpriseLangs.includes(lang) && !$enterpriseLicense) || + (script.kind == 'preprocessor' && !canHavePreprocessor(lang))} startIcon={{ icon: LanguageIcon, props: { lang } @@ -1667,9 +1664,7 @@ newItem={initialPath == ''} isFlow={false} {hasPreprocessor} - canHavePreprocessor={script.language === 'bun' || - script.language === 'deno' || - script.language === 'python3'} + canHavePreprocessor={canHavePreprocessor(script.language)} args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args} isDeployed={savedScript && !savedScript?.draft_only} schema={script.schema} diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index fdfbec6daa..a9fcdc5cba 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -48,6 +48,7 @@ import { aiChatManager, AIMode } from './copilot/chat/AIChatManager.svelte' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' import AssetsDropdownButton from './assets/AssetsDropdownButton.svelte' + import { canHavePreprocessor } from '$lib/script_helpers' import { assetEq, type AssetWithAltAccessType } from './assets/lib' import { editor as meditor } from 'monaco-editor' import type { ReviewChangesOpts } from './copilot/chat/monaco-adapter' @@ -741,7 +742,7 @@ 1, 'a' => 2] → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main($a, $b)\`. + * Ensure that the parameter names in \`main\` match the keys in the returned array. + * + * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors + */ + +` + +export const PHP_PREPROCESSOR_FLOW_INTRO = ` 1, 'a' => 2] → Calls the flow with \`a = 2\` and \`b = 1\`, assuming the flow has two inputs called \`a\` and \`b\`. + * Ensure that the input names of the flow match the keys in the returned array. + * + * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors + */ + +` + +export const PHP_PREPROCESSOR_MODULE_CODE = `function preprocessor(object $event) { + // $event can be one of the following types: + // + // Webhook event: + // ['kind' => 'webhook', 'body' => [...], 'raw_string' => '...', 'query' => [...], 'headers' => [...]] + // + // HTTP event: + // ['kind' => 'http', 'body' => [...], 'raw_string' => '...', 'route' => '...', 'path' => '...', + // 'method' => '...', 'params' => [...], 'query' => [...], 'headers' => [...]] + // + // Email event: + // ['kind' => 'email', 'parsed_email' => [...], 'raw_email' => '...', 'email_extra_args' => [...]] + // + // WebSocket event: + // ['kind' => 'websocket', 'msg' => '...', 'url' => '...'] + // + // Kafka event: + // ['kind' => 'kafka', 'payload' => '...', 'brokers' => [...], 'topic' => '...', 'group_id' => '...'] + // + // NATS event: + // ['kind' => 'nats', 'payload' => '...', 'servers' => [...], 'subject' => '...', + // 'headers' => [...], 'status' => 200, 'description' => '...', 'length' => 100] + // + // SQS event: + // ['kind' => 'sqs', 'msg' => '...', 'queue_url' => '...', 'message_id' => '...', + // 'receipt_handle' => '...', 'attributes' => [...], 'message_attributes' => [...]] + // + // MQTT event: + // ['kind' => 'mqtt', 'payload' => '...', 'topic' => '...', 'retain' => true, 'pkid' => 1, + // 'qos' => 1, 'v5' => [...]] + // + // GCP event: + // ['kind' => 'gcp', 'payload' => '...', 'message_id' => '...', 'subscription' => '...', + // 'ordering_key' => '...', 'attributes' => [...], 'delivery_type' => 'push', + // 'headers' => [...], 'publish_time' => '...', 'ack_id' => '...'] + // + // Postgres event: + // ['kind' => 'postgres', 'transaction_type' => 'insert', 'schema_name' => '...', + // 'table_name' => '...', 'old_row' => [...], 'row' => [...]] + + return [ + // return the args to be passed to the runnable + ]; +} +` + const DOCKER_INIT_CODE = `# shellcheck shell=bash # docker # The annotation "docker" above is important, it tells windmill that after @@ -1226,7 +1306,8 @@ export const INITIAL_CODE = { script: ORACLEDB_INIT_CODE }, php: { - script: PHP_INIT_CODE + script: PHP_INIT_CODE, + preprocessor: PHP_PREPROCESSOR_FLOW_INTRO + PHP_PREPROCESSOR_MODULE_CODE }, rust: { script: RUST_INIT_CODE @@ -1349,6 +1430,9 @@ export function initialCode( } else if (language == 'duckdb') { return INITIAL_CODE.duckdb.script } else if (language == 'php') { + if (kind == 'preprocessor') { + return INITIAL_CODE.php.preprocessor + } return INITIAL_CODE.php.script } else if (language == 'rust') { return INITIAL_CODE.rust.script @@ -1420,3 +1504,115 @@ export function getResetCode( return initialCode(language, kind, subkind) } } + +export const PREPROCESSOR_SUPPORTED_LANGUAGES = [ + 'typescript', + 'python', + 'python3', + 'deno', + 'bun', + 'php' +] as const + +export function canHavePreprocessor(language: string | undefined): boolean { + if (!language) { + return false + } + + return PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any) +} + +export function canHaveTrigger(language: SupportedLanguage | undefined): boolean { + if (!language) { + return false + } + + return ['python3', 'bun', 'deno', 'go'].includes(language) +} + +export function canHaveApproval(language: SupportedLanguage | undefined): boolean { + if (!language) { + return false + } + + return ['python3', 'bun', 'deno'].includes(language) +} + +export function canHaveFailure(language: SupportedLanguage | undefined): boolean { + if (!language) { + return false + } + + return ['python3', 'bun', 'deno', 'go'].includes(language) +} + +export function getPreprocessorIntro( + language: SupportedLanguage | 'docker' | 'bunnative' | undefined, + isFlow: boolean = false +): string { + if (!language || !PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any)) { + return '' + } + + switch (language) { + case 'python3': + return isFlow ? PYTHON_PREPROCESSOR_FLOW_INTRO : PYTHON_PREPROCESSOR_SCRIPT_INTRO + case 'deno': + case 'bun': + return isFlow ? TS_PREPROCESSOR_FLOW_INTRO : TS_PREPROCESSOR_SCRIPT_INTRO + case 'php': + return isFlow ? PHP_PREPROCESSOR_FLOW_INTRO : PHP_PREPROCESSOR_SCRIPT_INTRO + default: + return '' + } +} + +export function getPreprocessorModuleCode( + language: SupportedLanguage | 'docker' | 'bunnative' | undefined +): string { + if (!language || !PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any)) { + return '' + } + + switch (language) { + case 'python3': + return PYTHON_PREPROCESSOR_MODULE_CODE + case 'deno': + case 'bun': + return TS_PREPROCESSOR_MODULE_CODE + case 'php': + return PHP_PREPROCESSOR_MODULE_CODE + default: + return '' + } +} + +export function getPreprocessorFullCode( + language: SupportedLanguage | 'docker' | 'bunnative' | undefined, + isFlow: boolean = false +): string { + const intro = getPreprocessorIntro(language, isFlow) + const moduleCode = getPreprocessorModuleCode(language) + return intro + moduleCode +} + +export function getMainFunctionPattern( + language: SupportedLanguage | 'docker' | 'bunnative' | undefined +): string { + if (!language) { + return '' + } + + switch (language) { + case 'python3': + return 'def main' + case 'deno': + case 'bun': + case 'nativets': + return 'export async function main' + case 'php': + return 'function main' + default: + return 'main' + } +}