toga-ai 1.0.463 → 1.0.464
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/1.0/apps/library/features/toga2-api-client-and-bridge.md +39 -2
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/v2-reverse-hasmany-fields-whitelist.md +77 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/aig/profile.md +6 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-07-28
|
|
10
|
+
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- worker/crons/toga2/aig/sync_togasupply_aig.php
|
|
@@ -90,6 +90,17 @@ bool $enabled2to1, bool $enabled1to2)`. When `enabled2to1`:
|
|
|
90
90
|
- Pages 2.0 **`/contacts`** (for AIG, only those with `c_togaCustomerId IS NULL`) and upserts each
|
|
91
91
|
into the 1.0 client DB `Customers`/`Addresses` via `syncContactToToga1Customer`, then **writes
|
|
92
92
|
the new 1.0 id back** to the 2.0 contact's `c_togaCustomerId` (PUT `/contacts/{uuid}`).
|
|
93
|
+
- **Email storage (`syncContactToToga1Customer`, TRUE-79401):** the 1.0 `Customers.emailAddress`
|
|
94
|
+
column stores the contact's **full** list of 2.0 email addresses joined by the class constant
|
|
95
|
+
`CUSTOMER_EMAIL_DELIMITER = '; '`, not just the primary. The list is built from
|
|
96
|
+
`array_column((array)($togaContact->contactEmailAddresses ?? []), 'emailAddress')` →
|
|
97
|
+
trim / `array_filter` / `array_unique` → `implode` → `App_Database::sqlEscape` (escape **after**
|
|
98
|
+
imploding); an empty collection falls back to the single primary email (no regression). A
|
|
99
|
+
separately-escaped `$primaryEmailAddress` is retained **only** for the customer-dedupe lookup
|
|
100
|
+
(`WHERE Customers.emailAddress LIKE '$primaryEmailAddress'`) so storing a list doesn't break
|
|
101
|
+
record matching — dedupe is authoritatively keyed on **`c_togaCustomerId`**, with the primary-email
|
|
102
|
+
`LIKE` as a legacy fallback. Consuming the `contactEmailAddresses.emailAddress` collection required
|
|
103
|
+
adding it to the `/contacts` fetch `fields` whitelist (see gotcha).
|
|
93
104
|
- Pages 2.0 **`/entitlements`** without a `c_togaServiceRequestId` and inserts 1.0
|
|
94
105
|
`ServiceRequests` (+ a `Contacts` row if needed) via `syncEntitlementToToga1ServiceRequest`.
|
|
95
106
|
- The 1→2 direction (`enabled1to2`) is a stub (not yet implemented).
|
|
@@ -154,11 +165,37 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
154
165
|
the `App_Model` layer; follow the surrounding escaping discipline when modifying.
|
|
155
166
|
- **Per-record error isolation** in the TOGaDesk sync is via `try/catch` → `\Sentry\captureException`;
|
|
156
167
|
a thrown exception elsewhere (transport, checkpoint read) still aborts the whole run.
|
|
168
|
+
- **A reverse hasMany collection is only returned if you add it to the fetch `fields` whitelist.**
|
|
169
|
+
The `/contacts` fetch requests an **explicit** `fields` list from the 2.0 metadata API; a reverse
|
|
170
|
+
hasMany collection (e.g. `contactEmailAddresses.emailAddress`) is **not** returned unless it is
|
|
171
|
+
added to that list — so consuming a related collection means editing **both** the fetch field list
|
|
172
|
+
**and** the consuming code. (The general 2.0-side metadata behavior — reverse relations resolved via
|
|
173
|
+
the child model's `FOREIGNKEY_MODEL` and named as the camelCase plural of the child model — is a
|
|
174
|
+
framework-level fact captured separately.)
|
|
175
|
+
- **`TOGA_AIG.Customers.emailAddress` is `varchar(255)`** (V1 legacy, latin1_swedish_ci) — wide
|
|
176
|
+
enough to hold several `'; '`-joined emails, so TRUE-79401 needed **no** schema/dbchanges migration
|
|
177
|
+
(the earlier fear that it was ~VARCHAR(55) was wrong). `TOGA_AIG` is the 1.0 legacy DB;
|
|
178
|
+
`Client_Aig` is the 2.0 prod tenant.
|
|
179
|
+
- **The multi-email path is not yet exercised (open, TRUE-79401).** As of 2026-07-28 all 20
|
|
180
|
+
`Client_Aig.ContactEmailAddresses` rows are one-per-contact, and **no** api2/`_underscore` code
|
|
181
|
+
references `UserDefined3`/`UserDefined4` or creates `ContactEmailAddresses` rows. Whether an api2
|
|
182
|
+
intake mapping is still needed depends on how Staples / SA.com sends the extra emails: if they POST
|
|
183
|
+
nested `contactEmailAddresses` records to the V2 API the library change is complete; if they send
|
|
184
|
+
flat `UserDefined3`/`UserDefined4` fields expecting us to map them into email rows, an api2 intake
|
|
185
|
+
mapping must be built. Open question owned by **Paulina**.
|
|
157
186
|
- **PHP 7.2** target (prod worker/library) — no arrow functions, typed properties, `??=`, `match`.
|
|
158
187
|
Lint with `C:\xampp7\php\php.exe -l`.
|
|
159
188
|
|
|
160
189
|
## Change history
|
|
161
190
|
|
|
191
|
+
- 2026-07-28 — TRUE-79401: `syncContactToToga1Customer` now stores a contact's **full** email list in
|
|
192
|
+
1.0 `TOGA_AIG.Customers.emailAddress` as a `CUSTOMER_EMAIL_DELIMITER` (`'; '`)-joined, deduped,
|
|
193
|
+
escaped string (was primary-only) for AIG/Staples support lookup. Added
|
|
194
|
+
`contactEmailAddresses.emailAddress` to the `/contacts` fetch `fields` whitelist; kept a separate
|
|
195
|
+
escaped primary email for the dedupe `LIKE` (dedupe stays keyed on `c_togaCustomerId`); empty list
|
|
196
|
+
falls back to primary. No migration needed (`Customers.emailAddress` is `varchar(255)`). Scope stayed
|
|
197
|
+
library-only. Multi-email path not yet exercised — payload shape (nested `contactEmailAddresses` vs
|
|
198
|
+
flat `UserDefined3/4`) is open, owner Paulina. (mhammontree)
|
|
162
199
|
- 2026-06-23 — Initial documentation of the `App_Api_Toga2` transport (auth/token caching, options
|
|
163
200
|
DSL) and the 1.0↔2.0 bridge routines `syncWithToga` (Contacts/Entitlements → Customers/ServiceRequests)
|
|
164
201
|
and `syncWithTogadesk` (bi-directional ticket/repair-order/people/asset sync with dual checkpoint
|
|
@@ -18,5 +18,6 @@
|
|
|
18
18
|
| [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 |
|
|
19
19
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
20
20
|
| [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, _underscore/Model/Client/TrackingNumber.php |
|
|
21
|
+
| [V2 reverse hasMany collections must be named in the fetch fields whitelist](features/v2-reverse-hasmany-fields-whitelist.md) | In the V2 JSON engine, a **reverse hasMany** relationship — the collection of child records that foreign-key back to a parent (e.g. | api2/Component/Api/V2/V2.php |
|
|
21
22
|
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
|
|
22
23
|
| [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, dbchanges2/Logs/, _underscore/Route.php |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: V2 reverse hasMany collections must be named in the fetch fields whitelist
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-28
|
|
10
|
+
owners: [mhammontree]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
related:
|
|
14
|
+
- nested-fk-acl-embedding.md
|
|
15
|
+
- ../../../../1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What it is
|
|
19
|
+
|
|
20
|
+
In the V2 JSON engine, a **reverse hasMany** relationship — the collection of child records that
|
|
21
|
+
foreign-key back to a parent (e.g. a Contact's many `ContactEmailAddresses`) — is **not** included
|
|
22
|
+
in a response by default. It is only serialized when the caller **explicitly names it in the fetch
|
|
23
|
+
`fields` list**. A parent GET that does not list the reverse collection comes back with that key
|
|
24
|
+
absent — no error, no warning `message`, still HTTP 200 / `isSuccess: true`. This is a framework-level
|
|
25
|
+
fact about how the 2.0 metadata layer resolves and names reverse relations; it is the general form of
|
|
26
|
+
the AIG-specific `contactEmailAddresses.emailAddress` gotcha in the 1.0 `App_Api_Toga2` bridge.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
- **Reverse relations are metadata-resolved, not implicit.** The V2 engine builds the set of
|
|
31
|
+
embeddable relations for a model from the model metadata. A **forward** FK (parent points at one
|
|
32
|
+
child) resolves from the parent model's own FK field. A **reverse** hasMany (many children point
|
|
33
|
+
back at this parent) is resolved by scanning **child** models for a `FOREIGNKEY_MODEL` constant that
|
|
34
|
+
targets this model — the engine walks the child side to discover "who points at me".
|
|
35
|
+
- **Naming convention — camelCase plural of the child model.** A discovered reverse collection is
|
|
36
|
+
exposed under the **camelCase plural of the child model name**. So children modeled as
|
|
37
|
+
`ContactEmailAddress` (with `FOREIGNKEY_MODEL` → `Contact`) surface on the parent as
|
|
38
|
+
`contactEmailAddresses`; a nested field of that collection is dotted, e.g.
|
|
39
|
+
`contactEmailAddresses.emailAddress`. Get the pluralization/casing exactly right — a mismatched name
|
|
40
|
+
is treated as an unknown field and simply yields nothing rather than an error.
|
|
41
|
+
- **Whitelist-gated serialization.** The engine only serializes a reverse collection that appears in
|
|
42
|
+
the requested `fields` list (for the library options DSL, the nested-array form
|
|
43
|
+
`['contactEmailAddresses' => ['emailAddress']]`). Omit it and the collection is silently skipped —
|
|
44
|
+
it is opt-in, not opt-out. There is no default depth that pulls reverse hasMany collections in for
|
|
45
|
+
you.
|
|
46
|
+
- **Consuming a reverse collection is therefore a two-part change.** Any code that wants to read a
|
|
47
|
+
related collection off a parent must (1) add the collection (and the specific child fields) to the
|
|
48
|
+
**fetch `fields` whitelist**, and (2) add the **consuming code** that reads it. Adding only the
|
|
49
|
+
consumer leaves the key absent at runtime; adding only the field silently fetches data nobody uses.
|
|
50
|
+
|
|
51
|
+
## Relationship to ACL embedding
|
|
52
|
+
|
|
53
|
+
This whitelist gate is **orthogonal to** the child-record ACL gate documented in
|
|
54
|
+
[nested-fk-acl-embedding.md](nested-fk-acl-embedding.md). Naming a reverse collection in `fields` is
|
|
55
|
+
necessary but not sufficient: each embedded child record is still re-checked against its own
|
|
56
|
+
`AclRecordPermissions`, and an ungranted child is silently dropped. A missing nested collection can be
|
|
57
|
+
caused by **either** an omitted `fields` entry **or** a missing child-record ACL grant — check both.
|
|
58
|
+
|
|
59
|
+
## Gotchas
|
|
60
|
+
|
|
61
|
+
- **Silent, not erroring.** An unlisted (or misspelled) reverse collection produces no `messages[]`
|
|
62
|
+
entry — the symptom is purely a missing key in `data`. Confirm the exact camelCase-plural name from
|
|
63
|
+
the child model before assuming a data problem.
|
|
64
|
+
- **The 1.0 bridge hit this concretely.** TRUE-79401 needed
|
|
65
|
+
`contactEmailAddresses.emailAddress` added to the `/contacts` fetch `fields` list in
|
|
66
|
+
`App_Api_Toga2` before the 1.0 sync could read a contact's full email list — see
|
|
67
|
+
[App_Api_Toga2 — TOGa2 API Client & bridge](../../../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
|
|
68
|
+
|
|
69
|
+
## Change history
|
|
70
|
+
|
|
71
|
+
- 2026-07-28 — Captured the framework-level rule (split out of the TRUE-79401 library gotcha): V2
|
|
72
|
+
reverse hasMany collections are resolved via the child model's `FOREIGNKEY_MODEL`, exposed under the
|
|
73
|
+
camelCase plural of the child model, and are **only** serialized when explicitly named in the fetch
|
|
74
|
+
`fields` whitelist — otherwise the key is silently absent (200 / `isSuccess:true`, no message).
|
|
75
|
+
Orthogonal to, and stacks with, the child-record ACL embedding gate. (mhammontree)
|
|
76
|
+
</content>
|
|
77
|
+
</invoke>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 40 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 34 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
|
-
- **api2** (API) —
|
|
22
|
+
- **api2** (API) — 19 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)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
25
25
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -5,11 +5,12 @@ apps:
|
|
|
5
5
|
- _underscore
|
|
6
6
|
- api2
|
|
7
7
|
- dbchanges2
|
|
8
|
+
- library
|
|
8
9
|
project: API
|
|
9
10
|
client: aig
|
|
10
11
|
type: profile
|
|
11
12
|
status: active
|
|
12
|
-
updated: 2026-
|
|
13
|
+
updated: 2026-07-28
|
|
13
14
|
owners: ["mhammontree"]
|
|
14
15
|
files: []
|
|
15
16
|
related:
|
|
@@ -35,6 +36,10 @@ go out under the Staples Protection Plan brand and link to `staplesprotection.to
|
|
|
35
36
|
NetSuite item/SO/invoice traits).
|
|
36
37
|
- **`dbchanges2`** — `Client_Aig/` schema + reference-data migrations (e.g. the SaleItem code
|
|
37
38
|
catalog in `Client_Aig.Items`).
|
|
39
|
+
- **`library` (1.0)** — the `App_Api_Toga2` bridge syncs 2.0 AIG contacts/entitlements down into the
|
|
40
|
+
legacy 1.0 `TOGA_AIG` DB (`Customers`/`ServiceRequests`), run by a worker cron every ~10 min. Note
|
|
41
|
+
the two AIG databases: **`TOGA_AIG`** = V1 legacy sync target; **`Client_Aig`** = V2 prod tenant. See
|
|
42
|
+
[App_Api_Toga2 bridge](../../1.0/apps/library/features/toga2-api-client-and-bridge.md).
|
|
38
43
|
|
|
39
44
|
See [entitlement-intake.md](features/entitlement-intake.md) for how a payload becomes an
|
|
40
45
|
entitlement and how to load new sale-item codes.
|
package/package.json
CHANGED