@neetru/cli 2.8.0 → 2.9.3
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/CHANGELOG.md +128 -0
- package/dist/commands/admin/sdk-credential.d.ts +19 -0
- package/dist/commands/admin/sdk-credential.js +169 -0
- package/dist/commands/admin/sdk-credential.js.map +1 -0
- package/dist/commands/bug.d.ts +87 -0
- package/dist/commands/bug.js +419 -0
- package/dist/commands/bug.js.map +1 -0
- package/dist/commands/customers.d.ts +17 -0
- package/dist/commands/customers.js +160 -0
- package/dist/commands/customers.js.map +1 -0
- package/dist/commands/db.d.ts +4 -0
- package/dist/commands/db.js +380 -64
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.js +99 -7
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/marketplace.d.ts +36 -0
- package/dist/commands/marketplace.js +584 -0
- package/dist/commands/marketplace.js.map +1 -0
- package/dist/commands/new.js +196 -37
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/open.d.ts +8 -0
- package/dist/commands/open.js +61 -13
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/products.d.ts +23 -0
- package/dist/commands/products.js +39 -1
- package/dist/commands/products.js.map +1 -1
- package/dist/commands/tenants.js +24 -2
- package/dist/commands/tenants.js.map +1 -1
- package/dist/commands/ui.d.ts +1 -1
- package/dist/commands/ui.js +172 -2
- package/dist/commands/ui.js.map +1 -1
- package/dist/commands/workspaces.d.ts +10 -1
- package/dist/commands/workspaces.js +145 -23
- package/dist/commands/workspaces.js.map +1 -1
- package/dist/index.js +327 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/config-schema.d.ts +10 -10
- package/dist/lib/pickers.d.ts +25 -0
- package/dist/lib/pickers.js +75 -1
- package/dist/lib/pickers.js.map +1 -1
- package/package.json +5 -2
package/dist/commands/db.js
CHANGED
|
@@ -46,6 +46,14 @@ const ENGINES = [
|
|
|
46
46
|
'vm-mysql-single',
|
|
47
47
|
'vm-mysql-cluster',
|
|
48
48
|
];
|
|
49
|
+
/**
|
|
50
|
+
* Engines NoSQL — schemaless por natureza.
|
|
51
|
+
* Nesses engines o `neetru db apply` NÃO roda drizzle-kit nem Postgres efêmero:
|
|
52
|
+
* registra apenas um snapshot de fingerprint no Core (Fase 1 — sem JSON Schema export).
|
|
53
|
+
*
|
|
54
|
+
* Fase 2 (export de JSON Schema estruturado) é backlog — ticket pdv-agiliza #11.
|
|
55
|
+
*/
|
|
56
|
+
const NOSQL_ENGINES = ['nosql-vm', 'firestore-instance'];
|
|
49
57
|
/** Labels amigáveis por engine pra exibição no prompt. */
|
|
50
58
|
const ENGINE_LABELS = {
|
|
51
59
|
'firestore-instance': 'Firestore (instância isolada)',
|
|
@@ -147,7 +155,8 @@ export async function runDbList(opts = {}) {
|
|
|
147
155
|
{ header: 'Produto', value: (r) => r.productId, maxWidth: 20 },
|
|
148
156
|
{ header: 'Label', value: (r) => r.label ?? '—', maxWidth: 24 },
|
|
149
157
|
{ header: 'Engine', value: (r) => r.engine, maxWidth: 22 },
|
|
150
|
-
|
|
158
|
+
// gestovendas #11 fix: Core retorna `environment`; `env` é alias legado.
|
|
159
|
+
{ header: 'Env', value: (r) => r.environment ?? r.env ?? '—', maxWidth: 12 },
|
|
151
160
|
{ header: 'Status', value: (r) => statusColor(r.status), maxWidth: 16 },
|
|
152
161
|
{ header: 'Criado', value: (r) => fmtTimestamp(r.createdAt), maxWidth: 18 },
|
|
153
162
|
], resp.databases);
|
|
@@ -180,7 +189,7 @@ export async function runDbStatus(dbId, opts = {}) {
|
|
|
180
189
|
log.dim(` ID: ${db.id}`);
|
|
181
190
|
log.dim(` Produto: ${db.productId}`);
|
|
182
191
|
log.dim(` Engine: ${db.engine}`);
|
|
183
|
-
log.dim(` Env: ${db.env}`);
|
|
192
|
+
log.dim(` Env: ${db.environment ?? db.env ?? '—'}`);
|
|
184
193
|
log.dim(` Status: ${statusColor(db.status)}`);
|
|
185
194
|
if (db.region)
|
|
186
195
|
log.dim(` Região: ${db.region}`);
|
|
@@ -198,8 +207,49 @@ export async function runDbStatus(dbId, opts = {}) {
|
|
|
198
207
|
log.dim(` Criado: ${fmtTimestamp(db.createdAt)}`);
|
|
199
208
|
log.dim(` Atualizado: ${fmtTimestamp(db.updatedAt)}`);
|
|
200
209
|
}
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
// pdv #7 fix: template diferenciado por família de engine.
|
|
211
|
+
/** Template para engines NoSQL (firestore-instance). */
|
|
212
|
+
const SCHEMA_STARTER_NOSQL = (productId, engine) => `/**
|
|
213
|
+
* Schema do banco "${productId}" — ${ENGINE_LABELS[engine] ?? engine}.
|
|
214
|
+
*
|
|
215
|
+
* Gerado por \`neetru db init\`.
|
|
216
|
+
* Edite este arquivo com as collections do seu produto.
|
|
217
|
+
* Rode \`neetru db apply\` pra enviar a definição ao Core.
|
|
218
|
+
*
|
|
219
|
+
* Para bancos NoSQL (Firestore), o schema define a estrutura esperada
|
|
220
|
+
* dos documentos. O Core usa essa definição para validação e auditoria —
|
|
221
|
+
* o banco em si é schemaless (primeira escrita cria a estrutura).
|
|
222
|
+
*
|
|
223
|
+
* Documentação: https://core.neetru.com/docs/dev/db/nosql-schema
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
// Exemplo de collection com tipagem TypeScript pura:
|
|
227
|
+
|
|
228
|
+
export interface Produto {
|
|
229
|
+
id: string;
|
|
230
|
+
nome: string;
|
|
231
|
+
preco: number;
|
|
232
|
+
ativo: boolean;
|
|
233
|
+
criadoEm: Date;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface Pedido {
|
|
237
|
+
id: string;
|
|
238
|
+
produtoId: string;
|
|
239
|
+
quantidade: number;
|
|
240
|
+
total: number;
|
|
241
|
+
status: 'pendente' | 'pago' | 'cancelado';
|
|
242
|
+
criadoEm: Date;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Exporte as collections que o SDK vai usar:
|
|
246
|
+
// import { createNeetruClient } from '@neetru/sdk';
|
|
247
|
+
// const neetru = createNeetruClient({ ... });
|
|
248
|
+
// const produtos = neetru.db.collection<Produto>('produtos');
|
|
249
|
+
// const pedidos = neetru.db.collection<Pedido>('pedidos');
|
|
250
|
+
`;
|
|
251
|
+
/** Template padrão para engines SQL (PostgreSQL/MySQL). */
|
|
252
|
+
const SCHEMA_STARTER_SQL = (productId, engine) => `/**
|
|
203
253
|
* Schema do banco "${productId}" — ${ENGINE_LABELS[engine] ?? engine}.
|
|
204
254
|
*
|
|
205
255
|
* Gerado por \`neetru db init\`.
|
|
@@ -218,24 +268,46 @@ const SCHEMA_STARTER = (productId, engine) => `/**
|
|
|
218
268
|
// Remova o comentário acima e adapte ao seu schema.
|
|
219
269
|
export {};
|
|
220
270
|
`;
|
|
271
|
+
/** Conteúdo do `db/schema.ts` gerado durante o init. */
|
|
272
|
+
const SCHEMA_STARTER = (productId, engine) => {
|
|
273
|
+
// Engines NoSQL recebem template diferente.
|
|
274
|
+
if (engine === 'firestore-instance') {
|
|
275
|
+
return SCHEMA_STARTER_NOSQL(productId, engine);
|
|
276
|
+
}
|
|
277
|
+
return SCHEMA_STARTER_SQL(productId, engine);
|
|
278
|
+
};
|
|
279
|
+
// gestovendas #9: helper para saída JSON uniforme em db init / db apply.
|
|
280
|
+
function jsonError(message, error = 'error') {
|
|
281
|
+
console.log(JSON.stringify({ ok: false, error, message }));
|
|
282
|
+
}
|
|
221
283
|
export async function runDbInit(opts = {}) {
|
|
222
|
-
|
|
223
|
-
|
|
284
|
+
if (!opts.json) {
|
|
285
|
+
log.banner();
|
|
286
|
+
log.heading('neetru db init');
|
|
287
|
+
}
|
|
224
288
|
// ── D-3: opções legadas removidas (Sprint 10 — schema.manifest.json) ────
|
|
225
289
|
// `--out` e `--force` eram usados para gerar schema.manifest.json.
|
|
226
290
|
// O branch legado foi cortado. Passar essas flags agora é um erro.
|
|
227
291
|
if (opts.out !== undefined || opts.force !== undefined) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
const msg = 'As opções --out e --force foram removidas (D-3). Use: neetru db init --name <nome> --engine <engine> --env dev-local';
|
|
293
|
+
if (opts.json) {
|
|
294
|
+
jsonError(msg, 'invalid_option');
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
log.error(msg);
|
|
298
|
+
}
|
|
233
299
|
process.exit(1);
|
|
234
300
|
return;
|
|
235
301
|
}
|
|
236
302
|
const cfg = await loadProductConfig();
|
|
237
303
|
if (!cfg) {
|
|
238
|
-
|
|
304
|
+
const msg = 'neetru.config.json não encontrado. Rode `neetru init` primeiro.';
|
|
305
|
+
if (opts.json) {
|
|
306
|
+
jsonError(msg, 'config_not_found');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
log.error(msg);
|
|
310
|
+
}
|
|
239
311
|
process.exit(1);
|
|
240
312
|
return;
|
|
241
313
|
}
|
|
@@ -269,13 +341,34 @@ export async function runDbInit(opts = {}) {
|
|
|
269
341
|
dbName = dbJson.databases.length === 0 ? 'principal' : `db${dbJson.databases.length + 1}`;
|
|
270
342
|
}
|
|
271
343
|
if (dbJson.databases.some((d) => d.name === dbName)) {
|
|
272
|
-
|
|
344
|
+
const msg = `Banco "${dbName}" já registrado. Use outro nome ou edite .neetru/db.json.`;
|
|
345
|
+
if (opts.json) {
|
|
346
|
+
jsonError(msg, 'duplicate_name');
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
log.error(msg);
|
|
350
|
+
}
|
|
273
351
|
process.exit(2);
|
|
274
352
|
return;
|
|
275
353
|
}
|
|
276
354
|
// ── engine ───────────────────────────────────────────────────────────────
|
|
355
|
+
// pdv #4 fix: quando --engine é passado mas não reconhecido, abortar com
|
|
356
|
+
// mensagem clara em vez de ignorar silenciosamente e usar o default.
|
|
277
357
|
let engine;
|
|
278
|
-
if (opts.engine
|
|
358
|
+
if (opts.engine) {
|
|
359
|
+
if (!ENGINES.includes(opts.engine)) {
|
|
360
|
+
const msg = `Engine inválido: "${opts.engine}". Engines disponíveis: ${ENGINES.join(', ')}\n` +
|
|
361
|
+
`Nota: o campo "engine" no SDK (rest | firestore | nosql-vm) é o TRANSPORTE da conexão, ` +
|
|
362
|
+
`diferente do engine de ARMAZENAMENTO passado aqui para o plano de controle.`;
|
|
363
|
+
if (opts.json) {
|
|
364
|
+
jsonError(msg, 'invalid_engine');
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
log.error(msg);
|
|
368
|
+
}
|
|
369
|
+
process.exit(1);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
279
372
|
engine = opts.engine;
|
|
280
373
|
}
|
|
281
374
|
else if (opts.yes) {
|
|
@@ -306,16 +399,12 @@ export async function runDbInit(opts = {}) {
|
|
|
306
399
|
process.exit(1);
|
|
307
400
|
return;
|
|
308
401
|
}
|
|
309
|
-
// ──
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
writeDbJson(dbJsonPath, withStub);
|
|
316
|
-
log.success(`Banco "${dbName}" registrado localmente em ${chalk.bold(DB_JSON_REL)}`);
|
|
317
|
-
// ── BL-6 fix: registra o banco no Core e persiste os IDs por ambiente ────
|
|
318
|
-
const registerSpinner = ora({ text: 'Registrando banco no Core…', color: 'blue' }).start();
|
|
402
|
+
// ── HIGH-3: registra o banco no Core ANTES de gravar o stub local.
|
|
403
|
+
// Gravar localmente antes e deixar o registro falhar resultava em entrada
|
|
404
|
+
// órfã em .neetru/db.json — re-rodar db init adicionava banco duplicado.
|
|
405
|
+
const registerSpinner = opts.json
|
|
406
|
+
? null
|
|
407
|
+
: ora({ text: 'Registrando banco no Core…', color: 'blue' }).start();
|
|
319
408
|
let registerResp;
|
|
320
409
|
try {
|
|
321
410
|
registerResp = await apiRequest('/api/cli/v1/db/register', {
|
|
@@ -327,12 +416,20 @@ export async function runDbInit(opts = {}) {
|
|
|
327
416
|
provisioning: { type: engine.startsWith('vm-') ? 'vm' : 'managed' },
|
|
328
417
|
},
|
|
329
418
|
});
|
|
330
|
-
registerSpinner
|
|
419
|
+
registerSpinner?.succeed('Banco registrado no Core.');
|
|
331
420
|
}
|
|
332
421
|
catch (err) {
|
|
333
|
-
registerSpinner
|
|
422
|
+
registerSpinner?.fail('Falha ao registrar banco no Core.');
|
|
423
|
+
if (opts.json) {
|
|
424
|
+
jsonError(err.message ?? String(err), 'register_failed');
|
|
425
|
+
process.exit(4);
|
|
426
|
+
}
|
|
334
427
|
handleApiError(err);
|
|
428
|
+
return; // handleApiError nunca retorna mas TS não sabe disso
|
|
335
429
|
}
|
|
430
|
+
// ── Grava .neetru/db.json somente após sucesso do Core ───────────────────
|
|
431
|
+
if (!opts.json)
|
|
432
|
+
log.success(`Banco "${dbName}" registrado localmente em ${chalk.bold(DB_JSON_REL)}`);
|
|
336
433
|
// Persiste os IDs retornados pelo Core em `.neetru/db.json`.
|
|
337
434
|
const ids = {
|
|
338
435
|
'dev-local': registerResp.ids.devLocal,
|
|
@@ -342,32 +439,121 @@ export async function runDbInit(opts = {}) {
|
|
|
342
439
|
const finalEntry = { name: dbName, productId, engine, ids };
|
|
343
440
|
const withIds = {
|
|
344
441
|
version: 2,
|
|
345
|
-
databases: [...
|
|
442
|
+
databases: [...dbJson.databases, finalEntry],
|
|
346
443
|
};
|
|
347
444
|
writeDbJson(dbJsonPath, withIds);
|
|
348
|
-
|
|
445
|
+
if (!opts.json)
|
|
446
|
+
log.success(`IDs de ambiente gravados em ${chalk.bold(DB_JSON_REL)}`);
|
|
349
447
|
// ── scaffolda db/schema.ts se não existir ─────────────────────────────────
|
|
350
448
|
const schemaPath = path.resolve(cwd, DEFAULT_SCHEMA_PATH);
|
|
449
|
+
let schemaCreated = false;
|
|
351
450
|
if (!fsSync.existsSync(schemaPath)) {
|
|
352
451
|
await fs.mkdir(path.dirname(schemaPath), { recursive: true });
|
|
353
452
|
await fs.writeFile(schemaPath, SCHEMA_STARTER(productId, engine), 'utf8');
|
|
354
|
-
|
|
453
|
+
schemaCreated = true;
|
|
454
|
+
if (!opts.json)
|
|
455
|
+
log.success(`Schema gerado em ${chalk.bold(DEFAULT_SCHEMA_PATH)}`);
|
|
355
456
|
}
|
|
356
457
|
else {
|
|
357
|
-
|
|
458
|
+
if (!opts.json)
|
|
459
|
+
log.dim(` Schema existente em ${DEFAULT_SCHEMA_PATH} — não sobrescrito.`);
|
|
460
|
+
}
|
|
461
|
+
// gestovendas #9: saída JSON uniforme.
|
|
462
|
+
if (opts.json) {
|
|
463
|
+
console.log(JSON.stringify({
|
|
464
|
+
ok: true,
|
|
465
|
+
name: dbName,
|
|
466
|
+
productId,
|
|
467
|
+
engine,
|
|
468
|
+
env,
|
|
469
|
+
ids: {
|
|
470
|
+
devLocal: registerResp.ids.devLocal,
|
|
471
|
+
staging: registerResp.ids.staging,
|
|
472
|
+
production: registerResp.ids.production,
|
|
473
|
+
},
|
|
474
|
+
schemaPath: DEFAULT_SCHEMA_PATH,
|
|
475
|
+
schemaCreated,
|
|
476
|
+
}));
|
|
477
|
+
return;
|
|
358
478
|
}
|
|
359
479
|
log.dim('');
|
|
360
480
|
log.dim('Próximos passos:');
|
|
361
|
-
log.dim(` 1. Edite ${DEFAULT_SCHEMA_PATH} com as
|
|
362
|
-
|
|
363
|
-
|
|
481
|
+
log.dim(` 1. Edite ${DEFAULT_SCHEMA_PATH} com as estruturas do seu produto.`);
|
|
482
|
+
if (engine === 'firestore-instance') {
|
|
483
|
+
// pdv-agiliza #11 (Bug 11 Fase 1): branch NoSQL agora implementada no db apply.
|
|
484
|
+
log.dim(` 2. Para Firestore, o banco é schemaless — não há migração SQL.`);
|
|
485
|
+
log.dim(` O \`neetru db apply\` registra o fingerprint do schema no Core.`);
|
|
486
|
+
log.dim(` Use o SDK para criar collections diretamente:`);
|
|
487
|
+
log.dim(` neetru.db.collection<Produto>('produtos')`);
|
|
488
|
+
log.dim(` 3. O Core irá provisionar a instância ${chalk.bold(engine)} em ${chalk.bold(env)}.`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
log.dim(` 2. Execute \`neetru db apply\` pra enviar a migração ao Core.`);
|
|
492
|
+
log.dim(` 3. O Core irá provisionar o banco ${chalk.bold(engine)} em ${chalk.bold(env)}.`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Pipeline NoSQL — sem drizzle-kit, sem Postgres efêmero.
|
|
497
|
+
*
|
|
498
|
+
* Engines NoSQL (`nosql-vm`, `firestore-instance`) são schemaless — não há SQL
|
|
499
|
+
* pra gerar nem rehearsear. O apply registra apenas o fingerprint do schema.ts
|
|
500
|
+
* no Core, que persiste em `product_db_schema_snapshots/{id}`.
|
|
501
|
+
*
|
|
502
|
+
* Fase 1 (implementada): snapshot de fingerprint sem export de JSON Schema.
|
|
503
|
+
* Fase 2 (backlog pdv-agiliza #11): export de JSON Schema estruturado + validação de coleções.
|
|
504
|
+
*/
|
|
505
|
+
async function runNoSqlApply(input) {
|
|
506
|
+
const { dbId, productId, engine, schemaPath, env, dryRun } = input;
|
|
507
|
+
// Fase 1: calcular fingerprint do schema.ts
|
|
508
|
+
const { schemaFingerprint } = await import('../lib/db-local/index.js');
|
|
509
|
+
const { default: fsDefault } = await import('node:fs');
|
|
510
|
+
const schemaContent = fsDefault.readFileSync(schemaPath, 'utf8');
|
|
511
|
+
const fingerprint = schemaFingerprint(schemaContent);
|
|
512
|
+
if (dryRun) {
|
|
513
|
+
return {
|
|
514
|
+
ok: true,
|
|
515
|
+
status: 'dry_run',
|
|
516
|
+
engine,
|
|
517
|
+
snapshotId: null,
|
|
518
|
+
fingerprint,
|
|
519
|
+
dryRun: true,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const resp = await apiRequest('/api/cli/v1/db/apply', {
|
|
523
|
+
method: 'POST',
|
|
524
|
+
body: {
|
|
525
|
+
dbId,
|
|
526
|
+
productId,
|
|
527
|
+
engine,
|
|
528
|
+
fingerprint,
|
|
529
|
+
env,
|
|
530
|
+
// Fase 1 — JSON Schema export ainda não implementado (Fase 2 / pdv #11).
|
|
531
|
+
schemaDefinition: null,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
ok: true,
|
|
536
|
+
status: resp.status ?? 'applied',
|
|
537
|
+
engine,
|
|
538
|
+
snapshotId: resp.snapshotId ?? null,
|
|
539
|
+
fingerprint,
|
|
540
|
+
dryRun: false,
|
|
541
|
+
};
|
|
364
542
|
}
|
|
365
543
|
export async function runDbApply(opts = {}) {
|
|
366
|
-
|
|
367
|
-
|
|
544
|
+
if (!opts.json) {
|
|
545
|
+
log.banner();
|
|
546
|
+
log.heading('neetru db apply');
|
|
547
|
+
}
|
|
368
548
|
const cfg = await loadProductConfig();
|
|
369
549
|
if (!cfg) {
|
|
370
|
-
|
|
550
|
+
const msg = 'neetru.config.json não encontrado. Rode `neetru init` primeiro.';
|
|
551
|
+
if (opts.json) {
|
|
552
|
+
jsonError(msg, 'config_not_found');
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
log.error(msg);
|
|
556
|
+
}
|
|
371
557
|
process.exit(1);
|
|
372
558
|
return;
|
|
373
559
|
}
|
|
@@ -375,7 +561,13 @@ export async function runDbApply(opts = {}) {
|
|
|
375
561
|
const dbJsonPath = path.resolve(cwd, DB_JSON_REL);
|
|
376
562
|
const dbJson = readDbJson(dbJsonPath);
|
|
377
563
|
if (dbJson.databases.length === 0) {
|
|
378
|
-
|
|
564
|
+
const msg = 'Nenhum banco registrado em .neetru/db.json. Rode `neetru db init` primeiro.';
|
|
565
|
+
if (opts.json) {
|
|
566
|
+
jsonError(msg, 'no_databases');
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
log.error(msg);
|
|
570
|
+
}
|
|
379
571
|
process.exit(1);
|
|
380
572
|
return;
|
|
381
573
|
}
|
|
@@ -384,14 +576,39 @@ export async function runDbApply(opts = {}) {
|
|
|
384
576
|
if (opts.dbName) {
|
|
385
577
|
const found = dbJson.databases.find((d) => d.name === opts.dbName);
|
|
386
578
|
if (!found) {
|
|
387
|
-
|
|
388
|
-
|
|
579
|
+
const msg = `Banco "${opts.dbName}" não encontrado em .neetru/db.json. Disponíveis: ${dbJson.databases.map((d) => d.name).join(', ')}`;
|
|
580
|
+
if (opts.json) {
|
|
581
|
+
jsonError(msg, 'db_not_found');
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
log.error(msg);
|
|
585
|
+
}
|
|
389
586
|
process.exit(1);
|
|
390
587
|
return;
|
|
391
588
|
}
|
|
392
589
|
dbEntry = found;
|
|
393
590
|
}
|
|
394
|
-
else if (dbJson.databases.length > 1 &&
|
|
591
|
+
else if (dbJson.databases.length > 1 && (opts.json || opts.yes)) {
|
|
592
|
+
// HIGH-1: modo não-interativo (--json ou --yes) com múltiplos bancos e sem
|
|
593
|
+
// --db explícito é ambíguo — aplicar no primeiro silenciosamente pode causar
|
|
594
|
+
// migração no banco errado em CI. Emite erro estruturado ao invés de assumir.
|
|
595
|
+
const msg = 'Múltiplos bancos em .neetru/db.json. Use --db <nome>.';
|
|
596
|
+
if (opts.json) {
|
|
597
|
+
console.log(JSON.stringify({
|
|
598
|
+
ok: false,
|
|
599
|
+
error: 'db_required',
|
|
600
|
+
message: msg,
|
|
601
|
+
available: dbJson.databases.map((d) => d.name),
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
log.error(msg);
|
|
606
|
+
log.dim(` Bancos disponíveis: ${dbJson.databases.map((d) => d.name).join(', ')}`);
|
|
607
|
+
}
|
|
608
|
+
process.exit(1);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
else if (dbJson.databases.length > 1 && !opts.yes && !opts.json) {
|
|
395
612
|
const { chosen } = await inquirer.prompt([
|
|
396
613
|
{
|
|
397
614
|
type: 'list',
|
|
@@ -417,7 +634,13 @@ export async function runDbApply(opts = {}) {
|
|
|
417
634
|
env = normalizeEnv(opts.env ?? 'dev-local');
|
|
418
635
|
}
|
|
419
636
|
catch {
|
|
420
|
-
|
|
637
|
+
const msg = `Ambiente inválido: "${opts.env}". Use dev-local, staging ou production.`;
|
|
638
|
+
if (opts.json) {
|
|
639
|
+
jsonError(msg, 'invalid_env');
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
log.error(msg);
|
|
643
|
+
}
|
|
421
644
|
process.exit(1);
|
|
422
645
|
return;
|
|
423
646
|
}
|
|
@@ -425,7 +648,13 @@ export async function runDbApply(opts = {}) {
|
|
|
425
648
|
const schemaFile = opts.schemaPath ?? DEFAULT_SCHEMA_PATH;
|
|
426
649
|
const schemaAbs = path.resolve(cwd, schemaFile);
|
|
427
650
|
if (!fsSync.existsSync(schemaAbs)) {
|
|
428
|
-
|
|
651
|
+
const msg = `Schema não encontrado em ${schemaFile}. Rode \`neetru db init\` pra criar.`;
|
|
652
|
+
if (opts.json) {
|
|
653
|
+
jsonError(msg, 'schema_not_found');
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
log.error(msg);
|
|
657
|
+
}
|
|
429
658
|
process.exit(1);
|
|
430
659
|
return;
|
|
431
660
|
}
|
|
@@ -434,18 +663,82 @@ export async function runDbApply(opts = {}) {
|
|
|
434
663
|
const canonicalEnv = env;
|
|
435
664
|
const dbId = dbEntry.ids[canonicalEnv];
|
|
436
665
|
if (!dbId) {
|
|
437
|
-
|
|
438
|
-
`Execute \`neetru db init\` novamente ou verifique .neetru/db.json
|
|
666
|
+
const msg = `Banco "${dbEntry.name}" não tem ID registrado para o ambiente "${env}". ` +
|
|
667
|
+
`Execute \`neetru db init\` novamente ou verifique .neetru/db.json.`;
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
jsonError(msg, 'missing_db_id');
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
log.error(msg);
|
|
673
|
+
}
|
|
439
674
|
process.exit(1);
|
|
440
675
|
return;
|
|
441
676
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
677
|
+
if (!opts.json) {
|
|
678
|
+
log.dim(` Banco: ${dbEntry.name} (${dbEntry.engine})`);
|
|
679
|
+
log.dim(` Schema: ${schemaFile}`);
|
|
680
|
+
log.dim(` Env: ${env}`);
|
|
681
|
+
log.dim(` DB ID: ${dbId}`);
|
|
682
|
+
if (opts.dryRun)
|
|
683
|
+
log.warn(' Modo --dry-run: nenhuma alteração será enviada ao Core.');
|
|
684
|
+
}
|
|
685
|
+
// ── Branch NoSQL (pdv-agiliza #11 — Bug 11 Fase 1) ─────────────────────────
|
|
686
|
+
// Engines schemaless não passam por drizzle-kit nem Postgres efêmero.
|
|
687
|
+
// O Core registra o snapshot de fingerprint em `product_db_schema_snapshots/{id}`.
|
|
688
|
+
if (NOSQL_ENGINES.includes(dbEntry.engine)) {
|
|
689
|
+
const nosqlSpinner = opts.json
|
|
690
|
+
? null
|
|
691
|
+
: ora({ text: 'Aplicando schema NoSQL (sem rehearse — engine schemaless)…', color: 'blue' }).start();
|
|
692
|
+
try {
|
|
693
|
+
const nosqlResult = await runNoSqlApply({
|
|
694
|
+
dbId,
|
|
695
|
+
productId: dbEntry.productId,
|
|
696
|
+
engine: dbEntry.engine,
|
|
697
|
+
schemaPath: schemaAbs,
|
|
698
|
+
env: canonicalEnv,
|
|
699
|
+
dryRun: opts.dryRun,
|
|
700
|
+
});
|
|
701
|
+
nosqlSpinner?.stop();
|
|
702
|
+
// H-7 compat: persiste fingerprint pós-apply NoSQL também.
|
|
703
|
+
if (!opts.dryRun && nosqlResult.fingerprint) {
|
|
704
|
+
const dbJsonAfter = readDbJson(dbJsonPath);
|
|
705
|
+
const updatedDatabases = dbJsonAfter.databases.map((d) => d.name === dbEntry.name
|
|
706
|
+
? { ...d, lastAppliedFingerprint: nosqlResult.fingerprint }
|
|
707
|
+
: d);
|
|
708
|
+
writeDbJson(dbJsonPath, { ...dbJsonAfter, databases: updatedDatabases });
|
|
709
|
+
if (!opts.json)
|
|
710
|
+
log.dim(` Fingerprint gravado em ${chalk.bold(DB_JSON_REL)}`);
|
|
711
|
+
}
|
|
712
|
+
if (opts.json) {
|
|
713
|
+
console.log(JSON.stringify({
|
|
714
|
+
ok: true,
|
|
715
|
+
status: nosqlResult.status,
|
|
716
|
+
engine: nosqlResult.engine,
|
|
717
|
+
snapshotId: nosqlResult.snapshotId,
|
|
718
|
+
fingerprint: nosqlResult.fingerprint,
|
|
719
|
+
dryRun: nosqlResult.dryRun,
|
|
720
|
+
}));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (nosqlResult.status === 'dry_run') {
|
|
724
|
+
log.warn(' Dry-run concluído — nenhum snapshot gravado no Core.');
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
log.success(`Schema NoSQL registrado no Core${nosqlResult.snapshotId ? ` — snapshot ${chalk.bold(nosqlResult.snapshotId)}` : ''}.`);
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
nosqlSpinner?.fail('Apply NoSQL falhou.');
|
|
733
|
+
if (opts.json) {
|
|
734
|
+
jsonError(err.message ?? String(err), 'nosql_apply_failed');
|
|
735
|
+
process.exit(5);
|
|
736
|
+
}
|
|
737
|
+
handleApiError(err);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// Executa o pipeline SQL.
|
|
449
742
|
const phaseLabels = {
|
|
450
743
|
fingerprint: 'Calculando fingerprint…',
|
|
451
744
|
generate: 'Gerando SQL (drizzle-kit)…',
|
|
@@ -453,12 +746,13 @@ export async function runDbApply(opts = {}) {
|
|
|
453
746
|
classify: 'Classificando migração…',
|
|
454
747
|
push: 'Enviando ao Core…',
|
|
455
748
|
};
|
|
456
|
-
let spinner = ora({ text: 'Iniciando pipeline…', color: 'blue' }).start();
|
|
749
|
+
let spinner = opts.json ? null : ora({ text: 'Iniciando pipeline…', color: 'blue' }).start();
|
|
457
750
|
let result;
|
|
458
751
|
try {
|
|
459
752
|
const deps = buildPipelineDeps({
|
|
460
753
|
onPhase: (phase) => {
|
|
461
|
-
spinner
|
|
754
|
+
if (spinner)
|
|
755
|
+
spinner.text = phaseLabels[phase] ?? phase;
|
|
462
756
|
},
|
|
463
757
|
...(opts.dryRun
|
|
464
758
|
? {
|
|
@@ -474,33 +768,55 @@ export async function runDbApply(opts = {}) {
|
|
|
474
768
|
env: canonicalEnv,
|
|
475
769
|
dbId,
|
|
476
770
|
}, deps);
|
|
477
|
-
spinner
|
|
771
|
+
spinner?.stop();
|
|
478
772
|
}
|
|
479
773
|
catch (err) {
|
|
480
|
-
spinner
|
|
774
|
+
spinner?.fail('Pipeline falhou.');
|
|
481
775
|
if (isPipelineError(err)) {
|
|
482
776
|
const pe = err;
|
|
483
|
-
|
|
777
|
+
if (opts.json) {
|
|
778
|
+
jsonError(`Fase "${pe.phase}": ${pe.message}`, 'pipeline_failed');
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
log.error(`Fase "${pe.phase}": ${pe.message}`);
|
|
782
|
+
}
|
|
484
783
|
process.exit(5);
|
|
485
784
|
return;
|
|
486
785
|
}
|
|
487
|
-
|
|
786
|
+
if (opts.json) {
|
|
787
|
+
jsonError(err.message ?? String(err), 'pipeline_failed');
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
log.error(err.message ?? String(err));
|
|
791
|
+
}
|
|
488
792
|
process.exit(5);
|
|
489
793
|
return;
|
|
490
794
|
}
|
|
491
|
-
|
|
492
|
-
log.success('Schema sem alterações — nenhuma migração necessária.');
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
// H-7 fix: persiste o fingerprint após push bem-sucedido (não dry-run) para
|
|
496
|
-
// que o gate de deploy (`checkSchemaGate` em deploy.ts) consiga detectar drift.
|
|
795
|
+
// H-7 fix: persiste o fingerprint após push bem-sucedido (não dry-run).
|
|
497
796
|
if (!opts.dryRun && result.fingerprint) {
|
|
498
797
|
const dbJsonAfter = readDbJson(dbJsonPath);
|
|
499
798
|
const updatedDatabases = dbJsonAfter.databases.map((d) => d.name === dbEntry.name
|
|
500
799
|
? { ...d, lastAppliedFingerprint: result.fingerprint }
|
|
501
800
|
: d);
|
|
502
801
|
writeDbJson(dbJsonPath, { ...dbJsonAfter, databases: updatedDatabases });
|
|
503
|
-
|
|
802
|
+
if (!opts.json)
|
|
803
|
+
log.dim(` Fingerprint gravado em ${chalk.bold(DB_JSON_REL)}`);
|
|
804
|
+
}
|
|
805
|
+
// gestovendas #9: saída JSON uniforme no caminho de sucesso.
|
|
806
|
+
if (opts.json) {
|
|
807
|
+
console.log(JSON.stringify({
|
|
808
|
+
ok: true,
|
|
809
|
+
status: result.status,
|
|
810
|
+
migrationId: result.migrationId ?? null,
|
|
811
|
+
severity: result.severity ?? null,
|
|
812
|
+
fingerprint: result.fingerprint ?? null,
|
|
813
|
+
dryRun: !!opts.dryRun,
|
|
814
|
+
}));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (result.status === 'no_changes') {
|
|
818
|
+
log.success('Schema sem alterações — nenhuma migração necessária.');
|
|
819
|
+
return;
|
|
504
820
|
}
|
|
505
821
|
log.dim('');
|
|
506
822
|
log.dim(` Severidade: ${severityColor(result.severity)}`);
|