# The metadata-driven request lifecycle
The diagram you'll come back to. Every metadata-driven page in xly follows
this same flow; once you've internalised it, the rest of the framework is
variations on a theme.
## The flow
```
┌──────────────────────────────────────────────────────────────────────┐
│ Browser │
│ 1. Loads SPA shell at any URL (the server returns the same shell │
│ for every path; gdsroute is a *client-side* sidebar/deep-link │
│ whitelist, not a server-side 404 gate) │
│ 2. User clicks a sidebar item or navigates within the SPA │
│ 3. SPA decides which module to load → calls /business/... │
└──────────────────────────────────────────────────────────────────────┘
│
│ GET /xlyEntry/business/getModelBysId/{sModelsId}
│ ?sModelsId={sModelsId}
│ (the module's id appears in BOTH path and query —
│ the controller binds the path variable, but the
│ service reads sModelsId from the @RequestParam map,
│ so the SPA must include it in the query string too)
▼
┌──────────────────────────────────────────────────────────────────────┐
│ xlyEntry — BusinessBaseController.getModelBysId() │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ RequestAddParamUtil.addParams(params, userInfo) │ │
│ │ → sBrandsId, sSubsidiaryId, sUserId, sLanguage, … │ │
│ │ (16 keys total — see runtime.md) │ │
│ │ → tenant scope is now AVAILABLE for any downstream query │ │
│ │ that wants it. Framework-metadata reads filter by │ │
│ │ form-id only; per-tenant overlays + business data │ │
│ │ reads filter by sBrandsId/sSubsidiaryId. │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ BusinessBaseService.getModelBysId(map) │
│ │ │
│ ├── 1. formData │
│ │ └── gdsconfigformmaster (filtered by │
│ │ sParentId = sModelsId; gdsmodule itself │
│ │ is *not* SELECT-ed, only referenced by id) │
│ │ + LEFT JOIN gdsconfigformpersonalize │
│ │ (per-tenant overlay) │
│ │ + per-master gdsconfigformslave │
│ │ + per-master gdsconfigformcustomslave │
│ │ (per-tenant overlay) │
│ ├── 2. gdsformconst (filtered by sParentId only; │
│ │ NOT tenant-scoped; sLanguage selects which │
│ │ label column to return) │
│ ├── 3. sysjurisdiction (per-user/group grants joined │
│ │ to sftlogininfojurisdictiongroup + │
│ │ sisjurisdictionclassify; skipped for ADMIN. │
│ │ Returned under map key `gdsjurisdiction` — │
│ │ misleading name, the gdsjurisdiction table is │
│ │ the builder-side action catalogue, not what's │
│ │ read here) │
│ ├── 4. sysbillnosettings (per-tenant, per-form) │
│ └── 5. sysreport (per-tenant, per-form) │
└──────────────────────────────────────────────────────────────────────┘
│
│ Returns one composite map:
│ { formData, gdsformconst, gdsjurisdiction,
│ billnosetting, report }
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Browser SPA │
│ Renders the form using formData; populates dropdowns from │
│ gdsformconst and per-control SQL fetched separately │
│ │
│ Calls /business/getBusinessDataByFormcustomId/{formId} │
│ with sModelsId={moduleId} to load the actual rows │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ xlyEntry — BusinessBaseController.getBusinessDataByFormcustomId() │
│ │
│ Composes parameterised SQL from formMaster.sSqlStr / sWhere / │
│ sOrder, with sBrandsId / sSubsidiaryId injected; │
│ runs against the form's backing object (table | view | proc). │
└──────────────────────────────────────────────────────────────────────┘
│
▼
User sees the grid
```
## The same flow as a sequence
The ASCII above shows the order of operations; the sequence diagram
below shows *who calls whom*, which is what matters when you're
tracing a real request through the runtime.
```mermaid
sequenceDiagram
autonumber
participant SPA as Browser SPA
participant CTRL as BusinessBaseController
participant SVC as BusinessBaseServiceImpl
participant FORMS as BusinessGdsconfigformsServiceImpl
participant DB as MySQL
participant REDIS as Redis (RedisCacheManager)
SPA->>CTRL: GET /business/getModelBysId/{sModelsId}
?sModelsId=...&Authorization=
Note over CTRL: AuthorizationInterceptor.preHandle
resolves UserInfo from Redis
RequestAddParamUtil.addParams (16 keys)
CTRL->>SVC: getModelBysId(map)
Note over SVC: getModelConfigByModleId (inherited from BaseServiceImpl)
orchestrates the per-master form-master + slave loads
SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow
(form-master + slaves + overlays)
REDIS-->>FORMS: cache hit?
FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize; per master row, gdsconfigformslave + gdsconfigformcustomslave
DB-->>FORMS: rows
FORMS-->>SVC: formData
SVC->>FORMS: getFormconstData (form-id only, NOT tenant-scoped)
FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=...
DB-->>FORMS: rows
FORMS-->>SVC: gdsformconst
alt sUserType != ADMIN
SVC->>FORMS: getJurisdictionData (per-user grants)
FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup
DB-->>FORMS: rows
FORMS-->>SVC: gdsjurisdiction (map-key; source table is sysjurisdiction)
else ADMIN
Note over SVC: skip jurisdiction load
end
SVC->>FORMS: getBillnosettingData
FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant
DB-->>FORMS: row
FORMS-->>SVC: billnosetting
SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant
DB-->>SVC: report rows
SVC-->>CTRL: composite Map (5 keys)
CTRL-->>SPA: AjaxResult{code:1, dataset:{...}}
SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}
?sModelsId=...
Note over CTRL,SVC: same RequestAddParamUtil pass
then per-form sSqlStr / sWhere / sOrder
CTRL->>DB: parameterised SELECT against the form's backing table/view/proc
DB-->>CTRL: rows
CTRL-->>SPA: dataset
```
The two HTTP round-trips are visible at lines 1 and 22 in the
diagram. Everything between is server-side work the SPA never sees.
## The five-key composite
`getModelBysId` returns one Java `Map` with these keys, in this order:
| Key | Source | Used by the SPA for |
|---|---|---|
| `formData` | `gdsconfigformmaster` (filtered by `sParentId = sModelsId`) ⋈ `gdsconfigformpersonalize` (per-tenant overlay); per master row, `gdsconfigformslave` + `gdsconfigformcustomslave` overlays. `gdsmodule` is referenced only by id. | The form layout itself — every field, control, label, validation rule |
| `gdsformconst` | `gdsformconst` rows filtered by `sParentId` only — NOT tenant-scoped; `sLanguage` selects which label column to return | Form-level constants — labels, defaults, dropdown text |
| `gdsjurisdiction` | `sysjurisdiction` rows for the user (or for the user's group via `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`); skipped for ADMIN. The map-key name `gdsjurisdiction` is misleading — that table is the builder-side action *catalogue*, not what's read here. | Per-button and per-data permissions |
| `billnosetting` | `sysbillnosettings` row for this module (per-tenant) | Document-numbering rules (work-order numbers, quotation numbers) |
| `report` | `sysreport` rows linked to this form (per-tenant) | Print templates (Excel via jxls, PDF via iText) |
## What's *not* in this lifecycle
A few things readers expect to find here but don't:
- **The "save" path.** Save is its own endpoint
(`POST /business/addUpdateDelBusinessData`), bundling add+update+delete
in one request. See [Slice 1](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save).
- **The "open existing row for edit" fetch.** Same endpoint as the grid
load (`getBusinessDataByFormcustomId`) but with the row's `sId` in the
body — the handler branches on whether the body asks for one row or
many.
- **Workflow steps.** When a module has an active approval workflow
(`bCheck = 1`, `gdsmoduleflow` configured, deployed Activiti process,
and `ConstantUtils.bCheckflowCheck = true`), additional steps
interleave. None of those tables are populated in this dev DB; see
[Slice 7 (deferred)](../slices/07-workflow.md).
- **Cache invalidation.** When BACK changes a metadata row, the save path
synchronously calls `BusinessCleanRedisData` / `CleanRedisServiceImpl`,
which evicts Spring cache regions from shared Redis. The JMS
`ConsumerChangeGdsModuleThread` path is a separate base-data merge
channel, not cache invalidation.
## Variations covered by other slices
- [Slice 1](../slices/01-hello-world.md) — the canonical instance, with
observed network traffic.
- [Slice 2](../slices/02-multi-tenancy.md) — how the multi-tenant filter
threads through every step.
- [Slice 3](../slices/03-report.md) — view-backed instead of table-backed.
- [Slice 4](../slices/04-custom-field.md) — the `gdsconfigformcustomslave`
merge step.
- [Slice 5](../slices/05-customer-sql-override.md) — when a stored
procedure body has been replaced for a specific tenant.
## Read this page once and keep it open
If a future chapter uses a term you don't recognise, it almost certainly
refers to one of the boxes in the diagram above. Come back here.