@microteia/console 2.10.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,13 +29,14 @@ export interface Environment {
29
29
  deployed: string | null;
30
30
  };
31
31
  /**
32
- * Quais Cloud Functions de EXTENSÃO já foram deployadas neste projeto. Fica aqui, e não no banco,
33
- * porque é estado da instalação e não do escritório — e é o que permite ao `provision` deployar só o
34
- * delta e reconhecer o que ficou órfão depois de uma desinstalação.
32
+ * Quais Cloud Functions de EXTENSÃO já foram deployadas neste projeto, e de QUEM. Fica aqui, e não no
33
+ * banco, porque é estado da instalação e não do escritório — e é o que permite ao `provision`
34
+ * deployar só o delta e reconhecer o que ficou órfão depois de uma desinstalação.
35
35
  *
36
- * Uma lista num escalar separado por vírgula porque o leitor de YAML daqui entende mapas de dois
37
- * níveis e escalares, e trazer sequências para guardar meia dúzia de nomes seria pagar caro pela
38
- * generalidade.
36
+ * `nome@pluginId` separado por vírgula — ver `appliedFunctions.ts`, que o lê e o escreve. O escalar é
37
+ * porque o leitor de YAML daqui entende mapas de dois níveis e escalares, e trazer sequências para
38
+ * guardar meia dúzia de pares seria pagar caro pela generalidade. Uma entrada sem `@` é o que uma
39
+ * instalação anterior ao dono gravou, e continua valendo.
39
40
  */
40
41
  backend: {
41
42
  functions: string | null;
@@ -20,8 +20,26 @@ export type FirestoreTypedValue = {
20
20
  stringValue: string;
21
21
  } | {
22
22
  timestampValue: string;
23
+ } | {
24
+ integerValue: string;
25
+ } | {
26
+ arrayValue: {
27
+ values: FirestoreTypedValue[];
28
+ };
29
+ } | {
30
+ mapValue: {
31
+ fields: Record<string, FirestoreTypedValue>;
32
+ };
23
33
  };
24
34
  export declare const asString: (value: string) => FirestoreTypedValue;
35
+ /**
36
+ * Inteiro como INTEIRO. A REST o transporta em string — é assim que ela representa um int64 sem perder
37
+ * precisão em JSON —, mas o campo gravado é numérico, e é isso que faz o outro lado do produto poder
38
+ * compará-lo em vez de tentar convertê-lo.
39
+ */
40
+ export declare const asInteger: (value: number) => FirestoreTypedValue;
41
+ export declare const asArray: (values: FirestoreTypedValue[]) => FirestoreTypedValue;
42
+ export declare const asMap: (fields: Record<string, FirestoreTypedValue>) => FirestoreTypedValue;
25
43
  /**
26
44
  * Data como DATA, e não como texto que se parece com uma.
27
45
  *
@@ -37,3 +55,12 @@ export declare const asTimestamp: (value: Date) => FirestoreTypedValue;
37
55
  * Console carimbar o estado sem conhecer (nem arriscar reescrever) o pacote inteiro que vive ao lado.
38
56
  */
39
57
  export declare const patchMapField: (projectId: string, path: string, field: string, entries: Record<string, FirestoreTypedValue>) => Promise<void>;
58
+ /**
59
+ * Grava um documento inteiro, com `updateMask` listando cada campo escrito.
60
+ *
61
+ * A máscara é explícita e não omitida de propósito: sem ela o comportamento de um PATCH depende de o
62
+ * documento existir, e o que se quer aqui é convergência — o que é escrito passa a valer, e nada de uma
63
+ * execução anterior sobra. Como todos os campos são escritos a cada vez, a máscara completa é também o
64
+ * que apaga o que deixou de ser verdade.
65
+ */
66
+ export declare const patchFields: (projectId: string, path: string, fields: Record<string, FirestoreTypedValue>) => Promise<void>;
@@ -108,6 +108,14 @@ export const listDocuments = async (projectId, path) => {
108
108
  };
109
109
  export const documentId = (document) => document.name.slice(document.name.lastIndexOf('/') + 1);
110
110
  export const asString = (value) => ({ stringValue: value });
111
+ /**
112
+ * Inteiro como INTEIRO. A REST o transporta em string — é assim que ela representa um int64 sem perder
113
+ * precisão em JSON —, mas o campo gravado é numérico, e é isso que faz o outro lado do produto poder
114
+ * compará-lo em vez de tentar convertê-lo.
115
+ */
116
+ export const asInteger = (value) => ({ integerValue: String(value) });
117
+ export const asArray = (values) => ({ arrayValue: { values } });
118
+ export const asMap = (fields) => ({ mapValue: { fields } });
111
119
  /**
112
120
  * Data como DATA, e não como texto que se parece com uma.
113
121
  *
@@ -129,3 +137,18 @@ export const patchMapField = async (projectId, path, field, entries) => {
129
137
  body: { fields: { [field]: { mapValue: { fields: entries } } } },
130
138
  });
131
139
  };
140
+ /**
141
+ * Grava um documento inteiro, com `updateMask` listando cada campo escrito.
142
+ *
143
+ * A máscara é explícita e não omitida de propósito: sem ela o comportamento de um PATCH depende de o
144
+ * documento existir, e o que se quer aqui é convergência — o que é escrito passa a valer, e nada de uma
145
+ * execução anterior sobra. Como todos os campos são escritos a cada vez, a máscara completa é também o
146
+ * que apaga o que deixou de ser verdade.
147
+ */
148
+ export const patchFields = async (projectId, path, fields) => {
149
+ await request(projectId, path, {
150
+ method: 'PATCH',
151
+ query: { 'updateMask.fieldPaths': Object.keys(fields) },
152
+ body: { fields },
153
+ });
154
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * O portão de instalação, conferido na FONTE.
3
+ *
4
+ * `forInstalledExtension` é a metade da retirada que age no instante certo: a Cloud Function é do
5
+ * PROJETO e a instalação da extensão é do ESCRITÓRIO, então um `onDocumentWritten('tenants/{tenantId}/…')`
6
+ * declarado por uma extensão dispara para TODOS os escritórios do projeto — os que nunca a instalaram, e
7
+ * o que a desinstalou enquanto outro a mantém. Nesse último caso `--prune` não pode ajudar: a function
8
+ * tem de continuar no ar, porque alguém ainda precisa dela.
9
+ *
10
+ * Só que nada cobrava o portão. O `create function` o escrevia no molde, e era só isso: nenhuma
11
+ * verificação do empacotamento, do deploy ou do provisionamento olhava a fonte. Um gatilho sem portão
12
+ * passava por tudo e ia para o ar — e o sintoma dele é o pior que existe, porque não é uma falha: é
13
+ * código de uma extensão que ninguém tem mais rodando sobre os dados de quem nunca a pediu, sem erro em
14
+ * lugar nenhum.
15
+ *
16
+ * É recusa e não aviso pelo mesmo motivo do nome de function repetido (`functionProblems`): o defeito é
17
+ * invisível depois de instalado, e um aviso na saída de um deploy é lido uma vez.
18
+ *
19
+ * A análise é por texto, no idioma dos outros leitores de fonte do Console (`callableFunctionNames`,
20
+ * `declaredParametersIn`). Ela erra para o lado ALTO — o que ela não entende ela recusa —, e é por isso
21
+ * que o portão declarado num identificador ao lado é reconhecido: recusar código correto é o que ensina
22
+ * a desligar a verificação.
23
+ */
24
+ /**
25
+ * Os construtores de gatilho de Firestore.
26
+ *
27
+ * São os únicos eventos cujo caminho carrega `{tenantId}`, e portanto os únicos para os quais o portão
28
+ * existe. Um `onSchedule` ou um `onMessagePublished` não tem escritório de onde perguntar — envolvê-los
29
+ * no portão os deixaria permanentemente inertes, porque ele falha FECHADO. Eles ficam de fora desta
30
+ * verificação de propósito: exigir deles um portão que não funciona seria trocar um defeito por outro.
31
+ */
32
+ export declare const TENANT_TRIGGER_CONSTRUCTORS: string[];
33
+ /**
34
+ * Os construtores cujo evento NÃO carrega escritório no caminho.
35
+ *
36
+ * Para eles as duas saídas do autor eram erradas, e é isso que faz deles uma recusa e não um aviso: sem
37
+ * portão a function age sobre o projeto inteiro; COM portão ela nunca roda — nem enquanto a extensão está
38
+ * instalada —, porque `forInstalledExtension` falha FECHADO e nunca acha o `{tenantId}` que não existe.
39
+ * Um autor que seguisse a orientação genérica "envolva o handler no portão" produzia uma function que
40
+ * passava no deploy, não acusava nada e nunca fazia nada.
41
+ *
42
+ * A resposta é o laço por escritório (`forEachInstalledOffice`), e é por existir uma resposta que a
43
+ * recusa é honesta.
44
+ *
45
+ * A lista é EXPLÍCITA, e não "tudo o que não é gatilho de documento": o que não se conhece não se recusa.
46
+ * O RTDB fica de fora de propósito — o caminho dele TEM parâmetros, então um `{tenantId}` ali é possível
47
+ * —, e a plataforma não usa RTDB em lugar nenhum.
48
+ */
49
+ export declare const UNSCOPED_TRIGGER_CONSTRUCTORS: string[];
50
+ /**
51
+ * A fonte sem comentário nenhum, com as strings intactas.
52
+ *
53
+ * O JSDoc que o molde escreve CITA `forInstalledExtension` — é ali que ele explica o portão. Sem esta
54
+ * limpeza, um gatilho que perdesse a chamada e mantivesse o comentário passaria na verificação, o que é
55
+ * o caso exato que ela existe para pegar.
56
+ */
57
+ export declare const withoutComments: (source: string) => string;
58
+ export interface TriggerDeclaration {
59
+ /** O nome declarado — é por ele que se sabe se o `index.ts` o exporta. */
60
+ name: string;
61
+ /** O construtor usado. Aparece na mensagem: é ele que explica por que o portão é devido. */
62
+ trigger: string;
63
+ /** O id que `forInstalledExtension` recebeu, ou `null` quando o portão não está lá. */
64
+ gatedFor: string | null;
65
+ /** Todos os ids que as guardas daquele gatilho recebem — a comum guarda mais de um. */
66
+ guards: string[];
67
+ }
68
+ /** Os gatilhos de Firestore declarados numa fonte, com o estado do portão de cada um. */
69
+ export declare const triggerDeclarations: (rawSource: string) => TriggerDeclaration[];
70
+ /**
71
+ * As functions sem escritório no evento, e o id que o laço por escritório recebeu no ARQUIVO.
72
+ *
73
+ * O alcance é o arquivo, e não os parênteses da chamada — a diferença é deliberada. O portão de um
74
+ * gatilho de documento embrulha AQUELE handler, então ele tem de estar ali; o laço, ao contrário, roda
75
+ * DENTRO do handler e pode estar algumas chamadas abaixo, num `syncOffice(tenantId)` que o próprio autor
76
+ * escreveu. Exigi-lo nos parênteses recusaria código correto, e a verificação que erra assim é a que
77
+ * ensina a próxima pessoa a desligá-la.
78
+ *
79
+ * O preço é conhecido: dois agendados no mesmo arquivo, um com laço e outro sem, passam os dois. É menos
80
+ * grave que o inverso, e está dito.
81
+ */
82
+ export declare const unscopedDeclarations: (rawSource: string) => TriggerDeclaration[];
83
+ /**
84
+ * As callables declaradas, e o id que a guarda delas recebeu no ARQUIVO.
85
+ *
86
+ * O alcance é o arquivo pela mesma razão do laço por escritório: a guarda roda DENTRO do handler (ela
87
+ * precisa do `request`), e o guia manda o autor manter o handler fino, com a decisão num arquivo ao
88
+ * lado. Exigi-la nos parênteses recusaria justamente o código que o guia pede.
89
+ */
90
+ export declare const callableDeclarations: (rawSource: string) => TriggerDeclaration[];
91
+ /**
92
+ * O que uma origem de functions precisa ter para ser conferida. É a forma de `FunctionSource`, declarada
93
+ * aqui em vez de importada: assim este módulo se testa sozinho, sem montar diretório nenhum.
94
+ */
95
+ export interface GateSubject {
96
+ label: string;
97
+ /** `functions/` é comum a várias extensões, e não tem uma instalação de onde perguntar. */
98
+ shared: boolean;
99
+ exported: string[];
100
+ triggers: TriggerDeclaration[];
101
+ /** As functions cujo evento não carrega escritório: o agendado, a fila, o HTTP, o Storage. */
102
+ unscoped: TriggerDeclaration[];
103
+ /** Quem cada function comum declara servir. Vazio numa origem de extensão. */
104
+ serves: Record<string, string[]>;
105
+ /** As callables declaradas, com o estado da guarda de cada uma. */
106
+ callableGuards: TriggerDeclaration[];
107
+ }
108
+ /**
109
+ * Gatilho de extensão sem portão.
110
+ *
111
+ * `functions/` fica de fora porque ali a pergunta não tem resposta: a function é comum a várias
112
+ * extensões, nenhuma a reivindica, e não há um id para o portão receber. É lacuna conhecida e tem
113
+ * endereço próprio — enquanto ela não fechar, exigir aqui um portão impossível seria recusar a única
114
+ * forma que a plataforma oferece para uma function compartilhada.
115
+ */
116
+ export declare const ungatedTriggerProblems: (subjects: GateSubject[]) => string[];
117
+ /**
118
+ * Function de extensão cujo evento não tem escritório, e que não passa pelo laço.
119
+ *
120
+ * A recusa vem acompanhada do substituto porque sem ele ela seria só uma porta fechada: o laço por
121
+ * escritório (`forEachInstalledOffice`) existe justamente para este caso, e é ele que a mensagem nomeia.
122
+ *
123
+ * `functions/` fica de fora pelo mesmo motivo de sempre: a comum não tem um id de extensão para dar ao
124
+ * laço. Quem responde por ela é a declaração de quem ela serve.
125
+ */
126
+ export declare const unscopedTriggerProblems: (subjects: GateSubject[]) => string[];
127
+ /**
128
+ * Gatilho de uma function COMUM que declara quem serve, e não guarda ninguém.
129
+ *
130
+ * A comum ficava fora desta cobrança porque não havia id para dar à guarda: nenhuma extensão pode
131
+ * reivindicá-la. `microteia.serves` resolve exatamente isso — a partir dela existem ids, e a partir dos
132
+ * ids a cobrança passa a ser possível. Um gatilho comum sem guarda dispara para todo escritório do
133
+ * projeto, e é o mesmo defeito do gatilho de extensão sem portão, com um alcance maior.
134
+ *
135
+ * A comum SEM declaração continua de fora, e é deliberado: cobrar dela uma guarda sem ter ids a oferecer
136
+ * seria fechar a porta sem abrir nenhuma. O que ela recebe é o relato do provisionamento.
137
+ *
138
+ * A segunda recusa é a guarda que nomeia extensão que a function não declara servir: as duas listas
139
+ * respondem à mesma pergunta em dois lugares, e divergirem faz uma delas mentir — a `serves` decide a
140
+ * retirada, a guarda decide a execução, e a function passaria a agir onde não pode ser retirada.
141
+ */
142
+ export declare const unguardedSharedProblems: (subjects: GateSubject[]) => string[];
143
+ /**
144
+ * Callable de extensão sem a guarda de instalação.
145
+ *
146
+ * "Uma callable órfã é inerte" era verdade de INTERFACE, e não de segurança — e era a frase que o guia
147
+ * dizia. O deploy concede a `allUsers` o papel `roles/run.invoker` em cada callable; o que sobra depois é
148
+ * a validação do token do Firebase. Então a tela ir embora com a extensão não fecha nada: o endpoint
149
+ * segue no ar aceitando o token de QUALQUER usuário do projeto, de qualquer escritório, tendo ou não a
150
+ * extensão.
151
+ *
152
+ * E o escritório chega no PAYLOAD, porque `useCallable` manda só o que a tela passou. Sem a guarda, um
153
+ * usuário do escritório A chama a function apontando para o B — e o molde gerado conferia apenas se
154
+ * havia `request.auth`.
155
+ *
156
+ * A comum fica de fora: `requireInstalledExtension` pede UM id, e a comum serve vários. É lacuna
157
+ * conhecida e está registrada — a guarda "alguma das que eu sirvo" existe para gatilho, não para
158
+ * callable.
159
+ */
160
+ export declare const unguardedCallableProblems: (subjects: GateSubject[]) => string[];
161
+ export interface MisgatedTrigger {
162
+ /** A origem, como as mensagens a citam. */
163
+ label: string;
164
+ name: string;
165
+ /** O id que a guarda recebeu. */
166
+ gatedFor: string;
167
+ /** O id da extensão que reivindica a function. */
168
+ owner: string;
169
+ /** Qual guarda recebeu o id errado — é ela que o autor tem de corrigir. */
170
+ via: string;
171
+ }
172
+ /**
173
+ * Portão aberto para o id ERRADO.
174
+ *
175
+ * É o defeito do copiar-e-colar entre extensões, e o sintoma dele é o oposto do gatilho sem portão: a
176
+ * function sobe, o gatilho dispara, e o handler nunca roda em escritório nenhum. Nada falha — a extensão
177
+ * apenas não faz o que promete, e a causa está numa string.
178
+ *
179
+ * Devolve os casos, e não as frases, porque quem chama precisa dos dois: a mensagem para relatar e o id
180
+ * da extensão para pendurar. Deduzir o id de volta a partir do texto é o que faz um relatório recusar a
181
+ * extensão errada quando o nome de uma aparece dentro da mensagem de outra.
182
+ *
183
+ * Só se pergunta quando o nome tem UM reivindicante: com dois, `functionClaimProblems` já recusou antes,
184
+ * e apontar um deles como o dono certo seria inventar uma resposta.
185
+ */
186
+ export declare const misgatedTriggers: (subjects: GateSubject[], claimants: Map<string, string>) => MisgatedTrigger[];
187
+ /** Como uma guarda de id errado aparece num relatório. */
188
+ export declare const describeMisgated: (entry: MisgatedTrigger) => string;
189
+ /**
190
+ * Quem reivindica cada nome, quando é um só. Duas extensões pedindo o mesmo nome é problema de outra
191
+ * verificação, e o nome sai daqui em vez de receber um dono escolhido a esmo.
192
+ */
193
+ export declare const singleClaimants: (claims: {
194
+ pluginId: string;
195
+ backend: {
196
+ functions?: string[];
197
+ };
198
+ }[]) => Map<string, string>;