Appearance
Contrato do lookup de tenants — GET /tenants/lookup e GET /admin/tenants/lookup
Base URL
/api/v1. Erros emapplication/problem+json. Escopo dos dois trilhos: tenant-read-scoping-contract. Mesmo padrão de hidratação do diretório de usuários (§3.2).
1. Para que serve
Alimentar o campo de referência (type: 'reference') quando a referência é uma empresa ou um grupo: um select com busca no servidor que guarda só o id e mostra o nome — em formulário, coluna, chip e seleção múltipla. No admin-mfe ele substitui as listas escritas à mão de empresas do usuário, filtro por empresa, mover tenant para grupo e vincular empresa a grupo.
Não é a listagem. GET /tenants e GET /admin/tenants continuam sendo as telas de administração: devolvem o TenantDto inteiro, ordenam por qualquer coluna e paginam até 100. Usar a listagem para mostrar o nome de ids já gravados obriga a tela a paginar o catálogo inteiro ou a parar nos 100 primeiros; o lookup resolve os ids pedidos numa chamada só.
| Listagem | Lookup | |
|---|---|---|
| Item | TenantDto completo | { id, name, description, isActive? } |
| Ordenação | configurável | por nome, fixa |
pageSize | até 100 | até 50 |
| Inativos | filtro isActive | busca: nunca · hidratação: sempre, marcados |
| Excluídos | — | nunca |
| Nomes de ids gravados | paginar tudo | ?ids= (até 50 por chamada) |
2. Os dois trilhos
| Trilho | Rota | Permissão | Escopo |
|---|---|---|---|
| Tenant | GET /api/v1/tenants/lookup | read:tenants | os tenants que o caller pode ver (§2.1) |
| Admin | GET /api/v1/admin/tenants/lookup | admin:system ou manage:all-tenants | a plataforma inteira |
São as permissões e o escopo da listagem do mesmo trilho, lidos do mesmo código. Um picker nunca oferece um tenant que a listagem esconde, nem esconde um que ela mostra. Escolha o trilho pela capability tenants.default, como na listagem: perfil escopado usa /tenants/lookup, perfil global usa /admin/tenants/lookup.
2.1 Escopo do trilho tenant
- todo tenant onde o caller tem vínculo ativo (inclusive um tenant desativado — ele aparece na hidratação, marcado);
- mais as empresas ativas de todo grupo onde o caller é GroupAdmin.
Um admin:system que chama o trilho tenant recebe o próprio escopo; a visão global é do trilho admin.
⚠️ Uma empresa desativada de um grupo não entra no escopo do GroupAdmin (é a regra da listagem). Se uma tela do GroupAdmin guardou o id dessa empresa, a hidratação no trilho tenant não a devolve — trate como "Registro indisponível". No trilho admin ela vem com isActive: false.
3. Requisição
GET /api/v1/tenants/lookup?search=acme&page=1&pageSize=20&type=company&hierarchyScope=root
GET /api/v1/tenants/lookup?ids=3f1c…,9a72…,c0de…| Parâmetro | Default | Regras |
|---|---|---|
search | ausente | casa o nome (sem distinguir maiúsculas nem acentos: sao paulo acha São Paulo) ou o domínio (sem distinguir maiúsculas), em qualquer posição. Não casa a descrição. Máx. 128 caracteres. % e _ são literais |
page | 1 | 1-based. Ignorado com ids |
pageSize | 20 | máximo 50 — acima disso é 400 (não há clamp silencioso). Ignorado com ids |
ids | ausente | hidratação (§4.2). Repetido (ids=a&ids=b) ou separado por vírgula (ids=a,b) — é a mesma requisição. Duplicados ignorados; vazio = ausente. Máximo 50 distintos; acima disso, ou com um valor que não é um id, 400 |
type | ausente | company ou group; ausente = os dois |
hierarchyScope | all | all, root (sem pai: os grupos e as empresas fora de grupo), grouped (as empresas de qualquer grupo) ou children (as empresas de parentTenantId) |
parentTenantId | ausente | o grupo de hierarchyScope=children |
Regra de combinação — a mesma da listagem, com o mesmo código: hierarchyScope=children sem parentTenantId, ou parentTenantId sem hierarchyScope=children, é 400TENANT_FILTER_COMBINATION_INVALID. ids sozinho não entra na regra.
Não existe sortBy, sortDirection, includeDeleted nem isActive.
3.1 Filtros das telas do admin-mfe
| Campo | Parâmetros |
|---|---|
| Empresas do usuário / filtro por empresa | type=company |
| Grupo de destino (mover tenant para grupo) | type=group |
| Empresa sem grupo (vincular empresa a grupo) | type=company&hierarchyScope=root |
| Empresas de um grupo | hierarchyScope=children&parentTenantId={grupo} |
| Empresas de qualquer grupo | hierarchyScope=grouped |
hierarchyScope=root&type=company cobre exatamente "empresa que não pertence a nenhum grupo": o domínio não permite grupo dentro de grupo, então o único pai possível de uma empresa é um grupo.
4. Resposta
Envelope PagedResult<T> de sempre. Sem bloco scope ("scope": null): o trilho já é o escopo.
jsonc
{
"items": [
{ "id": "3f1c…", "name": "Acme Brasil", "description": "acme.com.br" },
{ "id": "9a72…", "name": "Acme SP", "description": null }
],
"totalCount": 42, "pageNumber": 1, "pageSize": 20,
"totalPages": 3, "hasNext": true, "hasPrevious": false,
"scope": null
}descriptioné o domínio do tenant — o que separa "Acme" de "Acme Brasil" numa lista. Vemnull(presente no payload) quando o tenant não tem domínio.isActivenão vem quando o tenant está ativo (ausente ⇒ ativo). Só aparece, comofalse, na hidratação (§4.2).
4.1 Busca (sem ids)
Responde "quais tenants eu posso escolher agora?": só tenants ativos e não excluídos, do escopo do trilho, por nome. Use hasNext para paginar o combobox.
4.2 Hidratação — ?ids=
Responde "qual o nome deste id que já está gravado?":
GET /api/v1/admin/tenants/lookup?ids=3f1c…,9a72…,c0de…jsonc
{
"items": [
{ "id": "3f1c…", "name": "Acme Brasil", "description": "acme.com.br" },
{ "id": "9a72…", "name": "Beta Ltda", "description": null, "isActive": false }
],
"totalCount": 2, "pageNumber": 1, "pageSize": 3,
"totalPages": 1, "hasNext": false, "hasPrevious": false,
"scope": null
}- Uma chamada para a tela inteira. Junte os ids pedidos por todos os componentes numa janela curta (o
getManydo react-admin) e faça uma requisição; mais de 50 ids distintos, quebre em lotes de 50. - Uma página só. Todo id encontrado volta na página 1;
pageepageSizenão se aplicam e opageSizeda resposta é a quantidade de ids distintos pedidos. - Tenant desativado vem, com
isActive: false: mostre o nome, mas não o ofereça de novo (desabilite a opção, marque o chip). - Id ausente da resposta = "Registro indisponível". Tenant excluído, fora do escopo do trilho ou inexistente simplesmente não vem — status 200, sem 404 e sem nada que diferencie "existe fora do seu escopo" de "não existe".
ids+search= AND (a interseção), etype/hierarchyScopetambém continuam valendo. O campo não precisa mandar os filtros na hidratação; se mandar, nenhum é ignorado em silêncio.
ts
// o front junta os ids da tela e hidrata em lotes de 50
const base = profile === "global" ? "/admin/tenants/lookup" : "/tenants/lookup";
const chunks = chunk([...new Set(ids)], 50);
const pages = await Promise.all(chunks.map(c => api.get(base, { params: { ids: c.join(",") } })));
const byId = new Map(pages.flatMap(p => p.items).map(t => [t.id, t]));
// byId.get(id) === undefined ⇒ "Registro indisponível"
// byId.get(id)?.isActive === false ⇒ mostra o nome, não oferece de novo5. Erros
| Situação | HTTP | code |
|---|---|---|
| sem token | 401 | — |
trilho tenant sem read:tenants | 403 | — |
trilho admin sem admin:system nem manage:all-tenants | 403 | — |
pageSize > 50 ou < 1, page < 1, search > 128 | 400 | validação |
mais de 50 ids distintos; ids com valor que não é um id | 400 | validação |
type ou hierarchyScope fora do vocabulário | 400 | validação |
children sem parentTenantId, ou parentTenantId sem children | 400 | TENANT_FILTER_COMBINATION_INVALID |
Ramifique sempre pelo code; title e detail são localizados (en, pt-BR, es-ES).
Não há orçamento de rate limit próprio (a listagem também não tem). Faça debounce de ~300 ms na busca e cancele a requisição anterior — o servidor propaga o cancelamento até a consulta.
6. Custo
Cada chamada é uma consulta ao banco com projeção leve (id, nome, domínio, ativo). Na busca, o total viaja no mesmo SELECT da página; na hidratação é um WHERE id = ANY(...) sobre a chave primária, sem COUNT.