Skip to content

Contrato do lookup de tenants — GET /tenants/lookup e GET /admin/tenants/lookup ​

Base URL /api/v1. Erros em application/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ó.

ListagemLookup
ItemTenantDto completo{ id, name, description, isActive? }
Ordenaçãoconfigurávelpor nome, fixa
pageSizeaté 100até 50
Inativosfiltro isActivebusca: nunca · hidratação: sempre, marcados
Excluídos—nunca
Nomes de ids gravadospaginar tudo?ids= (até 50 por chamada)

2. Os dois trilhos ​

TrilhoRotaPermissãoEscopo
TenantGET /api/v1/tenants/lookupread:tenantsos tenants que o caller pode ver (§2.1)
AdminGET /api/v1/admin/tenants/lookupadmin:system ou manage:all-tenantsa 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âmetroDefaultRegras
searchausentecasa 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
page11-based. Ignorado com ids
pageSize20máximo 50 — acima disso é 400 (não há clamp silencioso). Ignorado com ids
idsausentehidrataçã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
typeausentecompany ou group; ausente = os dois
hierarchyScopeallall, root (sem pai: os grupos e as empresas fora de grupo), grouped (as empresas de qualquer grupo) ou children (as empresas de parentTenantId)
parentTenantIdausenteo 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 ​

CampoParâmetros
Empresas do usuário / filtro por empresatype=company
Grupo de destino (mover tenant para grupo)type=group
Empresa sem grupo (vincular empresa a grupo)type=company&hierarchyScope=root
Empresas de um grupohierarchyScope=children&parentTenantId={grupo}
Empresas de qualquer grupohierarchyScope=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. Vem null (presente no payload) quando o tenant não tem domínio.
  • isActive não vem quando o tenant está ativo (ausente ⇒ ativo). Só aparece, como false, 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 getMany do 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; page e pageSize não se aplicam e o pageSize da 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), e type/hierarchyScope també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 novo

5. Erros ​

SituaçãoHTTPcode
sem token401—
trilho tenant sem read:tenants403—
trilho admin sem admin:system nem manage:all-tenants403—
pageSize > 50 ou < 1, page < 1, search > 128400validação
mais de 50 ids distintos; ids com valor que não é um id400validação
type ou hierarchyScope fora do vocabulário400validação
children sem parentTenantId, ou parentTenantId sem children400TENANT_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.

Released under the MIT License.