@microteia/console 2.38.0 → 2.42.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 +54 -0
- package/LICENSE +661 -0
- package/NOTICE +97 -0
- package/README.md +8 -1
- package/lib/console/backendApplied.d.ts +58 -0
- package/lib/console/backendApplied.js +81 -0
- package/lib/console/exportCatalog.d.ts +39 -210
- package/lib/console/exportCatalog.js +55 -310
- package/lib/console/firestoreRest.d.ts +2 -0
- package/lib/console/firestoreRest.js +13 -2
- package/lib/console/installedExtensions.d.ts +2 -0
- package/lib/console/installedExtensions.js +1 -0
- package/lib/console/provision.js +106 -21
- package/lib/index.js +1 -1
- package/lib/kernel/manifest.d.ts +17 -0
- package/lib/kernel/manifest.js +0 -14
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/lib/vendor/installationDocs.js +1 -1
- package/lib/vendor/pack.js +35 -1
- package/lib/vendor/packConsole.js +5 -0
- package/lib/vendor/packageTemplates.js +10 -2
- package/lib/vendor/publish.js +83 -0
- package/package.json +9 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
5
|
import { flagValue } from '../shared/args.js';
|
|
5
6
|
import { CLI_VERSION } from '../shared/paths.js';
|
|
@@ -21,7 +22,11 @@ import { WORKSPACE_DIR } from './workspace.js';
|
|
|
21
22
|
* A saída NÃO foi ligar o SDK ao Firestore. `microteia-plugin` é offline por desenho, e arrastar
|
|
22
23
|
* credencial para dentro da ferramenta de autoria trocaria uma cegueira por um problema pior. Quem já
|
|
23
24
|
* fala com o projeto do escritório é o Console — o token vem do `gcloud`, a cada execução, e nada é
|
|
24
|
-
* guardado.
|
|
25
|
+
* guardado. O Console LÊ; quem monta o arquivo é o escritor do SDK instalado (ver `loadCatalogWriter`).
|
|
26
|
+
*
|
|
27
|
+
* O token do `gcloud` é credencial de PROJETO: não passa pelas Security Rules, e por isso exige papel
|
|
28
|
+
* no Firebase inteiro. Quem administra só o escritório tira o mesmo retrato pela aplicação (Extensões →
|
|
29
|
+
* Exportar catálogo), lendo como membro, sob as regras.
|
|
25
30
|
*
|
|
26
31
|
* Três decisões que não são preferência:
|
|
27
32
|
*
|
|
@@ -34,313 +39,50 @@ import { WORKSPACE_DIR } from './workspace.js';
|
|
|
34
39
|
* adivinhação escreveria o vocabulário de um dentro do trabalho de outro. Sem a opção, o comando
|
|
35
40
|
* LISTA os que existem.
|
|
36
41
|
*/
|
|
37
|
-
/** O nome é contrato com o SDK, que o procura por este nome — ver `installedCatalog.ts` do kernel. */
|
|
38
|
-
export const CATALOG_FILE = 'installed-catalog.json';
|
|
39
42
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
43
|
+
* O nome do arquivo que o SDK procura. O valor de verdade vem do SDK carregado; este é só o que se diz
|
|
44
|
+
* ao autor ANTES de o SDK ter sido encontrado.
|
|
42
45
|
*/
|
|
43
|
-
|
|
44
|
-
const text = (value) => (typeof value === 'string' && value.trim() !== '' ? value : undefined);
|
|
45
|
-
const asRecord = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
|
-
? value
|
|
47
|
-
: undefined);
|
|
48
|
-
const FIELD_ORIGINS = ['builtin', 'plugin', 'design'];
|
|
46
|
+
const CATALOG_FILE_HINT = 'installed-catalog.json';
|
|
49
47
|
/**
|
|
50
|
-
*
|
|
51
|
-
* artefato do pacote que o atende, para ganhar tipo e nome.
|
|
48
|
+
* O escritor do retrato, carregado do SDK que o escritório tem em `extensions/`.
|
|
52
49
|
*
|
|
53
|
-
* O Console
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!Array.isArray(declared))
|
|
61
|
-
return [];
|
|
62
|
-
const pkg = asRecord(extension.data.package) ?? {};
|
|
63
|
-
const named = new Map();
|
|
64
|
-
const collect = (raw, kind) => {
|
|
65
|
-
for (const one of Array.isArray(raw) ? raw : []) {
|
|
66
|
-
const record = asRecord(one);
|
|
67
|
-
const id = text(record?.templateId);
|
|
68
|
-
if (id === undefined || named.has(id))
|
|
69
|
-
continue;
|
|
70
|
-
named.set(id, { id, kind, name: text(record?.name) ?? text(record?.title) ?? id });
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
collect(pkg.workflows, 'workflow');
|
|
74
|
-
collect(pkg.actions, 'action');
|
|
75
|
-
collect(pkg.scripts, 'script');
|
|
76
|
-
return declared
|
|
77
|
-
.filter((id) => typeof id === 'string' && id !== '')
|
|
78
|
-
.map((id) => named.get(id) ?? { id, kind: 'unknown', name: id })
|
|
79
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
80
|
-
};
|
|
81
|
-
const NAV_GROUP_POINT = 'ui.nav.group';
|
|
82
|
-
const MESSAGES_POINT = 'i18n.messages';
|
|
83
|
-
const DEFAULT_LOCALE = 'en';
|
|
84
|
-
const MESSAGE_REFERENCE = /^@([a-z0-9.-]+)\/(.+)$/;
|
|
85
|
-
/**
|
|
86
|
-
* Os grupos da barra lateral de uma cópia instalada, com o nome resolvido pelos `i18n.messages` do
|
|
87
|
-
* próprio manifesto. É a mesma leitura que `declaredNavGroupsOf` faz no kernel, repetida porque o
|
|
88
|
-
* Console não alcança `src/` — e é o teste que atravessa a fronteira que confere as duas.
|
|
89
|
-
*/
|
|
90
|
-
const navGroupsOf = (extension) => {
|
|
91
|
-
const contributes = Array.isArray(extension.manifest?.contributes) ? extension.manifest.contributes : [];
|
|
92
|
-
const contributions = contributes.map(asRecord).filter((one) => one !== undefined);
|
|
93
|
-
const bundles = contributions
|
|
94
|
-
.filter((one) => one.point === MESSAGES_POINT)
|
|
95
|
-
.map((one) => asRecord(one.metadata))
|
|
96
|
-
.filter((one) => one !== undefined);
|
|
97
|
-
const translationsOf = (label) => {
|
|
98
|
-
const match = MESSAGE_REFERENCE.exec(label);
|
|
99
|
-
if (!match)
|
|
100
|
-
return {};
|
|
101
|
-
const found = {};
|
|
102
|
-
for (const bundle of bundles) {
|
|
103
|
-
const locale = text(bundle.locale);
|
|
104
|
-
const message = asRecord(bundle.messages)?.[match[2]];
|
|
105
|
-
if (bundle.namespace !== match[1] || locale === undefined || typeof message !== 'string' || message.trim() === '')
|
|
106
|
-
continue;
|
|
107
|
-
if (found[locale] === undefined)
|
|
108
|
-
found[locale] = message;
|
|
109
|
-
}
|
|
110
|
-
return found;
|
|
111
|
-
};
|
|
112
|
-
return contributions
|
|
113
|
-
.filter((one) => one.point === NAV_GROUP_POINT)
|
|
114
|
-
.flatMap((one) => {
|
|
115
|
-
const metadata = asRecord(one.metadata) ?? {};
|
|
116
|
-
const key = text(metadata.key)?.trim();
|
|
117
|
-
if (key === undefined)
|
|
118
|
-
return [];
|
|
119
|
-
const raw = text(metadata.label) ?? key;
|
|
120
|
-
const translations = translationsOf(raw);
|
|
121
|
-
return [{
|
|
122
|
-
key,
|
|
123
|
-
label: translations[DEFAULT_LOCALE] ?? Object.values(translations)[0] ?? raw,
|
|
124
|
-
translations,
|
|
125
|
-
order: typeof metadata.order === 'number' ? metadata.order : 0,
|
|
126
|
-
}];
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
const DOCUMENT_TEMPLATE_POINT = 'document.template';
|
|
130
|
-
const documentParameterOf = (raw) => {
|
|
131
|
-
const record = asRecord(raw);
|
|
132
|
-
const key = text(record?.key);
|
|
133
|
-
const type = text(record?.type);
|
|
134
|
-
if (record === undefined || key === undefined || type === undefined)
|
|
135
|
-
return [];
|
|
136
|
-
const entityType = text(record.entityType);
|
|
137
|
-
return [{
|
|
138
|
-
key,
|
|
139
|
-
type,
|
|
140
|
-
...(record.required === true ? { required: true } : {}),
|
|
141
|
-
...(record.list === true ? { list: true } : {}),
|
|
142
|
-
...(entityType === undefined ? {} : { entityType }),
|
|
143
|
-
}];
|
|
144
|
-
};
|
|
145
|
-
/**
|
|
146
|
-
* Os modelos de documento de uma cópia instalada, como contrato. É a mesma leitura que
|
|
147
|
-
* `documentTemplateContractOf` faz no kernel, repetida porque o Console não alcança `src/` — e é o
|
|
148
|
-
* teste que atravessa a fronteira que confere as duas.
|
|
149
|
-
*/
|
|
150
|
-
const documentTemplatesOf = (extension) => {
|
|
151
|
-
const contributes = Array.isArray(extension.manifest?.contributes) ? extension.manifest.contributes : [];
|
|
152
|
-
return contributes
|
|
153
|
-
.map(asRecord)
|
|
154
|
-
.filter((one) => one?.point === DOCUMENT_TEMPLATE_POINT)
|
|
155
|
-
.flatMap((one) => {
|
|
156
|
-
const metadata = asRecord(one.metadata);
|
|
157
|
-
const key = text(metadata?.key);
|
|
158
|
-
if (metadata === undefined || key === undefined)
|
|
159
|
-
return [];
|
|
160
|
-
return [{
|
|
161
|
-
key,
|
|
162
|
-
source: metadata.source === 'payload' ? 'payload' : 'record',
|
|
163
|
-
parameters: (Array.isArray(metadata.parameters) ? metadata.parameters : []).flatMap(documentParameterOf),
|
|
164
|
-
}];
|
|
165
|
-
})
|
|
166
|
-
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
167
|
-
};
|
|
168
|
-
const FIELD_PRESET_POINT = 'data.field.preset';
|
|
169
|
-
/**
|
|
170
|
-
* Os presets de campo de uma cópia instalada. É a mesma leitura que `declaredFieldPresets` faz no
|
|
171
|
-
* kernel — chave e rótulo obrigatórios, e as partes pelo critério do editor de campo —, repetida porque
|
|
172
|
-
* o Console não alcança `src/`; o teste que atravessa a fronteira confere as duas.
|
|
173
|
-
*/
|
|
174
|
-
const fieldPresetsOf = (extension) => {
|
|
175
|
-
const contributes = Array.isArray(extension.manifest?.contributes) ? extension.manifest.contributes : [];
|
|
176
|
-
return contributes
|
|
177
|
-
.map(asRecord)
|
|
178
|
-
.filter((one) => one?.point === FIELD_PRESET_POINT)
|
|
179
|
-
.flatMap((one) => {
|
|
180
|
-
const metadata = asRecord(one.metadata);
|
|
181
|
-
const key = text(metadata?.key);
|
|
182
|
-
if (metadata === undefined || key === undefined || text(metadata.label) === undefined)
|
|
183
|
-
return [];
|
|
184
|
-
return [{
|
|
185
|
-
key,
|
|
186
|
-
slots: [
|
|
187
|
-
...(metadata.mask ? ['mask'] : []),
|
|
188
|
-
...(Array.isArray(metadata.format) && metadata.format.length > 0 ? ['format'] : []),
|
|
189
|
-
...(metadata.validate ? ['validate'] : []),
|
|
190
|
-
],
|
|
191
|
-
}];
|
|
192
|
-
})
|
|
193
|
-
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
194
|
-
};
|
|
195
|
-
/**
|
|
196
|
-
* A mesma dedução que a aplicação faz ao ler a tela (`screenDefinitionMapper.ts`): o `origin` gravado
|
|
197
|
-
* quando ele existe, senão `plugin` para quem tem dono e `design` para o que o escritório criou. Ela é
|
|
198
|
-
* repetida aqui porque o Console não alcança `src/` — e é conferida contra a instalação real por teste.
|
|
50
|
+
* O Console trazia uma CÓPIA do escritor, e o leitor morava no SDK: os dois lados eram compilados
|
|
51
|
+
* separadamente e casados só por teste, e o Console é distribuído à parte do kernel. Carregar o
|
|
52
|
+
* escritor de onde mora o leitor faz o formato ter um dono só — e é a mesma função que a aplicação usa
|
|
53
|
+
* no botão Exportar catálogo da tela de Extensões.
|
|
54
|
+
*
|
|
55
|
+
* O SDK de um kernel anterior a esse escritor não o tem, e aí o comando RECUSA dizendo como trazê-lo,
|
|
56
|
+
* em vez de escrever com uma cópia que já não é conferida contra nada.
|
|
199
57
|
*/
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
key,
|
|
217
|
-
label: text(field.label) ?? key,
|
|
218
|
-
type: text(field.type) ?? 'text',
|
|
219
|
-
required: field.required === true,
|
|
220
|
-
order: typeof field.order === 'number' ? field.order : index,
|
|
221
|
-
...(field.multi === true ? { multi: true } : {}),
|
|
222
|
-
...(text(field.refEntity) === undefined ? {} : { refEntity: text(field.refEntity) }),
|
|
223
|
-
...(text(field.refDenormKey) === undefined ? {} : { refDenormKey: text(field.refDenormKey) }),
|
|
224
|
-
...(text(field.lookupCollection) === undefined ? {} : { lookupCollection: text(field.lookupCollection) }),
|
|
225
|
-
...(text(field.section) === undefined ? {} : { section: text(field.section) }),
|
|
226
|
-
origin: originOf(field, screenOrigin),
|
|
227
|
-
...(text(field.ownerPluginId) === undefined ? {} : { ownerPluginId: text(field.ownerPluginId) }),
|
|
228
|
-
...(asRecord(field.aggregation) === undefined ? {} : { aggregation: asRecord(field.aggregation) }),
|
|
229
|
-
...(compute === undefined ? {} : { compute }),
|
|
230
|
-
...(field.unique === true ? { unique: true } : {}),
|
|
231
|
-
};
|
|
232
|
-
};
|
|
233
|
-
const normalizeExport = (raw) => {
|
|
234
|
-
const normalize = asRecord(raw);
|
|
235
|
-
if (normalize === undefined)
|
|
236
|
-
return undefined;
|
|
237
|
-
const formatPreset = text(normalize.formatPreset);
|
|
238
|
-
const picked = {
|
|
239
|
-
...(typeof normalize.trim === 'boolean' ? { trim: normalize.trim } : {}),
|
|
240
|
-
...(typeof normalize.caseInsensitive === 'boolean' ? { caseInsensitive: normalize.caseInsensitive } : {}),
|
|
241
|
-
...(formatPreset === undefined ? {} : { formatPreset }),
|
|
242
|
-
};
|
|
243
|
-
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
244
|
-
};
|
|
245
|
-
/** A chave como o kernel a grava, sem o que é apresentação. Sem nome ou sem campo, não é chave. */
|
|
246
|
-
const keyExport = (raw) => {
|
|
247
|
-
const key = asRecord(raw);
|
|
248
|
-
const name = text(key?.name);
|
|
249
|
-
const kind = text(key?.kind);
|
|
250
|
-
const fields = (Array.isArray(key?.fields) ? key.fields : []).filter((one) => typeof one === 'string' && one !== '');
|
|
251
|
-
if (key === undefined || name === undefined || kind === undefined || fields.length === 0)
|
|
252
|
-
return null;
|
|
253
|
-
const normalize = normalizeExport(key.normalize);
|
|
254
|
-
return {
|
|
255
|
-
name,
|
|
256
|
-
kind,
|
|
257
|
-
fields,
|
|
258
|
-
...(text(key.scope) === undefined ? {} : { scope: text(key.scope) }),
|
|
259
|
-
...(normalize === undefined ? {} : { normalize }),
|
|
260
|
-
...(typeof key.sparse === 'boolean' ? { sparse: key.sparse } : {}),
|
|
261
|
-
...(text(key.ownerPluginId) === undefined ? {} : { ownerPluginId: text(key.ownerPluginId) }),
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
const screenExport = (document) => {
|
|
265
|
-
const data = document.data;
|
|
266
|
-
const origin = originOf(data, 'design');
|
|
267
|
-
const fields = Array.isArray(data.fields) ? data.fields : [];
|
|
58
|
+
export const loadCatalogWriter = (directory) => {
|
|
59
|
+
if (!existsSync(join(directory, 'node_modules', '@microteia', 'sdk'))) {
|
|
60
|
+
throw new Error(`The authoring SDK is not installed in ${EXTENSIONS_DIR}/, and it is the SDK that writes the catalog —\n`
|
|
61
|
+
+ ' the same one that reads it. Run "microteia plugin-init" (or "npm install" inside it), then run this again.');
|
|
62
|
+
}
|
|
63
|
+
const sdk = createRequire(join(directory, 'package.json'))('@microteia/sdk');
|
|
64
|
+
const build = sdk.buildInstalledCatalog;
|
|
65
|
+
const serialize = sdk.serializeInstalledCatalog;
|
|
66
|
+
const file = sdk.INSTALLED_CATALOG_FILE;
|
|
67
|
+
if (typeof build !== 'function' || typeof serialize !== 'function' || typeof file !== 'string') {
|
|
68
|
+
throw new Error(`The authoring SDK in ${EXTENSIONS_DIR}/ is older than the catalog writer this Console relies on.\n`
|
|
69
|
+
+ ' Install a newer kernel ("microteia unpack --version latest"), which brings an SDK that writes it.\n'
|
|
70
|
+
+ ' Without access to the Firebase project, "Export catalog" on the Extensions screen of the\n'
|
|
71
|
+
+ ' application writes the same file, once the installation runs a kernel that has it.');
|
|
72
|
+
}
|
|
268
73
|
return {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
titleSingular: text(data.titleSingular) ?? document.id,
|
|
273
|
-
...(text(data.titlePlural) === undefined ? {} : { titlePlural: text(data.titlePlural) }),
|
|
274
|
-
titleFieldKey: text(data.titleFieldKey) ?? '',
|
|
275
|
-
origin,
|
|
276
|
-
...(text(data.ownerPluginId) === undefined ? {} : { ownerPluginId: text(data.ownerPluginId) }),
|
|
277
|
-
...(text(data.parentEntity) === undefined ? {} : { parentEntity: text(data.parentEntity) }),
|
|
278
|
-
...(text(data.parentFieldKey) === undefined ? {} : { parentFieldKey: text(data.parentFieldKey) }),
|
|
279
|
-
...(text(data.parentRelation) === undefined ? {} : { parentRelation: text(data.parentRelation) }),
|
|
280
|
-
...(text(data.tagCollection) === undefined ? {} : { tagCollection: text(data.tagCollection) }),
|
|
281
|
-
...(text(data.eventPrefix) === undefined ? {} : { eventPrefix: text(data.eventPrefix) }),
|
|
282
|
-
keys: (Array.isArray(data.keys) ? data.keys : [])
|
|
283
|
-
.map(keyExport)
|
|
284
|
-
.filter((key) => key !== null),
|
|
285
|
-
fields: fields
|
|
286
|
-
.map((field, index) => fieldExport(field, index, origin))
|
|
287
|
-
.filter((field) => field !== null),
|
|
74
|
+
file,
|
|
75
|
+
build: build,
|
|
76
|
+
serialize: serialize,
|
|
288
77
|
};
|
|
289
78
|
};
|
|
290
|
-
/**
|
|
291
|
-
* O id do documento É o nome da coleção da lista, tanto em `lookupDefinitions` quanto em
|
|
292
|
-
* `tagDefinitions` — a mesma convenção que os repositórios da aplicação usam ao mapeá-las.
|
|
293
|
-
*/
|
|
294
|
-
const listExport = (document) => ({
|
|
295
|
-
collection: document.id,
|
|
296
|
-
label: text(document.data.label) ?? document.id,
|
|
297
|
-
...(text(document.data.ownerPluginId) === undefined
|
|
298
|
-
? {}
|
|
299
|
-
: { ownerPluginId: text(document.data.ownerPluginId) }),
|
|
300
|
-
});
|
|
301
|
-
/**
|
|
302
|
-
* O documento, montado a partir do que foi lido. É PURA de propósito: o que ela produz é o contrato com
|
|
303
|
-
* o SDK, e um contrato que só pode ser exercitado com um projeto Firebase na frente é um contrato que
|
|
304
|
-
* ninguém confere.
|
|
305
|
-
*
|
|
306
|
-
* Uma tela DESLIGADA (`enabled: false`) fica de fora: a coleção dela pode até existir no banco, mas
|
|
307
|
-
* nada da plataforma a desenha, e trazê-la faria o catálogo afirmar que a entidade existe. Uma tela de
|
|
308
|
-
* extensão DESATIVADA continua: o documento dela segue gravado, e é ele que explica um campo que o
|
|
309
|
-
* autor vai encontrar.
|
|
310
|
-
*/
|
|
311
|
-
export const buildCatalogExport = (input) => ({
|
|
312
|
-
version: CATALOG_FORMAT_VERSION,
|
|
313
|
-
projectId: input.projectId,
|
|
314
|
-
tenantId: input.tenant.id,
|
|
315
|
-
tenantName: input.tenant.name,
|
|
316
|
-
exportedAt: input.exportedAt.toISOString(),
|
|
317
|
-
kernelVersion: input.kernelVersion,
|
|
318
|
-
consoleVersion: CLI_VERSION,
|
|
319
|
-
extensions: input.extensions
|
|
320
|
-
.map((one) => ({
|
|
321
|
-
id: one.id,
|
|
322
|
-
name: one.name,
|
|
323
|
-
version: one.version,
|
|
324
|
-
active: one.active,
|
|
325
|
-
exports: exportedArtifacts(one),
|
|
326
|
-
navGroups: navGroupsOf(one),
|
|
327
|
-
documentTemplates: documentTemplatesOf(one),
|
|
328
|
-
fieldPresets: fieldPresetsOf(one),
|
|
329
|
-
}))
|
|
330
|
-
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
331
|
-
screens: input.screens
|
|
332
|
-
.filter((document) => document.data.enabled !== false)
|
|
333
|
-
.map(screenExport)
|
|
334
|
-
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)),
|
|
335
|
-
lookups: input.lookups
|
|
336
|
-
.map(listExport)
|
|
337
|
-
.sort((a, b) => (a.collection < b.collection ? -1 : a.collection > b.collection ? 1 : 0)),
|
|
338
|
-
tagLists: input.tagLists
|
|
339
|
-
.map(listExport)
|
|
340
|
-
.sort((a, b) => (a.collection < b.collection ? -1 : a.collection > b.collection ? 1 : 0)),
|
|
341
|
-
});
|
|
342
79
|
const decode = (documents) => documents.map((document) => ({ id: documentId(document), data: decodeFields(document.fields) }));
|
|
343
80
|
const readSubcollection = async (projectId, tenantId, name) => decode(await listDocuments(projectId, `/tenants/${tenantId}/${name}`));
|
|
81
|
+
/** A cópia instalada como está gravada: o escritor lê o manifesto de dentro de `package`, como o kernel. */
|
|
82
|
+
const storedExtension = (extension) => ({
|
|
83
|
+
id: extension.documentId,
|
|
84
|
+
data: extension.data,
|
|
85
|
+
});
|
|
344
86
|
/**
|
|
345
87
|
* O kernel que está NO LUGAR nesta instalação, e não o que já foi publicado: o retrato descreve o
|
|
346
88
|
* contrato contra o qual o autor compila agora. Ausente, o SDK dirá que não sabe — o que é melhor que
|
|
@@ -364,14 +106,14 @@ const installedKernelVersion = async (root) => {
|
|
|
364
106
|
* ainda não chegou (um diretório sincronizado por um kernel anterior), diz qual comando a traz, em vez
|
|
365
107
|
* de deixar o retrato pronto para ser commitado sem ninguém notar.
|
|
366
108
|
*/
|
|
367
|
-
const checkIgnored = async (directory) => {
|
|
109
|
+
const checkIgnored = async (directory, catalogFile) => {
|
|
368
110
|
const path = join(directory, '.gitignore');
|
|
369
111
|
if (!existsSync(path))
|
|
370
112
|
return;
|
|
371
113
|
const lines = (await readFile(path, 'utf8')).split('\n').map((line) => line.trim());
|
|
372
|
-
if (lines.includes(
|
|
114
|
+
if (lines.includes(catalogFile) || lines.includes('*.json'))
|
|
373
115
|
return;
|
|
374
|
-
warn(`${relative(process.cwd(), path) || '.gitignore'} does not ignore ${
|
|
116
|
+
warn(`${relative(process.cwd(), path) || '.gitignore'} does not ignore ${catalogFile}.`);
|
|
375
117
|
info(' It carries screen and field names the office typed, which is tenant data and does not belong');
|
|
376
118
|
info(` in an extension repository. Run "npm run sync" in ${EXTENSIONS_DIR}/ to get the line, or add it`);
|
|
377
119
|
info(' by hand.');
|
|
@@ -412,12 +154,14 @@ export const exportCatalog = async (args) => {
|
|
|
412
154
|
}
|
|
413
155
|
const output = flagValue(args, 'output');
|
|
414
156
|
const directory = join(root, EXTENSIONS_DIR);
|
|
415
|
-
|
|
157
|
+
// Mesmo com `--output`: o escritor mora no SDK de `extensions/`, e sem ele não há o que escrever.
|
|
158
|
+
if (!existsSync(directory)) {
|
|
416
159
|
throw new Error(`There is no ${EXTENSIONS_DIR}/ in this installation, and that is where the catalog belongs — it\n`
|
|
417
|
-
+
|
|
418
|
-
+ ' Run "microteia plugin-init" first
|
|
160
|
+
+ ` exists to answer the authoring tools, and the SDK there is what writes ${CATALOG_FILE_HINT}.\n`
|
|
161
|
+
+ ' Run "microteia plugin-init" first.');
|
|
419
162
|
}
|
|
420
|
-
const
|
|
163
|
+
const writer = loadCatalogWriter(directory);
|
|
164
|
+
const target = output === undefined ? join(directory, writer.file) : resolve(output);
|
|
421
165
|
step(1, 2, `Reading what ${tenant.name} has`);
|
|
422
166
|
const [screens, lookups, tagLists, extensions] = await Promise.all([
|
|
423
167
|
readSubcollection(projectId, tenant.id, 'screenDefinitions'),
|
|
@@ -425,21 +169,22 @@ export const exportCatalog = async (args) => {
|
|
|
425
169
|
readSubcollection(projectId, tenant.id, 'tagDefinitions'),
|
|
426
170
|
readInstalledExtensions(projectId, tenant.id),
|
|
427
171
|
]);
|
|
428
|
-
const document =
|
|
172
|
+
const document = writer.build({
|
|
429
173
|
projectId,
|
|
430
174
|
tenant,
|
|
431
175
|
kernelVersion: await installedKernelVersion(root),
|
|
432
176
|
exportedAt: new Date(),
|
|
177
|
+
exporter: { tool: 'console', version: CLI_VERSION },
|
|
433
178
|
screens,
|
|
434
179
|
lookups,
|
|
435
180
|
tagLists,
|
|
436
|
-
extensions,
|
|
181
|
+
extensions: extensions.map(storedExtension),
|
|
437
182
|
});
|
|
438
183
|
step(2, 2, 'Writing the snapshot');
|
|
439
184
|
await mkdir(dirname(target), { recursive: true });
|
|
440
|
-
await writeFile(target,
|
|
185
|
+
await writeFile(target, writer.serialize(document));
|
|
441
186
|
const fromExtensions = document.screens.filter((screen) => screen.origin === 'plugin').length;
|
|
442
|
-
success(`${relative(process.cwd(), target) ||
|
|
187
|
+
success(`${relative(process.cwd(), target) || writer.file} — ${document.screens.length} screen(s), `
|
|
443
188
|
+ `${document.lookups.length} list(s), ${document.tagLists.length} tag list(s)`);
|
|
444
189
|
info(` ${fromExtensions} screen(s) come from the ${document.extensions.length} installed extension(s); `
|
|
445
190
|
+ `the rest the office built itself.`);
|
|
@@ -450,7 +195,7 @@ export const exportCatalog = async (args) => {
|
|
|
450
195
|
const templates = document.extensions.reduce((total, one) => total + one.documentTemplates.length, 0);
|
|
451
196
|
info(` ${templates} document template(s) are declared, with the shape of their parameters, for other extensions to generate.`);
|
|
452
197
|
blank();
|
|
453
|
-
await checkIgnored(directory);
|
|
198
|
+
await checkIgnored(directory, writer.file);
|
|
454
199
|
info('It is a SNAPSHOT, and it says so: the authoring tools report when it was taken and warn when it');
|
|
455
200
|
info('is old. Nothing keeps it in step with the office — run this again after screens change there.');
|
|
456
201
|
blank();
|
|
@@ -20,6 +20,8 @@ export declare const decodeFields: (fields: Record<string, unknown>) => Record<s
|
|
|
20
20
|
* erro. Documento de extensão é grande, mas são poucos por escritório, e isto roda uma vez por comando.
|
|
21
21
|
*/
|
|
22
22
|
export declare const listDocuments: (projectId: string, path: string) => Promise<FirestoreDocument[]>;
|
|
23
|
+
/** Os campos de UM documento, decodificados — `null` quando ele não existe. */
|
|
24
|
+
export declare const getDocument: (projectId: string, path: string) => Promise<Record<string, unknown> | null>;
|
|
23
25
|
export declare const documentId: (document: FirestoreDocument) => string;
|
|
24
26
|
/** Um valor com o tipo que a REST espera, montado por quem sabe o que o campo é. */
|
|
25
27
|
export type FirestoreTypedValue = {
|
|
@@ -62,6 +62,10 @@ const request = async (projectId, path, init = { method: 'GET' }) => {
|
|
|
62
62
|
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
63
63
|
});
|
|
64
64
|
const text = await response.text();
|
|
65
|
+
// Documento que ainda não existe é resposta, não falha, para quem pediu assim: é o caso de todo
|
|
66
|
+
// documento que o Console escreve pela primeira vez num projeto que nunca o recebeu.
|
|
67
|
+
if (response.status === 404 && init.allowMissing === true)
|
|
68
|
+
return null;
|
|
65
69
|
if (!response.ok) {
|
|
66
70
|
const detail = text.slice(0, 400);
|
|
67
71
|
if (response.status === 403) {
|
|
@@ -113,13 +117,20 @@ export const listDocuments = async (projectId, path) => {
|
|
|
113
117
|
method: 'GET',
|
|
114
118
|
query: { pageSize: '300', ...(pageToken === undefined ? {} : { pageToken }) },
|
|
115
119
|
});
|
|
116
|
-
for (const entry of page
|
|
120
|
+
for (const entry of page?.documents ?? []) {
|
|
117
121
|
documents.push({ name: entry.name, fields: entry.fields ?? {} });
|
|
118
122
|
}
|
|
119
|
-
pageToken = page
|
|
123
|
+
pageToken = page?.nextPageToken;
|
|
120
124
|
} while (pageToken !== undefined);
|
|
121
125
|
return documents;
|
|
122
126
|
};
|
|
127
|
+
/** Os campos de UM documento, decodificados — `null` quando ele não existe. */
|
|
128
|
+
export const getDocument = async (projectId, path) => {
|
|
129
|
+
const document = await request(projectId, path, { method: 'GET', allowMissing: true });
|
|
130
|
+
if (document === null)
|
|
131
|
+
return null;
|
|
132
|
+
return decodeFields(document.fields ?? {});
|
|
133
|
+
};
|
|
123
134
|
export const documentId = (document) => document.name.slice(document.name.lastIndexOf('/') + 1);
|
|
124
135
|
export const asString = (value) => ({ stringValue: value });
|
|
125
136
|
/**
|
|
@@ -16,6 +16,8 @@ export interface TenantSummary {
|
|
|
16
16
|
export interface InstalledExtensionRecord {
|
|
17
17
|
tenantId: string;
|
|
18
18
|
documentPath: string;
|
|
19
|
+
/** O id do documento, que pode diferir do `manifest.id` — é por ele que a cópia é endereçada. */
|
|
20
|
+
documentId: string;
|
|
19
21
|
id: string;
|
|
20
22
|
name: string;
|
|
21
23
|
version: string;
|
|
@@ -26,6 +26,7 @@ export const readInstalledExtensions = async (projectId, tenantId) => {
|
|
|
26
26
|
return {
|
|
27
27
|
tenantId,
|
|
28
28
|
documentPath: `/tenants/${tenantId}/installed_extensions/${fallbackId}`,
|
|
29
|
+
documentId: fallbackId,
|
|
29
30
|
id: String(manifest?.id ?? fallbackId),
|
|
30
31
|
name: String(manifest?.name ?? manifest?.id ?? fallbackId),
|
|
31
32
|
version: String(manifest?.version ?? '0.0.0'),
|