feat(W4): daily full reconciliation — user/role drift repair + ReconcileLogs audit
- w4-body.js: ONE-DIRECTIONAL sync (NocoDB status = SOT, never reverse);
drift matrix: missing user/role (provision w/ username lookup to avoid
duplicates), dangling refs (adopt/recreate), disabled<->status, canonical
base_path '/' + permission 16384, scopes vs calculateEffectiveGrants,
username drift detect+log_only; verify-after-repair on every change;
password never pushed on repair (方案 C); dry-run via ?dryRun=true
- gen_w4.js: daily 03:00 cron + webhook alist-w4-reconcile; API-import-safe
(UUID node ids, no pinData, webhookId)
- ReconcileLogs table mktsb7le6fqr2u1 (add_reconcilelogs_table.py, idempotent,
v2 API); FK column nc_jitu___Customers_id for bt-link writes (v2 ignores
link column itself)
- test_w4_reconcile.js: 34 assertions / 12 mock worlds, ALL PASSED
- live-verified against production: dry-run (3 drifts + 1 error found),
real repair (3 repaired, verify all passed, 4 log rows), idempotent rerun
- NocoDB 2026.08.x pitfalls documented (v1 col endpoints gone, row delete
via body [{Id}]); n8n API import/activation pitfalls (activeVersionId)
- docs: W4-DESIGN.md new; DATABASE_SCHEMA 8 tables; N8N_WORKFLOWS, SETUP,
PROJECT_STATE updated
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// Live dry-run of the W4 Code node against REAL AList + NocoDB, outside n8n.
|
||||
// Runs the exact inlined jsCode from W4-daily-reconcile.json in a vm sandbox,
|
||||
// with helpers.httpRequest backed by node fetch. ZERO writes (dryRun=true).
|
||||
// Usage: node scripts/live_dry_run_w4.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const wf = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'n8n', 'W4-daily-reconcile.json'), 'utf8'));
|
||||
const code = wf.nodes.find(n => n.name === 'Reconcile & Repair (Daily Full)').parameters.jsCode;
|
||||
|
||||
const ENV = {
|
||||
NOCODB_URL: 'https://nocodb.hoelee.com',
|
||||
NOCODB_TOKEN: process.env.NOCODB_TOKEN, // from env, never hardcoded — same for passwords below
|
||||
NOCODB_TABLE_CUSTOMERS: 'maqb9zgvza2te3k',
|
||||
NOCODB_TABLE_PRODUCTS: 'molyb3pxv81l75g',
|
||||
NOCODB_TABLE_RESOURCES: 'm6cm7tkxzd8m18c',
|
||||
NOCODB_TABLE_CUSTOMERPRODUCTS: 'mvyqbv8ao7u6bct',
|
||||
NOCODB_TABLE_ACCESSGRANTS: 'mxdx3yhfcyej3ao',
|
||||
NOCODB_TABLE_RECONCILELOGS: 'mktsb7le6fqr2u1',
|
||||
NOCODB_FK_RECONCILELOG_CUSTOMER: 'nc_jitu___Customers_id',
|
||||
NOCODB_LINK_PROD_RESOURCES: 'c4ko4x8kwi16swg',
|
||||
ALIST_URL: process.env.ALIST_URL || 'http://192.168.1.1:5244', // DSM host port (container DNS only inside bridge)
|
||||
ALIST_ADMIN_USERNAME: process.env.ALIST_ADMIN_USERNAME || 'hoelee',
|
||||
ALIST_ADMIN_PASSWORD: process.env.ALIST_ADMIN_PASSWORD, // from env
|
||||
W4_DRY_RUN: 'false',
|
||||
};
|
||||
if (!ENV.NOCODB_TOKEN || !ENV.ALIST_ADMIN_PASSWORD) { console.error('NOCODB_TOKEN + ALIST_ADMIN_PASSWORD env required'); process.exit(1); }
|
||||
|
||||
const httpRequest = async (opts) => {
|
||||
const { method = 'GET', url, headers = {}, body } = opts;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
let parsed = null;
|
||||
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const sandbox = {
|
||||
$env: ENV,
|
||||
$json: { query: { dryRun: 'true' } },
|
||||
console,
|
||||
};
|
||||
sandbox.helpers = { httpRequest };
|
||||
|
||||
(async () => {
|
||||
const fn = vm.runInNewContext(`(async function(){\n${code}\n})`, sandbox, { filename: 'w4-body-live.js' });
|
||||
const result = await fn();
|
||||
console.log(JSON.stringify(result.json, null, 1));
|
||||
})().catch(e => { console.error('LIVE RUN FAILED:', e); process.exit(1); });
|
||||
Reference in New Issue
Block a user