toga-ai 1.0.195 → 1.0.196
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/model-magic-field-access.md +57 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/language-translation-layer.md +48 -2
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
8
8
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
9
9
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php |
|
|
10
|
+
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
10
11
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
11
12
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
12
13
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: _Model magic-field access (__get without __isset)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Core/Model.php
|
|
13
|
+
related:
|
|
14
|
+
- ../../api2/features/language-translation-layer.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
`_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`.
|
|
20
|
+
Because PHP does not route `isset()` / null-coalescing through `__get()`, any `isset($model->field)` or
|
|
21
|
+
`$model->field ?? null` on a magic (DB) field is **always** `false` / `null` — even when the column
|
|
22
|
+
exists and has a value. And a bare `$model->field` read for a column that is **not** on the model
|
|
23
|
+
**throws**. This bites any code that probes for an optional/conditional field on a generic model.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
- `_Model::__get($name)` resolves a configured DB field and returns its value; for an unconfigured
|
|
28
|
+
field it throws (not a silent null).
|
|
29
|
+
- There is **no** `_Model::__isset()`, so PHP's `isset()`/`empty()`/`??` short-circuit to "not set"
|
|
30
|
+
for every magic field before `__get` is ever consulted. This is a PHP language rule, not a bug in
|
|
31
|
+
the value.
|
|
32
|
+
|
|
33
|
+
**Safe pattern for a possibly-absent field on a generic model:**
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
if (array_key_exists('uuid', $model->getFieldConfig())) {
|
|
37
|
+
$uuid = $model->uuid; // safe: __get won't throw, field is known to exist
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Check `array_key_exists('field', $model->getFieldConfig())` first, then read via `__get`. Do **not**
|
|
42
|
+
gate on `isset($model->field)` or `$model->field ?? $default` — both silently misbehave.
|
|
43
|
+
|
|
44
|
+
## Gotchas / known issues
|
|
45
|
+
|
|
46
|
+
- `isset($model->magicField)` / `$model->magicField ?? null` are **always** false/null for DB-backed
|
|
47
|
+
magic fields. Use `getFieldConfig()` + `array_key_exists` to test presence.
|
|
48
|
+
- A bare `__get` on an unconfigured field **throws** — never read a maybe-absent field without the
|
|
49
|
+
`getFieldConfig()` guard.
|
|
50
|
+
- This surfaced as a missing `uuid` in the API translation-layer fallback warning — see
|
|
51
|
+
[Language Translation Layer](../../api2/features/language-translation-layer.md).
|
|
52
|
+
|
|
53
|
+
## Change history
|
|
54
|
+
|
|
55
|
+
- 2026-06-25 — Documented the `__get`-without-`__isset` gap and the `getFieldConfig()` +
|
|
56
|
+
`array_key_exists` safe-read pattern, discovered while debugging a missing `uuid` in the API2
|
|
57
|
+
translation fallback warning. (jcardinal)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
|
-
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
|
|
6
|
+
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
|
|
7
7
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
8
8
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
9
9
|
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
|
|
@@ -6,10 +6,11 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- api2/Component/Api/V2/Response/Response.php
|
|
13
14
|
- _underscore/Model/Core/Setting.php
|
|
14
15
|
- _underscore/Model/Core/RecordField.php
|
|
15
16
|
- _underscore/Model/Core/DefaultGlobalSetting.php
|
|
@@ -20,6 +21,7 @@ files:
|
|
|
20
21
|
- dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql
|
|
21
22
|
related:
|
|
22
23
|
- ./acl-permission-chain.md
|
|
24
|
+
- ../../_underscore/features/model-magic-field-access.md
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Summary
|
|
@@ -55,6 +57,19 @@ persona** (in the user's persona order) that sets it wins. The resolved code is
|
|
|
55
57
|
response as `audience.language`. API-credential auth gets no language. Token refresh copies the claim,
|
|
56
58
|
so a language change requires re-authentication.
|
|
57
59
|
|
|
60
|
+
**Invalid-language hard failure (auth time).** The resolved code is validated against the client's
|
|
61
|
+
`Languages` table. An **unset/empty** setting defaults to `en` (no error). But a **non-empty** code
|
|
62
|
+
that does **not** exist in `Languages` now **fails authentication** with HTTP 400
|
|
63
|
+
`EV-16 DEFINED_MESSAGE_ERROR_INVALID_LANGUAGE_SETTING` (validation group, defined in `Response.php`),
|
|
64
|
+
with the offending `language` code in the message `identifiers` — instead of silently falling back to
|
|
65
|
+
`en`. A misconfigured language must surface loudly, not silently degrade.
|
|
66
|
+
|
|
67
|
+
**Auth-mint status finalization (subtle).** The auth success path sets `status = 201` and attaches the
|
|
68
|
+
tokens. EV-16 is signalled by a local flag `$invalidLanguageSetting` set at the resolution site; the
|
|
69
|
+
finalization branches `if (empty($invalidLanguageSetting))` → `201` + tokens, else
|
|
70
|
+
`addDefinedMessage(EV-16)` + `data = null`. See Gotchas — do **not** convert this to an
|
|
71
|
+
`is_null($this->response->status)` guard.
|
|
72
|
+
|
|
58
73
|
**Which fields are translatable (metadata-driven).** `Core.RecordFields.translationRecordFieldId`
|
|
59
74
|
(new column, positioned after `recordId`) points a source field's RecordField at the sidecar field's
|
|
60
75
|
RecordField. `buildLookups`/RecordFields-load resolves this into
|
|
@@ -65,7 +80,10 @@ serialization site (top-level full-model, custom-fields, FK child, both inherent
|
|
|
65
80
|
`getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` is called. When a non-base
|
|
66
81
|
language is active and the field is translatable, it loads the sidecar row for that source row +
|
|
67
82
|
`languageId` (cached per row so multiple fields = one load) and returns the sidecar value if non-null;
|
|
68
|
-
otherwise it returns the English default and queues a
|
|
83
|
+
otherwise it returns the English default and queues a fallback warning. The warning now carries the
|
|
84
|
+
missing record's `uuid` in its `identifiers` and dedupes per-field-**and**-per-record
|
|
85
|
+
(key `recordFieldId:uuid`) rather than once-per-field — so every record lacking a translation for a
|
|
86
|
+
field is reported individually.
|
|
69
87
|
|
|
70
88
|
**Write path.** On create and update, `extractTranslationWrites()` pulls translatable fields out of
|
|
71
89
|
the write set for a non-base language (so the English source is never overwritten), and after the
|
|
@@ -96,6 +114,26 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
96
114
|
per-field fallback warning signals the English fallback.
|
|
97
115
|
- The base language is `en`; when the resolved language is `en` (or API auth) all translation logic is
|
|
98
116
|
skipped and responses are byte-identical to pre-feature.
|
|
117
|
+
- **"Why isn't my language setting applying?"** A `ClientGlobalSettings` row with `isOverridable = 0`
|
|
118
|
+
**locks** the value against all downstream persona/user layers — a `UserGlobalSettings` (or persona)
|
|
119
|
+
override never wins while the client-global layer is locked. This is by design (the 8-layer cascade
|
|
120
|
+
with `isOverridable` locking). To let user/persona overrides apply, the locking layer must be
|
|
121
|
+
`isOverridable = 1` (or its value cleared). The cascade itself is correct — this is a data-config
|
|
122
|
+
gotcha, not a code bug.
|
|
123
|
+
- **Namespace gotcha in `V2.php` (`namespace api;`).** The api-namespaced classes
|
|
124
|
+
(`_Component_Api_V2_Response`, `_Component_Api_V2_Response_Message`) must be referenced **unqualified**
|
|
125
|
+
(resolves to `api\…`) or with the full `\api\_Component_Api_V2_Response_Message`. A bare leading
|
|
126
|
+
backslash (`\_Component_Api_V2_Response_Message`) forces the global namespace and throws
|
|
127
|
+
`EO-1 "Could not find required file"` via the autoloader. The translation fallback-warning code hit this.
|
|
128
|
+
- **Do NOT guard the auth-mint finalization with `is_null($this->response->status)`.** The auth response
|
|
129
|
+
auto-adds client `name` + user `firstName`/`lastName` and runs `processRoutePairs()`, which sets
|
|
130
|
+
`$this->response->status` to `200` **before** finalization. An `is_null` guard there suppresses the
|
|
131
|
+
`201` + tokens and yields a broken `200`/`data:null` login. Branch on the explicit
|
|
132
|
+
`$invalidLanguageSetting` flag instead.
|
|
133
|
+
- Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
|
|
134
|
+
`__get` — `isset()`/`?? null` always read false/null. See
|
|
135
|
+
[_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
|
|
136
|
+
cause of the uuid not appearing in the fallback warning.
|
|
99
137
|
- Migrations not yet executed at time of writing; needs live verification.
|
|
100
138
|
|
|
101
139
|
## Change history
|
|
@@ -103,6 +141,14 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
103
141
|
- 2026-06-23 — Initial build: audience.language + JWT embedding, ItemTranslations sidecar + metadata +
|
|
104
142
|
full ACL chain, and translation-aware read/write at the API layer. Also fixed a latent autoload bug
|
|
105
143
|
by renaming `DefaultFlobalSetting.php` → `DefaultGlobalSetting.php`. (jcardinal)
|
|
144
|
+
- 2026-06-25 — Added defined error `EV-16 INVALID_LANGUAGE_SETTING` (HTTP 400, in `Response.php`): a
|
|
145
|
+
non-empty language code absent from `Languages` now fails auth instead of silently falling back to
|
|
146
|
+
`en` (empty still defaults to `en`). Documented the auth-mint `201` finalization (branch on the
|
|
147
|
+
`$invalidLanguageSetting` flag, never an `is_null(status)` guard). Fixed the fallback-warning
|
|
148
|
+
namespace bug (no bare leading backslash on api-namespaced classes). Fallback warning now includes the
|
|
149
|
+
record `uuid` and dedupes per-record (`recordFieldId:uuid`). Documented the `isOverridable = 0`
|
|
150
|
+
client-global lock as a config gotcha. Discovered the `_Model` `__isset` gap behind the missing-uuid
|
|
151
|
+
bug (see linked _underscore doc). Live-tested on the local Compass DB. (jcardinal)
|
|
106
152
|
|
|
107
153
|
## Related docs
|
|
108
154
|
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
18
|
+
- **_underscore** (_Underscore) _(framework core)_ — 14 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
19
|
- **worker2** (Worker) — 14 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
20
|
- **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
package/package.json
CHANGED