Skip to main content

Multi-Level Validation

Overview

Every write operation passes through 5 validation layers before execution. This ensures data integrity, security, and business rule compliance.

Validation Layers:

  1. Token Validation - JWT signature, expiration, issuer
  2. Grant Validation - User permissions via TB_MENU
  3. Required Field Validation - TB_COST.REQUIRED='1'
  4. COD_ON_OFF Validation - Field visibility flags
  5. Business Logic Validation - Custom rules (optional)

Failure at any layer rejects the entire request.

Validation Flow

Request Processing

POST /api/v4/core/PRD

1. Token Validation
└─ Valid? → Continue
└─ Invalid? → 401 Unauthorized

2. Grant Validation
└─ Has permission? → Continue
└─ No permission? → 403 Forbidden

3. Required Field Validation
└─ All required fields present? → Continue
└─ Missing field? → 400 ValidationError

4. COD_ON_OFF Validation
└─ Fields allowed for operation? → Continue
└─ Field not allowed? → 400 ValidationError

5. Business Logic Validation (optional)
└─ Custom rules pass? → Continue
└─ Rule violation? → 400 BusinessRuleError

Execute Write Operation

Validation Middleware Architecture

Symfony Event Dispatcher Chain:

HTTP Request

CoreService (API Gateway) - Routing

CoreWrite (Symfony) - Event Dispatcher

RequestValidatorSubscriber::onKernelRequest
├─ TokenValidatorMiddleware
│ └─ JWT decode, signature verification, expiration check
│ └─ Success → Continue
│ └─ Failure → 401 Unauthorized
├─ GrantValidatorMiddleware
│ └─ TB_MENU nested set query, permission check
│ └─ Success → Continue
│ └─ Failure → 403 Forbidden
├─ RequiredFieldValidatorMiddleware
│ └─ TB_COST.REQUIRED='1' check
│ └─ Success → Continue
│ └─ Failure → 400 ValidationError
├─ COD_ON_OFF_ValidatorMiddleware
│ └─ TB_COST.COD_ON_OFF flag check (N/M based on operation)
│ └─ Success → Continue
│ └─ Failure → 400 ValidationError
└─ BusinessLogicValidatorMiddleware (optional)
└─ Custom rules, foreign key checks, AC/VARS validation
└─ Success → Controller
└─ Failure → 400 BusinessRuleError

Controller → DataStore → Database

Implementation:

// src/EventDispatcher/Subscriber/RequestValidatorSubscriber.php

class RequestValidatorSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['validateToken', 100], // Priority 100 - first
['validateGrant', 90], // Priority 90 - second
['validateRequiredFields', 80], // Priority 80 - third
['validateCOD_ON_OFF', 70], // Priority 70 - fourth
['validateBusinessLogic', 60] // Priority 60 - fifth
],
];
}
}

Key Points:

  • ✅ Middleware runs in priority order (100 → 60)
  • ✅ Failure at any middleware stops the chain
  • ✅ RequestValidatorSubscriber orchestrates all validation
  • ✅ Each middleware is single-purpose (SOLID principles)

Layer 1: Token Validation

JWT Validation

Validates Authorization header:

POST /api/v4/core/PRD
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Checks:

  • Signature valid (cryptographic verification)
  • Not expired (exp claim)
  • Correct issuer (iss claim)
  • Valid format (3-part JWT)

Success:

{
"user": "user@example.com",
"tenant": "company_abc",
"roles": ["admin", "product_manager"],
"exp": 1703012345
}

Failure:

{
"error": "UnauthorizedError",
"message": "Invalid or expired token",
"code": "TOKEN_INVALID",
"status": 401
}

Common Token Errors

// Expired token
{
"code": "TOKEN_EXPIRED",
"message": "Token has expired",
"status": 401
}

// Invalid signature
{
"code": "TOKEN_INVALID_SIGNATURE",
"message": "Token signature verification failed",
"status": 401
}

// Missing token
{
"code": "TOKEN_MISSING",
"message": "Authorization header missing",
"status": 401
}

Layer 2: Grant Validation

Permission Check via TB_MENU

TB_MENU nested set determines user permissions:

-- User grants stored in TB_MENU hierarchy
SELECT *
FROM TB_MENU
WHERE COD_MENU IN (user_grants)
AND NLEFT <= (SELECT NLEFT FROM TB_MENU WHERE COD_MENU = target_area)
AND NRIGHT >= (SELECT NRIGHT FROM TB_MENU WHERE COD_MENU = target_area);

Grant Hierarchy:

admin (NLEFT=1, NRIGHT=100)
├── products (NLEFT=2, NRIGHT=50)
│ ├── products.read (NLEFT=3, NRIGHT=20)
│ └── products.write (NLEFT=21, NRIGHT=49)
│ ├── products.create (NLEFT=22, NRIGHT=30)
│ ├── products.update (NLEFT=31, NRIGHT=39)
│ └── products.delete (NLEFT=40, NRIGHT=48)
└── orders (NLEFT=51, NRIGHT=99)

User with products.write grant can:

  • Create products (POST)
  • Update products (PUT/PATCH)
  • Delete products (DELETE)

User with only products.read grant cannot:

  • Create/update/delete products
  • Receives 403 Forbidden

Failure:

{
"error": "ForbiddenError",
"message": "Insufficient permissions for this operation",
"code": "GRANT_DENIED",
"required_grant": "products.write",
"user_grants": ["products.read"],
"status": 403
}

Layer 3: Required Field Validation

TB_COST.REQUIRED='1'

Metadata marks mandatory fields:

INSERT INTO TB_COST (COD_DIM, NUM_COST, COD_VAR, REQUIRED, DESCRIZIONE_COST) VALUES
('PRD', 1, 'XPRD01', 1, 'Product name'), -- Required
('PRD', 2, 'XPRD02', 1, 'Price'), -- Required
('PRD', 4, 'XPRD04', 0, 'Description'), -- Optional
('PRD', 5, 'XPRD05', 1, 'Category'); -- Required

Validation Logic:

// CoreWrite RequestValidatorSubscriber.php
foreach ($costs as $cost) {
if ($cost['REQUIRED'] == 1 && !isset($data[$cost['COD_VAR']])) {
throw new ValidationException(
"Required field missing: {$cost['COD_VAR']}",
'REQUIRED_FIELD_MISSING',
$cost['COD_VAR']
);
}
}

Request Missing Required Field:

Request:

POST /api/v4/core/PRD
{
"data": {
"XPRD02": 99.99,
"XPRD04": "Description"
// Missing XPRD01 (required)
}
}

Response:

{
"error": "ValidationError",
"message": "Required field missing: XPRD01",
"code": "REQUIRED_FIELD_MISSING",
"field": "XPRD01",
"fieldDescription": "Product name",
"status": 400
}

Layer 4: COD_ON_OFF Validation

Field Visibility Flags

TB_COST.COD_ON_OFF controls field usage:

INSERT INTO TB_COST (COD_DIM, NUM_COST, COD_VAR, COD_ON_OFF) VALUES
('PRD', 1, 'XPRD01', 'LDNMR'), -- Allowed: List, Detail, New, Modify, Ricerca
('PRD', 3, 'XPRD03', 'LDR'), -- Allowed: List, Detail, Ricerca (read-only counter)
('PRD', 12, 'XPRD12', 'LDM'); -- Allowed: List, Detail, Modify (not on creation)

Operation Checks:

OperationFlag RequiredExample
POSTN (New)Field can be set on creation
PUT/PATCHM (Modify)Field can be updated
GETL/D/RField visible in queries

Validation Example (POST):

Request:

POST /api/v4/core/PRD
{
"data": {
"XPRD01": "Product", // ✅ COD_ON_OFF='LDNMR' (N flag present)
"XPRD03": "PRD-2025-001", // ❌ COD_ON_OFF='LDR' (N flag missing)
"XPRD12": "image.jpg" // ❌ COD_ON_OFF='LDM' (N flag missing)
}
}

Response:

{
"error": "ValidationError",
"message": "Field not allowed in create operation: XPRD03",
"code": "FIELD_NOT_CREATEABLE",
"field": "XPRD03",
"cod_on_off": "LDR",
"required_flag": "N",
"status": 400
}

Validation Example (PUT/PATCH):

Request:

PATCH /api/v4/core/PRD/123
{
"data": {
"XPRD01": "Updated", // ✅ COD_ON_OFF='LDNMR' (M flag present)
"XPRD03": "PRD-2025-999" // ❌ COD_ON_OFF='LDR' (M flag missing)
}
}

Response:

{
"error": "ValidationError",
"message": "Field not modifiable: XPRD03",
"code": "FIELD_NOT_MODIFIABLE",
"field": "XPRD03",
"cod_on_off": "LDR",
"required_flag": "M",
"status": 400
}

COD_UTENTE Weight-Based Access Control

TB_COST.COD_UTENTE defines minimum user weight required to access a field:

INSERT INTO TB_COST (COD_DIM, NUM_COST, COD_VAR, COD_ON_OFF, COD_UTENTE) VALUES
('PRD', 1, 'XPRD01', 'LDNMR', '*'), -- All users can access
('PRD', 8, 'XPRD08', 'LDNM', '70'), -- Only users with peso >= 70
('PRD', 15, 'XPRD15', 'LDM', '90'); -- Only users with peso >= 90 (admin)

User Peso (Weight) Levels:

  • peso = 1-30: Admin users (full access)
  • peso = 31-60: Manager users (most fields)
  • peso = 61-90: Standard users (basic fields)
  • peso = 91-100: Limited users (read-only)

Validation Logic:

// Check user peso vs COD_UTENTE requirement
if ($cost['COD_UTENTE'] !== '*' && $userPeso > $cost['COD_UTENTE']) {
throw new ValidationException(
"Insufficient user weight to access field: {$cost['COD_VAR']}",
'USER_WEIGHT_INSUFFICIENT',
$cost['COD_VAR']
);
}

Example: Manager (peso=50) Attempting to Modify Admin Field:

Request:

PATCH /api/v4/core/PRD/123
Authorization: Bearer {token} # User with peso=50

{
"data": {
"XPRD01": "Product Name", // ✅ COD_UTENTE='*' (allowed)
"XPRD15": "Admin Value" // ❌ COD_UTENTE='90' (peso 50 < 90)
}
}

Response:

{
"error": "ValidationError",
"message": "Insufficient user weight to modify field: XPRD15",
"code": "USER_WEIGHT_INSUFFICIENT",
"field": "XPRD15",
"required_peso": 90,
"user_peso": 50,
"status": 400
}

Use Cases:

  • Sensitive financial fields (COD_UTENTE='10') - Only top admin access
  • Pricing fields (COD_UTENTE='70') - Manager and above
  • Standard product info (COD_UTENTE='*') - All users

Layer 5: Business Logic Validation

Custom Validation Rules

Application-specific constraints (optional):

// Custom validator in CoreWrite
class ProductValidator {
public function validate(array $data): void {
// Price must be positive
if (isset($data['XPRD02']) && $data['XPRD02'] <= 0) {
throw new BusinessRuleException(
'Price must be greater than zero',
'INVALID_PRICE'
);
}

// Stock cannot be negative
if (isset($data['XPRD09']) && $data['XPRD09'] < 0) {
throw new BusinessRuleException(
'Stock cannot be negative',
'INVALID_STOCK'
);
}

// Discount cannot exceed 100%
if (isset($data['XPRD_DISCOUNT']) && $data['XPRD_DISCOUNT'] > 100) {
throw new BusinessRuleException(
'Discount cannot exceed 100%',
'INVALID_DISCOUNT'
);
}
}
}

Failure:

{
"error": "BusinessRuleError",
"message": "Price must be greater than zero",
"code": "INVALID_PRICE",
"field": "XPRD02",
"value": -10.00,
"status": 400
}

AC/VARS Foreign Key Validation

AC (Autocomplete) and VARS (Select) fields are foreign keys that must reference existing records in the destination table. The platform prevents orphaned links by validating foreign key existence before insert/update.

Validation Steps:

  1. Type validation - Ensure value matches TIPO_CAMPO definition (AC or VARS)
  2. Foreign key existence check - Verify value exists in destination table
  3. Return error if not found - Prevent orphaned links

Metadata Structure:

-- TB_VAR: Field definition
COD_DIM: ART
COD_VAR: XART53
TIPO_CAMPO: VARS
TIPO_VAR: DEPFOOD -- Foreign key to TB_ANAG_DEPFOOD00

-- TB_OBJECT: Foreign key relationship
COD_DIM: ART (source dimension)
COD_VAR: XART53 (source field - stores foreign key)
COD_DIM_OBJ: DEPFOOD (destination dimension)
COD_VAR_OBJ: XDEPFOOD03 (destination field - primary key)

Relationship:

TB_ANAG_ART00.XART53 → TB_ANAG_DEPFOOD00.XDEPFOOD03

Validation Logic:

// CoreWrite validates foreign key existence
if ($var['TIPO_CAMPO'] === 'VARS' || $var['TIPO_CAMPO'] === 'AC') {
// Get destination table from TB_OBJECT
$foreignKey = $this->getForeignKeyDefinition($dim, $field);

// Check if foreign key value exists
$exists = $this->checkForeignKeyExists(
$foreignKey['COD_DIM_OBJ'],
$foreignKey['COD_VAR_OBJ'],
$value
);

if (!$exists) {
throw new ValidationException(
"Invalid foreign key: {$field} references {$foreignKey['COD_DIM_OBJ']}, but '{$value}' does not exist",
'INVALID_FOREIGN_KEY',
$field
);
}
}

Example: Valid Foreign Key:

Request:

POST /api/v4/core/ART
{
"data": {
"XART20": "Product Name",
"XART53": "rep1" // Must exist in TB_ANAG_DEPFOOD00.XDEPFOOD03
}
}

Validation Query:

SELECT COUNT(*) FROM TB_ANAG_DEPFOOD00
WHERE XDEPFOOD03 = 'rep1'; -- Must return > 0

Success: Record created if foreign key exists.

Example: Invalid Foreign Key:

Request:

POST /api/v4/core/ART
{
"data": {
"XART20": "Product Name",
"XART53": "invalid_dept" // Does NOT exist in TB_ANAG_DEPFOOD00
}
}

Response:

{
"error": "ValidationError",
"message": "Invalid foreign key: XART53 references DEPFOOD, but 'invalid_dept' does not exist",
"code": "INVALID_FOREIGN_KEY",
"field": "XART53",
"foreign_table": "TB_ANAG_DEPFOOD00",
"foreign_field": "XDEPFOOD03",
"value": "invalid_dept",
"status": 400
}

Why This Matters:

  • ✅ Prevents orphaned links (referencing non-existent records)
  • ✅ Ensures data integrity across dimensions
  • ✅ Validates foreign keys before database write
  • ✅ Client can query /api/v4/core/vars to get valid values

Related: See AC/VARS Queries for how to query valid foreign key values.

JavaScript Validation Handling

Client-Side Validation

class ProductValidator {
/**
* Validate before POST
*/
static validateCreate(data) {
const errors = [];

// Required field check
if (!data.XPRD01) {
errors.push({
field: 'XPRD01',
message: 'Product name is required'
});
}

if (!data.XPRD02) {
errors.push({
field: 'XPRD02',
message: 'Price is required'
});
}

if (!data.XPRD05) {
errors.push({
field: 'XPRD05',
message: 'Category is required'
});
}

// Business logic
if (data.XPRD02 && data.XPRD02 <= 0) {
errors.push({
field: 'XPRD02',
message: 'Price must be greater than zero'
});
}

if (data.XPRD09 && data.XPRD09 < 0) {
errors.push({
field: 'XPRD09',
message: 'Stock cannot be negative'
});
}

return errors;
}

/**
* Validate before PUT/PATCH
*/
static validateUpdate(data) {
const errors = [];

// Business logic
if (data.XPRD02 !== undefined && data.XPRD02 <= 0) {
errors.push({
field: 'XPRD02',
message: 'Price must be greater than zero'
});
}

if (data.XPRD09 !== undefined && data.XPRD09 < 0) {
errors.push({
field: 'XPRD09',
message: 'Stock cannot be negative'
});
}

return errors;
}
}

// Usage
async function createProduct(data) {
// Client-side validation
const errors = ProductValidator.validateCreate(data);
if (errors.length > 0) {
alert(`Validation errors:\n${errors.map(e => `- ${e.message}`).join('\n')}`);
return;
}

// API call
try {
const response = await fetch(`${apiBase}/api/v4/core/PRD`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data })
});

if (!response.ok) {
const error = await response.json();
handleValidationError(error);
return;
}

const result = await response.json();
alert('Product created successfully!');
return result;
} catch (error) {
alert('Network error');
}
}

Error Handling

function handleValidationError(error) {
switch (error.code) {
case 'TOKEN_INVALID':
case 'TOKEN_EXPIRED':
// Redirect to login
window.location.href = '/login';
break;

case 'GRANT_DENIED':
alert(`Access denied: You don't have permission for this operation`);
break;

case 'REQUIRED_FIELD_MISSING':
alert(`Required field missing: ${error.field}`);
highlightField(error.field);
break;

case 'FIELD_NOT_CREATEABLE':
alert(`Field ${error.field} cannot be set on creation`);
break;

case 'FIELD_NOT_MODIFIABLE':
alert(`Field ${error.field} is read-only`);
break;

case 'INVALID_PRICE':
case 'INVALID_STOCK':
case 'INVALID_DISCOUNT':
alert(error.message);
highlightField(error.field);
break;

default:
alert(`Validation error: ${error.message}`);
}
}

function highlightField(fieldName) {
const input = document.querySelector(`[name="${fieldName}"]`);
if (input) {
input.classList.add('error');
input.focus();
}
}

Best Practices

✅ DO:

Validate client-side before API call:

// ✅ Good - catch errors early
const errors = validateCreate(data);
if (errors.length > 0) {
showErrors(errors);
return;
}
await createProduct(data);

Handle specific error codes:

// ✅ Good - specific handling
if (error.code === 'REQUIRED_FIELD_MISSING') {
highlightField(error.field);
}

Provide clear error messages:

// ✅ Good - user-friendly
alert(`${error.fieldDescription} is required`);

❌ DON'T:

Don't skip client-side validation:

// ❌ Bad - unnecessary API roundtrip
await createProduct(data); // Will fail with obvious errors

Don't show technical error codes to users:

// ❌ Bad - confusing
alert(error.code); // "REQUIRED_FIELD_MISSING"

// ✅ Good - user-friendly
alert(`Please fill in ${error.fieldDescription}`);

Don't try to set read-only fields:

// ❌ Bad - will be rejected
{
"data": {
"XPRD03": "custom-code", // Auto-generated counter
"TREC": "N" // Auto-managed
}
}

Summary

  • ✅ 5 validation layers: Token → Grant → Required → COD_ON_OFF → Business Logic
  • ✅ Failure at any layer rejects entire request
  • ✅ Token validation ensures authentication (401 Unauthorized)
  • ✅ Grant validation ensures authorization (403 Forbidden)
  • ✅ Required field validation via TB_COST.REQUIRED='1'
  • ✅ COD_ON_OFF validation enforces N (create) and M (modify) flags
  • ✅ Business logic validation enforces custom rules

Key Takeaways:

  1. Validation happens at 5 layers before write execution
  2. Token validation: JWT signature, expiration, issuer
  3. Grant validation: TB_MENU nested set permissions
  4. Required fields: TB_COST.REQUIRED='1'
  5. COD_ON_OFF: N flag for POST, M flag for PUT/PATCH
  6. Client-side validation reduces unnecessary API calls
  7. Handle specific error codes for better UX

Error Code Reference:

CodeLayerHTTP StatusMeaning
TOKEN_INVALID1401Invalid/expired token
GRANT_DENIED2403Insufficient permissions
REQUIRED_FIELD_MISSING3400Mandatory field missing
FIELD_NOT_CREATEABLE4400Field missing N flag
FIELD_NOT_MODIFIABLE4400Field missing M flag
INVALID_*5400Business rule violation

Next: Transactions →