@microteia/console 2.7.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 +85 -0
- package/README.md +64 -0
- package/lib/console/backendEnv.d.ts +52 -0
- package/lib/console/backendEnv.js +118 -0
- package/lib/console/backendIndexes.d.ts +40 -0
- package/lib/console/backendIndexes.js +178 -0
- package/lib/console/backendParameters.d.ts +48 -0
- package/lib/console/backendParameters.js +100 -0
- package/lib/console/callableAccess.d.ts +39 -0
- package/lib/console/callableAccess.js +61 -0
- package/lib/console/channel.d.ts +49 -0
- package/lib/console/channel.js +145 -0
- package/lib/console/connect.d.ts +2 -0
- package/lib/console/connect.js +74 -0
- package/lib/console/customFunctions.d.ts +120 -0
- package/lib/console/customFunctions.js +298 -0
- package/lib/console/deploy.d.ts +2 -0
- package/lib/console/deploy.js +261 -0
- package/lib/console/deployFunctions.d.ts +45 -0
- package/lib/console/deployFunctions.js +204 -0
- package/lib/console/environment.d.ts +62 -0
- package/lib/console/environment.js +151 -0
- package/lib/console/firebaseCli.d.ts +15 -0
- package/lib/console/firebaseCli.js +90 -0
- package/lib/console/firestoreRest.d.ts +39 -0
- package/lib/console/firestoreRest.js +131 -0
- package/lib/console/hostingCheck.d.ts +29 -0
- package/lib/console/hostingCheck.js +44 -0
- package/lib/console/init.d.ts +2 -0
- package/lib/console/init.js +60 -0
- package/lib/console/pluginWorkspace.d.ts +48 -0
- package/lib/console/pluginWorkspace.js +148 -0
- package/lib/console/provision.d.ts +70 -0
- package/lib/console/provision.js +478 -0
- package/lib/console/regionOrphans.d.ts +43 -0
- package/lib/console/regionOrphans.js +99 -0
- package/lib/console/runtimeConfigShape.d.ts +25 -0
- package/lib/console/runtimeConfigShape.js +35 -0
- package/lib/console/unpack.d.ts +9 -0
- package/lib/console/unpack.js +333 -0
- package/lib/console/versions.d.ts +10 -0
- package/lib/console/versions.js +72 -0
- package/lib/console/workspace.d.ts +19 -0
- package/lib/console/workspace.js +33 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +136 -0
- package/lib/kernel/backendShape.d.ts +74 -0
- package/lib/kernel/backendShape.js +91 -0
- package/lib/kernel/channel.d.ts +79 -0
- package/lib/kernel/channel.js +157 -0
- package/lib/kernel/manifest.d.ts +138 -0
- package/lib/kernel/manifest.js +145 -0
- package/lib/shared/args.d.ts +14 -0
- package/lib/shared/args.js +44 -0
- package/lib/shared/digest.d.ts +30 -0
- package/lib/shared/digest.js +34 -0
- package/lib/shared/paths.d.ts +90 -0
- package/lib/shared/paths.js +97 -0
- package/lib/shared/run.d.ts +28 -0
- package/lib/shared/run.js +49 -0
- package/lib/shared/ui.d.ts +23 -0
- package/lib/shared/ui.js +43 -0
- package/lib/vendor/authoringGuide.d.ts +19 -0
- package/lib/vendor/authoringGuide.js +284 -0
- package/lib/vendor/bundleAudit.d.ts +19 -0
- package/lib/vendor/bundleAudit.js +95 -0
- package/lib/vendor/changelogs.d.ts +62 -0
- package/lib/vendor/changelogs.js +131 -0
- package/lib/vendor/exampleSources.d.ts +29 -0
- package/lib/vendor/exampleSources.js +126 -0
- package/lib/vendor/installationDocs.d.ts +35 -0
- package/lib/vendor/installationDocs.js +243 -0
- package/lib/vendor/pack.d.ts +2 -0
- package/lib/vendor/pack.js +276 -0
- package/lib/vendor/packConsole.d.ts +13 -0
- package/lib/vendor/packConsole.js +63 -0
- package/lib/vendor/packageTemplates.d.ts +66 -0
- package/lib/vendor/packageTemplates.js +200 -0
- package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
- package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
- package/lib/vendor/publish.d.ts +2 -0
- package/lib/vendor/publish.js +277 -0
- package/lib/vendor/repository.d.ts +9 -0
- package/lib/vendor/repository.js +26 -0
- package/package.json +21 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { hasFlag, requiredFlag, flagValue } from '../shared/args.js';
|
|
3
|
+
import { run } from '../shared/run.js';
|
|
4
|
+
import { blank, bullets, failure, heading, info, step, success, warn } from '../shared/ui.js';
|
|
5
|
+
import { backendFingerprint, claimedIndexes, claimedParameters, functionClaimProblems, hasBackendRequirement, indexKey, } from '../kernel/backendShape.js';
|
|
6
|
+
import { convergeIndexes, describeIndex, parseIndexState, readIndexFile, serializeIndexState, writeIndexFile, } from './backendIndexes.js';
|
|
7
|
+
import { officeEnvPath, stageOfficeEnv } from './backendEnv.js';
|
|
8
|
+
import { installPath, readInstalledManifest } from './unpack.js';
|
|
9
|
+
import { changedParameters, functionsDeclaringParameters, parameterDigests, parseParameterState, readBackendEnv, serializeParameterState, } from './backendParameters.js';
|
|
10
|
+
import { ENVIRONMENT_FILE, readEnvironment, writeEnvironment } from './environment.js';
|
|
11
|
+
import { FIREBASE_BIN } from './firebaseCli.js';
|
|
12
|
+
import { deployFunctionsOneByOne, reportFailures, reportNotices } from './deployFunctions.js';
|
|
13
|
+
import { clearRegionOrphans } from './regionOrphans.js';
|
|
14
|
+
import { checkCallables, invokerGrantCommand } from './callableAccess.js';
|
|
15
|
+
import { asString, asTimestamp, decodeFields, documentId, listDocuments, patchMapField } from './firestoreRest.js';
|
|
16
|
+
import { gatherCustomFunctions, readOfficeFunctions } from './customFunctions.js';
|
|
17
|
+
/**
|
|
18
|
+
* O que cada escritório do projeto instalou e exige do backend.
|
|
19
|
+
*
|
|
20
|
+
* A exigência vem do banco, então ela é DADO e não código — é por isso que `claimProblems` a confere
|
|
21
|
+
* contra `manifest.functions` antes de qualquer deploy. Nome de function fora do pacote é recusado com
|
|
22
|
+
* explicação; não há injeção possível, porque o código é sempre o do pacote.
|
|
23
|
+
*/
|
|
24
|
+
export const readInstalledClaims = async (projectId) => {
|
|
25
|
+
const tenants = await listDocuments(projectId, '/tenants');
|
|
26
|
+
const claims = [];
|
|
27
|
+
for (const tenant of tenants) {
|
|
28
|
+
const tenantId = documentId(tenant);
|
|
29
|
+
const extensions = await listDocuments(projectId, `/tenants/${tenantId}/installed_extensions`);
|
|
30
|
+
for (const document of extensions) {
|
|
31
|
+
const data = decodeFields(document.fields);
|
|
32
|
+
if (data.active === false)
|
|
33
|
+
continue;
|
|
34
|
+
const pkg = data.package;
|
|
35
|
+
const manifest = pkg?.manifest;
|
|
36
|
+
const backend = manifest?.backend;
|
|
37
|
+
if (!hasBackendRequirement(backend))
|
|
38
|
+
continue;
|
|
39
|
+
const state = data.backendState;
|
|
40
|
+
claims.push({
|
|
41
|
+
tenantId,
|
|
42
|
+
documentPath: `/tenants/${tenantId}/installed_extensions/${documentId(document)}`,
|
|
43
|
+
pluginId: String(manifest?.id ?? documentId(document)),
|
|
44
|
+
name: String(manifest?.name ?? manifest?.id ?? documentId(document)),
|
|
45
|
+
version: String(manifest?.version ?? '0.0.0'),
|
|
46
|
+
backend: backend,
|
|
47
|
+
applied: state?.fingerprint === backendFingerprint(backend),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return claims.sort((a, b) => (a.pluginId < b.pluginId ? -1 : a.pluginId > b.pluginId ? 1 : 0));
|
|
52
|
+
};
|
|
53
|
+
const uniqueByPlugin = (claims) => {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
return claims.filter((claim) => {
|
|
56
|
+
if (seen.has(claim.pluginId))
|
|
57
|
+
return false;
|
|
58
|
+
seen.add(claim.pluginId);
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
export const parseAppliedFunctions = (value) => (value ?? '').split(',').map((name) => name.trim()).filter((name) => name !== '');
|
|
63
|
+
/**
|
|
64
|
+
* O motor: alinha o projeto Firebase com o que as extensões instaladas exigem, e carimba nelas o
|
|
65
|
+
* estado. Um motor só, chamado pelo `deploy` no fim e pelo `provision` sozinho — duas implementações
|
|
66
|
+
* divergiriam exatamente no ponto em que ninguém olha.
|
|
67
|
+
*/
|
|
68
|
+
export const provisionBackend = async (options) => {
|
|
69
|
+
const { projectId, region, install, manifest, delaySeconds } = options;
|
|
70
|
+
const root = dirname(options.environmentFile);
|
|
71
|
+
const outcome = { deployed: [], failed: [], orphans: [], pending: [] };
|
|
72
|
+
/**
|
|
73
|
+
* O que uma extensão pode reivindicar é o que EXISTE para ser deployado: o que o pacote traz mais o
|
|
74
|
+
* que o escritório escreveu em `extensions/<plugin>/functions/` e em `functions/`.
|
|
75
|
+
*
|
|
76
|
+
* Conferir só contra o pacote recusava justamente o caso que o `microteia-plugin create function`
|
|
77
|
+
* gera — a extensão declarava a própria function em `manifest.backend.functions`, o nome não estava no
|
|
78
|
+
* pacote (nem poderia: o kernel não sabe o que o escritório vai escrever), e a extensão inteira era
|
|
79
|
+
* rejeitada com "this kernel package does not contain".
|
|
80
|
+
*/
|
|
81
|
+
const office = await readOfficeFunctions(root);
|
|
82
|
+
const deployable = [...manifest.functions, ...office.all];
|
|
83
|
+
const claims = await readInstalledClaims(projectId);
|
|
84
|
+
const problems = functionClaimProblems(claims, deployable);
|
|
85
|
+
const rejected = new Set();
|
|
86
|
+
if (problems.length > 0) {
|
|
87
|
+
warn('some extensions ask for backend artifacts that are nowhere to be found:');
|
|
88
|
+
bullets(problems);
|
|
89
|
+
// A mensagem sozinha é um beco sem saída: ela diz que o nome não existe, e não onde ele deveria
|
|
90
|
+
// existir. As duas respostas possíveis são estas, e a segunda é a que mais acontece — a function é
|
|
91
|
+
// da extensão, o autor a declarou, e ninguém a exportou do `index.ts` do diretório dela.
|
|
92
|
+
info(' a function comes from ONE of two places, and it has to be in one of them:');
|
|
93
|
+
bullets([
|
|
94
|
+
'this Microteia package — the extension claims a function the platform ships',
|
|
95
|
+
`extensions/<folder>/functions/index.ts of this installation — the extension brings its own`,
|
|
96
|
+
]);
|
|
97
|
+
info(` this installation currently exports ${office.all.length} function(s) of its own`
|
|
98
|
+
+ `${office.all.length === 0 ? '' : `: ${office.all.join(', ')}`}`);
|
|
99
|
+
for (const claim of claims) {
|
|
100
|
+
if (problems.some((problem) => problem.includes(claim.pluginId)))
|
|
101
|
+
rejected.add(claim.pluginId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const valid = uniqueByPlugin(claims.filter((claim) => !rejected.has(claim.pluginId)));
|
|
105
|
+
/**
|
|
106
|
+
* Os índices são resolvidos ANTES do desvio de "nenhuma extensão exige nada" — e não depois, onde
|
|
107
|
+
* estavam. Desinstalar a única extensão com backend caía justamente naquele desvio, então o índice
|
|
108
|
+
* dela não era relatado por ninguém: o aviso existia para o caso de sobrar um órfão entre outras
|
|
109
|
+
* extensões, e faltava no caso de sobrar sozinho.
|
|
110
|
+
*/
|
|
111
|
+
if (valid.length === 0) {
|
|
112
|
+
info(' no installed extension asks for backend artifacts');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
info(` ${valid.length} extension(s) ask for backend artifacts:`);
|
|
116
|
+
bullets(valid.map((claim) => `${claim.name} ${claim.version} (${claim.pluginId})`));
|
|
117
|
+
}
|
|
118
|
+
const settled = await settleIndexes(options, valid);
|
|
119
|
+
if (valid.length === 0) {
|
|
120
|
+
await recordBackendState(options, parseAppliedFunctions(options.environment.backend.functions), parseParameterState(options.environment.backend.parameters), settled.declared);
|
|
121
|
+
reportOrphanIndexes(projectId, settled.orphans, options.prune);
|
|
122
|
+
return outcome;
|
|
123
|
+
}
|
|
124
|
+
const wantedFunctions = deployable.filter((name) => valid.some((claim) => (claim.backend.functions ?? []).includes(name)));
|
|
125
|
+
const alreadyApplied = parseAppliedFunctions(options.environment.backend.functions);
|
|
126
|
+
const trackedParameters = claimedParameters(valid);
|
|
127
|
+
// A raiz, e não o `install/`: a fonte é o arquivo que o escritório edita ao lado do `microteia.yaml`.
|
|
128
|
+
const filledParameters = await readBackendEnv(dirname(options.environmentFile), projectId);
|
|
129
|
+
const recordedDigests = parseParameterState(options.environment.backend.parameters);
|
|
130
|
+
const currentDigests = parameterDigests(projectId, filledParameters, trackedParameters);
|
|
131
|
+
const missingParameters = trackedParameters.filter((name) => !filledParameters.has(name));
|
|
132
|
+
if (missingParameters.length > 0) {
|
|
133
|
+
warn(`${missingParameters.length} backend setting(s) are still empty in .env.${projectId}`);
|
|
134
|
+
info(` (${officeEnvPath(dirname(options.environmentFile), projectId)})`);
|
|
135
|
+
bullets(missingParameters);
|
|
136
|
+
info(' the functions deploy anyway, but the feature that needs them will fail at runtime.');
|
|
137
|
+
}
|
|
138
|
+
// Preencher o arquivo não alcança function nenhuma: o valor é carimbado no deploy de cada uma. O que
|
|
139
|
+
// já está aplicado com o valor anterior volta para a fila — sem isso, o parâmetro preenchido depois da
|
|
140
|
+
// instalação fica sem nenhum comando que o aplique, e a feature falha no runtime.
|
|
141
|
+
const changed = changedParameters(recordedDigests, currentDigests, trackedParameters);
|
|
142
|
+
const staleFunctions = new Set(functionsDeclaringParameters(valid, changed)
|
|
143
|
+
.filter((name) => wantedFunctions.includes(name) && alreadyApplied.includes(name)));
|
|
144
|
+
if (changed.length > 0) {
|
|
145
|
+
blank();
|
|
146
|
+
info(` ${changed.length} backend setting(s) changed since the last run: ${changed.join(', ')}`);
|
|
147
|
+
if (staleFunctions.size > 0) {
|
|
148
|
+
info(` ${staleFunctions.size} deployed function(s) still carry the previous value and are`);
|
|
149
|
+
info(' redeployed now — a Cloud Function only sees a setting from the deploy that stamps it.');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const prerequisites = valid.flatMap((claim) => (claim.backend.prerequisites ?? []).map((prerequisite) => `[${claim.name}] ${prerequisite.title}\n ${prerequisite.detail}`));
|
|
153
|
+
if (prerequisites.length > 0) {
|
|
154
|
+
blank();
|
|
155
|
+
info('Manual steps these extensions need, which no command can do for you:');
|
|
156
|
+
bullets(prerequisites);
|
|
157
|
+
// O caminho sai daqui, resolvido, e não do texto de cada extensão: era assim que ele estava, e foi
|
|
158
|
+
// assim que ficou errado quando a configuração se mudou para a raiz.
|
|
159
|
+
info(` Backend settings go in ${officeEnvPath(dirname(options.environmentFile), projectId)}`);
|
|
160
|
+
info(' After filling one in, run "microteia provision" again — the value is stamped into each');
|
|
161
|
+
info(' Cloud Function at ITS deploy, so a function already running keeps the previous one.');
|
|
162
|
+
blank();
|
|
163
|
+
}
|
|
164
|
+
const toDeploy = wantedFunctions.filter((name) => !alreadyApplied.includes(name) || staleFunctions.has(name));
|
|
165
|
+
if (toDeploy.length === 0) {
|
|
166
|
+
success(`the ${wantedFunctions.length} function(s) these extensions need are already deployed`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// A cópia que o `firebase-functions` lê é atualizada AGORA, e não só no `deploy`: este comando é o
|
|
170
|
+
// que aplica um parâmetro preenchido depois da instalação, e subir a function contra uma cópia
|
|
171
|
+
// velha carimbaria nela justamente o valor que se acabou de trocar.
|
|
172
|
+
await stageOfficeEnv(dirname(options.environmentFile), projectId, install);
|
|
173
|
+
// As fontes do escritório são remontadas AGORA quando alguma delas está na fila: este comando roda
|
|
174
|
+
// sozinho depois de instalar uma extensão, e nesse caminho ninguém passou pelo `deploy` que as
|
|
175
|
+
// monta. Subir sem isso deployaria o `custom/` da montagem anterior — ou nenhum.
|
|
176
|
+
if (toDeploy.some((name) => office.all.includes(name))) {
|
|
177
|
+
await gatherCustomFunctions(root, install, manifest.functions);
|
|
178
|
+
info(' installing function dependencies…');
|
|
179
|
+
await run('npm', ['install', '--no-audit', '--no-fund'], { cwd: join(install, 'functions') });
|
|
180
|
+
}
|
|
181
|
+
// A extensão que mudou de região deixa a cópia antiga para trás igual ao kernel, e ali o efeito
|
|
182
|
+
// é o mesmo: deploy recusado em toda tentativa, e gatilho disparando em duas regiões.
|
|
183
|
+
await clearRegionOrphans(projectId, toDeploy, region, options.prune);
|
|
184
|
+
info(` deploying ${toDeploy.length} function(s), one at a time…`);
|
|
185
|
+
const report = await deployFunctionsOneByOne(toDeploy, {
|
|
186
|
+
projectId, cwd: install, delaySeconds, force: false,
|
|
187
|
+
});
|
|
188
|
+
outcome.deployed = report.deployed;
|
|
189
|
+
outcome.failed = report.failed.map((entry) => entry.name);
|
|
190
|
+
reportFailures(report.failed);
|
|
191
|
+
reportNotices(report.notices);
|
|
192
|
+
await checkNewCallables([...(manifest.callables ?? []), ...office.callables], report.deployed, projectId, region);
|
|
193
|
+
}
|
|
194
|
+
const deployedNow = new Set([...alreadyApplied, ...outcome.deployed]);
|
|
195
|
+
for (const claim of valid) {
|
|
196
|
+
const satisfied = (claim.backend.functions ?? []).every((name) => deployedNow.has(name));
|
|
197
|
+
if (satisfied) {
|
|
198
|
+
await stampApplied(projectId, claims, claim);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
outcome.pending.push(claim);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const orphans = alreadyApplied.filter((name) => !wantedFunctions.includes(name));
|
|
205
|
+
outcome.orphans = orphans;
|
|
206
|
+
const remaining = await settleOrphans(options, orphans);
|
|
207
|
+
await recordBackendState(options, appliedAfterRun({
|
|
208
|
+
alreadyApplied,
|
|
209
|
+
wanted: wantedFunctions,
|
|
210
|
+
deployed: outcome.deployed,
|
|
211
|
+
keptOrphans: remaining,
|
|
212
|
+
}), appliedParameterState({
|
|
213
|
+
recorded: recordedDigests,
|
|
214
|
+
current: currentDigests,
|
|
215
|
+
changed,
|
|
216
|
+
failed: outcome.failed,
|
|
217
|
+
claims: valid,
|
|
218
|
+
}), settled.declared);
|
|
219
|
+
reportOrphanIndexes(projectId, settled.orphans, options.prune);
|
|
220
|
+
return outcome;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* O que o projeto tem de declarar de índice composto, e o que sobrou de uma extensão que saiu.
|
|
224
|
+
*
|
|
225
|
+
* A memória vem da RAIZ (`backend.indexes` do `microteia.yaml`), não do arquivo instalado: é o `unpack`
|
|
226
|
+
* que apaga `.microteia/install/` inteiro, e o pacote traz apenas a baseline. Enquanto o único registro
|
|
227
|
+
* era o arquivo de lá, trocar de kernel apagava a memória — e o `firebase deploy` seguinte oferecia
|
|
228
|
+
* apagar do PROJETO o índice de toda extensão instalada, o que é uma consulta em produção por resposta
|
|
229
|
+
* errada a uma pergunta feita no meio de um upgrade.
|
|
230
|
+
*
|
|
231
|
+
* O arquivo instalado entra na leitura só para ADOTAR o que uma execução anterior a este campo havia
|
|
232
|
+
* declarado. Passada essa vez, ele é derivado daqui.
|
|
233
|
+
*/
|
|
234
|
+
const settleIndexes = async (options, valid) => {
|
|
235
|
+
const baseline = options.manifest.baselineIndexes ?? [];
|
|
236
|
+
const baselineKeys = new Set(baseline.map(indexKey));
|
|
237
|
+
const file = await readIndexFile(options.install);
|
|
238
|
+
const remembered = new Map();
|
|
239
|
+
for (const index of parseIndexState(options.environment.backend.indexes)) {
|
|
240
|
+
remembered.set(indexKey(index), index);
|
|
241
|
+
}
|
|
242
|
+
for (const index of file.indexes) {
|
|
243
|
+
if (!baselineKeys.has(indexKey(index)))
|
|
244
|
+
remembered.set(indexKey(index), index);
|
|
245
|
+
}
|
|
246
|
+
const wanted = claimedIndexes(valid);
|
|
247
|
+
const wantedKeys = new Set(wanted.map(indexKey));
|
|
248
|
+
const orphans = [...remembered.values()].filter((index) => !wantedKeys.has(indexKey(index)));
|
|
249
|
+
/**
|
|
250
|
+
* O órfão CONTINUA declarado, a não ser que se peça `--prune`. Deixá-lo cair fora do arquivo é o que
|
|
251
|
+
* fazia o `firebase` oferecer a exclusão dele; mantê-lo declarado custa nada (o índice já existe) e
|
|
252
|
+
* torna a remoção um ato explícito. Com `--prune` ele sai do arquivo e da memória — e é a próxima
|
|
253
|
+
* publicação de índices que oferece apagá-lo do projeto, com a pergunta feita na hora certa.
|
|
254
|
+
*/
|
|
255
|
+
const declared = options.prune ? wanted : [...wanted, ...orphans];
|
|
256
|
+
const convergence = convergeIndexes(file.indexes, baseline, declared);
|
|
257
|
+
if (!convergence.changed) {
|
|
258
|
+
if (wanted.length > 0)
|
|
259
|
+
success('the composite indexes these extensions need are already declared');
|
|
260
|
+
return { declared, orphans };
|
|
261
|
+
}
|
|
262
|
+
await writeIndexFile(options.install, { ...file, indexes: convergence.desired });
|
|
263
|
+
if (convergence.missing.length === 0)
|
|
264
|
+
return { declared, orphans };
|
|
265
|
+
info(` creating ${convergence.missing.length} composite index(es)…`);
|
|
266
|
+
await run(FIREBASE_BIN, ['deploy', '--only', 'firestore:indexes', '--project', options.projectId], { cwd: options.install });
|
|
267
|
+
return { declared, orphans };
|
|
268
|
+
};
|
|
269
|
+
const checkNewCallables = async (known, deployed, projectId, region) => {
|
|
270
|
+
const callables = known.filter((name) => deployed.includes(name));
|
|
271
|
+
if (callables.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
const origin = `https://${projectId}.web.app`;
|
|
274
|
+
const checks = await checkCallables(callables, { projectId, region, origin });
|
|
275
|
+
const blocked = checks.filter((check) => check.access === 'blocked');
|
|
276
|
+
if (blocked.length === 0) {
|
|
277
|
+
success(`all ${checks.length} new callable(s) answer calls from ${origin}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
failure(`${blocked.length} of ${checks.length} new callable(s) refuse calls from ${origin}.`);
|
|
281
|
+
info(' The application will open and these calls will fail in the browser as what looks like a');
|
|
282
|
+
info(' CORS error. Run once per function:');
|
|
283
|
+
bullets(blocked.map((check) => invokerGrantCommand(region, projectId, check.name)));
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* O carimbo é gravado em TODAS as cópias daquela extensão — o artefato é do projeto, então se ele
|
|
287
|
+
* existe, existe para todo escritório que o instalou, e não só para aquele cuja cópia foi lida por
|
|
288
|
+
* último.
|
|
289
|
+
*/
|
|
290
|
+
const stampApplied = async (projectId, all, claim) => {
|
|
291
|
+
const fingerprint = backendFingerprint(claim.backend);
|
|
292
|
+
const appliedAt = new Date();
|
|
293
|
+
for (const copy of all.filter((entry) => entry.pluginId === claim.pluginId)) {
|
|
294
|
+
if (backendFingerprint(copy.backend) !== fingerprint)
|
|
295
|
+
continue;
|
|
296
|
+
await patchMapField(projectId, copy.documentPath, 'backendState', {
|
|
297
|
+
fingerprint: asString(fingerprint),
|
|
298
|
+
appliedAt: asTimestamp(appliedAt),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
success(`${claim.name} is ready in ${projectId}`);
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Function que nenhuma extensão instalada exige mais. Apagar é destrutivo e irreversível, então o
|
|
305
|
+
* padrão RELATA e só `--prune` remove — e o que foi removido sai da lista de aplicadas.
|
|
306
|
+
*/
|
|
307
|
+
const settleOrphans = async (options, orphans) => {
|
|
308
|
+
if (orphans.length === 0)
|
|
309
|
+
return [];
|
|
310
|
+
blank();
|
|
311
|
+
if (!options.prune) {
|
|
312
|
+
warn(`${orphans.length} function(s) are deployed but no installed extension needs them:`);
|
|
313
|
+
bullets(orphans);
|
|
314
|
+
info(' they keep working and cost nothing while idle. To remove them:');
|
|
315
|
+
bullets(['microteia provision --prune']);
|
|
316
|
+
blank();
|
|
317
|
+
return orphans;
|
|
318
|
+
}
|
|
319
|
+
info(` deleting ${orphans.length} orphan function(s)…`);
|
|
320
|
+
const deleted = [];
|
|
321
|
+
for (const name of orphans) {
|
|
322
|
+
try {
|
|
323
|
+
await run(FIREBASE_BIN, ['functions:delete', name, '--project', options.projectId, '--region', options.region, '--force'], { cwd: options.install });
|
|
324
|
+
deleted.push(name);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
warn(`could not delete ${name}; it stays recorded as deployed`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
success(`${deleted.length} orphan function(s) removed`);
|
|
331
|
+
return orphans.filter((name) => !deleted.includes(name));
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Índice órfão é RELATADO, nunca apagado às cegas: o `firebase deploy` não remove índice por conta, e a
|
|
335
|
+
* API que remove pede o id GERADO do índice — apagar o errado custa uma consulta em produção, e listá-lo
|
|
336
|
+
* custa três linhas de saída.
|
|
337
|
+
*
|
|
338
|
+
* O que mudou é que ele segue DECLARADO enquanto ninguém pedir o contrário. Antes ele caía fora do
|
|
339
|
+
* arquivo, e a exclusão acabava sendo oferecida pelo `firebase` na publicação seguinte — uma pergunta
|
|
340
|
+
* sobre a coleção de uma extensão, feita no meio de um upgrade, a quem está lendo saída de deploy.
|
|
341
|
+
*/
|
|
342
|
+
const reportOrphanIndexes = (projectId, orphans, prune) => {
|
|
343
|
+
if (orphans.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
blank();
|
|
346
|
+
warn(`${orphans.length} composite index(es) belong to extensions that are no longer installed:`);
|
|
347
|
+
bullets(orphans.map(describeIndex));
|
|
348
|
+
if (!prune) {
|
|
349
|
+
info(' they stay declared, so no deploy offers to delete them, and they cost only writes on the');
|
|
350
|
+
info(' collections they cover. To let them go:');
|
|
351
|
+
bullets(['microteia provision --prune']);
|
|
352
|
+
blank();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
warn(' --prune removed them from this installation, and nothing deleted them from the project:');
|
|
356
|
+
info(' the delete API needs each index\'s generated id, and guessing wrong costs a query in');
|
|
357
|
+
info(' production. The next index publish offers to delete them, and you answer it deliberately:');
|
|
358
|
+
bullets([
|
|
359
|
+
'microteia deploy --only indexes',
|
|
360
|
+
`gcloud firestore indexes composite list --project=${projectId}`,
|
|
361
|
+
]);
|
|
362
|
+
blank();
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* A lista aplicada CONVERGE para o que ainda é exigido: o que já estava e continua sendo pedido, mais
|
|
366
|
+
* o que subiu agora, mais o órfão que não foi removido. Reescrevê-la só com o que subiu NESTA execução
|
|
367
|
+
* a esvaziava na segunda passada — e a terceira redeployava tudo, achando que nada havia sido feito.
|
|
368
|
+
*/
|
|
369
|
+
export const appliedAfterRun = (input) => [...new Set([
|
|
370
|
+
...input.alreadyApplied.filter((name) => input.wanted.includes(name)),
|
|
371
|
+
...input.deployed,
|
|
372
|
+
...input.keptOrphans,
|
|
373
|
+
])].sort();
|
|
374
|
+
/**
|
|
375
|
+
* A impressão digital do parâmetro só avança quando TODAS as functions que o carregam subiram. Gravar
|
|
376
|
+
* antes disso é o que transformaria uma falha de deploy em silêncio permanente: o valor novo ficaria
|
|
377
|
+
* registrado como aplicado, e nenhuma execução seguinte tentaria de novo.
|
|
378
|
+
*
|
|
379
|
+
* O que não é mais reivindicado por extensão nenhuma fica registrado em vez de ser removido: apagar
|
|
380
|
+
* faria uma extensão reinstalada redeployar functions que já carregam o valor certo, e uma linha a mais
|
|
381
|
+
* no arquivo não decide nada.
|
|
382
|
+
*/
|
|
383
|
+
export const appliedParameterState = (input) => {
|
|
384
|
+
const failed = new Set(input.failed);
|
|
385
|
+
const next = new Map(input.recorded);
|
|
386
|
+
for (const name of input.changed) {
|
|
387
|
+
const carriers = functionsDeclaringParameters(input.claims, [name]);
|
|
388
|
+
if (carriers.some((functionName) => failed.has(functionName)))
|
|
389
|
+
continue;
|
|
390
|
+
const digest = input.current.get(name);
|
|
391
|
+
if (digest === undefined)
|
|
392
|
+
next.delete(name);
|
|
393
|
+
else
|
|
394
|
+
next.set(name, digest);
|
|
395
|
+
}
|
|
396
|
+
return next;
|
|
397
|
+
};
|
|
398
|
+
const recordBackendState = async (options, names, parameters, indexes) => {
|
|
399
|
+
const state = serializeIndexState(indexes);
|
|
400
|
+
/**
|
|
401
|
+
* Índice que a codificação não representa sem ambiguidade fica de fora COM AVISO, em vez de voltar do
|
|
402
|
+
* arquivo virado noutro índice. Só acontece com nome de coleção ou de campo que contenha um dos
|
|
403
|
+
* separadores, o que exigiria acento grave no caminho do campo — e "só aconteceria" é como um defeito
|
|
404
|
+
* silencioso começa.
|
|
405
|
+
*/
|
|
406
|
+
if (state.skipped.length > 0) {
|
|
407
|
+
warn(`${state.skipped.length} composite index(es) cannot be remembered across a kernel upgrade:`);
|
|
408
|
+
bullets(state.skipped.map(describeIndex));
|
|
409
|
+
info(' a collection or field name uses one of the separators. Rename it, or a future');
|
|
410
|
+
info(' "microteia deploy" will offer to delete the index.');
|
|
411
|
+
}
|
|
412
|
+
await writeEnvironment(options.environmentFile, {
|
|
413
|
+
...options.environment,
|
|
414
|
+
backend: {
|
|
415
|
+
functions: names.length === 0 ? null : names.join(','),
|
|
416
|
+
parameters: serializeParameterState(parameters),
|
|
417
|
+
indexes: state.value,
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
/** O comando: provisiona sem reenviar o kernel. É o que se roda depois de instalar uma extensão. */
|
|
422
|
+
export const provision = async (args) => {
|
|
423
|
+
const file = resolve(hasFlag(args, 'file') ? requiredFlag(args, 'file') : join(process.cwd(), ENVIRONMENT_FILE));
|
|
424
|
+
const root = dirname(file);
|
|
425
|
+
const delaySeconds = Number.parseInt(flagValue(args, 'delay') ?? '1', 10);
|
|
426
|
+
const environment = await readEnvironment(file);
|
|
427
|
+
const projectId = environment.firebase.projectId;
|
|
428
|
+
if (projectId === null) {
|
|
429
|
+
throw new Error('This installation is not connected yet. Run "microteia connect --project <id>".');
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Quem recusa não haver pacote no lugar é o `readInstalledManifest` abaixo, e com o comando que falta.
|
|
433
|
+
* A pergunta aqui é outra: houve deploy? Provisionar sobe a function de uma EXTENSÃO, e fazer isso
|
|
434
|
+
* contra um projeto que nunca recebeu o kernel entrega uma extensão pendurada num backend que não
|
|
435
|
+
* existe.
|
|
436
|
+
*
|
|
437
|
+
* É aviso e não recusa porque uma instalação anterior ao campo `deployed` não tem como provar que foi
|
|
438
|
+
* publicada — e recusar por falta de prova impediria de provisionar uma instalação que está no ar.
|
|
439
|
+
*/
|
|
440
|
+
if (environment.kernel.deployed === null) {
|
|
441
|
+
warn('there is no record of a kernel deploy in this installation.');
|
|
442
|
+
info(' if this project has never had one, run "microteia deploy" first: provisioning gives the');
|
|
443
|
+
info(' installed extensions their backend, not the platform itself.');
|
|
444
|
+
}
|
|
445
|
+
// Pela mesma porta que o `deploy` usa: ela recusa com o comando que falta, e confere a versão mínima
|
|
446
|
+
// do Console — montar o caminho à mão aqui devolveria um ENOENT cru.
|
|
447
|
+
const install = installPath(root);
|
|
448
|
+
const manifest = await readInstalledManifest(root);
|
|
449
|
+
const region = manifest.requires.functionsRegion;
|
|
450
|
+
heading(`Provisioning extension backends in ${projectId}`);
|
|
451
|
+
step(1, 1, 'Reading what the offices installed');
|
|
452
|
+
const outcome = await provisionBackend({
|
|
453
|
+
projectId,
|
|
454
|
+
region,
|
|
455
|
+
install,
|
|
456
|
+
manifest,
|
|
457
|
+
delaySeconds,
|
|
458
|
+
prune: hasFlag(args, 'prune'),
|
|
459
|
+
environmentFile: file,
|
|
460
|
+
environment,
|
|
461
|
+
});
|
|
462
|
+
blank();
|
|
463
|
+
if (outcome.failed.length > 0) {
|
|
464
|
+
failure(`${outcome.failed.length} function(s) did not deploy: ${outcome.failed.join(', ')}`);
|
|
465
|
+
info('Re-run to retry:');
|
|
466
|
+
bullets(['microteia provision']);
|
|
467
|
+
blank();
|
|
468
|
+
throw new Error('Some extension functions did not deploy.');
|
|
469
|
+
}
|
|
470
|
+
if (outcome.pending.length > 0) {
|
|
471
|
+
warn(`${outcome.pending.length} extension(s) are still pending: `
|
|
472
|
+
+ outcome.pending.map((claim) => claim.name).join(', '));
|
|
473
|
+
blank();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
info('Every installed extension has what it needs. Reload the application to use them.');
|
|
477
|
+
blank();
|
|
478
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface DeployedFunction {
|
|
2
|
+
name: string;
|
|
3
|
+
region: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A cópia da mesma function numa região que o pacote não usa mais.
|
|
7
|
+
*
|
|
8
|
+
* Mudar a região de uma Cloud Function não move nada: o Firebase cria a nova e a antiga fica. A
|
|
9
|
+
* partir daí o `firebase deploy --only functions:<nome>` PARA — o filtro `--only` casa por id e
|
|
10
|
+
* ignora região (`endpointMatchesFilter`), então a cópia velha entra no plano como algo a apagar, e
|
|
11
|
+
* apagar exige confirmação que este deploy não tem como dar. A recusa é a mesma em toda tentativa.
|
|
12
|
+
*
|
|
13
|
+
* E o deploy travado é o menor dos dois problemas. Enquanto as duas cópias existem, um gatilho de
|
|
14
|
+
* Firestore dispara NAS DUAS a cada gravação: linha de auditoria em dobro, agregado contado duas
|
|
15
|
+
* vezes, evento de workflow publicado duas vezes. Isso não aparece em lugar nenhum como erro.
|
|
16
|
+
*/
|
|
17
|
+
export interface RegionOrphan {
|
|
18
|
+
name: string;
|
|
19
|
+
region: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Devolve `null` quando não deu para perguntar — conta sem permissão de listar, projeto fora do ar.
|
|
23
|
+
* Não saber não é motivo para impedir um deploy que talvez funcionasse: a verificação existe para
|
|
24
|
+
* explicar uma falha, não para criar uma.
|
|
25
|
+
*/
|
|
26
|
+
export declare const listDeployedFunctions: (projectId: string) => Promise<DeployedFunction[] | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Só o que ESTE deploy vai tocar. Casar por "região diferente da esperada" acusaria também uma
|
|
29
|
+
* function que o escritório escreveu de propósito em outra região; casar pelos nomes que estão para
|
|
30
|
+
* subir é exatamente o conjunto que o `firebase` vai querer apagar, nem um a mais.
|
|
31
|
+
*/
|
|
32
|
+
export declare const regionOrphans: (deployed: DeployedFunction[], wanted: string[], region: string) => RegionOrphan[];
|
|
33
|
+
export declare const deleteCommand: (projectId: string, orphan: RegionOrphan) => string;
|
|
34
|
+
/**
|
|
35
|
+
* Avisa por padrão e só apaga com `--prune`, como `settleOrphans` faz com a function que perdeu dono.
|
|
36
|
+
* Apagar uma Cloud Function é irreversível e é do escritório, não nosso: o comando tem de ser pedido.
|
|
37
|
+
*/
|
|
38
|
+
export declare const settleRegionOrphans: (projectId: string, orphans: RegionOrphan[], prune: boolean) => Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve `true` quando o caminho está
|
|
41
|
+
* livre — inclusive quando não deu para verificar, pela mesma razão de `listDeployedFunctions`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const clearRegionOrphans: (projectId: string, wanted: string[], region: string, prune: boolean) => Promise<boolean>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { capture } from '../shared/run.js';
|
|
2
|
+
import { blank, bullets, failure, info, pending, success, warn } from '../shared/ui.js';
|
|
3
|
+
import { FIREBASE_BIN, parseJson } from './firebaseCli.js';
|
|
4
|
+
import { failureReason } from './deployFunctions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Devolve `null` quando não deu para perguntar — conta sem permissão de listar, projeto fora do ar.
|
|
7
|
+
* Não saber não é motivo para impedir um deploy que talvez funcionasse: a verificação existe para
|
|
8
|
+
* explicar uma falha, não para criar uma.
|
|
9
|
+
*/
|
|
10
|
+
export const listDeployedFunctions = async (projectId) => {
|
|
11
|
+
const result = await capture(FIREBASE_BIN, ['functions:list', '--project', projectId, '--json']);
|
|
12
|
+
if (result.code !== 0)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = parseJson(result.stdout);
|
|
16
|
+
if (!Array.isArray(parsed.result))
|
|
17
|
+
return null;
|
|
18
|
+
return parsed.result
|
|
19
|
+
.filter((entry) => entry !== null && typeof entry === 'object')
|
|
20
|
+
.map((entry) => ({ name: String(entry.id ?? ''), region: String(entry.region ?? '') }))
|
|
21
|
+
.filter((entry) => entry.name !== '' && entry.region !== '');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Só o que ESTE deploy vai tocar. Casar por "região diferente da esperada" acusaria também uma
|
|
29
|
+
* function que o escritório escreveu de propósito em outra região; casar pelos nomes que estão para
|
|
30
|
+
* subir é exatamente o conjunto que o `firebase` vai querer apagar, nem um a mais.
|
|
31
|
+
*/
|
|
32
|
+
export const regionOrphans = (deployed, wanted, region) => {
|
|
33
|
+
const names = new Set(wanted);
|
|
34
|
+
return deployed
|
|
35
|
+
.filter((entry) => names.has(entry.name) && entry.region !== region)
|
|
36
|
+
.map((entry) => ({ name: entry.name, region: entry.region }))
|
|
37
|
+
.sort((left, right) => (left.name === right.name
|
|
38
|
+
? left.region.localeCompare(right.region)
|
|
39
|
+
: left.name.localeCompare(right.name)));
|
|
40
|
+
};
|
|
41
|
+
export const deleteCommand = (projectId, orphan) => `firebase functions:delete ${orphan.name} --region ${orphan.region} --project ${projectId} --force`;
|
|
42
|
+
/**
|
|
43
|
+
* Avisa por padrão e só apaga com `--prune`, como `settleOrphans` faz com a function que perdeu dono.
|
|
44
|
+
* Apagar uma Cloud Function é irreversível e é do escritório, não nosso: o comando tem de ser pedido.
|
|
45
|
+
*/
|
|
46
|
+
export const settleRegionOrphans = async (projectId, orphans, prune) => {
|
|
47
|
+
if (orphans.length === 0)
|
|
48
|
+
return true;
|
|
49
|
+
blank();
|
|
50
|
+
warn(`${orphans.length} function(s) are still deployed in a region this package no longer uses:`);
|
|
51
|
+
bullets(orphans.map((orphan) => `${orphan.name} (${orphan.region})`));
|
|
52
|
+
info(' Changing the region of a function does not move it: Firebase created the new one and left');
|
|
53
|
+
info(' the old one behind. The deploy of each of these will refuse to run — it would have to');
|
|
54
|
+
info(' delete the old copy first, and it has no terminal to ask. Every attempt answers the same.');
|
|
55
|
+
info(' While both copies exist, a Firestore trigger fires TWICE on every write: audit rows,');
|
|
56
|
+
info(' aggregates and workflow events are all counted twice, and nothing reports that as an error.');
|
|
57
|
+
if (!prune) {
|
|
58
|
+
info(' To remove them:');
|
|
59
|
+
bullets(['microteia deploy --prune']);
|
|
60
|
+
info(' or one at a time:');
|
|
61
|
+
bullets(orphans.map((orphan) => deleteCommand(projectId, orphan)));
|
|
62
|
+
blank();
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const plural = orphans.length === 1 ? 'copy' : 'copies';
|
|
66
|
+
info(` --prune: removing the ${orphans.length} old ${plural} before deploying.`);
|
|
67
|
+
let cleared = true;
|
|
68
|
+
for (const orphan of orphans) {
|
|
69
|
+
const label = `${orphan.name} (${orphan.region})`;
|
|
70
|
+
pending(label);
|
|
71
|
+
const result = await capture(FIREBASE_BIN, [
|
|
72
|
+
'functions:delete', orphan.name,
|
|
73
|
+
'--region', orphan.region,
|
|
74
|
+
'--project', projectId,
|
|
75
|
+
'--force',
|
|
76
|
+
]);
|
|
77
|
+
if (result.code === 0) {
|
|
78
|
+
success(`${label} removed`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
failure(`${label} could not be removed`);
|
|
82
|
+
info(` ${failureReason(`${result.stdout}${result.stderr}`)}`);
|
|
83
|
+
info(` ${deleteCommand(projectId, orphan)}`);
|
|
84
|
+
cleared = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
blank();
|
|
88
|
+
return cleared;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve `true` quando o caminho está
|
|
92
|
+
* livre — inclusive quando não deu para verificar, pela mesma razão de `listDeployedFunctions`.
|
|
93
|
+
*/
|
|
94
|
+
export const clearRegionOrphans = async (projectId, wanted, region, prune) => {
|
|
95
|
+
const deployed = await listDeployedFunctions(projectId);
|
|
96
|
+
if (deployed === null)
|
|
97
|
+
return true;
|
|
98
|
+
return settleRegionOrphans(projectId, regionOrphans(deployed, wanted, region), prune);
|
|
99
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A configuração que o kernel espera encontrar em `window.__MICROTEIA_CONFIG__`.
|
|
3
|
+
*
|
|
4
|
+
* É o CONTRATO entre o Console e o kernel, e está declarado aqui de novo de propósito: o Console
|
|
5
|
+
* precisa poder ser publicado sem o `src/` do kernel junto, então não pode importar de lá. Para que
|
|
6
|
+
* as duas pontas não se separem em silêncio, `runtimeConfigShape.test.ts` confere esta lista contra
|
|
7
|
+
* `REQUIRED_CONFIG_KEYS` de `src/infrastructure/firebase/runtimeConfig.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface MicroteiaRuntimeConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
authDomain: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
storageBucket: string;
|
|
14
|
+
messagingSenderId: string;
|
|
15
|
+
appId: string;
|
|
16
|
+
measurementId?: string;
|
|
17
|
+
functionsRegion?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const REQUIRED_CONFIG_KEYS: readonly ["apiKey", "authDomain", "projectId", "storageBucket", "messagingSenderId", "appId"];
|
|
20
|
+
export declare const OPTIONAL_CONFIG_KEYS: readonly ["measurementId"];
|
|
21
|
+
/**
|
|
22
|
+
* Monta a configuração a partir do que o `firebase apps:sdkconfig` devolveu, recusando-se a seguir
|
|
23
|
+
* com campo faltando: uma instalação carimbada pela metade só falha quando alguém abre a aplicação.
|
|
24
|
+
*/
|
|
25
|
+
export declare const runtimeConfigFrom: (sdkConfig: Record<string, string>, functionsRegion: string) => MicroteiaRuntimeConfig;
|