fix(shared): link-field where query must use (customer.Id,eq,N) not (customer,eq,N)

- calculateEffectiveGrants queried CustomerProducts/AccessGrants with
  (customer,eq,N), which matches the link field's DISPLAY NAME and always
  returns 0 rows -> desiredPaths always [] -> W1/W2/W3 write empty scopes.
- Regression from the shared-lib refactor (3ecee30): W2 had already fixed
  this in 36a6b3b but the shared extract reverted to the old syntax.
- Correct syntax (customer.Id,eq,N) verified against live NocoDB for
  customer 6 -> /Class/Health, /Class/Other, /Template/Music.
- Updated all three test mocks to match the corrected where clause.
This commit is contained in:
2026-08-23 06:55:04 +08:00
parent 9bcdfc2049
commit 21e3a3b45f
7 changed files with 14 additions and 11 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -76,7 +76,10 @@ async function calculateEffectiveGrants(ctx, customerId) {
// (a) CustomerProducts -> Resources (via product M2M link), MAX expiry on
// the same path.
const cps = (await ncGet(ctx, `/api/v2/tables/${T_CUSTPROD}/records?where=(customer,eq,${customerId})&limit=1000`)).list || [];
// NOTE: `(customer,eq,N)` matches the link field's DISPLAY NAME and always
// returns 0 rows. Must use the `customer.Id` sub-field syntax (or the
// foreign-key column `nc_jitu___Customers_id`).
const cps = (await ncGet(ctx, `/api/v2/tables/${T_CUSTPROD}/records?where=(customer.Id,eq,${customerId})&limit=1000`)).list || [];
for (const c of cps) {
if (c.status && c.status !== 'active') continue;
if (c.expires_at && String(c.expires_at) <= today) continue; // expired (incl. today)
@@ -95,7 +98,7 @@ async function calculateEffectiveGrants(ctx, customerId) {
}
// (b) AccessGrants -> Resources (manual / materialised direct grants)
const grants = (await ncGet(ctx, `/api/v2/tables/${T_GRANT}/records?where=(customer,eq,${customerId})&limit=1000`)).list || [];
const grants = (await ncGet(ctx, `/api/v2/tables/${T_GRANT}/records?where=(customer.Id,eq,${customerId})&limit=1000`)).list || [];
for (const g of grants) {
if (g.status && g.status !== 'active') continue;
if (g.expires_at && String(g.expires_at) <= today) continue;
+2 -2
View File
@@ -64,11 +64,11 @@ function makeCtx() {
}
if (u.includes('/tr/records?limit=10000')) return { list: RESOURCES };
// CustomerProducts where customer eq
if (u.includes('/tcpc/records?where=(customer,eq,')) {
if (u.includes('/tcpc/records?where=(customer.Id,eq,')) {
return { list: CUSTOMERPRODUCTS };
}
// AccessGrants where customer eq
if (u.includes('/tag/records?where=(customer,eq,')) {
if (u.includes('/tag/records?where=(customer.Id,eq,')) {
return { list: ACCESSGRANTS };
}
// Product M2M link
+2 -2
View File
@@ -64,11 +64,11 @@ function mockHttp(opts) {
if (u.includes('/tr/records?limit=10000')) return { list: state.resources };
// CP where customer eq
const cpm = u.match(/tcpc\/records\?where=\(customer,eq,(\d+)\)/);
const cpm = u.match(/tcpc\/records\?where=\(customer\.Id,eq,(\d+)\)/);
if (cpm) return { list: state.cps.filter(c => c.customer === Number(cpm[1])) };
// grants where customer eq
const gm = u.match(/tag\/records\?where=\(customer,eq,(\d+)\)/);
const gm = u.match(/tag\/records\?where=\(customer\.Id,eq,(\d+)\)/);
if (gm) return { list: state.grants.filter(g => g.customer === Number(gm[1])) };
// product M2M
+2 -2
View File
@@ -54,9 +54,9 @@ sandbox.helpers = {
if (u.endsWith('/api/auth/login')) return { data: { token: 'ADMIN_TOKEN' } };
if (u.includes('/tr/records?limit=10000')) return { list: RESOURCES };
if (u.includes('/tc/records?limit=10000')) return { list: CUSTOMERS };
const cpm = u.match(/tcpc\/records\?where=\(customer,eq,(\d+)\)/);
const cpm = u.match(/tcpc\/records\?where=\(customer\.Id,eq,(\d+)\)/);
if (cpm) return { list: CPS[Number(cpm[1])] || [] };
const gm = u.match(/tag\/records\?where=\(customer,eq,(\d+)\)/);
const gm = u.match(/tag\/records\?where=\(customer\.Id,eq,(\d+)\)/);
if (gm) return { list: GRANTS[Number(gm[1])] || [] };
const pm = u.match(/tp\/links\/lnk\/records\/(\d+)/);
if (pm) return { list: PRODUCT_RES[Number(pm[1])] || [] };