164 lines
5.4 KiB
TypeScript
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;
|
|
}
|