Ligações
Uma ligação faz uma linha de uma tabela apontar para linhas de outra. Esta página diz como se cria o campo e como se preenche, pela API REST e pelo MCP, e que erros esperar. Os argumentos de cada ferramenta estão em Ferramentas MCP, gerados do código.
O que é uma ligação
No OutDo uma ligação é uma coluna com o componente linked_select. O config
dela diz para onde aponta:
linkedFeatureId: a tabela alvo.linkedValueColumn: o id da coluna da tabela alvo cujo valor identifica cada linha. Normalmente é a coluna de componenteid.linkedLabelColumn: o id da coluna que a interface mostra.isMultiSelect:truepara uma ligação múltipla.
O valor guardado na célula não é o id da linha alvo. É o conteúdo da célula
linkedValueColumn dessa linha (por exemplo o data.id gerado à parte). Numa
ligação simples a célula guarda uma string; numa múltipla, uma lista de strings.
Escrever o id da linha (Featurerow.id) cria uma ligação que nada resolve.
Não precisas de saber isto para ligar: com link_rows dás os ids das linhas alvo
e o servidor vai buscar a chave certa a cada uma. O update_row e o
PATCH /rows continuam a aceitar o valor cru, para quem já o tiver.
A coluna inversa de uma ligação (linked_back) vive na tabela alvo, mostra as
linhas que apontam para ela e partilha com a ligação um mesmo relationId. Não
guarda dados: calcula-se a partir das ligações.
Ligar registos: link_rows
Escreve na célula de ligação de uma linha, com os ids das linhas alvo.
| Argumento | Significado |
|---|---|
feature_id | A tabela da linha de origem. |
row_id | A linha de origem. |
column | A coluna de ligação, pelo nome ou pelo id. |
target_row_ids | Os ids (Featurerow.id) das linhas alvo. No máximo 100; repetidos contam uma vez. |
mode | set (omissão), add ou remove. |
idempotency_key | Opcional. Torna a repetição segura. |
Os três modos
set: a célula passa a ser exatamente as chaves pedidas. Uma lista vazia limpa a célula (a chave desaparece da linha).add: junta as chaves às que já lá estão, sem repetir, com as existentes primeiro.remove: tira as chaves pedidas; as que não lá estavam são ignoradas. Se a célula ficar vazia, é limpa.
Simples e múltipla
Numa ligação simples (isMultiSelect falso) cabe um só registo. set
aceita zero ou um id; add aceita um id e substitui o valor atual;
remove só limpa se a chave pedida for a que lá está. Mais de um id dá 422
single_link_column.
Numa ligação múltipla a célula é uma lista, e os três modos funcionam como descritos.
Exemplo no MCP
Ligar a linha de uma encomenda a dois clientes, juntando-os aos que já tinha:
{
"name": "link_rows",
"arguments": {
"feature_id": "6f1c0a52-3b7e-4c1d-9a2e-5d8b7c4e1a10",
"row_id": "0b9d2f64-8a31-4f7c-b5e0-2c6a91d3e4f8",
"column": "clientes",
"target_row_ids": [
"c3a8e1d0-5b42-4a96-8f17-9e0d6b2a7c54",
"e7f4b9a2-1c68-4d35-a0b3-6f2e8d1c9a07"
],
"mode": "add"
}
}
A resposta traz a linha atualizada em result.
Equivalente REST
POST /v1/features/{feature_id}/rows/{row_id}:link
Authorization: Bearer outdo_sk_...
Content-Type: application/json
{
"column": "clientes",
"target_row_ids": [
"c3a8e1d0-5b42-4a96-8f17-9e0d6b2a7c54",
"e7f4b9a2-1c68-4d35-a0b3-6f2e8d1c9a07"
],
"mode": "add"
}
Responde 200 com a linha. Um Idempotency-Key no cabeçalho torna a repetição
segura. Precisa do scope write e da permissão de editar registos na tabela de
origem; as linhas alvo só têm de estar visíveis para quem chama.
O que o servidor faz por ti
- Confirma que a coluna é
linked_selecte lê oconfigdela. - Lê as colunas da tabela alvo e descobre o nome da
linkedValueColumn. - Lê as linhas alvo pedidas. Se alguma não existir ou não estiver visível, a chamada falha e a resposta lista os ids em falta.
- Para cada linha usa
data[nome da linkedValueColumn]como chave. Uma célula vazia nunca é substituída pelo id da linha: é um erro. - Aplica o
modeao valor atual e grava só essa célula, com validação.
Criar o campo: create_link_column
Cria a coluna de ligação numa tabela, apontada para outra do mesmo
workspace, com o config completo. Sem esta ferramenta, escrever o config à
mão obriga a conhecer ids de colunas e a criar a inversa por fora.
| Argumento | Significado |
|---|---|
feature_id | A tabela de origem, onde nasce a coluna. |
name | O nome da coluna. |
target_feature_id | A tabela alvo. |
label | O rótulo. Por omissão, o name. |
multi | true para ligação múltipla. Por omissão, false. |
label_column | O nome de uma coluna da tabela alvo, para a interface mostrar. |
back_link | true cria também a coluna inversa na tabela alvo. |
back_link_label | O rótulo da inversa. |
required, hidden, order | Como em qualquer coluna. |
idempotency_key | Opcional. |
O que o servidor escolhe
linkedValueColumn: a coluna de componenteidda tabela alvo. Sem ela, 422target_without_id_column.linkedLabelColumn: a coluna pedida emlabel_column; se faltar, a primeira coluna da tabela alvo (por ordem) que seja visível, de texto, e não sejaid,linked_back,lookup,rollupnemformula; se não houver nenhuma, a própria colunaid. A escolha automática pode não ser a que querias: corrige-a comupdate_column.- O resto do
configfica com as omissões da interface.
Sem inversa
{
"name": "create_link_column",
"arguments": {
"feature_id": "6f1c0a52-3b7e-4c1d-9a2e-5d8b7c4e1a10",
"name": "clientes",
"label": "Clientes",
"target_feature_id": "a2d5c8f1-7e30-4b94-86c2-1f9a0e3d5b77",
"multi": true
}
}
A resposta traz column, a coluna criada. Depois preenche-a com link_rows.
Com inversa
Com back_link a tabela alvo ganha uma coluna linked_back, e as duas colunas
partilham um relationId novo:
{
"name": "create_link_column",
"arguments": {
"feature_id": "6f1c0a52-3b7e-4c1d-9a2e-5d8b7c4e1a10",
"name": "clientes",
"target_feature_id": "a2d5c8f1-7e30-4b94-86c2-1f9a0e3d5b77",
"multi": true,
"back_link": true,
"back_link_label": "Encomendas"
}
}
Sem back_link_label a inversa chama-se «rótulo da origem + ligados» (por exemplo, Encomendas ligados). A resposta
traz column e back_link_column. Criar a inversa mexe na estrutura da tabela
alvo, por isso exige permissão de gerir estrutura nas duas tabelas.
Se a inversa não puder ser criada, o servidor apaga a coluna de origem e
responde 502 back_link_failed. Se também não a conseguir apagar, responde 502
back_link_failed_forward_kept com o forward_column_id da coluna que ficou, para
a apagares com delete_column.
Equivalente REST
POST /v1/features/{feature_id}/columns:link
Authorization: Bearer outdo_sk_...
Content-Type: application/json
{
"name": "clientes",
"target_feature_id": "a2d5c8f1-7e30-4b94-86c2-1f9a0e3d5b77",
"multi": true,
"back_link": { "label": "Encomendas" }
}
Responde 201 com {column, back_link_column}. No REST, back_link é um objeto
({} pede a inversa com o rótulo por omissão). No MCP são os dois argumentos
back_link e back_link_label.
Scopes
| Operação | Scope da chave |
|---|---|
link_rows | write |
create_link_column, update_column, delete_column | write e structure (o structure é aditivo ao write) |
Os scopes só limitam: a permissão do utilizador por trás da chave continua a decidir, tabela a tabela.
Alterar e apagar o campo
update_column (PATCH /v1/features/{feature_id}/columns/{column_id}) muda só os
campos que enviares. Atenção: o config, quando vai, substitui o config
inteiro. Para mudar, por exemplo, a linkedLabelColumn, lê o config com
list_columns, altera essa chave e devolve-o completo, com o linkedFeatureId
e as outras chaves intactas.
delete_column (DELETE .../columns/{column_id}) apaga a definição da coluna.
Os valores já guardados nas linhas ficam como chaves órfãs. Apagar a coluna de
ligação não apaga a inversa na tabela alvo: apaga-a tu, com o id dela.
Erros
Os erros das rotas :link trazem um code estável no corpo do problema, repetido
em detail.code. As ferramentas MCP devolvem o mesmo code na mensagem.
| Estado | code | Quando |
|---|---|---|
| 422 | not_a_link_column | A coluna dada a link_rows não é linked_select. |
| 422 | unsupported_component | A coluna é auto_match, que a API não liga. |
| 422 | single_link_column | Mais de um id numa ligação simples. |
| 422 | cross_workspace_link | As duas tabelas são de workspaces diferentes. |
| 422 | target_without_id_column | A tabela alvo não tem coluna de componente id. |
| 404 | target_rows_not_found | Há linhas alvo inexistentes ou invisíveis; detail.missing lista os ids. |
| 404 | target_feature_not_found | A tabela alvo não existe ou não está visível. |
| 404 | label_column_not_found | O label_column pedido não é uma coluna da tabela alvo. |
| 409 | link_column_misconfigured | O config da coluna não tem linkedFeatureId ou linkedValueColumn válidos, ou a coluna da chave já não existe na alvo. |
| 409 | link_key_missing | Uma linha alvo tem a célula da chave vazia; detail.row_id diz qual. |
| 502 | back_link_failed | A inversa falhou; a coluna de origem foi removida. |
| 502 | back_link_failed_forward_kept | A inversa falhou e a coluna de origem ficou; detail.forward_column_id diz qual. |
Há ainda os erros comuns: 404 not_found para uma coluna ou uma linha de origem
que não existe, 403 sem a permissão ou o scope, e 422 do próprio corpo (um id que
não é uuid, ou mais de 100 em target_row_ids).
Limitações
- Concorrência em
adderemove. O servidor lê a célula e depois escreve a nova: as duas operações não são atómicas. Duas chamadas simultâneas sobre a mesma célula podem perder uma das escritas. É a mesma limitação doPATCHde linhas. Serializa as chamadas sobre a mesma célula, ou usasetcom a lista completa. - Dados antigos inválidos. A escrita valida a linha inteira: uma linha com um valor inválido noutra coluna recusa a ligação até se corrigir.
- Campos obrigatórios. Limpar uma ligação
requiredcomsete lista vazia dá 422, como qualquer outra escrita que a esvazie.
O que fica fora
auto_match. Não se liga por esta via (422unsupported_component).- Integridade referencial. O servidor não impede apagar uma linha que outras apontam, nem limpa as ligações órfãs: isso continua a ser feito pela aplicação.
- Ligações entre workspaces. Recusadas (422
cross_workspace_link).