toga-ai 1.0.834 → 1.0.835
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.
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -23,6 +23,7 @@ related:
|
|
|
23
23
|
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
24
24
|
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
25
25
|
- ../../../clients/elite/features/netsuite-togasupply-sync.md
|
|
26
|
+
- ../../../clients/elite/features/supply2-tableview-config-drift.md
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
## Summary
|
|
@@ -138,7 +139,20 @@ without `AclRecordPermissions` the API role cannot `POST /item-classes` at all.
|
|
|
138
139
|
sync re-walks transactions that reference them. That needs a
|
|
139
140
|
[cursor rewind](../../worker/features/netsuite-togasupply-per-client-sync.md).
|
|
140
141
|
|
|
142
|
+
- **The tree depth is not capped — consumers that walk it with fixed joins can break silently.**
|
|
143
|
+
`Client_Elite` is exactly **5 levels** deep today (4 roots: 1 Lifecycle Services, 4 Technology
|
|
144
|
+
Sales, 22 Advisory and Modernization, 40 True Partner Services; verified 2026-09-15 that every
|
|
145
|
+
5-level chain ends at a true root). Nothing stops NetSuite adding a 6th level. Anything that
|
|
146
|
+
resolves an item to its root class with a fixed number of `LEFT JOIN`s — such as Elite's
|
|
147
|
+
[Inventory service-item filter](../../../clients/elite/features/supply2-tableview-config-drift.md) —
|
|
148
|
+
will then silently mis-classify the deeper items with no error. Re-check depth when the NetSuite
|
|
149
|
+
tree changes.
|
|
150
|
+
|
|
141
151
|
## Change history
|
|
152
|
+
- 2026-09-17 — Added the **unbounded tree depth** caveat: `Client_Elite` is exactly 5 levels with 4
|
|
153
|
+
roots (1 / 4 / 22 / 40) and every chain verified to end at a true root, but nothing caps depth, so
|
|
154
|
+
any consumer walking to the root with a fixed join count breaks silently if NetSuite adds a level.
|
|
155
|
+
No code change. (rgirish)
|
|
142
156
|
- 2026-09-15 — Documented the feature from an audit of the deployed code (library `37b55595`, worker
|
|
143
157
|
`6637ae75`). **Found and fixed a field-name mismatch that made the feature a complete no-op since
|
|
144
158
|
deploy:** the PHP sent `c_netsuiteInternalItemClassId` in 5 places while the live column and
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["bala"]
|
|
9
|
+
updated: 2026-09-17
|
|
10
|
+
owners: ["bala", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/TableView.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
related:
|
|
16
16
|
- ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
|
|
17
17
|
- ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
|
|
18
|
+
- ../../../clients/elite/features/supply2-tableview-config-drift.md
|
|
18
19
|
- tableview-field-metadata.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -86,6 +87,34 @@ the existing Compass convention and the stored clauses stay uniform whether they
|
|
|
86
87
|
or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
|
|
87
88
|
condition **after** the wrapper is removed — the two are not in conflict.)
|
|
88
89
|
|
|
90
|
+
### Subqueries ARE possible — use `/**/` in place of every space
|
|
91
|
+
|
|
92
|
+
The parser splits the stored clause on **literal spaces**, so a normal SQL subquery (which needs
|
|
93
|
+
spaces) cannot be stored in `apiWhereClause`. The way around it: write `/**/` instead of every
|
|
94
|
+
space. MySQL treats `/**/` as whitespace, and the parser sees no space at all. That makes an
|
|
95
|
+
arbitrary **correlated subquery** usable as the *field* side of a condition.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
(SELECT/**/c5.id/**/FROM/**/ItemClasses/**/c1/**/WHERE/**/c1.id=Items.itemClassId):notin:1:22:40
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Confirmed by reading `parseOptionsWhere` and simulating it in PHP (2026-09-17):
|
|
102
|
+
|
|
103
|
+
- Conditions split on `,` **only at matching paren depth** — commas nested deeper (e.g. inside a
|
|
104
|
+
`COALESCE(a,b,c)` within the subquery) are safe.
|
|
105
|
+
- Each condition then splits on `:` into field / operator / value.
|
|
106
|
+
- **The field side passes through to SQL unescaped**, so it can be any expression that has no
|
|
107
|
+
literal space, `:` or `,` at top depth.
|
|
108
|
+
- `notin` splits its value on `:`, so `:notin:1:22:40` becomes the IN-list `(1,22,40)`.
|
|
109
|
+
|
|
110
|
+
**Precedent:** Elite's prod clause on slug `item-fulfillments-for-sales-orders` already uses this
|
|
111
|
+
same `/**/` trick with a `CONCAT((SELECT ...))` subquery, so it is an established pattern, not a
|
|
112
|
+
one-off.
|
|
113
|
+
|
|
114
|
+
**Caveat — leave a comment.** This is powerful but it makes the stored clause hard to read for
|
|
115
|
+
anyone looking straight at `TableViews`. Always add a `--` comment in the migration explaining
|
|
116
|
+
what the subquery does.
|
|
117
|
+
|
|
89
118
|
### Join alias rule (which table-qualified name to use)
|
|
90
119
|
|
|
91
120
|
In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
|
|
@@ -129,12 +158,27 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
129
158
|
starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
|
|
130
159
|
- **NULL FKs drop silently.** A plain `field:ne:x` excludes rows where `field IS NULL` too. Wrap
|
|
131
160
|
nullable columns in `IFNULL(col,<sentinel>)` when the intent is "exclude only these values."
|
|
161
|
+
- **A literal space ends the clause element** — that is why a subquery needs `/**/` for every
|
|
162
|
+
space (see above). The same applies to any expression field: no spaces, anywhere.
|
|
163
|
+
- **Filter on ids, not names.** Names in lookup tables are free-text imported from NetSuite and
|
|
164
|
+
can be renamed without the id changing, so a `name LIKE` match is the fragile choice. Proven on
|
|
165
|
+
Elite: filtering root classes by `name LIKE '%Services%'` left 94 items visible instead of the
|
|
166
|
+
correct 82, because "Advisory and Modernization" contains no "Services".
|
|
132
167
|
- **Operator allowlist.** `parseOptionsWhere` whitelists the operators above and rejects unknown
|
|
133
168
|
ones (e.g. `regexp` returns a 500) — see the Compass cost-centers doc. Numeric/regex filtering
|
|
134
169
|
that the grammar can't express must be done client-side or by adding an operator to api2.
|
|
135
170
|
|
|
136
171
|
## Change history
|
|
137
172
|
|
|
173
|
+
- 2026-09-17 — **Recorded that subqueries ARE possible in a stored `apiWhereClause`**: the parser
|
|
174
|
+
splits on literal spaces, so writing `/**/` in place of every space (MySQL reads it as
|
|
175
|
+
whitespace, the parser sees no space) lets an arbitrary correlated subquery act as the field
|
|
176
|
+
side of a condition. Confirmed the parser mechanics by reading `parseOptionsWhere` and
|
|
177
|
+
simulating it in PHP — commas split only at matching paren depth, the field side passes to SQL
|
|
178
|
+
unescaped, and `notin` splits its value on `:` so `:notin:1:22:40` yields the IN-list (1,22,40).
|
|
179
|
+
Noted the existing prod precedent (Elite's `item-fulfillments-for-sales-orders` clause already
|
|
180
|
+
uses `CONCAT((SELECT ...))` this way), and added the "filter on ids, not names" gotcha —
|
|
181
|
+
name-matching left 94 Elite items visible instead of 82. No code change. (rgirish)
|
|
138
182
|
- 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
|
|
139
183
|
clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
|
|
140
184
|
and is the house convention (this was previously only documented for the multi-condition form).
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: elite
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-17
|
|
10
10
|
owners: [tcox, bala, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Elite/
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
|
|
15
15
|
- dbchanges2/Client_Elite/2026-08-13a - InventoryGroupingsSurfaceOverrides.sql
|
|
16
16
|
- dbchanges2/Client_Elite/2026-09-04b - EliteInventoryHideServiceItems.sql
|
|
17
|
+
- dbchanges2/Client_Elite/2026-09-15a - InventoryHideServiceClassItems.sql
|
|
17
18
|
- dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql
|
|
18
19
|
- dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
|
|
19
20
|
- dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql
|
|
@@ -25,6 +26,8 @@ related:
|
|
|
25
26
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
26
27
|
- ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
|
|
27
28
|
- ../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md
|
|
29
|
+
- ../../../1.0/apps/library/features/netsuite-item-class-sync.md
|
|
30
|
+
- ../../../2.0/apps/_underscore/features/item-classification-itemclasses.md
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## Summary
|
|
@@ -276,33 +279,106 @@ the live Units view is `inventory_units` (`TableViews` id 21), not
|
|
|
276
279
|
This pairs with the existing team rule that **a committed migration records intent, not deployed
|
|
277
280
|
state** — the frontend repo records the *default*, not this client's state.
|
|
278
281
|
|
|
279
|
-
## Hiding service lines from Inventory — `2026-09-04b`
|
|
282
|
+
## Hiding service lines from Inventory — `2026-09-04b`, replaced by `2026-09-15a`
|
|
280
283
|
|
|
281
|
-
|
|
284
|
+
> **⚠ Current rule (2026-09-15): service items are hidden by their ROOT ItemClass, not by the
|
|
285
|
+
> `SVC-` partNumber prefix.** `2026-09-15a - InventoryHideServiceClassItems.sql` rewrites the
|
|
286
|
+
> `inventory_items` clause. The 2026-09-04b history below is kept because it explains the
|
|
287
|
+
> `IFNULL(...)` NULL-drop rule, which still applies.
|
|
288
|
+
|
|
289
|
+
`2026-09-15a - InventoryHideServiceClassItems.sql` rewrites `TableViews.apiWhereClause` for slug
|
|
290
|
+
`inventory_items` in `Client_Elite`. It walks the `ItemClasses` parent chain and excludes any item
|
|
291
|
+
whose **root** ItemClass id is **1** (Lifecycle Services), **22** (Advisory and Modernization) or
|
|
292
|
+
**40** (True Partner Services), using a correlated subquery written with `/**/` as whitespace —
|
|
293
|
+
see [apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md#subqueries-are-possible--use--in-place-of-every-space)
|
|
294
|
+
for the technique and the parser mechanics.
|
|
295
|
+
|
|
296
|
+
**No `_underscore` deploy is needed.** The whole check lives in the stored clause. An earlier draft
|
|
297
|
+
required a `_isServiceClass` model field on `_Model_Elite_Item`; **that field does not exist and is
|
|
298
|
+
not needed** — do not add one.
|
|
299
|
+
|
|
300
|
+
### Why the class check replaced the `SVC-` prefix
|
|
301
|
+
|
|
302
|
+
The `SVC-` prefix was an unreliable signal. Verified on **prod `Client_Elite`** (2026-09-15):
|
|
303
|
+
|
|
304
|
+
| Measure | Count |
|
|
305
|
+
|---|---|
|
|
306
|
+
| Items total | 122 |
|
|
307
|
+
| Service items by root ItemClass | 40 |
|
|
308
|
+
| Service items by `SVC-` prefix | 27 |
|
|
309
|
+
| Visible after the class filter | 82 |
|
|
310
|
+
|
|
311
|
+
The class check catches **18 services the prefix missed** — Cisco `CON-SNT-*` / `CON-ROB-*`
|
|
312
|
+
contracts, `LIC-ENT-*` licenses, `CarePacks - Fixed`, `PROJECT - CABLING`, `CONFIG/INSTALL` — with
|
|
313
|
+
**zero false hides**.
|
|
314
|
+
|
|
315
|
+
### Match on ids, never on class NAMES
|
|
316
|
+
|
|
317
|
+
Hardcoded ids 1 / 22 / 40 are the stable reference. Names are free-text imported from NetSuite and
|
|
318
|
+
can be renamed without the id changing. Proven wrong in practice here: filtering on
|
|
319
|
+
`name LIKE '%Services%'` returned **94** visible instead of the correct **82**, because "Advisory
|
|
320
|
+
and Modernization" contains no "Services".
|
|
321
|
+
|
|
322
|
+
### `Client_Elite` ItemClasses hierarchy (prod, verified 2026-09-15)
|
|
323
|
+
|
|
324
|
+
The table is **`ItemClasses`**, not `ItemClassifications`. Exactly **4 root rows**
|
|
325
|
+
(`parentItemClassId IS NULL`):
|
|
326
|
+
|
|
327
|
+
| id | Name | Kind |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| 1 | Lifecycle Services | service |
|
|
330
|
+
| 4 | Technology Sales | product |
|
|
331
|
+
| 22 | Advisory and Modernization | service |
|
|
332
|
+
| 40 | True Partner Services | service |
|
|
333
|
+
|
|
334
|
+
Tree depth is exactly **5 levels**, and every 5-level chain terminates at a true root (a query for
|
|
335
|
+
level-5 classes with a non-null parent returned 0 rows). So the migration's fixed 5-alias
|
|
336
|
+
`LEFT JOIN` walk resolves every item to its true root **today**. See
|
|
337
|
+
[the hierarchy import](../../../1.0/apps/library/features/netsuite-item-class-sync.md).
|
|
338
|
+
|
|
339
|
+
### Unclassified items stay visible — and 5 real services leak
|
|
340
|
+
|
|
341
|
+
**29 Items have `itemClassId` NULL.** The clause is a blocklist, so unclassified items stay
|
|
342
|
+
**visible** — the safer default, since hiding stock people expect to see is worse than showing a
|
|
343
|
+
few extra lines. But 5 of those 29 are real services by name and will newly appear in Inventory:
|
|
344
|
+
|
|
345
|
+
- `SVC-FS-SHIPPING-RECLAIM-LAPTOP`
|
|
346
|
+
- `SVC-FS-SHIPPING-SYSTEM SWAP-LAPTOP`
|
|
347
|
+
- `SVC-FS-DEPLOY-GW`
|
|
348
|
+
- `SVC-FS-SHIPPING-2DAY`
|
|
349
|
+
- `SVC-FS-ELITE-PERIPHERAL`
|
|
350
|
+
|
|
351
|
+
**Open decision left with the developer:** either set an ItemClass on those 5 in NetSuite, or AND
|
|
352
|
+
the old `SVC-` prefix check back into the clause.
|
|
353
|
+
|
|
354
|
+
### The previous rule — `2026-09-04b` (superseded for `inventory_items`)
|
|
355
|
+
|
|
356
|
+
`2026-09-04b - EliteInventoryHideServiceItems.sql` set:
|
|
282
357
|
|
|
283
358
|
| View | Clause |
|
|
284
359
|
|---|---|
|
|
285
|
-
| `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
286
|
-
| `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` |
|
|
360
|
+
| `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` — **still in force** |
|
|
361
|
+
| `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` — replaced |
|
|
287
362
|
|
|
288
|
-
`assetTypeId 2` = Elite's `Services` row. Notes
|
|
363
|
+
`assetTypeId 2` = Elite's `Services` row. Notes that still apply to anyone porting this:
|
|
289
364
|
|
|
290
365
|
- **The filter belongs in the database table view, not the frontend** — an explicit scope decision.
|
|
291
366
|
- **`IFNULL(...,0):ne:2` is mandatory, not cosmetic.** A bare `:ne:2` drops every `NULL` row and
|
|
292
|
-
would hide **all unstamped items
|
|
293
|
-
|
|
294
|
-
view. Grammar and the NULL-drop rule:
|
|
367
|
+
would hide **all unstamped items**. The shape is copied from Elite's existing
|
|
368
|
+
`item-fulfillments-for-sales-orders` view. Grammar and the NULL-drop rule:
|
|
295
369
|
[apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
|
|
296
|
-
- **The `SVC-` prefix rule is kept on `inventory_items`** because it still catches the three items
|
|
297
|
-
NetSuite mis-types as `InvtPart` (`SVC-TS-ELITE-HDONBOARDING`, `CONFIG/INSTALL`,
|
|
298
|
-
`CONF-RM-INSTALL-SUPP`) — see
|
|
299
|
-
[netsuite-togasupply-sync](./netsuite-togasupply-sync.md).
|
|
300
370
|
- **Both `UPDATE`s are guarded on the current value**, so a re-run is a no-op.
|
|
301
371
|
- `inventory_units` was targeted **because of the `SurfaceOverrides` finding above** — filtering
|
|
302
372
|
view 17 would have had no visible effect for Elite.
|
|
303
373
|
|
|
304
374
|
## Gotchas / known issues
|
|
305
375
|
|
|
376
|
+
- **A 6th NetSuite class level would silently leak services back into Inventory.** The
|
|
377
|
+
`2026-09-15a` clause walks a **fixed 5-alias** chain because Elite's tree is exactly 5 deep
|
|
378
|
+
today — but nothing in the schema caps the depth. If NetSuite adds a level under a service root,
|
|
379
|
+
those items reappear in Inventory with no error. Re-check the depth whenever the classification
|
|
380
|
+
tree changes.
|
|
381
|
+
|
|
306
382
|
- **A `Client_*` DB that predates a platform migration fails silently until a user opens the
|
|
307
383
|
view.** Nothing validates `TableViewJoins.parentRecordFieldId` against live `Core.RecordFields`,
|
|
308
384
|
so a deleted id sits there until it 500s. Worth checking for **any** client onboarded before
|
|
@@ -312,6 +388,18 @@ state** — the frontend repo records the *default*, not this client's state.
|
|
|
312
388
|
[error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
|
|
313
389
|
|
|
314
390
|
## Change history
|
|
391
|
+
- 2026-09-17 — **Replaced the `SVC-` partNumber prefix filter with an ItemClass-hierarchy check**
|
|
392
|
+
on the `inventory_items` view (`2026-09-15a - InventoryHideServiceClassItems.sql`): the clause
|
|
393
|
+
now walks the `ItemClasses` parent chain and excludes items whose ROOT class id is 1, 22 or 40,
|
|
394
|
+
via a `/**/`-whitespace correlated subquery. Prod numbers: 122 items, 40 service by class vs 27
|
|
395
|
+
by prefix — the class check catches 18 the prefix missed (Cisco CON-SNT-*/CON-ROB-*, LIC-ENT-*,
|
|
396
|
+
CarePacks, PROJECT - CABLING, CONFIG/INSTALL) with zero false hides; 82 visible after filtering.
|
|
397
|
+
**No `_underscore` deploy** — an earlier draft's `_isServiceClass` model field does not exist and
|
|
398
|
+
is not needed. Recorded the verified `Client_Elite` hierarchy (4 roots, exactly 5 levels, every
|
|
399
|
+
chain terminating at a true root) and the decision to match on **ids, not names** (a
|
|
400
|
+
`name LIKE '%Services%'` filter left 94 visible, not 82, because "Advisory and Modernization"
|
|
401
|
+
has no "Services" in it). Open item: 29 items have a NULL `itemClassId` and stay visible by
|
|
402
|
+
design, but 5 of them are real services and will newly appear. (rgirish)
|
|
315
403
|
- 2026-09-04 — **Corrected which table view Elite's Inventory page actually reads, then filtered
|
|
316
404
|
it.** `toga25-supply/src` shows the DEFAULT groupings using `inventory_items` +
|
|
317
405
|
`units-for-items-for-purchase-orders`, and `inventory_units` appears nowhere in `src` — reading as
|
package/package.json
CHANGED