toga-ai 1.0.374 → 1.0.376
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +0 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/client-api-fetch-fields.md +136 -0
- package/knowledge/2.0/apps/toga25-supply/features/client-configurable-fields.md +14 -0
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +0 -4
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
- package/knowledge/2.0/apps/_underscore/architecture/external-identifiers-map-table.md +0 -97
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
-
| [Proposed — ExternalIdentifiers map table for integration id ↔ external-system linkage](architecture/external-identifiers-map-table.md) | A **proposal / counter-proposal (status: proposed — nothing here is built)** that cross-system identifier linkage (e.g. | _underscore/Model/Forecast/Opportunity.php, worker2/Worker/Netsuite/Opportunity.php |
|
|
6
5
|
| [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
|
|
7
6
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
8
7
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts |
|
|
6
6
|
| [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
7
|
+
| [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
|
|
7
8
|
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
|
|
8
9
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
|
|
9
10
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Client API-Fetch Fields (useClientApiFields / apiFields.json)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga25-supply
|
|
5
|
+
project: TOGa 2.5 Supply
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-supply/src/fieldsConfig/resolveApiConfig.ts
|
|
13
|
+
- toga25-supply/src/fieldsConfig/useClientApiFields.ts
|
|
14
|
+
- toga25-supply/src/fieldsConfig/index.ts
|
|
15
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json
|
|
16
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx
|
|
17
|
+
- toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json
|
|
18
|
+
- toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx
|
|
19
|
+
related:
|
|
20
|
+
- client-configurable-fields.md
|
|
21
|
+
- surface-frontend.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What it is
|
|
25
|
+
|
|
26
|
+
The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` /
|
|
27
|
+
`join` / `where` to request from the TOGa API for a given page — from **one colocated
|
|
28
|
+
`apiFields.json` file per page**, keyed by client slug. It is the fetch-projection sibling of
|
|
29
|
+
[client-configurable-fields](client-configurable-fields.md) (`useClientFields`): that hook
|
|
30
|
+
resolves **presentation** config (now largely moving to the DB-backed
|
|
31
|
+
[Surface layer](surface-frontend.md)); this hook resolves **what data to fetch**.
|
|
32
|
+
|
|
33
|
+
The split exists because presentation config migrated to Surface, leaving the API-fetch wiring
|
|
34
|
+
coupled to a now-legacy `orderViewFields`/`*RecordViewFields` blob (formerly the
|
|
35
|
+
`salesOrderDetailsConfig` / `itemRecordDetailsConfig` keys). This mechanism extracts the fetch
|
|
36
|
+
projection into its own scalable per-page file + a shared resolver, keeping the central
|
|
37
|
+
`fieldsConfig/index.ts` registry lean — **each page owns its own `apiFields.json`** rather than
|
|
38
|
+
folding every page's fetch config into `fieldsConfig/index.ts`.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
Two new files in `src/fieldsConfig/`, re-exported from `fieldsConfig/index.ts`:
|
|
43
|
+
|
|
44
|
+
1. **`apiFields.json` (per page, colocated in the page's `viewModel/FIELDS/`).** A map keyed by
|
|
45
|
+
client slug (uppercase — `DEFAULT`, `COMPASS`, `COMPASSCANADA`, `NYCHH`, …). The `DEFAULT`
|
|
46
|
+
block is the **full base**. Each per-client block is one of two shapes:
|
|
47
|
+
- **Delta** — `{ "extends": "DEFAULT", ...only the differences }`, merged over its parent.
|
|
48
|
+
Use for the common 1–3 field tweak; adding a field to `DEFAULT` then reaches every client.
|
|
49
|
+
- **Full override** — a complete config with **no `extends`**, returned verbatim. Use when a
|
|
50
|
+
client diverges substantially (many fields / different joins / different `where` clauses).
|
|
51
|
+
|
|
52
|
+
2. **`resolveApiConfig(map, clientSlug)`** — a pure, recursive deep-merge resolver:
|
|
53
|
+
- **Array fields** (`fields`, `ojoin`, `join`) support `add<Field>` / `remove<Field>` sugar
|
|
54
|
+
(`addFields`, `removeFields`, `addOjoin`, …) to tweak the parent's array, **or** you restate
|
|
55
|
+
the array literally to replace it wholesale (a literal wins over add/remove).
|
|
56
|
+
- **Nested objects** merge recursively; **scalars / new keys** replace the parent value.
|
|
57
|
+
- **Array element identity** for dedupe/removal: strings by value, join objects by their
|
|
58
|
+
**first object key** (e.g. `{ "Items": {...} }` → `Items`), else by JSON form. Results are
|
|
59
|
+
deduped.
|
|
60
|
+
- Supports **chained `extends`** (a client can extend another client) with a **cycle guard**
|
|
61
|
+
(a re-visited slug returns its own delta un-merged). An **unknown client falls back to
|
|
62
|
+
`DEFAULT`**; a missing `DEFAULT` yields `{}`.
|
|
63
|
+
|
|
64
|
+
3. **`useClientApiFields(map)`** — reads the client slug via
|
|
65
|
+
`useHostnameStore.getState().getHostname()` (the **same** slug source as `useClientFields`),
|
|
66
|
+
then `useMemo`s `resolveApiConfig(map, slug)` so consumers get a **stable object reference**
|
|
67
|
+
and the react-query fetch cache key does not churn every render. API-fetch fields are
|
|
68
|
+
**client-only** — there is no role dimension (they describe *what to fetch*, not a per-role
|
|
69
|
+
view).
|
|
70
|
+
|
|
71
|
+
The view model consumes it directly:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const salesOrderDetailsConfig = useClientApiFields(apiFields) as Parameters<
|
|
75
|
+
typeof useFetchSalesOrderRecord
|
|
76
|
+
>[1];
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Hybrid dedup model (decision)
|
|
80
|
+
|
|
81
|
+
The per-client shape is deliberately a **hybrid**: `DEFAULT` base + per-client delta, with a
|
|
82
|
+
full-override escape hatch. Chosen over full-copy-per-client because differences are usually
|
|
83
|
+
1–3 fields (the delta case, kept DRY so a field added to `DEFAULT` reaches every client) but can
|
|
84
|
+
be 5+ fields plus different joins/where clauses (the full-override case). Full-copy-per-client
|
|
85
|
+
drifts too fast across 6+ clients. The resolver location is likewise a decision: a **generic
|
|
86
|
+
colocated hook** (per-page `apiFields.json` + one shared hook), **not** folding every page's
|
|
87
|
+
fetch config into the central `fieldsConfig/index.ts`.
|
|
88
|
+
|
|
89
|
+
## Adopting the pattern on a page
|
|
90
|
+
|
|
91
|
+
1. Add `viewModel/FIELDS/apiFields.json`: a `DEFAULT` full base, plus a block per client that
|
|
92
|
+
diverges (`extends: "DEFAULT"` + `add<Field>`/`remove<Field>` deltas, or a no-`extends` full
|
|
93
|
+
override). A client whose config is byte-identical to `DEFAULT` can be **omitted** — the
|
|
94
|
+
resolver's DEFAULT fallback covers it.
|
|
95
|
+
2. Wire the view model to `useClientApiFields(apiFields)`, casting to the fetch hook's param type
|
|
96
|
+
(`Parameters<typeof useFetch...>[1]`).
|
|
97
|
+
3. Delete the now-dead fetch block from the legacy presentation JSON (single source of truth).
|
|
98
|
+
|
|
99
|
+
**Adopted so far:**
|
|
100
|
+
- **SalesOrder record modal** — `SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json`
|
|
101
|
+
(DEFAULT full base; COMPASS / COMPASSCANADA as `extends: DEFAULT` + `addFields` deltas, e.g.
|
|
102
|
+
`_totalLease`, `c_erpEntityId`). The view model's `salesOrderDetailsConfig` now comes from
|
|
103
|
+
`useClientApiFields(apiFields)` instead of `tenantFields.salesOrderDetailsConfig`; the dead
|
|
104
|
+
`salesOrderDetailsConfig` block was removed from `FIELDS/DEFAULT/orderViewFields.json`.
|
|
105
|
+
- **Item record modal** — `ItemRecordModalLayout/viewModel/FIELDS/apiFields.json` (DEFAULT full
|
|
106
|
+
base of 31 fields; COMPASS + COMPASSCANADA as `extends: DEFAULT` + a 6-field `addFields` delta:
|
|
107
|
+
`_leasePrice`, `_leasePrice12Months`, 4× `personaItems.*`; **NYCHH omitted** — byte-identical to
|
|
108
|
+
DEFAULT). The view model consumes `useClientApiFields(apiFields)` (dropping its own
|
|
109
|
+
`useClientFields`; view sections still read `useClientFields` in the view components). The dead
|
|
110
|
+
`itemRecordDetailsConfig` block was removed from all four client
|
|
111
|
+
`itemRecordViewFields.json` files, which now hold only presentation keys
|
|
112
|
+
(`header`/`sections`/`editItem`).
|
|
113
|
+
|
|
114
|
+
## Gotchas
|
|
115
|
+
|
|
116
|
+
- **Field order in a `fields` projection array is irrelevant to the TOGa API fetch.** The API
|
|
117
|
+
returns requested fields regardless of order, and the reshape/view logic reads by key — so
|
|
118
|
+
append-style delta merges (added fields land at the end) are **safe** and do not change
|
|
119
|
+
behavior. This is why `add<Field>` sugar can append without needing positional control.
|
|
120
|
+
- **Omit a client that matches `DEFAULT`.** Don't author a byte-identical client block — the
|
|
121
|
+
resolver falls back to `DEFAULT` for any unknown/absent client, so an identical block is pure
|
|
122
|
+
drift risk (NYCHH on the Item modal is deliberately absent).
|
|
123
|
+
- **Same slug source as presentation.** The slug comes from `useHostnameStore.getHostname()` (the
|
|
124
|
+
same source as `useClientFields`); don't introduce a second slug resolution path.
|
|
125
|
+
|
|
126
|
+
## Change history
|
|
127
|
+
- 2026-07-20 — Built the client-based API-fetch fields mechanism: `resolveApiConfig` (pure
|
|
128
|
+
recursive deep-merge with `add`/`remove` array sugar, literal-replace, chained `extends` +
|
|
129
|
+
cycle guard, DEFAULT fallback) + `useClientApiFields` (client-only, slug via `useHostnameStore`,
|
|
130
|
+
memoized for stable fetch cache keys), each page owning a colocated `apiFields.json`. Adopted on
|
|
131
|
+
the SalesOrder and Item record modals; removed the dead `salesOrderDetailsConfig` /
|
|
132
|
+
`itemRecordDetailsConfig` fetch blocks from the legacy presentation JSON. Decision: hybrid
|
|
133
|
+
DEFAULT-base + per-client-delta with a full-override escape hatch (over full-copy-per-client).
|
|
134
|
+
Discovered: `fields`-array order is irrelevant to the API fetch. (apeterson)
|
|
135
|
+
</content>
|
|
136
|
+
</invoke>
|
|
@@ -22,6 +22,7 @@ related:
|
|
|
22
22
|
- ../architecture.md
|
|
23
23
|
- column-visibility.md
|
|
24
24
|
- action-button-rule-engine.md
|
|
25
|
+
- client-api-fetch-fields.md
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
## What it is
|
|
@@ -31,6 +32,14 @@ filter buttons, group-by options, column pickers, layout toggles — without har
|
|
|
31
32
|
the view model or rebuilding per client. All such config resolves through `src/fieldsConfig`
|
|
32
33
|
and the `useClientFields()` hook.
|
|
33
34
|
|
|
35
|
+
> **Scope — presentation, not fetch.** This doc covers **presentation/UI** config. A client's
|
|
36
|
+
> **API-fetch projection** (which `fields`/`ojoin`/`join`/`where` to request) now lives in a
|
|
37
|
+
> separate per-page `apiFields.json` resolved by `useClientApiFields` — see
|
|
38
|
+
> [client-api-fetch-fields](client-api-fetch-fields.md). The old fetch keys
|
|
39
|
+
> (`salesOrderDetailsConfig` in `orderViewFields`, `itemRecordDetailsConfig` in
|
|
40
|
+
> `itemRecordViewFields`) were **extracted out** of these presentation JSON files; those files
|
|
41
|
+
> now hold presentation keys only.
|
|
42
|
+
|
|
34
43
|
## How it works
|
|
35
44
|
|
|
36
45
|
1. **Per-client JSON lives next to the consuming page _or layout_**, under `FIELDS/<CLIENT>/`
|
|
@@ -159,6 +168,11 @@ modal's chrome from the same per-client JSON, not from the component:
|
|
|
159
168
|
`vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
|
|
160
169
|
|
|
161
170
|
## Change history
|
|
171
|
+
- 2026-07-20 — Extracted the **API-fetch projection** out of these presentation JSON blobs into a
|
|
172
|
+
new per-page `apiFields.json` + `useClientApiFields` mechanism (see
|
|
173
|
+
[client-api-fetch-fields](client-api-fetch-fields.md)). The `salesOrderDetailsConfig` block was
|
|
174
|
+
removed from `FIELDS/DEFAULT/orderViewFields.json` and the `itemRecordDetailsConfig` block from
|
|
175
|
+
all four `itemRecordViewFields.json` files; those files now hold presentation keys only. (apeterson)
|
|
162
176
|
- 2026-07-01 — De-roled `orderViewFields` from client×role to **client-only**: COMPASS/COMPASSCANADA
|
|
163
177
|
now read a flat client JSON via `mergeOrderView(...)` in `fieldsConfig/index.ts` (COMPASSCANADA's
|
|
164
178
|
JSON flattened from byte-identical `{ADMIN,MANAGER}`). Fixed a latent bug where COMPASS's
|
|
@@ -31,7 +31,6 @@ files:
|
|
|
31
31
|
related:
|
|
32
32
|
- ./netsuite-salesorder-open-orders-sync.md
|
|
33
33
|
- ../architecture.md
|
|
34
|
-
- ../../_underscore/architecture/external-identifiers-map-table.md
|
|
35
34
|
---
|
|
36
35
|
|
|
37
36
|
## Summary
|
|
@@ -178,9 +177,6 @@ tasks. The fix is a **race-safe DB claim** on the **existing `Forecast.Opportuni
|
|
|
178
177
|
`NetsuiteOpportunityClickupTask` table in `Client_True`; reviewer Rohan Girish rejected that and mandated
|
|
179
178
|
reusing `Forecast.Opportunities`.)
|
|
180
179
|
|
|
181
|
-
A counter-proposal (ExternalIdentifiers map table) has been raised — see
|
|
182
|
-
`_underscore/architecture/external-identifiers-map-table.md`; unresolved.
|
|
183
|
-
|
|
184
180
|
How the claim is race-safe:
|
|
185
181
|
- **Conditional UPDATE = the atomic claim.** A raw `_Query`:
|
|
186
182
|
`UPDATE Opportunities SET clickupTaskId='<uuid sentinel>' WHERE netsuiteOpportunityInternalId=X AND
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 34 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -29,7 +29,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
29
29
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
30
30
|
- **ai-bdr** (AI-BDR) — 7 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
31
31
|
- **toga2-commerce** (TOGa Commerce) — 9 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
32
|
-
- **toga25-supply** (TOGa 2.5 Supply) —
|
|
32
|
+
- **toga25-supply** (TOGa 2.5 Supply) — 10 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
33
33
|
- **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
34
34
|
- **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
|
|
35
35
|
|
package/package.json
CHANGED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Proposed — ExternalIdentifiers map table for integration id ↔ external-system linkage
|
|
3
|
-
framework: "2.0"
|
|
4
|
-
repo: _underscore
|
|
5
|
-
project: _Underscore
|
|
6
|
-
client: shared
|
|
7
|
-
type: architecture
|
|
8
|
-
status: proposed
|
|
9
|
-
updated: 2026-07-17
|
|
10
|
-
owners: ["dfranks"]
|
|
11
|
-
files:
|
|
12
|
-
- _underscore/Model/Forecast/Opportunity.php
|
|
13
|
-
- worker2/Worker/Netsuite/Opportunity.php
|
|
14
|
-
related:
|
|
15
|
-
- ../features/model-save-vs-query-atomic-update.md
|
|
16
|
-
- ../../worker2/features/netsuite-opportunity-sync.md
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Summary
|
|
20
|
-
|
|
21
|
-
A **proposal / counter-proposal (status: proposed — nothing here is built)** that cross-system
|
|
22
|
-
identifier linkage (e.g. a NetSuite opportunity ↔ its ClickUp task id) live in a dedicated
|
|
23
|
-
**`ExternalIdentifiers` map table** rather than as ad-hoc columns bolted onto whichever business
|
|
24
|
-
table happens to be nearby. The immediate driver is the TRUE-80044 atomic ClickUp-task claim, whose
|
|
25
|
-
currently-mandated approach adds a `clickupTaskId` column to `Forecast.Opportunities`. This doc is
|
|
26
|
-
`dfranks`'s architectural counter-proposal to that mandate, raised for open discussion — **not** a
|
|
27
|
-
silent supersede of the reviewer decision. See the rebuttal section below.
|
|
28
|
-
|
|
29
|
-
**Critical framing:** This is a **proposal under open discussion**, not the current system and not a
|
|
30
|
-
settled reversal. The mandated approach on record (a `clickupTaskId` column on
|
|
31
|
-
`Forecast.Opportunities`) remains the standing decision until the disagreement is resolved with the
|
|
32
|
-
reviewer.
|
|
33
|
-
|
|
34
|
-
## Design
|
|
35
|
-
|
|
36
|
-
A single **source-generic, single-anchor** mapping table in the integration schema
|
|
37
|
-
(`Client_True` / `DB_TRUE`, **not** Forecast). A source system + its record id anchor the entity; a
|
|
38
|
-
target system + its id are the mapping. NetSuite is a `sourceSystem` *value* (`'netsuite'`), never a
|
|
39
|
-
privileged column, and there is deliberately **no** cross-system correlation key (`entityUuid`):
|
|
40
|
-
every integration asks the same single-anchor question — "for this source record, what's its
|
|
41
|
-
ClickUp/Salesforce/… id?" — which needs no id-to-id web.
|
|
42
|
-
|
|
43
|
-
- **Columns:** `id`, `entityType` (e.g. `opportunity`), `sourceSystem` (e.g. `netsuite`),
|
|
44
|
-
`sourceId` (the source record id), `targetSystem` (e.g. `clickup`), `targetExternalId` (the value;
|
|
45
|
-
`NULL` = claimed/in-flight), `dtCreated`/`dtUpdated`. No synthetic row `uuid` — `uq_claim` is the
|
|
46
|
-
natural key.
|
|
47
|
-
- **Uniqueness:** `UNIQUE (entityType, sourceSystem, sourceId, targetSystem)` = the atomic claim
|
|
48
|
-
(one mapping per source record per target system); `UNIQUE (targetSystem, entityType,
|
|
49
|
-
targetExternalId)` = no two source records share one target id (NULLs exempt, so many in-flight
|
|
50
|
-
rows coexist).
|
|
51
|
-
- **Atomic claim = one `INSERT IGNORE` + `getAffectedRows()`** — no sentinel value, no correlation
|
|
52
|
-
step: the row's presence is the claim, `targetExternalId IS NULL` marks in-flight. Raw `_Query` is
|
|
53
|
-
required because the ORM `save()` cannot express a conditional/affected-rows write — see
|
|
54
|
-
[model-save-vs-query-atomic-update](../features/model-save-vs-query-atomic-update.md). Winner
|
|
55
|
-
writes `targetExternalId` back via the model; failure path deletes the in-flight row
|
|
56
|
-
(`… AND targetExternalId IS NULL`). Because the table stands alone, the claim has **no ordering
|
|
57
|
-
dependency** on the Forecast import (unlike a column claim, which needs the opportunity row
|
|
58
|
-
upserted first).
|
|
59
|
-
- **Scales without schema change:** a new integration adds rows with a new `targetSystem` value; a
|
|
60
|
-
new source adds a new `sourceSystem` value. Keyed on the source record id in the integration
|
|
61
|
-
schema, the mapping is independent of the Forecast DB by construction.
|
|
62
|
-
|
|
63
|
-
The `entityUuid`/cross-system-correlation variant is deliberately avoided — it only pays off if the
|
|
64
|
-
*source* system (NetSuite) itself stopped being the identity anchor, which is not a concern here.
|
|
65
|
-
|
|
66
|
-
## Rebuttal to the clickupTaskId-column mandate
|
|
67
|
-
|
|
68
|
-
`2.0/apps/worker2/features/netsuite-opportunity-sync.md` records reviewer **Rohan Girish** mandating
|
|
69
|
-
(2026-07-15) that the ClickUp-task dedup claim be implemented as a **`clickupTaskId` column on
|
|
70
|
-
`Forecast.Opportunities`**, and rejecting the earlier plan's dedicated table. This doc is `dfranks`'s
|
|
71
|
-
**counter-proposal**, offered as an open architectural disagreement to be resolved *with* the
|
|
72
|
-
reviewer — the mandate stands until then. The three rebuttal arguments:
|
|
73
|
-
|
|
74
|
-
1. **Separation of concerns.** A ClickUp task id is *integration-linkage* state, not a business
|
|
75
|
-
attribute of a Forecast opportunity. Storing it on `Forecast.Opportunities` conflates the
|
|
76
|
-
integration/mapping layer with the forecasting domain model.
|
|
77
|
-
2. **Forecast-schema dumping-ground.** Every future external linkage (a second external system, a
|
|
78
|
-
different id kind) under the column approach means another one-off nullable column on a business
|
|
79
|
-
table, accreting integration cruft on the Forecast schema. A map table absorbs all of them with
|
|
80
|
-
no schema change per linkage.
|
|
81
|
-
3. **Forecast-sync lifecycle decoupling.** The integration mapping must be able to **outlive a
|
|
82
|
-
possibly-discontinued Forecast sync.** Binding the ClickUp linkage to the Forecast opportunity row
|
|
83
|
-
ties its lifetime to the Forecast importer; if that sync is ever retired or reworked, the mapping
|
|
84
|
-
goes with it. A standalone map table keeps the NS↔ClickUp linkage durable independently of
|
|
85
|
-
Forecast.
|
|
86
|
-
|
|
87
|
-
This is framed as a disagreement to resolve, not a reversal already taken. If the reviewer position
|
|
88
|
-
holds, the mandated column approach is what ships.
|
|
89
|
-
|
|
90
|
-
## Change history
|
|
91
|
-
|
|
92
|
-
- 2026-07-17 — Proposed as a rebuttal to the reviewer-mandated clickupTaskId-column approach
|
|
93
|
-
(TRUE-80044). (dfranks)
|
|
94
|
-
- 2026-07-17 — Refined to a source-generic, single-anchor design: `(sourceSystem, sourceId,
|
|
95
|
-
targetSystem)` key with a single `INSERT IGNORE` claim; dropped the `entityUuid` correlation/two-
|
|
96
|
-
step as over-engineered for the stated concerns (future integrations + Forecast-sync decoupling),
|
|
97
|
-
both of which the single-anchor key already satisfies. (dfranks)
|