|
8 | 8 | pickHeuristic, |
9 | 9 | parseM2AToken, |
10 | 10 | buildM2AFieldPath, |
| 11 | + isM2APrefix, |
| 12 | + stripM2AFieldPrefix, |
| 13 | + M2A_COLLECTION_TOKEN, |
11 | 14 | } from './displayHeuristics'; |
12 | 15 | import { resolveM2ARelation } from './resolveM2ARelation'; |
13 | 16 |
|
@@ -160,26 +163,54 @@ export function expandTokensThroughRelation( |
160 | 163 | if (isM2A) { |
161 | 164 | const m2a = resolveM2ARelation(parentCollection, fieldKey, relationsStore, fieldsStore); |
162 | 165 | if (!m2a) return []; |
163 | | - const { itemField, discriminator, allowedCollections } = m2a; |
| 166 | + const { itemField, discriminator, allowedCollections, junctionCollection } = m2a; |
164 | 167 |
|
165 | 168 | // The discriminator is always needed so the renderer knows which target |
166 | 169 | // collection each row points at before resolving per-collection tokens. |
167 | 170 | const expanded: string[] = [`${fieldKey}.${discriminator}`]; |
168 | | - for (const tok of tokens) { |
169 | | - if (tok === discriminator) continue; |
170 | | - // Per-collection M2A token: "item:collection.path". Bare tokens are dropped |
171 | | - // on purpose — they would resolve against the wrong collection and 403. |
| 171 | + const add = (path: string) => { |
| 172 | + if (!expanded.includes(path)) expanded.push(path); |
| 173 | + }; |
| 174 | + |
| 175 | + for (const rawTok of tokens) { |
| 176 | + // The native picker prefixes relation tokens with the field key. A prefix |
| 177 | + // means a junction/item field; a bare token is a parent-level field. |
| 178 | + const hadPrefix = rawTok.startsWith(`${fieldKey}.`); |
| 179 | + const tok = stripM2AFieldPrefix(rawTok, fieldKey); |
| 180 | + // The discriminator (and its conventional `collection` alias) is always |
| 181 | + // emitted above; skip so it's never re-emitted as a junction/parent field. |
| 182 | + if (tok === discriminator || tok === M2A_COLLECTION_TOKEN) continue; |
| 183 | + |
| 184 | + // Per-collection item token: "item:collection.path". |
172 | 185 | const parsed = parseM2AToken(tok); |
173 | | - if (!parsed) continue; |
174 | | - const { prefix, collection: col, path } = parsed; |
175 | | - if (prefix !== itemField && prefix !== 'item') continue; |
176 | | - if (allowedCollections.length > 0 && !allowedCollections.includes(col)) continue; |
177 | | - // Only the first path segment is validated against the target — deep |
178 | | - // leaves are unvalidated, matching the M2M/M2O branches below. |
179 | | - const firstSegment = (path.split('.')[0] ?? '') as string; |
180 | | - if (!fieldsStore.getField(col, firstSegment)) continue; |
181 | | - const expandedPath = buildM2AFieldPath(fieldKey, itemField, col, path); |
182 | | - if (!expanded.includes(expandedPath)) expanded.push(expandedPath); |
| 186 | + if (parsed && isM2APrefix(parsed.prefix, fieldKey, itemField)) { |
| 187 | + const { collection: col, path } = parsed; |
| 188 | + if (allowedCollections.length > 0 && !allowedCollections.includes(col)) continue; |
| 189 | + // Only the first path segment is validated against the target — deep |
| 190 | + // leaves are unvalidated, matching the M2M/M2O branches below. |
| 191 | + const firstSegment = (path.split('.')[0] ?? '') as string; |
| 192 | + if (!fieldsStore.getField(col, firstSegment)) continue; |
| 193 | + add(buildM2AFieldPath(fieldKey, itemField, col, path)); |
| 194 | + continue; |
| 195 | + } |
| 196 | + |
| 197 | + const firstSegment = (tok.split('.')[0] ?? '') as string; |
| 198 | + if (!firstSegment) continue; |
| 199 | + |
| 200 | + if (hadPrefix) { |
| 201 | + // Junction-level field (e.g. `treatment.sort`); validate against the |
| 202 | + // junction so an unknown token never reaches the API and 403s. |
| 203 | + if (junctionCollection && fieldsStore.getField(junctionCollection, firstSegment)) { |
| 204 | + add(`${fieldKey}.${tok}`); |
| 205 | + } |
| 206 | + continue; |
| 207 | + } |
| 208 | + |
| 209 | + // Bare token → parent-level field (e.g. `code`), fetched at the top level. |
| 210 | + // Validated against the parent so a stray token is dropped, not 403'd. |
| 211 | + if (fieldsStore.getField(parentCollection, firstSegment)) { |
| 212 | + add(tok); |
| 213 | + } |
183 | 214 | } |
184 | 215 | return expanded; |
185 | 216 | } |
|
0 commit comments