Files
hoelee 9bcdfc2049 fix(W1): self-write-back suppression via old/new field comparison
- Replace brittle sync_status==='synced' loop guard with NocoDB v2
  old/new row comparison (previous_rows vs rows).
- businessChanged() = business field move OR sync_required set; echo
  (bookkeeping-only delta) -> skip_writeback_echo, zero AList mutation.
- writeBack() value-change guard: GET live record, PATCH only fields
  whose value actually changes (no-change -> no PATCH -> no webhook).
- sync_required Checkbox honored as force-reprocess flag, cleared on write-back.
- Extract-id node passes previousRow/newRow through Load Env to lifecycle.
- W1 lifecycle test now covers echo suppression (26 assertions).
- Customers.status still NOT consumed by permission calc (TBD semantics).
- W2/W3 effective-grants semantics unchanged.
2026-08-23 06:43:01 +08:00

68 lines
3.4 KiB
JavaScript

// Builds n8n/W1-customer-provision.json — Customer webhook with status lifecycle.
// Single source of truth: shared/effective-grants.js (inlined) + w1-body.js.
// Run: node n8n/gen_w1.js
const fs = require('fs');
const path = require('path');
const { sharedSource } = require('./inline_shared');
let body = fs.readFileSync(path.join(__dirname, 'w1-body.js'), 'utf8');
body = body.replace('// __SHARED_INLINE__ (replaced by shared/effective-grants.js source at build)', sharedSource());
const workflow = {
name: 'W1 - Customer Provision & Status Lifecycle',
nodes: [
{
parameters: {
httpMethod: 'POST',
path: 'alist-customer-provision',
responseMode: 'onReceived',
options: {}
},
id: 'trigger-webhook',
name: 'NocoDB: Customer Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [0, 0],
webhookId: 'customer-provision'
},
{
parameters: {
jsCode: "const body = $input.item.json.body ?? $input.item.json;\nconst data = body.data ?? {};\n// NocoDB v2: data.rows = post-update rows, data.previous_rows = pre-update rows.\nconst newRows = data.rows ?? data.rows_new ?? [];\nconst oldRows = data.previous_rows ?? data.rows_old ?? [];\nconst newRow = newRows[0] ?? {};\nconst oldRow = oldRows[0] ?? {};\nconst id = newRow.Id ?? oldRow.Id;\nif (!id) throw new Error('no customer Id in webhook');\nreturn { json: { customerId: Number(id), eventType: body.type ?? '', previousRow: oldRow, newRow } };\n"
},
id: 'extract-id',
name: 'Extract Customer Record Id',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [220, 0]
},
{
parameters: {
jsCode: "// pass-through env + webhook fields (custom env vars readable ONLY via $env here)\nconst prev = $input.item.json;\nreturn { json: {\n customerId: prev.customerId,\n eventType: prev.eventType,\n previousRow: prev.previousRow,\n newRow: prev.newRow,\n NOCODB_URL: $env.NOCODB_URL,\n NOCODB_TOKEN: $env.NOCODB_TOKEN,\n NOCODB_TABLE_CUSTOMERS: $env.NOCODB_TABLE_CUSTOMERS,\n NOCODB_TABLE_CUSTOMERPRODUCTS: $env.NOCODB_TABLE_CUSTOMERPRODUCTS,\n NOCODB_TABLE_PRODUCTS: $env.NOCODB_TABLE_PRODUCTS,\n NOCODB_TABLE_RESOURCES: $env.NOCODB_TABLE_RESOURCES,\n NOCODB_TABLE_ACCESSGRANTS: $env.NOCODB_TABLE_ACCESSGRANTS,\n NOCODB_LINK_PROD_RESOURCES: $env.NOCODB_LINK_PROD_RESOURCES,\n ALIST_URL: $env.ALIST_URL,\n ALIST_ADMIN_USERNAME: $env.ALIST_ADMIN_USERNAME,\n ALIST_ADMIN_PASSWORD: $env.ALIST_ADMIN_PASSWORD,\n} };\n"
},
id: 'load-env',
name: 'Load Env',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [220, 0]
},
{
parameters: { jsCode: body },
id: 'lifecycle',
name: 'Provision & Apply Lifecycle',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [440, 0]
}
],
connections: {
'NocoDB: Customer Webhook': { main: [[{ node: 'Extract Customer Record Id', type: 'main', index: 0 }]] },
'Extract Customer Record Id': { main: [[{ node: 'Load Env', type: 'main', index: 0 }]] },
'Load Env': { main: [[{ node: 'Provision & Apply Lifecycle', type: 'main', index: 0 }]] }
},
settings: { executionOrder: 'v1' },
pinData: {}
};
fs.writeFileSync(path.join(__dirname, 'W1-customer-provision.json'), JSON.stringify(workflow, null, 2) + '\n');
console.log('wrote W1-customer-provision.json (' + body.length + ' chars body)');