toga-ai 1.0.727 → 1.0.728
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/api2/features/tableview-apiwhereclause-row-filtering.md +45 -3
- package/knowledge/clients/compass-canada/profile.md +8 -0
- package/knowledge/clients/compass-usa/features/item-catalogs-and-duplicate-items.md +39 -3
- package/knowledge/clients/quad/profile.md +12 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- toga2-supply/src/api/toga.ts
|
|
15
15
|
related:
|
|
16
16
|
- ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
|
|
17
|
+
- ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
|
|
17
18
|
- tableview-field-metadata.md
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -36,8 +37,9 @@ view with a migration and no application code.
|
|
|
36
37
|
- **Consumption** — `toga2-supply/src/api/toga.ts` (~line 943-951): pushes the `apiWhereClause`
|
|
37
38
|
literal string into `options.where.and`, and (~line 631-641) builds `options.join` / `ojoin`
|
|
38
39
|
from the view's `TableViewJoins`.
|
|
39
|
-
- **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line
|
|
40
|
-
|
|
40
|
+
- **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line 8200-8390, re-located
|
|
41
|
+
2026-09-01; it was ~7574 when this doc was first written, so search for the function name rather
|
|
42
|
+
than trusting the line number): turns the clause string into SQL.
|
|
41
43
|
|
|
42
44
|
## How it works
|
|
43
45
|
|
|
@@ -68,6 +70,22 @@ The field slot accepts a **raw SQL expression**, not just a plain column — e.g
|
|
|
68
70
|
exclusion on the Compass fulfillment view): `(IFNULL(Items.assetTypeId,0):ne:<FEE id>)` — the
|
|
69
71
|
`IFNULL(...,0)` keeps rows with a NULL FK from being silently dropped.
|
|
70
72
|
|
|
73
|
+
### A single-condition clause is wrapped in parens too
|
|
74
|
+
|
|
75
|
+
`parseOptionsWhere` **strips one leading `(` and one trailing `)` from the whole clause before it
|
|
76
|
+
scans for top-depth commas** (V2.php ~line 8222-8229). Those outer parens are the group wrapper,
|
|
77
|
+
not part of the first condition, so the house form for a one-condition filter is:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
(Items.catalogId:eq:1)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The wrapper comes off first, leaving `Items.catalogId:eq:1`, which is then read as a plain
|
|
84
|
+
condition — it is never mistaken for a nested group needing recursion. Write the parens; they match
|
|
85
|
+
the existing Compass convention and the stored clauses stay uniform whether they hold one condition
|
|
86
|
+
or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
|
|
87
|
+
condition **after** the wrapper is removed — the two are not in conflict.)
|
|
88
|
+
|
|
71
89
|
### Join alias rule (which table-qualified name to use)
|
|
72
90
|
|
|
73
91
|
In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
|
|
@@ -77,6 +95,19 @@ referencing `Items.<field>` only binds correctly when the view joins `Items` **e
|
|
|
77
95
|
Before writing a clause, confirm the target table is joined once, or qualify against the correct
|
|
78
96
|
suffixed alias.
|
|
79
97
|
|
|
98
|
+
**A view's base record is not a join, so it never gets a suffix.** Worked example — the shared
|
|
99
|
+
`items` view (`TableViews.slug = 'items'`, `id 9`, `recordId 21` = `_Model_Client_Item`, route
|
|
100
|
+
`items`; the same shape in `Client_Compass`, `Client_CompassCanada` and `Client_Quad`). Its four
|
|
101
|
+
`TableViewJoins` are `joinRecordId` **20** Manufacturers, **33** ItemTypes, **32** AssetTypes and
|
|
102
|
+
**34** ItemCategories, all INNER. `Items` appears only as the base record and is never re-joined,
|
|
103
|
+
so the bare `Items.` qualifier is correct and there is no `_B`/`_C` to worry about. Check this the
|
|
104
|
+
same way on any view before writing a clause: list the view's `TableViewJoins` and count how many
|
|
105
|
+
resolve to the table you want to filter.
|
|
106
|
+
|
|
107
|
+
Two schema notes for anyone writing the migration: `TableViews` has **no `name` or `tableName`
|
|
108
|
+
column** in these client schemas — identify a view by its `slug` (UNIQUE) plus `recordId`, which is
|
|
109
|
+
what makes `WHERE slug = 'items'` a safe, id-agnostic target across tenants.
|
|
110
|
+
|
|
80
111
|
## Data model
|
|
81
112
|
|
|
82
113
|
- `TableViews.apiWhereClause` (TEXT, nullable) — the view-level row filter.
|
|
@@ -91,6 +122,8 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
91
122
|
|
|
92
123
|
- **Comma inside a function is safe.** The parser only splits conditions on top-depth commas, so
|
|
93
124
|
a comma inside `IFNULL(x,0)` or `IN(...)` does not break the clause.
|
|
125
|
+
- **A one-condition clause still needs its outer parens** — they are stripped before parsing, so
|
|
126
|
+
`(Field:eq:value)` and `Field:eq:value` behave identically; use the wrapped form for consistency.
|
|
94
127
|
- **An expression field must NOT start with `(`.** A condition that starts with `(` is treated by
|
|
95
128
|
the parser as a nested group and recursed into — so wrap an expression field in a function that
|
|
96
129
|
starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
|
|
@@ -102,6 +135,15 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
102
135
|
|
|
103
136
|
## Change history
|
|
104
137
|
|
|
138
|
+
- 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
|
|
139
|
+
clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
|
|
140
|
+
and is the house convention (this was previously only documented for the multi-condition form).
|
|
141
|
+
Added a worked alias-verification example on the shared `items` view (id 9, recordId 21, four
|
|
142
|
+
INNER `TableViewJoins` 20/33/32/34, `Items` only as base record → bare `Items.` qualifier), noted
|
|
143
|
+
that `TableViews` carries no `name`/`tableName` column so `slug` is the id-agnostic target, and
|
|
144
|
+
re-pointed `parseOptionsWhere` at its current location (~8200, was ~7574). No code change —
|
|
145
|
+
verified while writing the catalog-1 filter migration for the `items` view in `Client_Compass`,
|
|
146
|
+
`Client_CompassCanada` and `Client_Quad`. (bala)
|
|
105
147
|
- 2026-07-15 — Documented the end-to-end `apiWhereClause` row-filter mechanism (emission →
|
|
106
148
|
consumption → `parseOptionsWhere` grammar), the operator→SQL table, the raw-expression field
|
|
107
149
|
slot, the leading-paren / top-depth-comma parser behavior, and the `_underscore` join-alias
|
|
@@ -196,4 +196,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
196
196
|
numbers — because it does **not** use NetSuite, so it never hit the inventory-adjustment
|
|
197
197
|
dupe-item bug that fed the merge. The catalog-fold damage and reversal were Compass USA only.
|
|
198
198
|
See [Compass USA catalog-fold reversal](../compass-usa/workflows/catalog-fold-reversal.md).
|
|
199
|
+
- **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied).** A migration
|
|
200
|
+
sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on `Client_CompassCanada`'s shared
|
|
201
|
+
`items` view (`slug = 'items'`, id 9, recordId 21), alongside the same change in `Client_Compass`
|
|
202
|
+
and `Client_Quad`. **Canada is the harmless case today** — Canada has a **single catalog**
|
|
203
|
+
("compass", see the bullet above), so every `Client_CompassCanada.Items` row is already catalog 1 and so the filter changes nothing visible now; it only pins the listing so a
|
|
204
|
+
future non-catalog-1 item (e.g. anything a NetSuite-style sync creates) would silently not appear.
|
|
205
|
+
Mechanism: [TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
|
|
206
|
+
catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
199
207
|
- Related: [Compass USA](../compass-usa/profile.md).
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
18
18
|
related:
|
|
19
19
|
- odp-edi-850-item-resolution.md
|
|
20
|
+
- ../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md
|
|
20
21
|
- mits-po-to-so-item-linking.md
|
|
21
22
|
- mits-sales-order-transmission-alerting.md
|
|
22
23
|
- isfulfillable-data-quality-and-type-rule.md
|
|
@@ -132,6 +133,32 @@ Plus `-1` / `-2` suffixed copies: items **2431, 2432, 2443, 2653**.
|
|
|
132
133
|
(e.g. the `#B1H` / `-S` variant forms above) or genuinely-shared ones before treating table-wide
|
|
133
134
|
`partNumber` uniqueness as settled.
|
|
134
135
|
|
|
136
|
+
## Filtering by catalog: fine for *display*, never for *resolution*
|
|
137
|
+
|
|
138
|
+
The blanket rule below ("never scope a Compass item lookup to one catalog") is about **resolving a
|
|
139
|
+
part number to a record** — order import, PO→SO linking, vendor-item matching. It does **not** mean
|
|
140
|
+
a catalog filter is banned everywhere. A **listing** is the legitimate exception: showing a user
|
|
141
|
+
only the customer-facing Compass catalog is a product decision, and nothing downstream re-resolves
|
|
142
|
+
off that list.
|
|
143
|
+
|
|
144
|
+
**In flight as of 2026-09-01 (SQL written, NOT yet applied):** the shared TOGa Supply `items` table
|
|
145
|
+
view (`TableViews.slug = 'items'`, `id 9`, `recordId 21`) is being restricted to catalog 1 by
|
|
146
|
+
setting `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` — one `UPDATE … WHERE slug = 'items'`
|
|
147
|
+
per schema, in `Client_Compass`, `Client_CompassCanada` **and** `Client_Quad` (the same view exists
|
|
148
|
+
with the same id/recordId in all three). No application code changes; the mechanism is
|
|
149
|
+
[TableView row-filtering via apiWhereClause](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
|
|
150
|
+
The migration was written as a loose `items_tableview_catalog_filter_2026-09-01.sql` in the
|
|
151
|
+
workspace root and handed to a developer to run (the DB tooling is read-only) — it still needs to
|
|
152
|
+
land in `dbchanges2/Client_<schema>/` under the mandatory `YYYY-MM-DD<letter> - Name.sql` filename
|
|
153
|
+
before it is a real change-set.
|
|
154
|
+
|
|
155
|
+
**Open decision on the clause, and it is the NULL question again.** `Items.catalogId` is nullable in
|
|
156
|
+
all three schemas, so a bare `:eq:1` silently drops the NULL-catalog items (a real, non-trivial
|
|
157
|
+
population here — see the gotcha below, and it is proportionally much larger in `Client_Quad`). If
|
|
158
|
+
those items must stay visible in the listing, the clause has to be
|
|
159
|
+
`(IFNULL(Items.catalogId,1):eq:1)` instead. Decide before applying; the two forms are not
|
|
160
|
+
interchangeable.
|
|
161
|
+
|
|
135
162
|
## "Who changed this part number?" — the audit trail says nobody
|
|
136
163
|
|
|
137
164
|
The reflex answer to a part number that looks edited is to hunt for the person who edited it. For
|
|
@@ -151,9 +178,10 @@ children" rule:
|
|
|
151
178
|
|
|
152
179
|
## Gotchas
|
|
153
180
|
|
|
154
|
-
- **⚠ Never scope a Compass item lookup to one catalog** unless you have proved the item can only
|
|
155
|
-
come from that source.
|
|
156
|
-
|
|
181
|
+
- **⚠ Never scope a Compass item *lookup* to one catalog** unless you have proved the item can only
|
|
182
|
+
come from that source. (A read-only **listing** filter is the one sanctioned exception — see
|
|
183
|
+
[Filtering by catalog](#filtering-by-catalog-fine-for-display-never-for-resolution) above.) The
|
|
184
|
+
correct default is `Items.partNumber IN (...)` with **no** catalog condition, and then reference the resolved record **by `uuid`**. **The catalog filter never buys
|
|
157
185
|
uniqueness on Compass** — `Items.partNumber` is unique across the whole table (re-verified prod
|
|
158
186
|
2026-09-01) — so every catalog condition is pure downside.
|
|
159
187
|
- **⚠ The 2.0 side hardcodes the OTHER catalog, and it broke MR purchase orders.**
|
|
@@ -193,6 +221,14 @@ children" rule:
|
|
|
193
221
|
**unresolved tension**: that reversal folded/restored 199 catalog-1 items by `UPPER(partNumber)`,
|
|
194
222
|
which implies shared cross-catalog part numbers pre-merge and does not square with the same-day
|
|
195
223
|
"partNumber is unique table-wide" verification — needs a prod re-check. (jcardinal)
|
|
224
|
+
- 2026-09-01 — Split the catalog-filter rule into **display vs. resolution**: the "never scope to one
|
|
225
|
+
catalog" ban covers part-number *resolution*, while a read-only *listing* filter is sanctioned.
|
|
226
|
+
Recorded the in-flight (written, **not yet applied**) change setting
|
|
227
|
+
`TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on the shared `items` view
|
|
228
|
+
(`slug = 'items'`, id 9, recordId 21) in `Client_Compass`, `Client_CompassCanada` and
|
|
229
|
+
`Client_Quad`, and the open decision it carries: `Items.catalogId` is nullable, so bare `:eq:1`
|
|
230
|
+
hides NULL-catalog items and `IFNULL(Items.catalogId,1):eq:1` is the alternative. Migration still
|
|
231
|
+
needs moving into `dbchanges2/Client_<schema>/`. (bala)
|
|
196
232
|
- 2026-09-01 — Recorded a **second, independent victim of the same catalog-scoping trap, on the 2.0
|
|
197
233
|
side**: `_Model_Compass_PurchaseOrder::handleMrOrder` references MR sales-order items with a
|
|
198
234
|
hardcoded `COMPASS_CATALOG_UUID` (`961dbcbc…` = catalog **1**), which is the mirror image of the
|
|
@@ -142,4 +142,16 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
|
|
|
142
142
|
Quad's **storefront only** (not orders/transmit), and Quad was the **only other client besides
|
|
143
143
|
Compass** that needed a reversal. See
|
|
144
144
|
[Quad catalog-fold reversal](./workflows/catalog-fold-reversal.md).
|
|
145
|
+
- **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied) — verify the
|
|
146
|
+
intent for Quad first.** A migration sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'`
|
|
147
|
+
on `Client_Quad`'s shared `items` view (`slug = 'items'`, id 9, recordId 21); the same one-line
|
|
148
|
+
change is going to `Client_Compass` and `Client_CompassCanada`. Read it against the bullet above:
|
|
149
|
+
for Quad, catalog 1 is the **storefront (Etilize)** catalog and catalog 2 is the **procurement
|
|
150
|
+
(Agilant/NetSuite)** catalog that Quad's storefront orders actually sit on, so `:eq:1` hides the
|
|
151
|
+
procurement half of `Client_Quad.Items` from the Supply items listing — a much bigger cut than the
|
|
152
|
+
equivalent change on Compass, and a majority of Quad's items. `Items.catalogId` is also nullable
|
|
153
|
+
here and a bare `:eq:1` drops those rows too; `IFNULL(Items.catalogId,1):eq:1` recovers the NULLs
|
|
154
|
+
but not catalog 2. Mechanism:
|
|
155
|
+
[TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
|
|
156
|
+
catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
145
157
|
- This profile is a starting point; expand as more Quad-specific behavior is captured.
|
package/package.json
CHANGED