- 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
62 lines
2.1 KiB
JavaScript
62 lines
2.1 KiB
JavaScript
// Builds n8n/W4-daily-reconcile.json — the daily full reconciliation workflow.
|
|
// Single source of truth: shared/effective-grants.js (inlined) + w4-body.js.
|
|
// Run: node n8n/gen_w4.js
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { sharedSource } = require('./inline_shared');
|
|
|
|
let body = fs.readFileSync(path.join(__dirname, 'w4-body.js'), 'utf8');
|
|
body = body.replace('// __SHARED_INLINE__ (replaced by shared/effective-grants.js source at build)', sharedSource());
|
|
|
|
// n8n API import (POST /api/v1/workflows) strictly validates: node ids must be
|
|
// UUIDs and top-level must NOT carry pinData (class-validator whitelist).
|
|
const uuid = () => require('crypto').randomUUID();
|
|
const node = (o) => Object.assign({ id: uuid() }, o);
|
|
|
|
const workflow = {
|
|
name: 'W4 - Daily Full Reconciliation (AList Drift Repair)',
|
|
nodes: [
|
|
node({
|
|
parameters: {
|
|
rule: {
|
|
interval: [{ field: 'cronExpression', expression: '0 3 * * *' }]
|
|
}
|
|
},
|
|
name: 'Daily 03:00',
|
|
type: 'n8n-nodes-base.scheduleTrigger',
|
|
typeVersion: 1.2,
|
|
position: [-420, 0]
|
|
}),
|
|
node({
|
|
parameters: {
|
|
httpMethod: 'POST',
|
|
path: 'alist-w4-reconcile',
|
|
options: {}
|
|
},
|
|
name: 'Manual / Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 2,
|
|
webhookId: 'alist-w4-reconcile', // required: without it the production URL never registers
|
|
position: [-420, 240]
|
|
}),
|
|
node({
|
|
parameters: { jsCode: body },
|
|
name: 'Reconcile & Repair (Daily Full)',
|
|
type: 'n8n-nodes-base.code',
|
|
typeVersion: 2,
|
|
position: [0, 120]
|
|
})
|
|
],
|
|
connections: {
|
|
'Daily 03:00': {
|
|
main: [[{ node: 'Reconcile & Repair (Daily Full)', type: 'main', index: 0 }]]
|
|
},
|
|
'Manual / Webhook': {
|
|
main: [[{ node: 'Reconcile & Repair (Daily Full)', type: 'main', index: 0 }]]
|
|
}
|
|
},
|
|
settings: { executionOrder: 'v1' }
|
|
};
|
|
|
|
fs.writeFileSync(path.join(__dirname, 'W4-daily-reconcile.json'), JSON.stringify(workflow, null, 2) + '\n');
|
|
console.log('wrote W4-daily-reconcile.json (' + body.length + ' chars body)'); |