@microteia/console 2.10.0 → 2.19.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.
- package/CONSOLE-VERSIONS.md +57 -0
- package/README.md +1 -1
- package/lib/console/appliedFunctions.d.ts +114 -0
- package/lib/console/appliedFunctions.js +123 -0
- package/lib/console/backendResidue.d.ts +55 -0
- package/lib/console/backendResidue.js +53 -0
- package/lib/console/customFunctions.d.ts +51 -1
- package/lib/console/customFunctions.js +93 -1
- package/lib/console/deploy.js +31 -7
- package/lib/console/environment.d.ts +7 -6
- package/lib/console/firestoreRest.d.ts +27 -0
- package/lib/console/firestoreRest.js +23 -0
- package/lib/console/functionGate.d.ts +228 -0
- package/lib/console/functionGate.js +437 -0
- package/lib/console/provision.d.ts +26 -13
- package/lib/console/provision.js +393 -58
- package/lib/kernel/backendShape.d.ts +27 -0
- package/lib/kernel/backendShape.js +38 -0
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/lib/vendor/packageTemplates.d.ts +12 -1
- package/lib/vendor/packageTemplates.js +42 -2
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { callableFunctionNames, declaredParametersIn } from '../kernel/manifest.js';
|
|
4
|
+
import { callableDeclarations, triggerDeclarations, unscopedDeclarations, } from './functionGate.js';
|
|
4
5
|
/**
|
|
5
6
|
* As Cloud Functions do ESCRITÓRIO — as que ele escreve, não as do kernel.
|
|
6
7
|
*
|
|
@@ -79,6 +80,33 @@ const readDependencies = async (path, label) => {
|
|
|
79
80
|
}
|
|
80
81
|
return parsed.dependencies;
|
|
81
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* `microteia.serves` do `package.json` do diretório: nome de function -> os ids que ela serve.
|
|
85
|
+
*
|
|
86
|
+
* OPCIONAL, como as `dependencies` ao lado. A ausência dela é o comportamento anterior — a function comum
|
|
87
|
+
* sobe sempre e nunca é retirada —, e o provisionamento diz quais estão nessa situação em vez de calar.
|
|
88
|
+
*/
|
|
89
|
+
const readServes = async (path, label) => {
|
|
90
|
+
const manifest = join(path, 'package.json');
|
|
91
|
+
if (!await exists(manifest))
|
|
92
|
+
return {};
|
|
93
|
+
const parsed = JSON.parse(await readFile(manifest, 'utf8'));
|
|
94
|
+
const declared = parsed.microteia?.serves;
|
|
95
|
+
if (declared === undefined)
|
|
96
|
+
return {};
|
|
97
|
+
if (typeof declared !== 'object' || declared === null || Array.isArray(declared)) {
|
|
98
|
+
throw new Error(`${label}/package.json has a "microteia.serves" that is not an object.`);
|
|
99
|
+
}
|
|
100
|
+
const serves = {};
|
|
101
|
+
for (const [name, ids] of Object.entries(declared)) {
|
|
102
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== 'string')) {
|
|
103
|
+
throw new Error(`${label}/package.json declares "microteia.serves.${name}" as something other than a list of `
|
|
104
|
+
+ 'extension ids.');
|
|
105
|
+
}
|
|
106
|
+
serves[name] = [...new Set(ids)];
|
|
107
|
+
}
|
|
108
|
+
return serves;
|
|
109
|
+
};
|
|
82
110
|
const readSource = async (path, label, slug, shared) => {
|
|
83
111
|
const entry = join(path, 'index.ts');
|
|
84
112
|
if (!await exists(entry)) {
|
|
@@ -88,11 +116,19 @@ const readSource = async (path, label, slug, shared) => {
|
|
|
88
116
|
}
|
|
89
117
|
const exported = exportedNames(await readFile(entry, 'utf8'));
|
|
90
118
|
const callables = new Set();
|
|
119
|
+
const triggers = [];
|
|
120
|
+
const unscoped = [];
|
|
121
|
+
const callableGuards = [];
|
|
91
122
|
const parameters = new Map();
|
|
92
123
|
for (const file of await sourceFiles(path)) {
|
|
93
124
|
const source = await readFile(file, 'utf8');
|
|
94
125
|
for (const name of callableFunctionNames(source))
|
|
95
126
|
callables.add(name);
|
|
127
|
+
// O gatilho é declarado no arquivo dele e reexportado pelo `index.ts` — é o que o molde escreve —,
|
|
128
|
+
// então a varredura é a mesma das callables: todo `.ts`, e o `index.ts` decide o que conta.
|
|
129
|
+
triggers.push(...triggerDeclarations(source));
|
|
130
|
+
unscoped.push(...unscopedDeclarations(source));
|
|
131
|
+
callableGuards.push(...callableDeclarations(source));
|
|
96
132
|
// Um nome declarado em vários lugares é UM parâmetro, e ele é exigido se ALGUMA declaração o pede
|
|
97
133
|
// sem `default`: é essa que recusa o deploy quando o valor falta, e a conservadora é a que avisa.
|
|
98
134
|
for (const parameter of declaredParametersIn(source)) {
|
|
@@ -112,8 +148,12 @@ const readSource = async (path, label, slug, shared) => {
|
|
|
112
148
|
// Só as que o `index.ts` de fato exporta: uma callable escrita e não exportada não é deployada, e
|
|
113
149
|
// conferir o alcance dela mandaria procurar um erro que não existe.
|
|
114
150
|
callables: exported.filter((name) => callables.has(name)),
|
|
151
|
+
triggers,
|
|
152
|
+
unscoped,
|
|
153
|
+
callableGuards,
|
|
115
154
|
parameters: [...parameters.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
116
155
|
dependencies: await readDependencies(path, label),
|
|
156
|
+
serves: await readServes(path, label),
|
|
117
157
|
};
|
|
118
158
|
};
|
|
119
159
|
/** Onde o escritório pode ter escrito function, na ordem em que as mensagens as citam. */
|
|
@@ -146,10 +186,21 @@ export const officeFunctionsOf = (sources) => {
|
|
|
146
186
|
});
|
|
147
187
|
}
|
|
148
188
|
}
|
|
189
|
+
const shared = sources.filter((source) => source.shared);
|
|
190
|
+
const declared = new Map();
|
|
191
|
+
for (const source of shared) {
|
|
192
|
+
for (const [name, ids] of Object.entries(source.serves))
|
|
193
|
+
declared.set(name, ids);
|
|
194
|
+
}
|
|
195
|
+
const sharedNames = shared.flatMap((source) => source.exported);
|
|
149
196
|
return {
|
|
150
197
|
sources,
|
|
151
198
|
all: sources.flatMap((source) => source.exported),
|
|
152
|
-
shared:
|
|
199
|
+
shared: sharedNames,
|
|
200
|
+
served: sharedNames
|
|
201
|
+
.filter((name) => declared.has(name))
|
|
202
|
+
.map((name) => ({ name, serves: declared.get(name) ?? [] })),
|
|
203
|
+
unserved: sharedNames.filter((name) => !declared.has(name)),
|
|
153
204
|
callables: sources.flatMap((source) => source.callables),
|
|
154
205
|
parameters: [...parameters.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
155
206
|
};
|
|
@@ -165,6 +216,41 @@ export const readOfficeFunctions = async (root) => officeFunctionsOf(await findF
|
|
|
165
216
|
* silêncio — e substituir um gatilho do kernel é o tipo de defeito que só aparece quando algo deixa de
|
|
166
217
|
* acontecer, sem erro em lugar nenhum.
|
|
167
218
|
*/
|
|
219
|
+
/**
|
|
220
|
+
* O que impede `microteia.serves` de ser levado a sério.
|
|
221
|
+
*
|
|
222
|
+
* As três recusas têm o mesmo motivo: a declaração é o que autoriza a plataforma a RETIRAR a function
|
|
223
|
+
* comum, e uma declaração que não fecha faria a retirada acontecer pelo motivo errado — ou não acontecer,
|
|
224
|
+
* e nada diria por quê.
|
|
225
|
+
*/
|
|
226
|
+
export const servesProblems = (sources) => {
|
|
227
|
+
const problems = [];
|
|
228
|
+
for (const source of sources) {
|
|
229
|
+
const declared = Object.entries(source.serves);
|
|
230
|
+
if (declared.length === 0)
|
|
231
|
+
continue;
|
|
232
|
+
// A function de extensão já tem dono, e ele vem do LUGAR do arquivo. Declarar de novo, e num lugar
|
|
233
|
+
// que só a comum consulta, criaria duas autoridades sobre a mesma retirada.
|
|
234
|
+
if (!source.shared) {
|
|
235
|
+
problems.push(`${source.label}/package.json declares "microteia.serves", which only ${FUNCTIONS_DIR}/ uses — a `
|
|
236
|
+
+ "function of an extension is already owned by the folder it is in, and is declared in that "
|
|
237
|
+
+ 'extension\'s manifest.backend.functions');
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const exported = new Set(source.exported);
|
|
241
|
+
for (const [name, ids] of declared) {
|
|
242
|
+
if (!exported.has(name)) {
|
|
243
|
+
problems.push(`${source.label}/package.json says "${name}" serves ${ids.join(', ') || 'nothing'}, but `
|
|
244
|
+
+ `${source.label}/index.ts does not export it`);
|
|
245
|
+
}
|
|
246
|
+
if (ids.length === 0) {
|
|
247
|
+
problems.push(`${source.label}/package.json declares "${name}" as serving no extension at all — name the `
|
|
248
|
+
+ 'extensions it exists for, or leave it out of microteia.serves');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return problems;
|
|
253
|
+
};
|
|
168
254
|
export const functionProblems = (sources, kernelFunctions) => {
|
|
169
255
|
const problems = [];
|
|
170
256
|
const kernel = new Set(kernelFunctions);
|
|
@@ -186,6 +272,12 @@ export const functionProblems = (sources, kernelFunctions) => {
|
|
|
186
272
|
}
|
|
187
273
|
}
|
|
188
274
|
}
|
|
275
|
+
problems.push(...servesProblems(sources));
|
|
276
|
+
// O portão de instalação NÃO entra aqui. Esta porta responde "dá para montar o conjunto?", e a
|
|
277
|
+
// resposta dela vale para toda fonte que existe no disco — inclusive a da extensão que foi
|
|
278
|
+
// desinstalada e cujo diretório ficou. O portão responde outra coisa: "esta function pode ir para o
|
|
279
|
+
// ar?", e ela só é devida de quem vai. Quem pergunta é `gateProblemsFor`, no ponto em que os nomes
|
|
280
|
+
// que sobem já estão decididos: `deploy` contra a baseline mais as comuns, `provision` contra a fila.
|
|
189
281
|
return problems;
|
|
190
282
|
};
|
|
191
283
|
const barrel = (sources) => `// Generated by "microteia deploy". Do not edit.
|
package/lib/console/deploy.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ENVIRONMENT_FILE, readEnvironment, writeEnvironment } from './environme
|
|
|
7
7
|
import { FIREBASE_BIN } from './firebaseCli.js';
|
|
8
8
|
import { reportFailures, reportNotices } from './deployFunctions.js';
|
|
9
9
|
import { clearRegionOrphans, listDeployedFunctions } from './regionOrphans.js';
|
|
10
|
+
import { gateProblemsFor } from './functionGate.js';
|
|
10
11
|
import { deployFunctionsThatChanged } from './functionsUpToDate.js';
|
|
11
12
|
import { checkCallables, invokerGrantCommand } from './callableAccess.js';
|
|
12
13
|
import { checkHostingServed, htmlIsRevalidated } from './hostingCheck.js';
|
|
@@ -125,17 +126,40 @@ export const deploy = async (args) => {
|
|
|
125
126
|
// comuns) e `extensions/<plugin>/functions/` (as de cada extensão). O `predeploy` do pacote compila
|
|
126
127
|
// só o `custom/`, então o que não estiver aqui não sobe.
|
|
127
128
|
const gathered = await gatherCustomFunctions(root, install, manifest.functions);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
/**
|
|
130
|
+
* O que sobe aqui é a baseline MAIS as functions comuns que NÃO dizem a quem servem.
|
|
131
|
+
*
|
|
132
|
+
* As de `extensions/<plugin>/functions/` têm dono e sobem pelo provisionamento, no fim deste
|
|
133
|
+
* comando — é o mesmo "empacotar não é deployar" que vale para as do kernel reivindicadas por
|
|
134
|
+
* extensão. E a comum que DECLARA quem serve passou a ter a mesma condição, pela mesma razão: ela
|
|
135
|
+
* é devida enquanto alguma das extensões servidas estiver instalada, e subi-la aqui de qualquer
|
|
136
|
+
* forma faria o `--prune` apagá-la e este passo repô-la, indefinidamente.
|
|
137
|
+
*
|
|
138
|
+
* A comum sem declaração continua subindo sempre. É o comportamento anterior, e mantê-lo é o que
|
|
139
|
+
* impede esta versão de tirar do ar function de uma instalação que já existe.
|
|
140
|
+
*/
|
|
141
|
+
const own = [...baseline, ...gathered.unserved];
|
|
142
|
+
/**
|
|
143
|
+
* O portão de instalação, cobrado do que ESTE passo sobe — e não de toda fonte que existe no
|
|
144
|
+
* disco.
|
|
145
|
+
*
|
|
146
|
+
* Na prática ele quase nunca tem o que dizer aqui, e isso é a confirmação de que o lugar é este:
|
|
147
|
+
* o que sobe no `deploy` é a baseline (do kernel) e a comum sem declaração (de quem não há id a
|
|
148
|
+
* cobrar). É no `provision` que a function de extensão entra na fila, e é lá que a recusa morde.
|
|
149
|
+
* Cobrar aqui mantém a regra atada ao ato — se um dia esta lista crescer, ela vem coberta.
|
|
150
|
+
*/
|
|
151
|
+
const refused = gateProblemsFor(gathered.sources, own);
|
|
152
|
+
if (refused.length > 0) {
|
|
153
|
+
throw new Error(`These Cloud Functions cannot go up as they are:\n${refused.map((line) => ` ${line}`).join('\n')}`);
|
|
154
|
+
}
|
|
132
155
|
step(current, total, `Cloud Functions (${own.length}, one at a time)`);
|
|
133
156
|
if (gathered.sources.length > 0) {
|
|
134
157
|
success(`${gathered.functions.length} function(s) of your own, from `
|
|
135
158
|
+ `${gathered.sources.map((source) => `${source.label}/`).join(', ')}`);
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
info(` ${
|
|
159
|
+
const provisioned = gathered.all.length - gathered.unserved.length;
|
|
160
|
+
if (provisioned > 0) {
|
|
161
|
+
info(` ${provisioned} of them go up with "provision", only where they are needed`);
|
|
162
|
+
}
|
|
139
163
|
}
|
|
140
164
|
if (Object.keys(gathered.dependencies.added).length > 0) {
|
|
141
165
|
success(`${Object.keys(gathered.dependencies.added).length} npm dependency(ies) your functions declare: `
|
|
@@ -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
|
|
33
|
-
* porque é estado da instalação e não do escritório — e é o que permite ao `provision`
|
|
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
|
-
*
|
|
37
|
-
*
|
|
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,228 @@
|
|
|
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: o defeito é invisível depois de
|
|
17
|
+
* instalado, e um aviso na saída de um deploy é lido uma vez. Mas ela é cobrada da function que VAI
|
|
18
|
+
* SUBIR, e não de toda fonte que existe no disco — ver `gateProblemsFor`. Toda origem é lida, sempre, e
|
|
19
|
+
* o que não vai subir é relatado pelo `provision`: quem tem a fonte parada ao lado do `microteia.yaml`
|
|
20
|
+
* fica sabendo dela, sem ter o comando trancado por ela.
|
|
21
|
+
*
|
|
22
|
+
* A análise é por texto, no idioma dos outros leitores de fonte do Console (`callableFunctionNames`,
|
|
23
|
+
* `declaredParametersIn`). Ela erra para o lado ALTO — o que ela não entende ela recusa —, e é por isso
|
|
24
|
+
* que o portão declarado num identificador ao lado é reconhecido: recusar código correto é o que ensina
|
|
25
|
+
* a desligar a verificação.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Os construtores de gatilho de Firestore.
|
|
29
|
+
*
|
|
30
|
+
* São os únicos eventos cujo caminho carrega `{tenantId}`, e portanto os únicos para os quais o portão
|
|
31
|
+
* existe. Um `onSchedule` ou um `onMessagePublished` não tem escritório de onde perguntar — envolvê-los
|
|
32
|
+
* no portão os deixaria permanentemente inertes, porque ele falha FECHADO. Eles ficam de fora desta
|
|
33
|
+
* verificação de propósito: exigir deles um portão que não funciona seria trocar um defeito por outro.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TENANT_TRIGGER_CONSTRUCTORS: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Os construtores cujo evento NÃO carrega escritório no caminho.
|
|
38
|
+
*
|
|
39
|
+
* Para eles as duas saídas do autor eram erradas, e é isso que faz deles uma recusa e não um aviso: sem
|
|
40
|
+
* portão a function age sobre o projeto inteiro; COM portão ela nunca roda — nem enquanto a extensão está
|
|
41
|
+
* instalada —, porque `forInstalledExtension` falha FECHADO e nunca acha o `{tenantId}` que não existe.
|
|
42
|
+
* Um autor que seguisse a orientação genérica "envolva o handler no portão" produzia uma function que
|
|
43
|
+
* passava no deploy, não acusava nada e nunca fazia nada.
|
|
44
|
+
*
|
|
45
|
+
* A resposta é o laço por escritório (`forEachInstalledOffice`), e é por existir uma resposta que a
|
|
46
|
+
* recusa é honesta.
|
|
47
|
+
*
|
|
48
|
+
* 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.
|
|
49
|
+
* O RTDB fica de fora de propósito — o caminho dele TEM parâmetros, então um `{tenantId}` ali é possível
|
|
50
|
+
* —, e a plataforma não usa RTDB em lugar nenhum.
|
|
51
|
+
*/
|
|
52
|
+
export declare const UNSCOPED_TRIGGER_CONSTRUCTORS: string[];
|
|
53
|
+
/**
|
|
54
|
+
* A fonte sem comentário nenhum, com as strings intactas.
|
|
55
|
+
*
|
|
56
|
+
* O JSDoc que o molde escreve CITA `forInstalledExtension` — é ali que ele explica o portão. Sem esta
|
|
57
|
+
* limpeza, um gatilho que perdesse a chamada e mantivesse o comentário passaria na verificação, o que é
|
|
58
|
+
* o caso exato que ela existe para pegar.
|
|
59
|
+
*/
|
|
60
|
+
export declare const withoutComments: (source: string) => string;
|
|
61
|
+
export interface TriggerDeclaration {
|
|
62
|
+
/** O nome declarado — é por ele que se sabe se o `index.ts` o exporta. */
|
|
63
|
+
name: string;
|
|
64
|
+
/** O construtor usado. Aparece na mensagem: é ele que explica por que o portão é devido. */
|
|
65
|
+
trigger: string;
|
|
66
|
+
/** O id que `forInstalledExtension` recebeu, ou `null` quando o portão não está lá. */
|
|
67
|
+
gatedFor: string | null;
|
|
68
|
+
/** Todos os ids que as guardas daquele gatilho recebem — a comum guarda mais de um. */
|
|
69
|
+
guards: string[];
|
|
70
|
+
}
|
|
71
|
+
/** Os gatilhos de Firestore declarados numa fonte, com o estado do portão de cada um. */
|
|
72
|
+
export declare const triggerDeclarations: (rawSource: string) => TriggerDeclaration[];
|
|
73
|
+
/**
|
|
74
|
+
* As functions sem escritório no evento, e o id que o laço por escritório recebeu no ARQUIVO.
|
|
75
|
+
*
|
|
76
|
+
* O alcance é o arquivo, e não os parênteses da chamada — a diferença é deliberada. O portão de um
|
|
77
|
+
* gatilho de documento embrulha AQUELE handler, então ele tem de estar ali; o laço, ao contrário, roda
|
|
78
|
+
* DENTRO do handler e pode estar algumas chamadas abaixo, num `syncOffice(tenantId)` que o próprio autor
|
|
79
|
+
* escreveu. Exigi-lo nos parênteses recusaria código correto, e a verificação que erra assim é a que
|
|
80
|
+
* ensina a próxima pessoa a desligá-la.
|
|
81
|
+
*
|
|
82
|
+
* O preço é conhecido: dois agendados no mesmo arquivo, um com laço e outro sem, passam os dois. É menos
|
|
83
|
+
* grave que o inverso, e está dito.
|
|
84
|
+
*/
|
|
85
|
+
export declare const unscopedDeclarations: (rawSource: string) => TriggerDeclaration[];
|
|
86
|
+
/**
|
|
87
|
+
* As callables declaradas, e o id que a guarda delas recebeu no ARQUIVO.
|
|
88
|
+
*
|
|
89
|
+
* O alcance é o arquivo pela mesma razão do laço por escritório: a guarda roda DENTRO do handler (ela
|
|
90
|
+
* precisa do `request`), e o guia manda o autor manter o handler fino, com a decisão num arquivo ao
|
|
91
|
+
* lado. Exigi-la nos parênteses recusaria justamente o código que o guia pede.
|
|
92
|
+
*/
|
|
93
|
+
export declare const callableDeclarations: (rawSource: string) => TriggerDeclaration[];
|
|
94
|
+
/**
|
|
95
|
+
* O que uma origem de functions precisa ter para ser conferida. É a forma de `FunctionSource`, declarada
|
|
96
|
+
* aqui em vez de importada: assim este módulo se testa sozinho, sem montar diretório nenhum.
|
|
97
|
+
*/
|
|
98
|
+
export interface GateSubject {
|
|
99
|
+
label: string;
|
|
100
|
+
/** `functions/` é comum a várias extensões, e não tem uma instalação de onde perguntar. */
|
|
101
|
+
shared: boolean;
|
|
102
|
+
exported: string[];
|
|
103
|
+
triggers: TriggerDeclaration[];
|
|
104
|
+
/** As functions cujo evento não carrega escritório: o agendado, a fila, o HTTP, o Storage. */
|
|
105
|
+
unscoped: TriggerDeclaration[];
|
|
106
|
+
/** Quem cada function comum declara servir. Vazio numa origem de extensão. */
|
|
107
|
+
serves: Record<string, string[]>;
|
|
108
|
+
/** As callables declaradas, com o estado da guarda de cada uma. */
|
|
109
|
+
callableGuards: TriggerDeclaration[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Um problema de portão, e a function a que ele pertence.
|
|
113
|
+
*
|
|
114
|
+
* O nome está aqui porque é ele que decide o PESO do problema. A verificação lê toda fonte que existe no
|
|
115
|
+
* disco — é assim que ela acha o que precisa achar —, mas o que existe no disco não é o que vai subir:
|
|
116
|
+
* `extensions/<x>/functions/` de uma extensão que ninguém instalou é lida, copiada e nunca deployada.
|
|
117
|
+
* Sem o nome, quem chama só tem a frase, e a única coisa que dá para fazer com uma frase é recusar tudo.
|
|
118
|
+
* Foi o que acontecia: uma extensão desinstalada, com a fonte parada ao lado do `microteia.yaml`,
|
|
119
|
+
* trancava o `deploy` inteiro — hosting, rules e índices junto — por causa de uma function que nenhum
|
|
120
|
+
* comando teria subido.
|
|
121
|
+
*/
|
|
122
|
+
export interface GateProblem {
|
|
123
|
+
/** A function a que o problema pertence. É por ela que se sabe se ela vai, de fato, subir. */
|
|
124
|
+
name: string;
|
|
125
|
+
message: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gatilho de extensão sem portão.
|
|
129
|
+
*
|
|
130
|
+
* `functions/` fica de fora porque ali a pergunta não tem resposta: a function é comum a várias
|
|
131
|
+
* extensões, nenhuma a reivindica, e não há um id para o portão receber. É lacuna conhecida e tem
|
|
132
|
+
* endereço próprio — enquanto ela não fechar, exigir aqui um portão impossível seria recusar a única
|
|
133
|
+
* forma que a plataforma oferece para uma function compartilhada.
|
|
134
|
+
*/
|
|
135
|
+
export declare const ungatedTriggerProblems: (subjects: GateSubject[]) => GateProblem[];
|
|
136
|
+
/**
|
|
137
|
+
* Function de extensão cujo evento não tem escritório, e que não passa pelo laço.
|
|
138
|
+
*
|
|
139
|
+
* A recusa vem acompanhada do substituto porque sem ele ela seria só uma porta fechada: o laço por
|
|
140
|
+
* escritório (`forEachInstalledOffice`) existe justamente para este caso, e é ele que a mensagem nomeia.
|
|
141
|
+
*
|
|
142
|
+
* `functions/` fica de fora pelo mesmo motivo de sempre: a comum não tem um id de extensão para dar ao
|
|
143
|
+
* laço. Quem responde por ela é a declaração de quem ela serve.
|
|
144
|
+
*/
|
|
145
|
+
export declare const unscopedTriggerProblems: (subjects: GateSubject[]) => GateProblem[];
|
|
146
|
+
/**
|
|
147
|
+
* Gatilho de uma function COMUM que declara quem serve, e não guarda ninguém.
|
|
148
|
+
*
|
|
149
|
+
* A comum ficava fora desta cobrança porque não havia id para dar à guarda: nenhuma extensão pode
|
|
150
|
+
* reivindicá-la. `microteia.serves` resolve exatamente isso — a partir dela existem ids, e a partir dos
|
|
151
|
+
* ids a cobrança passa a ser possível. Um gatilho comum sem guarda dispara para todo escritório do
|
|
152
|
+
* projeto, e é o mesmo defeito do gatilho de extensão sem portão, com um alcance maior.
|
|
153
|
+
*
|
|
154
|
+
* A comum SEM declaração continua de fora, e é deliberado: cobrar dela uma guarda sem ter ids a oferecer
|
|
155
|
+
* seria fechar a porta sem abrir nenhuma. O que ela recebe é o relato do provisionamento.
|
|
156
|
+
*
|
|
157
|
+
* A segunda recusa é a guarda que nomeia extensão que a function não declara servir: as duas listas
|
|
158
|
+
* respondem à mesma pergunta em dois lugares, e divergirem faz uma delas mentir — a `serves` decide a
|
|
159
|
+
* retirada, a guarda decide a execução, e a function passaria a agir onde não pode ser retirada.
|
|
160
|
+
*/
|
|
161
|
+
export declare const unguardedSharedProblems: (subjects: GateSubject[]) => GateProblem[];
|
|
162
|
+
/**
|
|
163
|
+
* Callable de extensão sem a guarda de instalação.
|
|
164
|
+
*
|
|
165
|
+
* "Uma callable órfã é inerte" era verdade de INTERFACE, e não de segurança — e era a frase que o guia
|
|
166
|
+
* dizia. O deploy concede a `allUsers` o papel `roles/run.invoker` em cada callable; o que sobra depois é
|
|
167
|
+
* a validação do token do Firebase. Então a tela ir embora com a extensão não fecha nada: o endpoint
|
|
168
|
+
* segue no ar aceitando o token de QUALQUER usuário do projeto, de qualquer escritório, tendo ou não a
|
|
169
|
+
* extensão.
|
|
170
|
+
*
|
|
171
|
+
* E o escritório chega no PAYLOAD, porque `useCallable` manda só o que a tela passou. Sem a guarda, um
|
|
172
|
+
* usuário do escritório A chama a function apontando para o B — e o molde gerado conferia apenas se
|
|
173
|
+
* havia `request.auth`.
|
|
174
|
+
*
|
|
175
|
+
* A comum fica de fora: `requireInstalledExtension` pede UM id, e a comum serve vários. É lacuna
|
|
176
|
+
* conhecida e está registrada — a guarda "alguma das que eu sirvo" existe para gatilho, não para
|
|
177
|
+
* callable.
|
|
178
|
+
*/
|
|
179
|
+
export declare const unguardedCallableProblems: (subjects: GateSubject[]) => GateProblem[];
|
|
180
|
+
/** Todo problema de portão de um conjunto de origens, na ordem em que as mensagens os citam. */
|
|
181
|
+
export declare const gateProblems: (subjects: GateSubject[]) => GateProblem[];
|
|
182
|
+
/**
|
|
183
|
+
* Os problemas das functions que ESTÃO subindo — a lista que autoriza uma recusa.
|
|
184
|
+
*
|
|
185
|
+
* É a diferença entre "a fonte foi lida" e "a function vai para o ar", e ela não é sutil: a primeira
|
|
186
|
+
* inclui toda extensão cujo diretório ficou no disco depois de desinstalada, e recusar por causa dela é
|
|
187
|
+
* recusar um comando que não faria nada de errado. A segunda é o momento em que a recusa tem
|
|
188
|
+
* consequência — e é o único em que ela é devida.
|
|
189
|
+
*/
|
|
190
|
+
export declare const gateProblemsFor: (subjects: GateSubject[], deploying: string[]) => string[];
|
|
191
|
+
export interface MisgatedTrigger {
|
|
192
|
+
/** A origem, como as mensagens a citam. */
|
|
193
|
+
label: string;
|
|
194
|
+
name: string;
|
|
195
|
+
/** O id que a guarda recebeu. */
|
|
196
|
+
gatedFor: string;
|
|
197
|
+
/** O id da extensão que reivindica a function. */
|
|
198
|
+
owner: string;
|
|
199
|
+
/** Qual guarda recebeu o id errado — é ela que o autor tem de corrigir. */
|
|
200
|
+
via: string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Portão aberto para o id ERRADO.
|
|
204
|
+
*
|
|
205
|
+
* É o defeito do copiar-e-colar entre extensões, e o sintoma dele é o oposto do gatilho sem portão: a
|
|
206
|
+
* function sobe, o gatilho dispara, e o handler nunca roda em escritório nenhum. Nada falha — a extensão
|
|
207
|
+
* apenas não faz o que promete, e a causa está numa string.
|
|
208
|
+
*
|
|
209
|
+
* Devolve os casos, e não as frases, porque quem chama precisa dos dois: a mensagem para relatar e o id
|
|
210
|
+
* da extensão para pendurar. Deduzir o id de volta a partir do texto é o que faz um relatório recusar a
|
|
211
|
+
* extensão errada quando o nome de uma aparece dentro da mensagem de outra.
|
|
212
|
+
*
|
|
213
|
+
* Só se pergunta quando o nome tem UM reivindicante: com dois, `functionClaimProblems` já recusou antes,
|
|
214
|
+
* e apontar um deles como o dono certo seria inventar uma resposta.
|
|
215
|
+
*/
|
|
216
|
+
export declare const misgatedTriggers: (subjects: GateSubject[], claimants: Map<string, string>) => MisgatedTrigger[];
|
|
217
|
+
/** Como uma guarda de id errado aparece num relatório. */
|
|
218
|
+
export declare const describeMisgated: (entry: MisgatedTrigger) => string;
|
|
219
|
+
/**
|
|
220
|
+
* Quem reivindica cada nome, quando é um só. Duas extensões pedindo o mesmo nome é problema de outra
|
|
221
|
+
* verificação, e o nome sai daqui em vez de receber um dono escolhido a esmo.
|
|
222
|
+
*/
|
|
223
|
+
export declare const singleClaimants: (claims: {
|
|
224
|
+
pluginId: string;
|
|
225
|
+
backend: {
|
|
226
|
+
functions?: string[];
|
|
227
|
+
};
|
|
228
|
+
}[]) => Map<string, string>;
|