toga-ai 1.0.608 → 1.0.609
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 +1 -0
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +171 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +41 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/elite/INDEX.md +1 -1
- package/knowledge/clients/elite/features/supply2-scope.md +17 -0
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +9 -2
- package/package.json +1 -1
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
40
40
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
41
41
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
42
|
+
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
42
43
|
| [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
|
|
43
44
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql |
|
|
44
45
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | _underscore/Model/Client/ServiceRequest.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql |
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-18
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/TableView.php
|
|
13
|
+
- dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
|
|
14
|
+
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
15
|
+
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
16
|
+
related:
|
|
17
|
+
- ../../api2/features/tableview-field-metadata.md
|
|
18
|
+
- ./page-meta-context-field-settings.md
|
|
19
|
+
- ./tableview-hyperlink-columns.md
|
|
20
|
+
- ../../api2/features/tableview-apiwhereclause-row-filtering.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its
|
|
26
|
+
base record. `_Model_Client_TableView::meta()` (`_underscore/Model/Client/TableView.php`) turns each
|
|
27
|
+
row into one entry of `table.joins` (`table`, `alias`, `type`, `onA`, `onB`). Two things about that
|
|
28
|
+
translation are load-bearing and non-obvious:
|
|
29
|
+
|
|
30
|
+
- **`onB` is emitted with the RAW TABLE NAME of the parent field's record, never with an alias.**
|
|
31
|
+
- **`joinOnTableViewJoinId` does not affect the emitted ON clause at all.**
|
|
32
|
+
|
|
33
|
+
Together those mean a **two-hop (chained) join works only because the first join of a table is
|
|
34
|
+
aliased to the plain table name**. Get the order or the duplication wrong and the view emits SQL
|
|
35
|
+
referencing a table that was never joined.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
The joins for a view are read with (paraphrased, ~line 55-73):
|
|
40
|
+
|
|
41
|
+
```sql
|
|
42
|
+
SELECT TableViewJoins.id, joinRecordId, joinOnRecordFieldId, joinOnTableViewJoinId,
|
|
43
|
+
parentRecordFieldId, parentCustomRecordFieldId, `type`
|
|
44
|
+
FROM TableViewJoins
|
|
45
|
+
INNER JOIN AclRecordPermissions ON AclRecordPermissions.recordId = TableViewJoins.joinRecordId
|
|
46
|
+
WHERE TableViewJoins.tableViewId = <id> AND AclRecordPermissions.roleId IN (<user roles>)
|
|
47
|
+
ORDER BY TableViewJoins.sortOrder ASC, TableViewJoins.id ASC
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then, per join:
|
|
51
|
+
|
|
52
|
+
- **Alias.** A counter (`$tableViewAliasOrds`) starts at `65` (`A`) and is seeded with the view's
|
|
53
|
+
**base** table. The first join of a given table gets `ord == 65`, which means its alias is the
|
|
54
|
+
**plain table name**. Any later join of that same table becomes `<Table>_<letter>`.
|
|
55
|
+
- **ON clause.**
|
|
56
|
+
- `onA` = `` `<this join's alias>`.`<joinOnRecordField.field>` ``
|
|
57
|
+
- `onB` = `` `<($parentRecord->model)::TABLE>`.`<parentRecordField.field>` `` — the **raw table
|
|
58
|
+
name** of the record that owns `parentRecordFieldId`.
|
|
59
|
+
- `joinOnTableViewJoinId` is used for exactly one thing: setting `joinOnTableAlias` on the join
|
|
60
|
+
object. The current code **never emits `joinOnTableAlias` into `onA`/`onB`**. Treat it as
|
|
61
|
+
documentation of the chain, not the mechanism.
|
|
62
|
+
|
|
63
|
+
## Chained (multi-hop) joins
|
|
64
|
+
|
|
65
|
+
Example shipped 2026-08-18 on the `inventory_units` view (base record **Units**, 31): to show
|
|
66
|
+
`Manufacturers.name` you need **Units → Items → Manufacturers**.
|
|
67
|
+
|
|
68
|
+
- Hop 1: join **Items** (record 21) `ON Items.id (105) = Units.itemId (238)`
|
|
69
|
+
- Hop 2: join **Manufacturers** (record 20) `ON Manufacturers.id (104) = Items.manufacturerId (107)`
|
|
70
|
+
|
|
71
|
+
Hop 2's `onB` renders as `` `Items`.`manufacturerId` ``. That resolves **only** because hop 1 is the
|
|
72
|
+
first Items join on the view and therefore aliased `Items`. So:
|
|
73
|
+
|
|
74
|
+
- **The parent hop must sort first.** Joins are ordered `sortOrder ASC, id ASC`, so give the parent
|
|
75
|
+
hop the lower `sortOrder` or the SQL references a table that has not been joined yet.
|
|
76
|
+
- **A chain breaks if its parent table is joined more than once on the same view.** The extra join
|
|
77
|
+
gets a suffixed alias while `onB` still says the bare table name, so the chain silently binds to
|
|
78
|
+
the first join — or fails outright if the bare-name join is not the one you meant.
|
|
79
|
+
- Before this change there were **zero** rows with a non-null `joinOnTableViewJoinId` anywhere in
|
|
80
|
+
`Client_Elite`, so chained joins were previously unused in these schemas.
|
|
81
|
+
|
|
82
|
+
## Look for an existing unused join before adding one
|
|
83
|
+
|
|
84
|
+
The `inventory_units` view already carried a Units → Items join
|
|
85
|
+
(`TableViewJoins.uuid = '7894941a-d00b-49db-af5f-63835c29b1a4'`, joinRecordId 21, joinOnRecordFieldId
|
|
86
|
+
105 `Items.id`, parentRecordFieldId 238 `Units.itemId`, `OUTER`, `sortOrder 0`) in **both** clients
|
|
87
|
+
and in **both** prod and client-sandbox — referenced by **zero** `TableViewFields`. It came in with
|
|
88
|
+
the all-clients template `dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql`.
|
|
89
|
+
Only the second hop had to be created. Always check for a usable join first; a duplicate Items join
|
|
90
|
+
would also have broken the chain above.
|
|
91
|
+
|
|
92
|
+
## The joined record needs an AclRecordPermissions row
|
|
93
|
+
|
|
94
|
+
The joins query **INNER JOINs `AclRecordPermissions` on `joinRecordId`**. With no row for the
|
|
95
|
+
caller's role, the join is silently dropped from the result set, and any `TableViewFields` row
|
|
96
|
+
pointing at it then throws:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
There is no AclRecordPermissions record permitting access to TableViewJoins.id = N for this user
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
So a new joined table (here `Manufacturers`, record 20) needs its `AclRecordPermissions` grant in the
|
|
103
|
+
same migration.
|
|
104
|
+
|
|
105
|
+
## Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
106
|
+
|
|
107
|
+
`meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
|
|
108
|
+
Client / Persona / User / Language `RecordFieldSettings` tables) only to decide **which settings row
|
|
109
|
+
supplies the column label**. When no context row exists for `(recordFieldId, contextRecordFieldId)`,
|
|
110
|
+
it nulls `contextTable` / `contextField` and falls back to the field's own settings. A new joined
|
|
111
|
+
column therefore does **not** require adding `RecordFieldSettings` rows just to appear — those rows
|
|
112
|
+
are only needed to control the **header text** (see
|
|
113
|
+
[page-meta-context-field-settings](./page-meta-context-field-settings.md), which is also where the
|
|
114
|
+
"header renders as a camelCase slug" fallback is explained).
|
|
115
|
+
|
|
116
|
+
## Authoring the migration (house style used here)
|
|
117
|
+
|
|
118
|
+
The 2026-08-18 change added Part Number (`Items.partNumber` 108), Manufacturer
|
|
119
|
+
(`Manufacturers.name` 103) and Item Description (`Items.description` 109) to `inventory_units`
|
|
120
|
+
(`TableViews.uuid 86b3f376-2a15-4a46-ac0a-9b25146e40ac`), positioned directly after Asset Tag,
|
|
121
|
+
mirroring how `inventory_items` surfaces the same three fields. Applied to **prod and
|
|
122
|
+
client-sandbox for both Elite and NYCHH** as one file per client with identical content.
|
|
123
|
+
|
|
124
|
+
- **Key everything off `uuid`, never off `id`.** `TableViews.id` happened to be 21 in both clients,
|
|
125
|
+
but the pre-existing `TableViewJoins.id` differed (Elite 142, NYCHH 165). Resolve ids with
|
|
126
|
+
`(SELECT id FROM TableViewJoins WHERE uuid = ...)`.
|
|
127
|
+
- **One idempotent file beats an insert file plus a follow-up reorder file.** The insert had already
|
|
128
|
+
run in all four targets when the reposition was requested; rather than ship a second migration, the
|
|
129
|
+
same file was rewritten as `INSERT ... ON DUPLICATE KEY UPDATE` keyed on `uuid` for the join and the
|
|
130
|
+
three fields, followed by an unconditional renumber. On an already-applied schema the net effect is
|
|
131
|
+
just the reorder — no duplicate rows, no row-id churn.
|
|
132
|
+
- **`@` session variables mean the file must run in ONE connection.** Splitting the statements across
|
|
133
|
+
connections (or across two client-tool tabs) silently loses the variables and the renumber writes
|
|
134
|
+
garbage.
|
|
135
|
+
- Reordering columns has its own hazard — `TableViewFields` has `UNIQUE (tableViewId, index)`, so a
|
|
136
|
+
naive `index = index + N` can collide mid-statement. The safe park-shift-pull pattern is documented
|
|
137
|
+
in [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
|
|
138
|
+
|
|
139
|
+
Core-level reference ids used (identical across all clients and environments): **Records** — Units
|
|
140
|
+
31, Items 21, Manufacturers 20. **RecordFields** — `Units.itemId` 238, `Items.id` 105,
|
|
141
|
+
`Items.partNumber` 108, `Items.description` 109, `Items.manufacturerId` 107, `Manufacturers.id` 104,
|
|
142
|
+
`Manufacturers.name` 103.
|
|
143
|
+
|
|
144
|
+
## Gotchas
|
|
145
|
+
|
|
146
|
+
- **`onB` never uses an alias.** Any chain depends on the parent hop owning the bare table name.
|
|
147
|
+
- **`joinOnTableViewJoinId` is inert in the emitted SQL** — setting it does not make a chain work,
|
|
148
|
+
and omitting it does not break one. It is metadata for humans until the ON-clause builder starts
|
|
149
|
+
using `joinOnTableAlias`.
|
|
150
|
+
- **Alias suffixes are not `_B` for the first duplicate.** The counter is incremented both inside the
|
|
151
|
+
`isset` branch and again at the end of the loop, so the second join of a non-base table lands on
|
|
152
|
+
`chr(67)` → `<Table>_C`; `<Table>_B` only appears when the view joins its own **base** table. Do
|
|
153
|
+
not hand-write an alias from memory — check the counter logic before relying on one.
|
|
154
|
+
- **A missing `AclRecordPermissions` row for the joined record drops the join, not just the column** —
|
|
155
|
+
and the resulting error names a `TableViewJoins.id`, which is why it reads like config corruption
|
|
156
|
+
rather than an ACL gap.
|
|
157
|
+
- **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
|
|
158
|
+
is what breaks chained joins.
|
|
159
|
+
|
|
160
|
+
## Change history
|
|
161
|
+
|
|
162
|
+
- 2026-08-18 — Documented how `TableViewJoins` compile into SQL after adding a chained
|
|
163
|
+
**Units → Items → Manufacturers** join for the Part Number / Manufacturer / Item Description columns
|
|
164
|
+
on `inventory_units` (Elite + NYCHH): `onB` is emitted with the **raw table name** (so a chain only
|
|
165
|
+
works off the first, unaliased join of a table), `joinOnTableViewJoinId` feeds only
|
|
166
|
+
`joinOnTableAlias` and is **not** in the ON clause, joins are ordered `sortOrder ASC, id ASC`, the
|
|
167
|
+
joins query INNER JOINs `AclRecordPermissions` on `joinRecordId` (missing grant = dropped join plus
|
|
168
|
+
the "no AclRecordPermissions record permitting access to TableViewJoins.id" error), and context
|
|
169
|
+
`RecordFieldSettings` are optional for rendering (label-only). Also recorded the pre-existing unused
|
|
170
|
+
Units→Items join `7894941a-...` present in both clients, and the uuid-keyed idempotent single-file
|
|
171
|
+
migration style. (bala)
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
|
|
19
19
|
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
20
20
|
| [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
|
|
21
|
-
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql |
|
|
21
|
+
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
22
22
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
23
23
|
| [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
|
|
24
24
|
| [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-18
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
13
13
|
- api2/Component/Api/V2/V2.php
|
|
@@ -15,9 +15,12 @@ files:
|
|
|
15
15
|
- dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql
|
|
16
16
|
- dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql
|
|
17
17
|
- dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql
|
|
18
|
+
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
19
|
+
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
18
20
|
related:
|
|
19
21
|
- tableview-apiwhereclause-row-filtering.md
|
|
20
22
|
- ../../toga25-supply/features/meta-driven-table-data.md
|
|
23
|
+
- ../../_underscore/features/tableview-joins.md
|
|
21
24
|
---
|
|
22
25
|
|
|
23
26
|
## Summary
|
|
@@ -52,6 +55,8 @@ it. (Reference: `Core.RecordFields` id 369 = `ItemFulfillmentItems.quantity` set
|
|
|
52
55
|
`type=NUMBER`, `precision=0`, which normalizes that column across all fulfillment views/clients.)
|
|
53
56
|
- **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
|
|
54
57
|
request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
|
|
58
|
+
How a join row becomes SQL — aliasing, chained multi-hop joins, and the ACL row the joined record
|
|
59
|
+
needs — is in [tableview-joins](../../_underscore/features/tableview-joins.md).
|
|
55
60
|
|
|
56
61
|
### `recordFieldId` is a cross-DB reference into shared `Core.RecordFields`
|
|
57
62
|
|
|
@@ -65,6 +70,32 @@ Reference ids observed for the `Items` record: **`Core.Records` id 21 = Items**,
|
|
|
65
70
|
**`Core.RecordFields` id 106 = `Items.uuid`**. (Look these up per field before writing a migration —
|
|
66
71
|
do not assume ids across environments.)
|
|
67
72
|
|
|
73
|
+
## Column order (`index`) — the UNIQUE keys make reordering non-trivial
|
|
74
|
+
|
|
75
|
+
`TableViewFields` carries **`UNIQUE (tableViewId, index)`** and **`UNIQUE (tableViewId, slug)`**.
|
|
76
|
+
Columns are emitted `ORDER BY TableViewFields.index ASC`, so repositioning a column means rewriting
|
|
77
|
+
other rows' `index` — and a plain `UPDATE ... SET index = index + N` can hit the unique key
|
|
78
|
+
**mid-statement**, because MySQL checks per row, not at statement end.
|
|
79
|
+
|
|
80
|
+
The safe pattern (used to slot Part Number / Manufacturer / Item Description in right after Asset
|
|
81
|
+
Tag on `inventory_units`, 2026-08-18) is **park → shift → pull → drop**, so that every target slot is
|
|
82
|
+
empty at the moment it is written:
|
|
83
|
+
|
|
84
|
+
1. Park the moving rows in a high band (`200+`).
|
|
85
|
+
2. Shift the rows that must move down into a second band (`100+`).
|
|
86
|
+
3. Pull those back down into the freed low slots.
|
|
87
|
+
4. Drop the parked rows into their final positions.
|
|
88
|
+
|
|
89
|
+
Notes that matter when writing it:
|
|
90
|
+
|
|
91
|
+
- **`index` is `TINYINT UNSIGNED`** — the parking band must stay under 256.
|
|
92
|
+
- **Gaps in `index` are harmless** (it is display order only), but finishing with a contiguous
|
|
93
|
+
renumber makes the script **re-runnable to the same final state**.
|
|
94
|
+
- Do the whole thing **uuid-keyed** and unconditional at the end, so an already-applied migration
|
|
95
|
+
can simply be re-run instead of shipping a separate "reorder" file — see the authoring notes in
|
|
96
|
+
[tableview-joins](../../_underscore/features/tableview-joins.md). If the script uses `@` session
|
|
97
|
+
variables, all statements must run in **one connection**.
|
|
98
|
+
|
|
68
99
|
## Hidden (projected-but-not-rendered) columns
|
|
69
100
|
|
|
70
101
|
A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
|
|
@@ -148,6 +179,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
|
|
|
148
179
|
`meta.table.sort=[{slug,dir}]`; and the JOIN-`TableViewFields`-on-slug UPDATE recipe that
|
|
149
180
|
avoids MySQL error 1093 (a self-subquery on `TableViews` is rejected). Applied to Quad's
|
|
150
181
|
sales-orders listing (default `dateOrder DESC`). (apeterson)
|
|
182
|
+
- 2026-08-18 — Recorded that `TableViewFields` has **UNIQUE (tableViewId, `index`)** and
|
|
183
|
+
**UNIQUE (tableViewId, slug)**, so a column reorder cannot be a plain `index = index + N` (the
|
|
184
|
+
unique key is checked per row, mid-statement); documented the park → shift → pull → drop pattern,
|
|
185
|
+
the `TINYINT UNSIGNED` ceiling on the parking band, and that a closing contiguous renumber is what
|
|
186
|
+
makes such a migration re-runnable. Found while inserting Part Number / Manufacturer / Item
|
|
187
|
+
Description after Asset Tag on the `inventory_units` view for Elite + NYCHH. Cross-linked the new
|
|
188
|
+
[tableview-joins](../../_underscore/features/tableview-joins.md) doc for how a join row becomes
|
|
189
|
+
SQL. (bala)
|
|
151
190
|
- 2026-08-17 — Documented that a column's `type`/`precision` are canonical from `Core.RecordFields`
|
|
152
191
|
(no per-view override — a change hits every view/client) while `isCopyable`/`isSortable`/
|
|
153
192
|
`isFilterable`/`isVisible` are per-view `TableViewFields` flags. Confirmed setting
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 59 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 51 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 24 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
|
|
6
6
|
| [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite orders created in Toga are pushed into NetSuite **event-driven**, not on a cron. | _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
|
|
7
7
|
| [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | When an Elite sales order's **stage** changes, a reply is posted on the originating **TOGa Desk (1.0)** ticket so the requester sees progress where they raised | worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php |
|
|
8
|
-
| [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
|
|
8
|
+
| [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
9
9
|
| [Elite — stale TableView config (11 dead Core.RecordFields across 9 views; 2 now fixed)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
10
10
|
| [Elite — raising a Service Request from a TOGa Desk ticket (App_Api_ServiceRequest)](features/togadesk-service-request-intake.md) | 1.0 | An Elite agent raises a **Service Request** from a TOGa Desk (1.0) ticket via a modal. | library/app/api/servicerequest.php, library/app/model/togadesk/ticket.php, togadesk/desk/includes/classes/class.ticket.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
|
|
11
11
|
| [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
|
|
@@ -32,11 +32,13 @@ files:
|
|
|
32
32
|
- toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx
|
|
33
33
|
- toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx
|
|
34
34
|
- toga2-supply/package.json
|
|
35
|
+
- dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
35
36
|
related:
|
|
36
37
|
- ../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md
|
|
37
38
|
- ../../../2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md
|
|
38
39
|
- ./supply2-tableview-config-drift.md
|
|
39
40
|
- ../profile.md
|
|
41
|
+
- ../../../2.0/apps/_underscore/features/tableview-joins.md
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
## Summary
|
|
@@ -73,6 +75,15 @@ One host-scoped group-by view only: **"Units By Items"**, no group-by modal.
|
|
|
73
75
|
- Local run: `npm run elite` (`vite --host elite.togasupply`) plus a
|
|
74
76
|
`127.0.0.1 elite.togasupply` hosts-file entry.
|
|
75
77
|
|
|
78
|
+
**The nested `inventory_units` view now shows what the hardware actually is (2026-08-18).** Unit rows
|
|
79
|
+
previously carried only serial / asset tag, so a user could not identify the item without opening it.
|
|
80
|
+
Part Number, Manufacturer and Item Description were added directly after Asset Tag, mirroring
|
|
81
|
+
`inventory_items`, via `dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql` (an
|
|
82
|
+
identical file was applied for NYCHH). Manufacturer needs a **chained** Units → Items → Manufacturers
|
|
83
|
+
join — mechanism, ACL requirement and the column-reorder hazard are in
|
|
84
|
+
[tableview-joins](../../../2.0/apps/_underscore/features/tableview-joins.md). Purely backend metadata;
|
|
85
|
+
no frontend change.
|
|
86
|
+
|
|
76
87
|
**Hardening from the CodeRabbit review (2026-08-10)** — guardrail-preserving variants, not the
|
|
77
88
|
reviewer's literal diffs:
|
|
78
89
|
|
|
@@ -200,6 +211,12 @@ byte-identical in behavior** — that is the bar for editing shared UI here (see
|
|
|
200
211
|
2026-08-07 prod→beta refresh that wiped the hand-built `service-requests` view.
|
|
201
212
|
|
|
202
213
|
## Change history
|
|
214
|
+
- 2026-08-18 — Added **Part Number / Manufacturer / Item Description** to the `inventory_units` table
|
|
215
|
+
view directly after Asset Tag (`dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql`,
|
|
216
|
+
same file content shipped for NYCHH; prod + client-sandbox), so unit rows identify the hardware
|
|
217
|
+
instead of showing serial/asset tag alone. Manufacturer comes through the first chained
|
|
218
|
+
Units → Items → Manufacturers join in `Client_Elite`; backend metadata only, no frontend change.
|
|
219
|
+
(bala)
|
|
203
220
|
- 2026-08-18 - Added `ELITE: "Elite"` to `formatCustomerName.tsx`'s `CUSTOMER_MAP` so the
|
|
204
221
|
order-details **Customer** row shows a name instead of a dash. Recorded that this row is
|
|
205
222
|
**hardcoded in the frontend for every host** - the shared `valueKey`
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. |
|
|
5
|
+
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
@@ -15,11 +15,13 @@ project: _Underscore
|
|
|
15
15
|
client: nychh
|
|
16
16
|
type: profile
|
|
17
17
|
status: active
|
|
18
|
-
updated: 2026-08-
|
|
18
|
+
updated: 2026-08-18
|
|
19
19
|
owners: ["jcardinal", "apeterson", "bala"]
|
|
20
|
-
files:
|
|
20
|
+
files:
|
|
21
|
+
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
21
22
|
related:
|
|
22
23
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
24
|
+
- ../../2.0/apps/_underscore/features/tableview-joins.md
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Summary
|
|
@@ -34,6 +36,11 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
34
36
|
run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
|
|
35
37
|
|
|
36
38
|
## Key features (this client)
|
|
39
|
+
- **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
|
|
40
|
+
and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
|
|
41
|
+
Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
|
|
42
|
+
to Elite's file; applied in prod and client-sandbox). Mechanism and the reorder hazard:
|
|
43
|
+
[tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
|
|
37
44
|
- Item-fulfillment table views (`item-fulfillments-for-sales-orders`,
|
|
38
45
|
`item-fulfillments-for-sales-order-items`) rebuilt 2026-06-19 to mirror the Compass fix —
|
|
39
46
|
re-rooted at `ItemFulfillmentItems` with item-level (record 318) tracking. See
|
package/package.json
CHANGED