Files
alist-serve-resource/n8n/w2-compute.js
T
hoelee 266a3442c8 fix(W2): graceful skip when customer link not yet filled (no crash)
When CP record is drafted with product set before customer,
the webhook fires with customerId=null -> now returns
skipWriteBack=true (routed to No-op) instead of throwing.
Record re-fires webhook properly once customer field is set.
2026-08-23 02:41:54 +08:00

115 lines
3.7 KiB
JavaScript

// W2 core computation: effective desired grants + diff vs AList.
// Uses this.helpers.httpRequest (n8n Code node) for all HTTP.
// Config read via process.env (n8n $env proxy is unreliable for custom vars).
const NC = process.env.NOCODB_URL;
const NC_TOKEN = process.env.NOCODB_TOKEN;
const ALIST = process.env.ALIST_URL;
const T_CUSTPROD = process.env.NOCODB_TABLE_CUSTOMERPRODUCTS;
const T_PROD = process.env.NOCODB_TABLE_PRODUCTS;
const T_CUST = process.env.NOCODB_TABLE_CUSTOMERS;
const LNK_PROD_RES = process.env.NOCODB_LINK_PROD_RESOURCES; // c4ko4x8kwi16swg
async function ncGet(path) {
return await this.helpers.httpRequest({
method: 'GET',
url: NC + path,
headers: { 'xc-token': NC_TOKEN },
json: true,
});
}
async function alistReq(method, path, body, token) {
return await this.helpers.httpRequest({
method,
url: ALIST + path,
headers: token ? { Authorization: token } : {},
body,
json: true,
});
}
function linkId(v) {
if (v == null) return null;
if (typeof v === 'object' && !Array.isArray(v)) return v.Id ?? v.id ?? null;
if (Array.isArray(v)) return v[0]?.Id ?? v[0]?.id ?? null;
return v;
}
const custProdId = $json.customerProductId;
// 1. re-fetch the CP record
const cp = await ncGet(`/api/v2/tables/${T_CUSTPROD}/records/${custProdId}`);
const customerId = linkId(cp.customer);
if (!customerId) {
// Gracefully skip when customer link not yet filled (CP record is being drafted).
// The webhook will re-fire when customer is set later.
return { json: { skipWriteBack: true, customerProductId: custProdId, reason: 'no customer link yet' } };
}
// 2. load all active CPs for this customer
const cps = await ncGet(`/api/v2/tables/${T_CUSTPROD}/records?where=(customer,eq,${customerId})&limit=1000`);
const active = (cps.list || []).filter(r => {
if (r.status && r.status !== 'active') return false;
return true;
});
// 3. build resource path -> max expires_at
const effective = {};
for (const c of active) {
const pid = linkId(c.product);
if (!pid) continue;
const resList = await ncGet(`/api/v2/tables/${T_PROD}/links/${LNK_PROD_RES}/records/${pid}`);
const exp = c.expires_at; // 'YYYY-MM-DD'
for (const res of (resList.list || [])) {
const path = res.alist_path;
if (!path) continue;
const cur = effective[path];
if (!cur || !cur.expires || (exp && exp > cur.expires)) {
effective[path] = { expires: exp || null, permission: 0 };
}
}
}
// 4. admin login
const login = await alistReq('POST', '/api/auth/login', {
username: process.env.ALIST_ADMIN_USERNAME,
password: process.env.ALIST_ADMIN_PASSWORD,
}, null);
const token = login?.data?.token;
if (!token) throw new Error('AList login failed');
// customer's role id
const cust = await ncGet(`/api/v2/tables/${T_CUST}/records/${customerId}`);
const roleId = cust.alist_role_id;
let currentScopes = [];
if (roleId) {
const role = await alistReq('GET', `/api/admin/role/get?id=${roleId}`, null, token);
currentScopes = role?.data?.permission_scopes ?? [];
}
const desiredPaths = Object.keys(effective);
const currentPaths = currentScopes.map(s => s.path);
const toAdd = desiredPaths.filter(p => !currentPaths.includes(p));
const toRemove = currentPaths.filter(p => !desiredPaths.includes(p));
let roleName = null, roleDesc = null;
if (roleId) {
const role = await alistReq('GET', `/api/admin/role/get?id=${roleId}`, null, token);
roleName = role?.data?.name;
roleDesc = role?.data?.description;
}
return {
json: {
customerId,
roleId,
roleName,
roleDesc,
token,
effective,
desiredPaths,
currentScopes,
diff: { toAdd, toRemove },
newScopes: desiredPaths.map(p => ({ path: p, permission: effective[p].permission })),
},
};