164 lines
5.4 KiB
TypeScript

import type {
Binding,
CanvasComponent,
ProjectDocument,
RestAction,
Variable,
VariableType,
} from '../../types/project';
import { isTargetPropertySupported } from '../Preview/bindingUtils';
export type ResponseTargetOption = { value: string; label: string };
function nextId(prefix: string, used: Set<string>): string {
let index = 1;
while (used.has(`${prefix}_${index}`)) index += 1;
return `${prefix}_${index}`;
}
export function responseTargetOptions(
components: CanvasComponent[],
variables: Record<string, Variable>,
): ResponseTargetOption[] {
const options: ResponseTargetOption[] = [];
const nameCounts = new Map<string, number>();
for (const component of components) {
nameCounts.set(component.name, (nameCounts.get(component.name) ?? 0) + 1);
}
for (const component of components) {
// Runtime resolution requires an unambiguous component name. Diagnostics
// still report existing bindings that point at duplicate names, but the
// visual editor must not create a new ambiguous target.
if (nameCounts.get(component.name) !== 1) continue;
for (const property of ['value', 'options', 'rows']) {
if (isTargetPropertySupported(component.type, property)) {
options.push({
value: `components.${component.name}.${property}`,
label: `${component.name} (${component.type}.${property})`,
});
}
}
}
for (const name of Object.keys(variables)) {
options.push({ value: `variables.${name}`, label: `${name} (variable)` });
}
return options;
}
export function createResponseBinding(
bindings: Binding[],
action: RestAction,
target: string,
): Binding {
return {
id: nextId('binding_response', new Set(bindings.map((binding) => binding.id))),
source: `actions.${action.id}.response.body`,
target,
trigger: 'onSuccess',
};
}
export function replaceBindingAt(
doc: ProjectDocument,
index: number,
binding: Binding,
): ProjectDocument {
const bindings = [...doc.project.bindings];
bindings[index] = binding;
return { ...doc, project: { ...doc.project, bindings } };
}
export function removeBindingAt(doc: ProjectDocument, index: number): ProjectDocument {
return {
...doc,
project: {
...doc.project,
bindings: doc.project.bindings.filter((_binding, candidate) => candidate !== index),
},
};
}
export function validateVariableName(
name: string,
variables: Record<string, Variable>,
originalName?: string,
): string | null {
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) {
return 'Use a letter or underscore first, followed by letters, numbers, underscores, or hyphens.';
}
if (name !== originalName && Object.prototype.hasOwnProperty.call(variables, name)) {
return `Variable "${name}" already exists.`;
}
return null;
}
export function parseVariableDefault(type: VariableType, raw: string): unknown {
if (type === 'string') return raw;
if (raw.trim() === '') return undefined;
if (type === 'number') {
const value = Number(raw);
if (!Number.isFinite(value)) throw new Error('Enter a finite number.');
return value;
}
if (type === 'boolean') {
if (raw === 'true') return true;
if (raw === 'false') return false;
throw new Error('Enter true or false.');
}
let value: unknown;
try { value = JSON.parse(raw); } catch (_error) { throw new Error('Enter valid JSON.'); }
if (type === 'array' && !Array.isArray(value)) throw new Error('Enter a JSON array.');
if (type === 'object' && (value === null || Array.isArray(value) || typeof value !== 'object')) {
throw new Error('Enter a JSON object.');
}
return value;
}
export function formatVariableDefault(variable: Variable): string {
if (variable.defaultValue === undefined) return '';
if (variable.type === 'string') return String(variable.defaultValue);
if (variable.type === 'object' || variable.type === 'array') {
return JSON.stringify(variable.defaultValue, null, 2);
}
return String(variable.defaultValue);
}
export function setVariable(
doc: ProjectDocument,
name: string,
variable: Variable,
originalName?: string,
): ProjectDocument {
const variables = { ...doc.project.variables };
if (originalName && originalName !== name) delete variables[originalName];
variables[name] = variable;
return { ...doc, project: { ...doc.project, variables } };
}
export function removeVariable(doc: ProjectDocument, name: string): ProjectDocument {
const variables = { ...doc.project.variables };
delete variables[name];
return { ...doc, project: { ...doc.project, variables } };
}
export function findVariableReferences(doc: ProjectDocument, name: string): string[] {
const needle = `variables.${name}`;
const references: string[] = [];
doc.project.bindings.forEach((binding) => {
if (binding.source === needle || binding.target === needle) references.push(`binding "${binding.id}"`);
});
doc.project.actions.forEach((action) => {
const values = [
action.url,
action.bodyTemplate,
...Object.values(action.headers ?? {}),
...Object.values(action.queryParameters ?? {}),
// Variable path-parameter templates are compatibility-only and produce a
// diagnostic, but they are still canonical references worth warning about.
...Object.values(action.pathParameters ?? {}),
];
if (values.some((value) => value?.includes(`{{${needle}}}`))) references.push(`action "${action.name}"`);
});
return references;
}