# 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}<br/>?sModelsId=...&Authorization=<bearer>
    Note over CTRL: AuthorizationInterceptor.preHandle<br/>resolves UserInfo from Redis<br/>RequestAddParamUtil.addParams (16 keys)

    CTRL->>SVC: getModelBysId(map)
    Note over SVC: getModelConfigByModleId (inherited from BaseServiceImpl)<br/>orchestrates the per-master form-master + slave loads
    SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow<br/>(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}<br/>?sModelsId=...
    Note over CTRL,SVC: same RequestAddParamUtil pass<br/>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.
