diff --git a/bin/si-api-test/sdf_api_client.ts b/bin/si-api-test/sdf_api_client.ts index 43e17cb5c7..2a8f5daf7a 100644 --- a/bin/si-api-test/sdf_api_client.ts +++ b/bin/si-api-test/sdf_api_client.ts @@ -49,6 +49,11 @@ export const ROUTES = { path: () => `/component/set_type`, method: "POST", }, + dvu_roots: { + path: (vars: ROUTE_VARS) => + `/diagram/dvu_roots?visibility_change_set_pk=${vars.changeSetId}&workspaceId=${vars.workspaceId}`, + method: "GET", + }, // Component Management ------------------------------------------------------- delete_component: { @@ -85,6 +90,19 @@ export const ROUTES = { path: () => `/variant/create_variant`, method: "POST", }, + save_variant: { + path: () => `/variant/save_variant`, + method: "POST", + }, + regenerate_variant: { + path: () => `/variant/regenerate_variant`, + method: "POST", + }, + get_variant: { + path: (vars: ROUTE_VARS) => + `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/schema-variants/${vars.schemaVariantId}`, + method: "GET", + }, // Action Management ----------------------------------------------------------- action_list: { @@ -106,6 +124,26 @@ export const ROUTES = { `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/funcs`, method: "GET", }, + create_func: { + path: (vars: ROUTE_VARS) => + `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/funcs`, + method: "POST", + }, + create_func_arg: { + path: (vars: ROUTE_VARS) => + `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/funcs/${vars.funcId}/arguments`, + method: "POST", + }, + create_func_binding: { + path: (vars: ROUTE_VARS) => + `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/funcs/${vars.funcId}/bindings`, + method: "PUT", + }, + update_func_code: { + path: (vars: ROUTE_VARS) => + `/v2/workspaces/${vars.workspaceId}/change-sets/${vars.changeSetId}/funcs/${vars.funcId}/code`, + method: "PUT", + }, // Websockets ----------------------------------------- workspace_updates_ws: { @@ -243,6 +281,39 @@ export class SdfApiClient { dvuListener.listen(); } + public async waitForDVURoots( + changeSetId: string, + interval_ms: number, + timeout_ms: number, + ): Promise { + console.log(`Waiting on DVUs for ${this.workspaceId}...`); + const dvuPromise = new Promise((resolve) => { + const interval = setInterval(async () => { + const remainingRoots = await this.call({ route: "dvu_roots", routeVars: { changeSetId } }); + if (remainingRoots?.count === 0) { + console.log(`All DVUs for ${this.workspaceId} finished!`); + clearInterval(interval); + resolve(); + } else { + console.log( + `Waiting for DVUs in workspace ${this.workspaceId} to finish, ${remainingRoots?.count} remain...`, + ); + } + }, interval_ms); + }); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + console.log( + `Timeout reached while waiting for DVUs in workspace ${this.workspaceId}.`, + ); + reject(new Error("Timeout while waiting for DVUs to finish.")); + }, timeout_ms); + }); + + return Promise.race([dvuPromise, timeoutPromise]); + } + public async waitForDVUs( interval_ms: number, timeout_ms: number, diff --git a/bin/si-api-test/test_helpers.ts b/bin/si-api-test/test_helpers.ts index 6c73258e24..2cbf21e4ac 100644 --- a/bin/si-api-test/test_helpers.ts +++ b/bin/si-api-test/test_helpers.ts @@ -1,5 +1,7 @@ import { SdfApiClient } from "./sdf_api_client.ts"; import assert from "node:assert"; +import { ulid } from "https://deno.land/x/ulid@v0.3.0/mod.ts"; + export function sleep(ms: number) { const natural_ms = Math.max(0, Math.floor(ms)); @@ -87,3 +89,241 @@ export async function runWithTemporaryChangeset( throw err; } } + +export const nilId = "00000000000000000000000000"; + +// Diagram Helpers ------------------------------------------------------------ + + +export async function getQualificationSummary(sdf: SdfApiClient, changeSetId: string) { + return await sdf.call({ + route: "qualification_summary", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + }); +} + +export async function getActions(sdf: SdfApiClient, changeSetId: string) { + return await sdf.call({ + route: "action_list", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + }); +} + +export async function getFuncs(sdf: SdfApiClient, changeSetId: string) { + return await sdf.call({ + route: "func_list", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + }); +} + + +// Prop Helpers ------------------------------------------------------------ + + +export async function getPropertyEditor( + sdf: SdfApiClient, + changeSetId: string, + componentId: string, +) { + const values = await sdf.call({ + route: "get_property_values", + routeVars: { + componentId, + changeSetId, + }, + }); + assert(typeof values?.values === "object", "Expected prop values"); + assert(typeof values?.childValues === "object", "Expected prop childValues:"); + + const schema = await sdf.call({ + route: "get_property_schema", + routeVars: { + componentId, + changeSetId, + }, + }); + assert(typeof schema?.rootPropId === "string", "Expected rootPropId"); + assert(typeof schema?.props === "object", "Expected props"); + assert(typeof schema?.childProps === "object", "Expected childProps list"); + + return { + values, + schema, + }; +} + +export async function setAttributeValue( + sdf: SdfApiClient, + changeSetId: string, + componentId: string, + attributeValueId: string, + parentAttributeValueId: string, + propId: string, + value: unknown, +) { + const updateValuePayload = { + visibility_change_set_pk: changeSetId, + componentId, + attributeValueId, + parentAttributeValueId, + propId, + value, + isForSecret: false, + }; + + await sdf.call({ + route: "update_property_value", + body: updateValuePayload, + }); +} + +export function attributeValueIdForPropPath( + propPath: string, + propList: any[], + attributeValuesView: { + values: any[]; + childValues: any[]; + }, +) { + const prop = propList.find((p) => p.path === propPath); + assert(prop, `Expected to find ${propPath} prop`); + + let attributeValueId; + let value; + for (const attributeValue in attributeValuesView.values) { + if (attributeValuesView.values[attributeValue]?.propId === prop.id) { + attributeValueId = attributeValue; + value = attributeValuesView.values[attributeValue]?.value; + } + } + assert(attributeValueId, "Expected source attribute value"); + + let parentAttributeValueId; + for (const attributeValue in attributeValuesView?.childValues) { + const avChildren = attributeValuesView?.childValues[attributeValue] ?? []; + if (avChildren.includes(attributeValueId)) { + parentAttributeValueId = attributeValue; + } + } + assert(parentAttributeValueId, "Expected parent of source attribute value"); + + return { + attributeValueId, + parentAttributeValueId, + propId: prop.id, + value, + }; +} + + +// Schema Variant Helpers ------------------------------------------------------------ + +export async function createAsset( + sdf: SdfApiClient, + changeSetId: string, + name: string, +): Promise { + const createAssetPayload = { + visibility_change_set_pk: changeSetId, + name, + color: "#AAFF00", + }; + + const createResp = await sdf.call({ route: "create_variant", body: createAssetPayload }); + const schemaVariantId = createResp?.schemaVariantId; + assert(schemaVariantId, "Expected to get a schema variant id after creation"); + return schemaVariantId; +} + +export async function updateAssetCode( + sdf: SdfApiClient, + changeSetId: string, + schemaVariantId: string, + newCode: string, +): Promise { + // Get variant + const variant = await sdf.call({ + route: "get_variant", routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + schemaVariantId, + }, + }); + // update variant + const updateVariantBody = { + visibility_change_set_pk: changeSetId, + code: newCode, + variant, + }; + + const saveResp = await sdf.call({ + route: "save_variant", + body: updateVariantBody, + }); + const success = saveResp.success; + assert(success, "save was successful"); + + const regenBody = { + visibility_change_set_pk: changeSetId, + variant, + }; + + const regenerateResp = await sdf.call({ + route: "regenerate_variant", + body: regenBody, + }); + + const maybeNewId = regenerateResp.schemaVariantId; + assert(maybeNewId, "Expected to get a schema variant id after regenerate"); + return maybeNewId; +} + + +// Component Helpers ------------------------------------------------------------ + +export async function createComponent( + sdf: SdfApiClient, + changeSetId: string, + schemaVariantId: string, + x: number, + y: number, + parentId?: string, + newCreateComponentApi?: boolean, +): Promise { + const parentArgs = parentId ? { parentId } : {}; + const payload = { + schemaType: newCreateComponentApi ? "installed" : undefined, + schemaVariantId, + x: x.toString(), + y: y.toString(), + visibility_change_set_pk: changeSetId, + workspaceId: sdf.workspaceId, + ...parentArgs, + }; + const createResp = await sdf.call({ + route: "create_component", + body: payload, + }); + const componentId = createResp?.componentId; + assert(componentId, "Expected to get a component id after creation"); + + // Run side effect calls + await Promise.all([ + getQualificationSummary(sdf, changeSetId), + getActions(sdf, changeSetId), + getFuncs(sdf, changeSetId), + getPropertyEditor(sdf, changeSetId, componentId), + ]); + + return componentId; +} + diff --git a/bin/si-api-test/tests/5-emulate_paul_stack.ts b/bin/si-api-test/tests/5-emulate_paul_stack.ts index d2472446e1..cae2855d71 100644 --- a/bin/si-api-test/tests/5-emulate_paul_stack.ts +++ b/bin/si-api-test/tests/5-emulate_paul_stack.ts @@ -5,6 +5,13 @@ import { runWithTemporaryChangeset, sleep, sleepBetween, + createComponent, + getPropertyEditor, + setAttributeValue, + attributeValueIdForPropPath, + getQualificationSummary, + getActions, + getFuncs, } from "../test_helpers.ts"; import { ulid } from "https://deno.land/x/ulid@v0.3.0/mod.ts"; @@ -225,45 +232,31 @@ async function emulate_paul_stack_inner( } } - await sdf.waitForDVUs(2000, 60000); + await sdf.waitForDVURoots(changeSetId, 2000, 60000); + } // REQUEST HELPERS WITH VALIDATIONS -async function createComponent( + +async function getDiagram( sdf: SdfApiClient, changeSetId: string, - schemaVariantId: string, - x: number, - y: number, - parentId?: string, - newCreateComponentApi?: boolean, -): Promise { - const parentArgs = parentId ? { parentId } : {}; - const payload = { - schemaType: newCreateComponentApi ? "installed" : undefined, - schemaVariantId, - x: x.toString(), - y: y.toString(), - visibility_change_set_pk: changeSetId, - workspaceId: sdf.workspaceId, - ...parentArgs, - }; - const createResp = await sdf.call({ - route: "create_component", - body: payload, +): Promise<{ components: any[]; edges: any[] }> { + const diagram = await sdf.call({ + route: "get_diagram", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, }); - const componentId = createResp?.componentId; - assert(componentId, "Expected to get a component id after creation"); - // Run side effect calls - await Promise.all([ - getQualificationSummary(sdf, changeSetId), - getActions(sdf, changeSetId), - getFuncs(sdf, changeSetId), - getPropertyEditor(sdf, changeSetId, componentId), - ]); + assert( + Array.isArray(diagram?.components), + "Expected components list on the diagram", + ); + assert(Array.isArray(diagram?.edges), "Expected edges list on the diagram"); - return componentId; + return diagram; } async function setComponentGeometry( @@ -347,52 +340,6 @@ async function setComponentType( return result; } -async function getDiagram( - sdf: SdfApiClient, - changeSetId: string, -): Promise<{ components: any[]; edges: any[] }> { - const diagram = await sdf.call({ - route: "get_diagram", - routeVars: { - workspaceId: sdf.workspaceId, - changeSetId, - }, - }); - - assert( - Array.isArray(diagram?.components), - "Expected components list on the diagram", - ); - assert(Array.isArray(diagram?.edges), "Expected edges list on the diagram"); - - return diagram; -} - -async function setAttributeValue( - sdf: SdfApiClient, - changeSetId: string, - componentId: string, - attributeValueId: string, - parentAttributeValueId: string, - propId: string, - value: unknown, -) { - const updateValuePayload = { - visibility_change_set_pk: changeSetId, - componentId, - attributeValueId, - parentAttributeValueId, - propId, - value, - isForSecret: false, - }; - - await sdf.call({ - route: "update_property_value", - body: updateValuePayload, - }); -} - async function getSchemaVariants(sdf: SdfApiClient, changeSetId: string) { let schemaVariants = await sdf.call({ route: "schema_variants", @@ -415,67 +362,7 @@ async function getSchemaVariants(sdf: SdfApiClient, changeSetId: string) { return { schemaVariants, newCreateComponentApi }; } -async function getPropertyEditor( - sdf: SdfApiClient, - changeSetId: string, - componentId: string, -) { - const values = await sdf.call({ - route: "get_property_values", - routeVars: { - componentId, - changeSetId, - }, - }); - assert(typeof values?.values === "object", "Expected prop values"); - assert(typeof values?.childValues === "object", "Expected prop childValues:"); - const schema = await sdf.call({ - route: "get_property_schema", - routeVars: { - componentId, - changeSetId, - }, - }); - assert(typeof schema?.rootPropId === "string", "Expected rootPropId"); - assert(typeof schema?.props === "object", "Expected props"); - assert(typeof schema?.childProps === "object", "Expected childProps list"); - - return { - values, - schema, - }; -} - -async function getQualificationSummary(sdf: SdfApiClient, changeSetId: string) { - return await sdf.call({ - route: "qualification_summary", - routeVars: { - workspaceId: sdf.workspaceId, - changeSetId, - }, - }); -} - -async function getActions(sdf: SdfApiClient, changeSetId: string) { - return await sdf.call({ - route: "action_list", - routeVars: { - workspaceId: sdf.workspaceId, - changeSetId, - }, - }); -} - -async function getFuncs(sdf: SdfApiClient, changeSetId: string) { - return await sdf.call({ - route: "func_list", - routeVars: { - workspaceId: sdf.workspaceId, - changeSetId, - }, - }); -} // Data Extractors function extractSchemaVariant( @@ -497,37 +384,4 @@ function extractSchemaVariant( return variant; } -function attributeValueIdForPropPath( - propPath: string, - propList: any[], - attributeValuesView: { - values: any[]; - childValues: any[]; - }, -) { - const prop = propList.find((p) => p.path === propPath); - assert(prop, `Expected to find ${propPath} prop`); - let attributeValueId; - for (const attributeValue in attributeValuesView.values) { - if (attributeValuesView.values[attributeValue]?.propId === prop.id) { - attributeValueId = attributeValue; - } - } - assert(attributeValueId, "Expected source attribute value"); - - let parentAttributeValueId; - for (const attributeValue in attributeValuesView?.childValues) { - const avChildren = attributeValuesView?.childValues[attributeValue] ?? []; - if (avChildren.includes(attributeValueId)) { - parentAttributeValueId = attributeValue; - } - } - assert(parentAttributeValueId, "Expected parent of source attribute value"); - - return { - attributeValueId, - parentAttributeValueId, - propId: prop.id, - }; -} diff --git a/bin/si-api-test/tests/7-emulate_authoring.ts b/bin/si-api-test/tests/7-emulate_authoring.ts new file mode 100644 index 0000000000..3fc71fe322 --- /dev/null +++ b/bin/si-api-test/tests/7-emulate_authoring.ts @@ -0,0 +1,462 @@ +// deno-lint-ignore-file no-explicit-any +import assert from "node:assert"; +import { SdfApiClient } from "../sdf_api_client.ts"; +import { + runWithTemporaryChangeset, + sleep, + sleepBetween, + createComponent, + createAsset, + updateAssetCode, + nilId, + getQualificationSummary, + getPropertyEditor, + attributeValueIdForPropPath, + setAttributeValue, +} from "../test_helpers.ts"; +import { ulid } from "https://deno.land/x/ulid@v0.3.0/mod.ts"; + +export default async function emulate_authoring(sdfApiClient: SdfApiClient) { + await sleepBetween(0, 750); + return runWithTemporaryChangeset(sdfApiClient, emulate_authoring_inner); +} + +async function emulate_authoring_inner(sdf: SdfApiClient, + changeSetId: string, +) { + sdf.listenForDVUs(); + + // Create new asset + let schemaVariantId = await createAsset(sdf, changeSetId, "doubler"); + + // Update Code and Regenerate + schemaVariantId = await updateAssetCode(sdf, changeSetId, schemaVariantId, doublerAssetCode); + + let doublerVariant = await sdf.call({ + route: "get_variant", routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + schemaVariantId, + }, + }); + assert(doublerVariant?.props, "Expected props list"); + assert(doublerVariant?.inputSockets, "Expected input sockets list"); + assert(doublerVariant?.inputSockets.length === 0, "Expected no input sockets"); + assert(doublerVariant?.outputSockets, "Expected output sockets list"); + assert(doublerVariant?.outputSockets.length === 0, "Expected no output sockets"); + + // Add an attribute function + const outputProp = doublerVariant.props.find((p) => p.path === "/root/domain/doubled"); + assert(outputProp, "Expected to find output prop"); + let inputProp = doublerVariant.props.find((p) => p.path === "/root/domain/input"); + assert(inputProp, "Expected to find input prop"); + const args = [ + { + name: "input", + kind: "string", + propId: inputProp.id, + inputSocketId: null, + }, + ]; + + const doubleFuncId = await createAttributeFunction(sdf, changeSetId, "double", doublerVariant.id, doubleFuncCode, outputProp.id, args); + + + // create a component for the doubler + const doublerComponentId = await createComponent(sdf, changeSetId, doublerVariant.schemaVariantId, 0, 0, undefined, true); + + // update input prop to be a number + const { values: doublerPropValues } = await getPropertyEditor( + sdf, + changeSetId, + doublerComponentId, + ); + + + const { attributeValueId, parentAttributeValueId, propId } = + attributeValueIdForPropPath( + "/root/domain/input", + doublerVariant.props, + doublerPropValues, + ); + const inputValue = "2"; + await setAttributeValue( + sdf, + changeSetId, + doublerComponentId, + attributeValueId, + parentAttributeValueId, + propId, + inputValue, + ); + + + // now add a qualification and check that the component gets it + const qualification = await createQualification( + sdf, + changeSetId, "doublerQualification", + schemaVariantId, + doublerQualificationCode + ); + + await sdf.waitForDVURoots(changeSetId, 2000, 60000); + + // now let's check everything + const qualSummary = await getQualificationSummary(sdf, changeSetId); + assert( + Array.isArray(qualSummary?.components), + "Expected arguments list", + ); + const componentQual = qualSummary?.components.find((c) => c.componentId === doublerComponentId); + assert(componentQual, "Expected to find qualification summary for created component"); + assert(componentQual?.succeeded === 1, "Expected to have one qualification passing"); + const { values: doublerValues } = await getPropertyEditor( + sdf, + changeSetId, + doublerComponentId, + ); + + const doubleAV = + attributeValueIdForPropPath( + "/root/domain/doubled", + doublerVariant.props, + doublerValues, + ); + + assert(doubleAV?.value === "4", `Expected doubled attribute value to be 4 but found ${doubleAV?.value}`); + + // Now let's add a prop, regenerate, add an attribute func, and make sure it all works + schemaVariantId = await updateAssetCode(sdf, changeSetId, schemaVariantId, doublerAssetCodeAddedTripled); + doublerVariant = await sdf.call({ + route: "get_variant", routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + schemaVariantId, + }, + }); + + + // Add an attribute function + const tripleProp = doublerVariant.props.find((p) => p.path === "/root/domain/tripled"); + assert(tripleProp, "Expected to find output prop"); + inputProp = doublerVariant.props.find((p) => p.path === "/root/domain/input"); + assert(inputProp, "Expected to find input prop"); + const triplerArgs = [ + { + name: "input", + kind: "string", + propId: inputProp.id, + inputSocketId: null, + }, + ]; + + const tripleFuncId = await createAttributeFunction(sdf, changeSetId, "triple", schemaVariantId, tripleFuncCode, tripleProp.id, triplerArgs); + + const newDoublerVariant = await sdf.call({ + route: "get_variant", routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + schemaVariantId, + }, + }); + await sdf.waitForDVURoots(changeSetId, 2000, 60000); + + // Let's make sure this updated accurately on the component + const { values: triplerValues } = await getPropertyEditor( + sdf, + changeSetId, + doublerComponentId, + ); + + const tripleAV = + attributeValueIdForPropPath( + "/root/domain/tripled", + newDoublerVariant.props, + triplerValues, + ); + assert(tripleAV?.value === "6", `Expected doubled attribute value to be 6 but found ${tripleAV?.value}`); + +} + + +const doubleFuncCode = `async function main(input: Input): Promise < Output > { + const number = parseInt(input?.input); + if (!number) { + return String(0); + } + + return String(number * 2); +} +`; + +const tripleFuncCode = `async function main(input: Input): Promise < Output > { + const number = parseInt(input?.input); + if (!number) { + return String(0); + } + + return String(number * 3); +} +`; + +const doublerAssetCode = `function main() { + const asset = new AssetBuilder(); + + const inputProp = new PropBuilder() + .setName("input") + .setKind("string") + .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build()) + .build(); + asset.addProp(inputProp); + + const doublerProp = new PropBuilder() + .setName("doubled") + .setKind("string") + .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build()) + .build(); + + asset.addProp(doublerProp); + + return asset.build(); +}`; + +const doublerAssetCodeAddedTripled = `function main() { + const asset = new AssetBuilder(); + + const inputProp = new PropBuilder() + .setName("input") + .setKind("string") + .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build()) + .build(); + asset.addProp(inputProp); + + const doublerProp = new PropBuilder() + .setName("doubled") + .setKind("string") + .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build()) + .build(); + + asset.addProp(doublerProp); + + const triplerProp = new PropBuilder() + .setName("tripled") + .setKind("string") + .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build()) + .build(); + + asset.addProp(triplerProp); + + return asset.build(); +}`; + +const doublerQualificationCode = `async function main(component: Input): Promise < Output > { + + const doubler = component?.domain?.doubler; + if (doubler) { + const val = parseInt(doubler); + if (val > 0) { + return { + result: 'success', + message: 'Component qualified' + }; + } + } + + return { + result: 'failure', + message: 'Component not qualified' + }; +}`; + +// REQUEST HELPERS WITH VALIDATIONS + +async function createQualification(sdf: SdfApiClient, changeSetId: string, name: string, schemaVariantId: string, code: string) { + const createFuncPayload = { + name, + displayName: name, + description: "", + binding: { + funcId: nilId, + schemaVariantId, + bindingKind: "qualification", + inputs: [], + }, + kind: "Qualification", + requestUlid: changeSetId, + }; + const createFuncResp = await sdf.call({ + route: "create_func", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + body: createFuncPayload, + }); + // now list funcs and let's make sure we see it + const funcs = await sdf.call({ + route: "func_list", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + }); + + const createdFunc = funcs.find((f) => f.name === name); + assert(createdFunc, "Expected to find newly created func"); + const funcId = createdFunc.funcId; + const codePayload = { + code, + requestUlid: changeSetId, + }; + + // save the code + const updateFuncResp = await sdf.call({ + route: "update_func_code", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + funcId, + }, + body: codePayload, + }); + return funcId; + +} + +async function createAttributeFunction(sdf: SdfApiClient, changeSetId: string, name: string, schemaVariantId: string, code: string, outputLocationId: string, funcArguments: any[]) { + const createFuncPayload = { + name, + displayName: name, + description: "", + binding: { + funcId: nilId, + schemaVariantId: schemaVariantId, + bindingKind: "attribute", + argumentBindings: [], + propId: outputLocationId, + }, + kind: "Attribute", + requestUlid: changeSetId, + }; + + const createFuncResp = await sdf.call({ + route: "create_func", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + body: createFuncPayload, + }); + + // now list funcs and let's make sure we see it + const funcs = await sdf.call({ + route: "func_list", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + }, + }); + + const createdFunc = funcs.find((f) => f.name === name); + assert(createdFunc, "Expected to find newly created func"); + const funcId = createdFunc.funcId; + const codePayload = { + code, + requestUlid: changeSetId, + }; + + // save the code + const updateCodeResp = await sdf.call({ + route: "update_func_code", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + funcId, + }, + body: codePayload, + }); + + // create func arguments + let numArgs = 0; + for (const funcArg of funcArguments) { + // create the argument + let argPayload = { + created_at: new Date(), + updated_at: new Date(), + name: funcArg.name, + id: nilId, + kind: funcArg.kind, + requestUlid: changeSetId, + }; + const createArgResp = await sdf.call({ + route: "create_func_arg", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + funcId, + }, + body: argPayload, + }); + const args = createArgResp.arguments; + assert( + Array.isArray(createArgResp?.arguments), + "Expected arguments list", + ); + numArgs++; + assert(createArgResp?.arguments.length === numArgs, `Expected ${numArgs} but found ${createArgResp?.arguments.length}`); + const createdArg = args.find((arg) => arg.name === funcArg.name); + const attributePrototypeArgumentId = createdArg.id; + const attributePrototypeId = createArgResp?.bindings[0].attributePrototypeId; + // now update the argument bindings + + const bindingPayload = { + funcId, + bindings: [ + { + bindingKind: "attribute", + attributePrototypeId: attributePrototypeId, + funcId, + propId: outputLocationId, + componentId: null, + outputSocketId: null, + schemaVariantId, + argumentBindings: [ + { + funcArgumentId: createdArg.id, + propId: funcArg.propId, + attributePrototypeArgumentId: attributePrototypeArgumentId, + inputSocketId: funcArg.inputSocketId, + } + ] + } + ] + } + const updateBindingResp = await sdf.call({ + route: "create_func_binding", + routeVars: { + workspaceId: sdf.workspaceId, + changeSetId, + funcId, + }, + body: bindingPayload, + }); + + assert( + Array.isArray(updateBindingResp), + "Expected bindings list", + ); + assert( + Array.isArray(updateBindingResp[0].argumentBindings), + "Expected argument bindings list", + ); + assert( + updateBindingResp[0].argumentBindings.length === numArgs, + "Expected argument bindings list", + ); + } + + return funcId; + +} + +