REST, sem SDK
O que atravessa o fio. É a forma de quem integra em outra linguagem, a partir da OpenAPI publicada em /openapi.json.
Login e sessão
# Policial: a matrícula já traduzida para o e-mail do deployment
curl -X POST https://geohub.exemplo.gov.br/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"123456@pm.br","password":"…","mode":"json"}'
# 200 {"data":{"access_token":"…","refresh_token":"…","expires":900000}} (expires em ms)
curl "https://geohub.exemplo.gov.br/users/me?fields=id,role" -H 'Authorization: Bearer …'
# 200 {"data":{"id":"<UsuarioId>","role":"<PapelId>"}}
curl -X POST https://geohub.exemplo.gov.br/auth/refresh \
-H 'Content-Type: application/json' -d '{"refresh_token":"…","mode":"json"}'
curl -X POST https://geohub.exemplo.gov.br/auth/logout \
-H 'Content-Type: application/json' -d '{"refresh_token":"…","mode":"json"}'O entrega toda rejeição de login como 401; o que as separa é errors[0].extensions.code: INVALID_CREDENTIALS, INVALID_OTP, USER_SUSPENDED, TOKEN_EXPIRED, INVALID_TOKEN. Erro 5xx ou sem rede é "central indisponível". Renove antes de vencer e, num 401 em qualquer rota, renove uma vez e repita; um 403 não renova. O refresh token é de uso único: duas renovações ao mesmo tempo precisam dividir a mesma chamada.
O não faz login: usa o token estático do usuário de serviço direto como Bearer (ou troca e-mail e senha do usuário de serviço por um token em /auth/login, como o exemplo faz).
Os três cabeçalhos do protocolo
| cabeçalho | quem manda | o que é |
|---|---|---|
Authorization: Bearer … | todos | quem entrou: o access token da sessão, ou o token estático do serviço. O cookie de sessão do painel também vale |
x-solicitacao-id: <uuid v7> | obrigatório para integrador; recomendado para o app | chave de , cunhada pelo cliente antes da primeira tentativa, uma por comando. Repetir a mesma chave devolve o mesmo desfecho com repetido: true. GET não leva |
x-autoria: {"tipo":"integrador","id":"…"} | opcional | em nome de quem se age. É conferido contra a sessão, nunca obedecido: divergência é 403 AUTORIA_FORJADA |
Sem x-autoria, a é a da sessão: mandá-lo serve só para a conferência. O x-solicitacao-id tem de ser um UUID v7 (no npm, a função v7 do pacote uuid); um v4 é 400 SOLICITACAO_INVALIDA.
Toda rota de plugin responde 200 { "dados": …, "repetido"?: true } em sucesso e { "erro": { "codigo", "mensagem"?, "contexto"?, "faltando"?, "detalhes"? } } em erro. Decida sempre pelo codigo. Rotas de ficam em /plugins/…; verbo sem mapeamento de rota vira POST /plugins/<plugin>/<verbo>.
/**
* O `fetch` autenticado da REST gerada — o que o integrador em outra
* linguagem escreveria à mão a partir da OpenAPI.
*
* Os três cabeçalhos são o protocolo inteiro: o token diz quem entrou, o
* `x-autoria` diz em nome de quem se age (e vai para ser **conferido**,
* nunca para mandar) e o `x-solicitacao-id` é a chave de idempotência. O
* corpo é o argumento do verbo, e nada mais.
*/
async requisitar(
metodo: 'GET' | 'PATCH' | 'POST',
caminho: string,
opcoes: { corpo?: unknown; solicitacaoId?: string } = {},
): Promise<Response> {
const temCorpo = opcoes.corpo !== undefined;
return this.buscar(`${this.plugins}${caminho}`, {
...(temCorpo ? { body: JSON.stringify(opcoes.corpo) } : {}),
headers: {
Authorization: `Bearer ${this.token}`,
...(temCorpo ? { 'Content-Type': 'application/json' } : {}),
...(metodo === 'GET'
? {}
: { 'x-autoria': JSON.stringify(this.autoria) }),
...(opcoes.solicitacaoId === undefined
? {}
: { 'x-solicitacao-id': opcoes.solicitacaoId }),
},
method: metodo,
});
}/**
* O `fetch` autenticado da REST gerada — o que o integrador em outra
* linguagem escreveria à mão a partir da OpenAPI.
*
* Os três cabeçalhos são o protocolo inteiro: o token diz quem entrou, o
* `x-autoria` diz em nome de quem se age (e vai para ser **conferido**,
* nunca para mandar) e o `x-solicitacao-id` é a chave de idempotência. O
* corpo é o argumento do verbo, e nada mais.
*/
async requisitar(metodo, caminho, opcoes = {}) {
const temCorpo = opcoes.corpo !== undefined;
return this.buscar(`${this.plugins}${caminho}`, {
...(temCorpo ? { body: JSON.stringify(opcoes.corpo) } : {}),
headers: {
Authorization: `Bearer ${this.token}`,
...(temCorpo ? { 'Content-Type': 'application/json' } : {}),
...(metodo === 'GET'
? {}
: { 'x-autoria': JSON.stringify(this.autoria) }),
...(opcoes.solicitacaoId === undefined
? {}
: { 'x-solicitacao-id': opcoes.solicitacaoId }),
},
method: metodo,
});
}Um comando completo
curl -X POST "https://geohub.exemplo.gov.br/plugins/ocorrencias/$OID/despachos" \
-H "Authorization: Bearer $T" \
-H "Content-Type: application/json" \
-H "x-solicitacao-id: 0199c0a1-7b2e-7c3d-8e4f-5a6b7c8d9e0f" \
-H 'x-autoria: {"tipo":"integrador","id":"'$INTEGRADOR_ID'"}' \
-d '{"recurso":{"tipo":"guarnicao","guarnicaoId":"…","viatura":"RP-1207","composicao":[],"identificador":"RP-1207"}}'As respostas que não são desfecho:
409 {"erro":{"codigo":"SOLICITACAO_EM_ANDAMENTO"}} + Retry-After: 2 → espere e repita a MESMA chave
422 {"erro":{"codigo":"SOLICITACAO_REUSADA"}} → defeito seu: uma chave por comando
400 {"erro":{"codigo":"SOLICITACAO_AUSENTE"}} → integrador sem x-solicitacao-idE as que são:
409 {"erro":{"codigo":"referencia-externa-ja-registrada","contexto":{"ocorrenciaId":"…"}}}
422 {"erro":{"codigo":"viatura-sem-guarnicao-disponivel"}}
409 {"erro":{"codigo":"transicao-invalida","mensagem":"…"}}
403 {"erro":{"codigo":"SEM_CAPACIDADE","faltando":["ocorrencia.atender"]}}
403 {"erro":{"codigo":"GUARDA_DE_DOMINIO","mensagem":"O despacho é de outra guarnição."}}O exemplo do integrador escreve exatamente essa tradução, e é por ela que o roteiro roda pela REST sem um caso editado:
/** O envelope de resposta de toda rota do plugin. */
interface RespostaDoPlugin {
readonly dados?: unknown;
readonly erro?: {
readonly codigo?: string;
readonly contexto?: Readonly<Record<string, unknown>>;
readonly mensagem?: string;
};
}
/**
* Os códigos que são recusa de **protocolo** — defeito do integrador, não
* regra de negócio. Eles viram exceção; o resto vira `Desfecho`.
*/
const RECUSAS_DE_PROTOCOLO = new Set([
'AUTORIA_FORJADA',
'CORPO_INVALIDO',
'ERRO_INTERNO',
'NAO_AUTENTICADO',
'SEM_CAPACIDADE',
'SOLICITACAO_AUSENTE',
'SOLICITACAO_EM_ANDAMENTO',
'SOLICITACAO_INVALIDA',
'SOLICITACAO_REUSADA',
'VERBO_DESCONHECIDO',
]);
export { ErroDeProtocolo } from './erro-de-protocolo.js';
/** O erro do fio: texto até ser lido pelo contrato. */
interface ErroNoFio {
readonly codigo: string;
readonly contexto?: Readonly<Record<string, unknown>>;
readonly detalhe: string;
}
/**
* Traduz a resposta HTTP em `Desfecho`, com `ler` dando forma ao `dados` e
* `lerFalha` dando forma à falha pelo contrato do plugin. Um código que o
* contrato não conhece não vira desfecho: é o contrato que divergiu, e isso
* é recusa de protocolo, como as outras.
*
* `GUARDA_DE_DOMINIO` fica de fora das recusas de protocolo de propósito:
* ela é um `403` que diz "esta guarnição não é sua", e o integrador a trata
* como resposta — não como bug.
*/
async function desfechoDe<T, F extends { readonly codigo: string }>(
resposta: Response,
ler: (dados: unknown) => T,
lerFalha: (erro: ErroNoFio) => F | undefined,
): Promise<Desfecho<T, F>> {
const corpo = (await resposta.json()) as RespostaDoPlugin;
if (resposta.ok) return { aceito: true, valor: ler(corpo.dados) };
const codigo = corpo.erro?.codigo ?? 'ERRO_DESCONHECIDO';
const mensagem = corpo.erro?.mensagem ?? `HTTP ${resposta.status}`;
if (RECUSAS_DE_PROTOCOLO.has(codigo)) {
const retryAfter = Number(resposta.headers.get('Retry-After') ?? '');
throw new ErroDeProtocolo(
resposta.status,
codigo,
mensagem,
Number.isInteger(retryAfter) && retryAfter > 0 ? retryAfter : undefined,
);
}
const falha = lerFalha({
codigo,
...(corpo.erro?.contexto === undefined
? {}
: { contexto: corpo.erro.contexto }),
detalhe: mensagem,
});
if (falha === undefined) {
throw new ErroDeProtocolo(
resposta.status,
codigo,
CODIGOS_CONHECIDOS.has(codigo)
? `a falha ${codigo} veio sem o contexto que o contrato promete: ${mensagem}`
: `código fora do contrato do plugin: ${mensagem}`,
);
}
return { aceito: false, ...falha };
}
const CODIGOS_CONHECIDOS = new Set<string>([
...Object.values(CODIGO_DE_FALHA_DE_JORNADA),
...Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA),
]);
/** A falha de jornada: todo código do contrato se explica sozinho. */
const falhaDeJornada = (erro: ErroNoFio): FalhaDeJornada | undefined => {
for (const codigo of Object.values(CODIGO_DE_FALHA_DE_JORNADA)) {
if (codigo === erro.codigo) return { codigo, detalhe: erro.detalhe };
}
return undefined;
};
/** A falha de ocorrência: a referência repetida traz o id da que já existe. */
const falhaDeOcorrencia = (erro: ErroNoFio): FalhaDeOcorrencia | undefined => {
if (
erro.codigo ===
CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA
) {
const existente = erro.contexto?.['ocorrenciaId'];
return typeof existente === 'string'
? {
codigo: erro.codigo,
contexto: { ocorrenciaId: ocorrenciaId(existente) },
detalhe: erro.detalhe,
}
: undefined;
}
for (const codigo of Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA)) {
if (
codigo !==
CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA &&
codigo === erro.codigo
) {
return { codigo, detalhe: erro.detalhe };
}
}
return undefined;
};import {
CODIGO_DE_FALHA_DE_JORNADA,
guarnicaoId,
jornadaId,
selarGuarnicaoElegivel,
usuarioId,
viaturaId,
} from '@geocontrol/geohub-sdk/jornada';
import {
CODIGO_DE_FALHA_DE_OCORRENCIA,
despachoId,
ocorrenciaId,
} from '@geocontrol/geohub-sdk/ocorrencia';
import { ErroDeProtocolo } from './erro-de-protocolo.js';
/**
* Os códigos que são recusa de **protocolo** — defeito do integrador, não
* regra de negócio. Eles viram exceção; o resto vira `Desfecho`.
*/
const RECUSAS_DE_PROTOCOLO = new Set([
'AUTORIA_FORJADA',
'CORPO_INVALIDO',
'ERRO_INTERNO',
'NAO_AUTENTICADO',
'SEM_CAPACIDADE',
'SOLICITACAO_AUSENTE',
'SOLICITACAO_EM_ANDAMENTO',
'SOLICITACAO_INVALIDA',
'SOLICITACAO_REUSADA',
'VERBO_DESCONHECIDO',
]);
export { ErroDeProtocolo } from './erro-de-protocolo.js';
/**
* Traduz a resposta HTTP em `Desfecho`, com `ler` dando forma ao `dados` e
* `lerFalha` dando forma à falha pelo contrato do plugin. Um código que o
* contrato não conhece não vira desfecho: é o contrato que divergiu, e isso
* é recusa de protocolo, como as outras.
*
* `GUARDA_DE_DOMINIO` fica de fora das recusas de protocolo de propósito:
* ela é um `403` que diz "esta guarnição não é sua", e o integrador a trata
* como resposta — não como bug.
*/
async function desfechoDe(resposta, ler, lerFalha) {
const corpo = await resposta.json();
if (resposta.ok) return { aceito: true, valor: ler(corpo.dados) };
const codigo = corpo.erro?.codigo ?? 'ERRO_DESCONHECIDO';
const mensagem = corpo.erro?.mensagem ?? `HTTP ${resposta.status}`;
if (RECUSAS_DE_PROTOCOLO.has(codigo)) {
const retryAfter = Number(resposta.headers.get('Retry-After') ?? '');
throw new ErroDeProtocolo(
resposta.status,
codigo,
mensagem,
Number.isInteger(retryAfter) && retryAfter > 0 ? retryAfter : undefined,
);
}
const falha = lerFalha({
codigo,
...(corpo.erro?.contexto === undefined
? {}
: { contexto: corpo.erro.contexto }),
detalhe: mensagem,
});
if (falha === undefined) {
throw new ErroDeProtocolo(
resposta.status,
codigo,
CODIGOS_CONHECIDOS.has(codigo)
? `a falha ${codigo} veio sem o contexto que o contrato promete: ${mensagem}`
: `código fora do contrato do plugin: ${mensagem}`,
);
}
return { aceito: false, ...falha };
}
const CODIGOS_CONHECIDOS = new Set([
...Object.values(CODIGO_DE_FALHA_DE_JORNADA),
...Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA),
]);
/** A falha de jornada: todo código do contrato se explica sozinho. */
const falhaDeJornada = (erro) => {
for (const codigo of Object.values(CODIGO_DE_FALHA_DE_JORNADA)) {
if (codigo === erro.codigo) return { codigo, detalhe: erro.detalhe };
}
return undefined;
};
/** A falha de ocorrência: a referência repetida traz o id da que já existe. */
const falhaDeOcorrencia = (erro) => {
if (
erro.codigo ===
CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA
) {
const existente = erro.contexto?.['ocorrenciaId'];
return typeof existente === 'string'
? {
codigo: erro.codigo,
contexto: { ocorrenciaId: ocorrenciaId(existente) },
detalhe: erro.detalhe,
}
: undefined;
}
for (const codigo of Object.values(CODIGO_DE_FALHA_DE_OCORRENCIA)) {
if (
codigo !==
CODIGO_DE_FALHA_DE_OCORRENCIA.REFERENCIA_EXTERNA_JA_REGISTRADA &&
codigo === erro.codigo
) {
return { codigo, detalhe: erro.detalhe };
}
}
return undefined;
};
// #endregion desfecho-rest
const nada = () => undefined;
const texto = (dados) => String(dados);
/**
* O selo de elegibilidade é do Geohub: a rota só devolve guarnição
* `disponivel`, então selar aqui é registrar que foi ele quem conferiu.
*/
const guarnicaoElegivel = (dados) => {
if (dados === null || dados === undefined) return undefined;
const lida = dados;
return selarGuarnicaoElegivel({
composicao: lida.composicao.map((i) => ({
posto: i.posto,
usuario: usuarioId(i.usuario),
})),
guarnicaoId: guarnicaoId(lida.guarnicaoId),
viatura: viaturaId(lida.viatura),
});
};
const url = (partes, ...valores) =>
partes.reduce(
(acumulado, parte, indice) =>
acumulado +
parte +
(indice < valores.length ? encodeURIComponent(valores[indice]) : ''),
'',
);
// #region jornada-rest
/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao) {
return {
assumirComando: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/guarnicoes/${dados.guarnicaoId}/comando`,
{ corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
),
nada,
falhaDeJornada,
),
),
constituirGuarnicao: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', '/guarnicoes', {
corpo: dados,
solicitacaoId,
}),
(id) => guarnicaoId(texto(id)),
falhaDeJornada,
),
),
declararProntidao: (id) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
// Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
// `GET` não muda nada.
guarnicaoDisponivelNaViatura: async (viatura) => {
const resposta = await sessao.requisitar(
'GET',
url`/viaturas/${viatura}/guarnicao-disponivel`,
);
const desfecho = await desfechoDe(
resposta,
guarnicaoElegivel,
falhaDeJornada,
);
return desfecho.aceito ? desfecho.valor : undefined;
},
iniciarJornada: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
corpo: { posto: dados.posto },
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
registrarApresentacao: (usuario) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/policiais/${usuario}/apresentacao`,
{ solicitacaoId },
),
(id) => jornadaId(texto(id)),
falhaDeJornada,
),
),
};
}
// #endregion jornada-rest
// #region ocorrencia-rest
/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(sessao) {
return {
atualizarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
corpo: { alteracoes: dados.alteracoes },
solicitacaoId,
}),
nada,
falhaDeOcorrencia,
),
),
despachar: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
{
corpo: {
...(dados.instrucoes === undefined
? {}
: { instrucoes: dados.instrucoes }),
recurso: dados.recurso,
},
solicitacaoId,
},
),
(id) => despachoId(texto(id)),
falhaDeOcorrencia,
),
),
registrarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', '/ocorrencias', {
corpo: dados,
solicitacaoId,
}),
(id) => ocorrenciaId(texto(id)),
falhaDeOcorrencia,
),
),
};
}
// #endregion ocorrencia-rest/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao: SessaoDoIntegrador): PortaDeJornada {
return {
assumirComando: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/guarnicoes/${dados.guarnicaoId}/comando`,
{ corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
),
nada,
falhaDeJornada,
),
),
constituirGuarnicao: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe<GuarnicaoId, FalhaDeJornada>(
await sessao.requisitar('POST', '/guarnicoes', {
corpo: dados,
solicitacaoId,
}),
(id) => guarnicaoId(texto(id)),
falhaDeJornada,
),
),
declararProntidao: (id) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
// Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
// `GET` não muda nada.
guarnicaoDisponivelNaViatura: async (viatura) => {
const resposta = await sessao.requisitar(
'GET',
url`/viaturas/${viatura}/guarnicao-disponivel`,
);
const desfecho = await desfechoDe(
resposta,
guarnicaoElegivel,
falhaDeJornada,
);
return desfecho.aceito ? desfecho.valor : undefined;
},
iniciarJornada: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
corpo: { posto: dados.posto },
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
registrarApresentacao: (usuario) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe<JornadaId, FalhaDeJornada>(
await sessao.requisitar(
'POST',
url`/policiais/${usuario}/apresentacao`,
{ solicitacaoId },
),
(id) => jornadaId(texto(id)),
falhaDeJornada,
),
),
};
}/** O plugin `jornada` pelos caminhos da OpenAPI. */
export function jornadaPorRest(sessao) {
return {
assumirComando: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/guarnicoes/${dados.guarnicaoId}/comando`,
{ corpo: { jornadaId: dados.jornadaId }, solicitacaoId },
),
nada,
falhaDeJornada,
),
),
constituirGuarnicao: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', '/guarnicoes', {
corpo: dados,
solicitacaoId,
}),
(id) => guarnicaoId(texto(id)),
falhaDeJornada,
),
),
declararProntidao: (id) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/guarnicoes/${id}/prontidao`, {
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
// Leitura não leva `solicitacaoId`: consultar não é agir, e repetir um
// `GET` não muda nada.
guarnicaoDisponivelNaViatura: async (viatura) => {
const resposta = await sessao.requisitar(
'GET',
url`/viaturas/${viatura}/guarnicao-disponivel`,
);
const desfecho = await desfechoDe(
resposta,
guarnicaoElegivel,
falhaDeJornada,
);
return desfecho.aceito ? desfecho.valor : undefined;
},
iniciarJornada: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', url`/jornadas/${dados.id}/inicio`, {
corpo: { posto: dados.posto },
solicitacaoId,
}),
nada,
falhaDeJornada,
),
),
registrarApresentacao: (usuario) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/policiais/${usuario}/apresentacao`,
{ solicitacaoId },
),
(id) => jornadaId(texto(id)),
falhaDeJornada,
),
),
};
}/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(
sessao: SessaoDoIntegrador,
): PortaDeOcorrencia {
return {
atualizarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
corpo: { alteracoes: dados.alteracoes },
solicitacaoId,
}),
nada,
falhaDeOcorrencia,
),
),
despachar: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe<DespachoId, FalhaDeOcorrencia>(
await sessao.requisitar(
'POST',
url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
{
corpo: {
...(dados.instrucoes === undefined
? {}
: { instrucoes: dados.instrucoes }),
recurso: dados.recurso,
},
solicitacaoId,
},
),
(id) => despachoId(texto(id)),
falhaDeOcorrencia,
),
),
registrarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe<OcorrenciaId, FalhaDeOcorrencia>(
await sessao.requisitar('POST', '/ocorrencias', {
corpo: dados,
solicitacaoId,
}),
(id) => ocorrenciaId(texto(id)),
falhaDeOcorrencia,
),
),
};
}/** O plugin `ocorrencia` pelos caminhos da OpenAPI. */
export function ocorrenciaPorRest(sessao) {
return {
atualizarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('PATCH', url`/ocorrencias/${dados.id}`, {
corpo: { alteracoes: dados.alteracoes },
solicitacaoId,
}),
nada,
falhaDeOcorrencia,
),
),
despachar: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar(
'POST',
url`/ocorrencias/${dados.ocorrenciaId}/despachos`,
{
corpo: {
...(dados.instrucoes === undefined
? {}
: { instrucoes: dados.instrucoes }),
recurso: dados.recurso,
},
solicitacaoId,
},
),
(id) => despachoId(texto(id)),
falhaDeOcorrencia,
),
),
registrarOcorrencia: (dados) =>
sessao.comando(async (solicitacaoId) =>
desfechoDe(
await sessao.requisitar('POST', '/ocorrencias', {
corpo: dados,
solicitacaoId,
}),
(id) => ocorrenciaId(texto(id)),
falhaDeOcorrencia,
),
),
};
}As rotas de cada estão nas páginas dos casos; a lista completa, na referência e na API.
Eventos
Polling por inscrição, o WebSocket do e o do integrador estão em tempo real.
Consultas
As consultas de placa (GET /consulta-placa, GET /omitir-alerta) e o registro de pessoa estão em consultar placa e pessoa.