toga-ai 1.0.1082 → 1.0.1083
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 +2 -1
- package/knowledge/2.0/apps/_underscore/features/service-request-status-and-stages.md +60 -0
- package/knowledge/2.0/apps/_underscore/features/sla-engine.md +30 -19
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +6 -2
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +4 -2
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +3 -1
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -63,8 +63,9 @@
|
|
|
63
63
|
| [SO↔PO and TO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | The two SO↔PO (and two TO↔PO) bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO/TO↔PO query returns an empty |
|
|
64
64
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | Per-client status-filter dropdown on the sales-orders table, driven by a Surface `FILTER_SET`; open when the offered statuses are wrong for a tenant or when onb |
|
|
65
65
|
| [Service request links (ServiceRequests_Tickets, ServiceRequests_ServiceRequests bridges)](features/service-request-links.md) | Many-to-many links from a service request to tickets and to other service requests; open before linking SRs, reading `_linkCount`, or touching the old `ServiceR |
|
|
66
|
+
| [2.0 Service Request status (`_status`) is derived; stages are manual-only; standard schedule / crew / location columns](features/service-request-status-and-stages.md) | How a 2.0 service request's `_status` is computed, what stages are for, the standard SR columns, and the set-stage write; open when an SR shows the wrong status |
|
|
66
67
|
| [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
|
|
67
|
-
| [
|
|
68
|
+
| [SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute](features/sla-engine.md) | How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`); open when an SLA countdown |
|
|
68
69
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
|
|
69
70
|
| [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | Resolver capability that folds `Core.RecordFields` input-contract metadata (maxLength/validationType/isWritable) onto a surface FIELD element; open when a 2.5 s |
|
|
70
71
|
| [DECLINED 2026-10-08 — re-key Client.SurfaceOverrides from numeric Core ids to uuids](features/surface-overrides-uuid-rekey.md) | **Decision 2026-10-08 (jcardinal): keep numeric ids; no uuid re-key.** The plan below is not scheduled; see the Layer boundary in [surface-resolver](surface-res |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 2.0 Service Request status (`_status`) is derived; stages are manual-only; standard schedule / crew / location columns
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-19
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/ServiceRequest.php
|
|
13
|
+
- _underscore/Model/Client/ServiceRequestNote.php
|
|
14
|
+
- _underscore/Model/Client/User.php
|
|
15
|
+
- _underscore/Model/Client/Case.php
|
|
16
|
+
- _underscore/Model/Prudential/ServiceRequest.php
|
|
17
|
+
- _underscore/Model/Growrk/ServiceRequest.php
|
|
18
|
+
- _underscore/Model/Adyen/ServiceRequest.php
|
|
19
|
+
- _underscore/Model/Elite/ServiceRequest.php
|
|
20
|
+
- dbchanges2/Core/2026-10-19i - TRUE-82915 ServiceRequestInterceptors.sql
|
|
21
|
+
- dbchanges2/Client_Prudential/2026-10-19a - TRUE-82915 ServiceRequestStageStatusLinks.sql
|
|
22
|
+
- dbchanges2/Client_Growrk/2026-10-19a - TRUE-82915 ServiceRequestStageStatusLinks.sql
|
|
23
|
+
related:
|
|
24
|
+
- ./ticket-status-from-stage.md
|
|
25
|
+
- ./case-status-and-stage-gate.md
|
|
26
|
+
- ./sla-engine.md
|
|
27
|
+
- ./service-request-links.md
|
|
28
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
How a 2.0 service request's `_status` is computed, what stages are for, the standard SR columns, and the set-stage write; open when an SR shows the wrong status, you add a stage, or you add a base SR hook.
|
|
32
|
+
|
|
33
|
+
## Summary
|
|
34
|
+
|
|
35
|
+
`_Model_Client_ServiceRequest::_status` is a `FIELD_SQL` calculated field (same idea as Tickets and Cases). Status comes from the SR's own fields; stages hold only manual states that cannot be derived. Full detail: toga25-desk `docs/decisions/0166-sr-status-derived-from-fields-stages-manual-only.md` and `0167-sr-site-schedule-crew-tags-reported-by.md`.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
`_status` returns, in order:
|
|
40
|
+
1. the linked status slug, when the stage has `serviceRequestStatusId`;
|
|
41
|
+
2. `closed`, when the stage is unlinked and `isOpen = 0`;
|
|
42
|
+
3. `in-progress`, when a Lead (`assignedToContactId`) or crew (`ServiceRequests_Users`) is set;
|
|
43
|
+
4. `scheduled`, when `dtScheduledStart` is set;
|
|
44
|
+
5. else `new`.
|
|
45
|
+
|
|
46
|
+
- **Stages = manual, non-derivable states only:** blockers (roadblocked / on-hold / awaiting-user / awaiting-parts — each needs a reason note in `ServiceRequestNotes`), pending-client-approval, resolved, closed. No stage is the normal flow.
|
|
47
|
+
- **Per-client stage → status links** go in `Client_<Name>/` files (e.g. `Client_Prudential` / `Client_Growrk` `2026-10-19a`), never a `Client/` fan-out.
|
|
48
|
+
- **Standard columns:** `assignedToContactId` (Lead), `reportedByContactId`, `locationId` (the site — NOT `deliverToAddressId`, which is the shipping address), `isRemote`, `dtScheduledStart` / `dtScheduledEnd` (NOT `dateDelivery`). `ServiceRequestTypes`: `isSiteVisit`, `iconName`, `colorHex`. Bridges `ServiceRequests_Users` (crew), `_Tags`, `_Files` (Core.Records 422-424, RecordFields 3106-3152).
|
|
49
|
+
- **Set stage** = table-based `PUT /v2/service-requests/{uuid}` with nested `serviceRequestNotes` (the reason note). A stage PUT without a uuid = 400 for signed-in users (CSO).
|
|
50
|
+
- **Crew "required"** is a UI rule only — integration-created SRs send no crew.
|
|
51
|
+
- **Base hooks** (`prePost` / `postPost` / `postPut` / `postPatch`) are registered for every client by `Core/2026-10-19i` (`clientId` / `apiId` NULL). They run the schedule check, the set-stage gate, and the [SLA engine](./sla-engine.md).
|
|
52
|
+
|
|
53
|
+
## Gotchas
|
|
54
|
+
|
|
55
|
+
- **⚠ Never delete or rename existing stage rows.** Client integrations match stage NAMES: Prudential `postPut` ('Cancelled' / 'Update'), Growrk ('Completed'), worker 1.0 cron ('Exception' + stage id 2).
|
|
56
|
+
- **⚠ A client SR override must call `parent::` in every hook it overrides.** Before 2026-10-19 the Prudential / Growrk / Adyen / Elite `ServiceRequest` overrides did not, so every base SR hook was silently skipped for those clients. Adding a base hook? Check each `_Model_<Client>_ServiceRequest` calls `parent::` first (and that the parent declares the method — see [interceptors](../../api2/features/api-payload-interceptors.md#the-client-model-resolution-that-decides-which-class-must-define-the-method)).
|
|
57
|
+
- **⚠ A NULL stage is now the normal case — never INNER JOIN the SR stage.** `User::_openWorkCount` and `Case::_resolvedCount` used INNER JOIN on stage + `isOpen` and dropped stageless SRs. Rule: open = no stage OR stage `isOpen = 1`; open work counts the SR's own crew.
|
|
58
|
+
|
|
59
|
+
## Change history
|
|
60
|
+
- 2026-10-19 — `_status` derived from fields; stages manual-only; schedule / crew / location / reported-by columns; client overrides call `parent::`; open counts include stageless SRs (TRUE-82915). (jcardinal)
|
|
@@ -1,65 +1,76 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Component/Sla/Sla.php
|
|
13
13
|
- _underscore/Model/Client/Ticket.php
|
|
14
14
|
- _underscore/Model/Client/TicketStage.php
|
|
15
15
|
- _underscore/Model/Client/TicketNote.php
|
|
16
|
+
- _underscore/Model/Client/ServiceRequest.php
|
|
17
|
+
- _underscore/Model/Client/ServiceRequestNote.php
|
|
16
18
|
- _underscore/Model/Core/Client.php
|
|
17
19
|
- dbchanges2/Core/2026-10-16a - SlaReplyClockCore.sql
|
|
18
20
|
- dbchanges2/Client/2026-10-16a - SlaReplyClockFanout.sql
|
|
19
21
|
- dbchanges2/Client_True/2026-10-16a - SlaStageDataRepair.sql
|
|
20
|
-
- dbchanges2/
|
|
22
|
+
- dbchanges2/Core/2026-10-19k - TRUE-82915 ServiceRequestSlaCore.sql
|
|
23
|
+
- dbchanges2/Client/2026-10-19i - TRUE-82915 ServiceRequestSla.sql
|
|
21
24
|
related:
|
|
22
25
|
- ./ticket-status-from-stage.md
|
|
26
|
+
- ./service-request-status-and-stages.md
|
|
23
27
|
- ../../api2/features/api-payload-interceptors.md
|
|
24
28
|
- ../../api2/features/tickets-api.md
|
|
25
29
|
- ../../dbchanges2/features/rerunnable-additive-inserts.md
|
|
26
30
|
---
|
|
27
31
|
|
|
28
|
-
How 2.0
|
|
32
|
+
How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`); open when an SLA countdown is wrong, a stage does not pause, you need to recompute a client, or you want SLA on another record type.
|
|
29
33
|
|
|
30
34
|
## Summary
|
|
31
35
|
|
|
32
|
-
`_Component_Sla` (`_underscore/Component/Sla/Sla.php`) is the ONLY code that writes `
|
|
36
|
+
`_Component_Sla` (`_underscore/Component/Sla/Sla.php`) is the ONLY code that writes the stored SLA columns (`sla*` / `dtSla*` / `dtFirstResponse`). One engine, record-generic: every record-specific name comes from `_Component_Sla::descriptor($record)`. Static, shared by every 2.0 app, called from api2 payload interceptors (not model save hooks). Two clocks — resolution and reply (first response) — each with its own pause bookkeeping, driven by per-stage effects stored as client data.
|
|
33
37
|
|
|
34
38
|
## How it works
|
|
35
39
|
|
|
36
|
-
- **
|
|
40
|
+
- **Descriptor:** `descriptor(RECORD_TICKET | RECORD_SERVICE_REQUEST)` (`'ticket'` / `'serviceRequest'`) returns `record, label, table, typeColumn, stageColumn, contactColumn, targetsTable, stagesTable, hasReplyEffectColumn, notesTable, noteRecordColumn, noteStageColumn, noteAuthorUserColumn`. `allRecordTypes()` = what the client switch and recompute cover. **New record type = a new descriptor + the same columns — never a copy of the engine** (decided, CTO). Detail: toga25-desk `docs/decisions/0168-sla-engine-record-generic.md`.
|
|
41
|
+
- **Tickets (descriptor 1):** `TicketSlas`, `TicketStages.slaEffect` + `slaFirstResponseEffect`, `TicketNotes`, contact = `contactId`. Moving tickets onto the descriptor changed nothing (275-scenario old-vs-new harness and a live recompute replay: 0 diffs).
|
|
42
|
+
- **Service requests (descriptor 2):** same 9 stored SLA columns + 11 calc fields on `ServiceRequests`; targets `ServiceRequestSlas`; effect `ServiceRequestStages.slaEffect`. No reply-effect column (`hasReplyEffectColumn = false`) — the reply clock follows `slaEffect`. Contact = `requestedForContactId`. First response = first external note with a signed-in author (`authorUserId`) OR a contact other than the requester.
|
|
43
|
+
- **Targets:** `(typeColumn, urgencyId)` → minutes (MINUTES ×1, HOURS ×60, DAYS ×1440). Any other unit (e.g. `BUSINESS_DAYS`) = no target. No business-day math.
|
|
37
44
|
- **Due:** `dtCreated + targetMinutes + pausedMinutes`, per clock.
|
|
38
|
-
- **Resolution clock:**
|
|
39
|
-
- **Reply clock:**
|
|
40
|
-
- **`resolvePauseState(params)`** is clock-agnostic
|
|
41
|
-
- **
|
|
42
|
-
- **Read side:** `_slaTime`, `_slaState`, `_slaPausedAt` (FIELD_SQL on
|
|
45
|
+
- **Resolution clock:** stage `slaEffect` (`RUN`/`PAUSE`/`STOP`, no stage = RUN). Columns `dtSlaPaused`, `slaPausedMinutes`. STOP time is charged like pause time; `_slaState` shows `STOPPED`.
|
|
46
|
+
- **Reply clock (tickets):** `TicketStages.slaFirstResponseEffect` (`RUN`/`PAUSE`, column default `PAUSE`, no stage = RUN). Columns `dtSlaFirstResponsePaused`, `slaFirstResponsePausedMinutes`. Seeded RUN only for new / open / reopened stages. Keeps tracking after the first response so stamps stay right.
|
|
47
|
+
- **`resolvePauseState(params)`** is clock-agnostic, called once per clock. Do not fork the math.
|
|
48
|
+
- **Ticket first response** = `MIN(TicketNotes.dtStamp)` of external notes by a contact other than the ticket's own contact. Authorless notes never count.
|
|
49
|
+
- **Read side:** `_slaTime`, `_slaState`, `_slaPausedAt` (FIELD_SQL on the record model). `_slaPausedAt` returns the paused stamp of the clock being shown (reply while unanswered and a reply due exists, else resolution). UIs read it, not the raw columns.
|
|
43
50
|
- **Time = DB clock** (`SELECT NOW()` on the client writer), read once, then advanced by PHP elapsed time.
|
|
44
|
-
- **Client switch:** `Core.Clients.isSlaEnabled
|
|
45
|
-
- **Operator recompute (no OFF/ON):** `PUT /v2/clients/{uuid}` body `{"recomputeSla": true}
|
|
46
|
-
- **One run per client:** the
|
|
47
|
-
- **Cross-client:**
|
|
51
|
+
- **Client switch:** `Core.Clients.isSlaEnabled` covers every record type. OFF = every SLA column NULL. Flipping it in `PUT /v2/clients/{uuid}` runs `applyClientSwitch` (backfill, skips STOP-stage records when turning on).
|
|
52
|
+
- **Operator recompute (no OFF/ON):** `PUT /v2/clients/{uuid}` body `{"recomputeSla": true}` — all record types. `_Model_Core_Client::prePut` strips the key (not a column), requires boolean `true`, parks it on `$api->passThrough`; `postPut` runs `recomputeClient` (STOP included). Allowed only for callers who can WRITE `Clients.isSlaEnabled`. A body empty after the strip becomes `{}` in V2.php and `postPut` still runs. Pattern: [command keys in interceptors](../../api2/features/api-payload-interceptors.md#passing-state-from-a-pre-hook-to-a-post-hook).
|
|
53
|
+
- **One run per client:** the backfill takes `GET_LOCK('sla-recompute-<clientId>', 0)` on the SAME connection as its writes, `RELEASE_LOCK` in `finally`. Lock busy → `_Exception_Validation` 400. Any failure throws, so the API write rolls back. Reuse this shape for any "one at a time per client" operation.
|
|
54
|
+
- **Cross-client:** switch/recompute use `DB_TARGET_CLIENT` (`registerTargetClientDatabase`), never the caller's `DB_CLIENT`. Core and client data are never joined.
|
|
48
55
|
|
|
49
56
|
## Gotchas
|
|
50
57
|
|
|
51
|
-
- **⚠ A stage that "never pauses" is usually data, not code.** Check `
|
|
58
|
+
- **⚠ A stage that "never pauses" is usually data, not code.** Check the stage table's `slaEffect` (and `slaFirstResponseEffect` for tickets) in that environment first. QA alpha `Client_True` (2026-10-16): every ticket stage stayed RUN because the `Client/2026-09-29d` seed matched by slug before the slugs existed; fixed by `Client_True/2026-10-16a - SlaStageDataRepair.sql`. See [fan-out seed keyed on a slug](../../dbchanges2/features/rerunnable-additive-inserts.md#gotchas).
|
|
59
|
+
- **⚠ Writes that skip api2 skip the SLA.** worker 1.0 Prudential crons set `ServiceRequests.serviceRequestStageId = 2` by direct SQL — no pause is recorded until the next api2 write or a recompute. Accepted gap.
|
|
60
|
+
- **⚠ Full recompute resets accrued paused minutes on records no longer paused** (both engines, pre-existing). Know this before running `recomputeSla` on a client with long past pauses.
|
|
52
61
|
- **⚠ The lock is released before api2 commits.** A second recompute started right after can overlap the first's uncommitted writes. Accepted (operator-only, rare) — do not copy this gap into a high-traffic operation; hold the lock past commit there.
|
|
53
|
-
- **No worker job for big clients** (decided).
|
|
54
|
-
- **`FIELD_LIST` does not validate ENUMs** — `TicketStage::save()` checks both effects explicitly. Keep that check when adding a value.
|
|
55
|
-
- **Changing a stage's effect does not move existing
|
|
62
|
+
- **No worker job for big clients** (decided). Recompute runs inside the request; a very large client can hit the 300 s request cap. Paged at 500 records.
|
|
63
|
+
- **`FIELD_LIST` does not validate ENUMs** — `TicketStage::save()` checks both effects explicitly. Keep that check (and add it on any new stage model) when adding a value.
|
|
64
|
+
- **Changing a stage's effect does not move existing records** until their next write or a recompute. Run `{"recomputeSla": true}` after a stage-effect data fix.
|
|
56
65
|
|
|
57
66
|
## Related
|
|
58
67
|
|
|
59
68
|
- [Ticket status from stage](./ticket-status-from-stage.md)
|
|
69
|
+
- [Service request status and stages](./service-request-status-and-stages.md)
|
|
60
70
|
- [API payload interceptors](../../api2/features/api-payload-interceptors.md)
|
|
61
71
|
- [Tickets API](../../api2/features/tickets-api.md)
|
|
62
72
|
- [Re-runnable additive migrations](../../dbchanges2/features/rerunnable-additive-inserts.md)
|
|
63
73
|
|
|
64
74
|
## Change history
|
|
75
|
+
- 2026-10-19 — Engine made record-generic via `descriptor()`; service requests are descriptor 2 (same columns, `ServiceRequestSlas`, reply clock follows `slaEffect`); tickets unchanged (0 diffs) (TRUE-82915). (jcardinal)
|
|
65
76
|
- 2026-10-16 — Reply clock gets its own per-stage effect (`slaFirstResponseEffect`, pauses outside new/open/reopened); operator recompute via `{"recomputeSla": true}`; per-client named lock (TRUE-82849). (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -50,6 +50,7 @@ related:
|
|
|
50
50
|
- ./language-translation-layer.md
|
|
51
51
|
- ../../_underscore/features/persona-nav-visibility-gate.md
|
|
52
52
|
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
53
|
+
- ../../_underscore/features/service-request-status-and-stages.md
|
|
53
54
|
---
|
|
54
55
|
|
|
55
56
|
How `_Model` prePost/postPost/prePut/postPut hooks are dispatched by the api2 /v2 engine only when a metadata row exists — the missing-row silent no-op, the wrong-row hard fatal, and the persistence/transaction traps; open before writing, activating, or debugging any interceptor.
|
|
@@ -343,7 +344,8 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
343
344
|
- **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
|
|
344
345
|
- **⚠ An own-row PRE guard that checks only `userId` is bypassable** — alias keys (`user`) map after PRE, and a parent-route POST overwrites the stamped FK. Allowlist keys + refuse parent routes. See [own-row guards](#own-row-guards-in-a-pre-interceptor).
|
|
345
346
|
- **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before concluding a client has no interceptors.
|
|
346
|
-
-
|
|
347
|
+
- **Duplicate rows are de-duplicated (since 2026-10-19, TRUE-82915).** `registerApiPayloadInterceptorLookupEntry` (`V2.php`) keeps ONE entry per `(record, PRE|POST, httpMethod)` across Core and client tables; the highest `minDepth` wins (NULL lowest). So a generic row (`clientId` / `apiId` NULL) plus a client's own row dispatches once. Before this, both fired (Adyen 2026-10-08: double worker jobs; Elite / Adyen SR `postPost` → double sales-order tasks). **Rollback order:** deactivate generic rows that overlap client rows BEFORE rolling api2 back, or the double dispatch returns. Pre-release check query: header of `dbchanges2/Core/2026-10-19i - TRUE-82915 ServiceRequestInterceptors.sql`. Still keep each key in one table.
|
|
348
|
+
- **⚠ A client override that skips `parent::` silently disables every base hook for that client.** Found 2026-10-19 on four `ServiceRequest` overrides — see [SR status and stages](../../_underscore/features/service-request-status-and-stages.md#gotchas). Adding a base hook = check every client subclass calls `parent::`.
|
|
347
349
|
- **The method name is derived, so a typo'd enum silently misses.** A row with `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine finds no method and moves on.
|
|
348
350
|
- **Registration is per-environment.** Like `RecordFields` / `RecordScripts` / ACL rows, this is metadata — "works in prod, not in beta/dev-sandbox" is the signature of drift, not of a bad deploy. See the [non-prod metadata drift repair workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
349
351
|
- **⚠ An in-request read-after-write in an interceptor works on beta and fails 100% in production.** Reads default to the **reader** connection, which cannot see the request's own uncommitted row; dev-sandbox points writer and reader at the same endpoint, so it hides the bug. Pin with `setIsReadHostEnabled(false)` (restore in `finally`).
|
|
@@ -368,6 +370,7 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
368
370
|
- [Email template sending](../../_underscore/features/email-template-sending.md)
|
|
369
371
|
- [Elite SalesOrder → NetSuite push](../../../clients/elite/features/salesorder-netsuite-push.md)
|
|
370
372
|
- [Compass sales-order line renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md)
|
|
373
|
+
- [Service request status and stages](../../_underscore/features/service-request-status-and-stages.md)
|
|
371
374
|
|
|
372
375
|
## Change history
|
|
373
376
|
- 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
|
|
@@ -376,3 +379,4 @@ Refuse API-key callers when the rule is "the signed-in user" (no user on the tok
|
|
|
376
379
|
- 2026-10-16 — Command keys: strip in `prePut`, park on `passThrough`, act in `postPut`; empty body → `{}` (TRUE-82849). (jcardinal)
|
|
377
380
|
- 2026-10-18 — Write intent that writes another record via internal request (caller ACL, rollback, `addNote` after the call) (TRUE-82876). (jcardinal)
|
|
378
381
|
- 2026-10-17 — "Action table" pattern for audited actions; write intents before a stage gate; new interceptor rows ship in a file run after the code deploy (TRUE-82889). (jcardinal)
|
|
382
|
+
- 2026-10-19 — Lookup de-duplicates rows per record / PRE|POST / method (highest `minDepth` wins); client overrides must call `parent::` (TRUE-82915). (jcardinal)
|
|
@@ -49,7 +49,9 @@ controller.
|
|
|
49
49
|
`fields`, `where`, `join` / `ojoin`, `sort`, `group`, `distinct`, `page`, `recordsPerPage`,
|
|
50
50
|
`depth`, `calcDepth` (or one base64(JSON) `_` blob).
|
|
51
51
|
|
|
52
|
-
- **Pagination is `page` + `recordsPerPage`. There is no `limit`/`offset`.**
|
|
52
|
+
- **Pagination is `page` + `recordsPerPage`. There is no `limit`/`offset`.** A `limit=5` on a
|
|
53
|
+
list GET is not ignored — it returns **404 `EV-14`** "Expecting one record ... multiple records
|
|
54
|
+
were found". Defaults:
|
|
53
55
|
`recordsPerPage` **25**, `depth` **3**. Max `recordsPerPage` **10000** — exceeding it returns
|
|
54
56
|
**`EV-13`** (HTTP 400).
|
|
55
57
|
- **Sort:** `-field` = DESC, `+field` = ASC, bare `field` = ASC. All three are handled
|
|
@@ -656,6 +658,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
656
658
|
[V2 request logging](request-logging.md).
|
|
657
659
|
|
|
658
660
|
## Change history
|
|
661
|
+
- 2026-10-19 — `limit` on a list GET = 404 `EV-14`, use `recordsPerPage` (TRUE-82915). (jcardinal)
|
|
659
662
|
- 2026-10-19 — Self-join `where` needs an `ojoin` alias (`Locations@p`); `_` field ACL bypass does not apply cross-client (TRUE-82911). (jcardinal)
|
|
660
663
|
- 2026-10-18 — Grouped queries: stored root group key now selected + ACL-gated; known gaps (joined key, total count, `ORDER BY uuid` under ONLY_FULL_GROUP_BY) (TRUE-82876). (jcardinal)
|
|
661
664
|
- 2026-10-17 — Child-path `where` must start at an inherent child; FK-first path → TableView join alias (TRUE-82872). (jcardinal)
|
|
@@ -667,7 +670,6 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
667
670
|
- 2026-09-18 — `in` instead of an OR-chain of `contains` shortens SQL only, not speed. (apeterson)
|
|
668
671
|
- 2026-09-17 — Bulk DELETE by `where`: exists, unused, unsafe; zero matches looks like an ACL denial. (rgirish)
|
|
669
672
|
- 2026-09-14 — `where` on a calculated field works with the bare name; `calcDepth` does not gate a named calc field; list GET cost model. (apeterson)
|
|
670
|
-
- 2026-09-08 — `depth` read-back cost (~830s at `depth=5` on a write). (jcardinal)
|
|
671
673
|
- 2026-09-04 — URL-encoded `where` → 500/`EO-1` (`Logs.Issue` 97). (rgirish)
|
|
672
674
|
- 2026-09-01/02 — `_` field in `fields=` is never ACL-checked; inherent-child keys govern writes; bridge `InherentRecordChildren` rows. (apeterson, bala)
|
|
673
675
|
- 2026-07-29 → 08-30 — Created; then: `fields=` does not narrow SELECT, `depth` races deletes, sort `+`/DISTINCT, uuid-READ ignores `where`, joins drive filter not shape, `calcDepth` default 1, FK omitted at max depth, PUT echo can omit fields. (tcox, bala, apeterson, jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-19
|
|
10
10
|
owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
|
|
@@ -491,6 +491,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
|
|
|
491
491
|
- **⚠ `NOT NULL DEFAULT <value>` does not save a `_Model` column** — the ORM writes an explicit NULL,
|
|
492
492
|
which skips the DEFAULT and errors 1048. Ship a model default with the ALTER.
|
|
493
493
|
- **⚠ `col = 0` never matches a NULL** — guard flag-enabling UPDATEs with `COALESCE(col, 0) = 0`, or the migration reports success and changes nothing.
|
|
494
|
+
- **⚠ A `WHERE col <> 'New text'` guard SKIPS a case-only change on a `_ci` column.** `'service request' <> 'Service Request'` is FALSE under `utf8mb4_general_ci`, so the copy UPDATE runs and changes nothing. Guard copy updates with `BINARY col <> 'New text'`. Bug `Core/2026-10-19j`; fixed in `Core/2026-10-19n`..`p` (TRUE-82915).
|
|
494
495
|
- **⚠ An unguarded ACL grant migration duplicates silently — `AclRecordPermissions` has no unique key
|
|
495
496
|
that stops it (found in prod 2026-09-15).** `_modules/netsuite/2026-09-11c - ItemClassRecordPermission.sql`
|
|
496
497
|
is a plain `INSERT … VALUES` for `(recordId 105, roleId 3)` with **no `NOT EXISTS` guard**. Every
|
|
@@ -560,6 +561,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
|
|
|
560
561
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
561
562
|
|
|
562
563
|
## Change history
|
|
564
|
+
- 2026-10-19 — `<>` copy-update guard skips a case-only change on a `_ci` column; use `BINARY` (TRUE-82915). (jcardinal)
|
|
563
565
|
- 2026-10-17 — `TicketTypes.code` has no UNIQUE and may hold duplicates; guard seeds on `code` (TRUE-82889). (jcardinal)
|
|
564
566
|
- 2026-10-16 — Backfill an api2-identifier column before the deploy (seed file), stragglers + ALTER after (TRUE-82841). (jcardinal)
|
|
565
567
|
- 2026-10-16 — NOT NULL alternative for big tables: strict session + `ALGORITHM=INPLACE, LOCK=NONE`
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
|
|
22
22
|
## 2.0 framework
|
|
23
23
|
|
|
24
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
24
|
+
- **_underscore** (_Underscore) _(framework core)_ — 111 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
25
25
|
- **worker2** (Worker) — 91 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
26
26
|
- **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
27
27
|
- **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
package/package.json
CHANGED