1) Concept framing (short)
- What problem it solves
- Lets you define CRUD screens/forms (fields, labels, types), validations, and permissions in tables, so behavior can be adjusted after deployment without code changes.
- Supports “FoxPro/4GL ergonomics”: tables → forms/behavior (and later reports) with metadata as the source of truth.
- What changes post-deploy (and who can change it)
- Admin/config editors can change:
- Which fields appear on a form, their order, labels, help text
- Validation rules (required, regex, min/max, length limits)
- Role-based permissions (view/edit/create/delete)
- Overrides per environment (dev/test/prod) and/or tenant
- Changes take effect immediately (with caching + hot-reload strategy below).
- What must remain code (safety boundaries)
- DB access and parameterization (no dynamic SQL from config)
- Allow-lists for:
- Validation operators (
required, regex, min, max, min_len, max_len)
- Field types (
text, int, decimal, date, bool)
- Any “execute action” / “call method” behavior must be explicitly mapped in code (config may only select from an allow-list).
2) Canonical data model (portable)
PostgreSQL DDL
-- Schema versioning (simple + effective)
create table if not exists tdd_schema_version (
id bigserial primary key,
version int not null unique,
applied_at timestamptz not null default now()
);
-- Entities (one per screen/form)
create table if not exists tdd_entity (
id bigserial primary key,
entity_key text not null unique, -- e.g. "customer"
display_name text not null,
table_name text not null, -- physical table target, allow-listed by code if needed
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- Fields on a form (metadata-driven form layout)
create table if not exists tdd_field (
id bigserial primary key,
entity_id bigint not null references tdd_entity(id) on delete cascade,
field_key text not null, -- e.g. "email"
column_name text not null, -- e.g. "email"
field_type text not null, -- allow-list: text,int,decimal,date,bool
label text not null,
help_text text null,
is_required boolean not null default false,
sort_order int not null default 0,
-- override dimensions
env text null, -- e.g. "dev","prod"
tenant_id text null, -- e.g. "acme"
-- conflict resolution
priority int not null default 0, -- higher wins within same specificity
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (entity_id, field_key, env, tenant_id)
);
create index if not exists ix_tdd_field_entity on tdd_field(entity_id);
create index if not exists ix_tdd_field_lookup on tdd_field(entity_id, field_key);
-- Validations are separate rows (more flexible than columns)
create table if not exists tdd_validation (
id bigserial primary key,
entity_id bigint not null references tdd_entity(id) on delete cascade,
field_key text not null,
rule_op text not null, -- allow-list: required,regex,min,max,min_len,max_len
rule_arg text null, -- e.g. regex pattern, numeric threshold
message text not null,
env text null,
tenant_id text null,
priority int not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists ix_tdd_val_entity_field on tdd_validation(entity_id, field_key);
-- Permissions (role → allowed operations)
create table if not exists tdd_permission (
id bigserial primary key,
entity_id bigint not null references tdd_entity(id) on delete cascade,
role_key text not null, -- e.g. "clerk","admin"
can_view boolean not null default true,
can_create boolean not null default false,
can_edit boolean not null default false,
can_delete boolean not null default false,
env text null,
tenant_id text null,
priority int not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (entity_id, role_key, env, tenant_id)
);
create index if not exists ix_tdd_perm_entity_role on tdd_permission(entity_id, role_key);
SQLite DDL
- Same as PostgreSQL, except:
- Use
integer primary key autoincrement instead of bigserial
- Use
datetime('now') instead of now()
- No
timestamptz type (use text or integer)
3) Runtime algorithm
- Load config
- Cache key:
(entity_key, tenant_id, env, role_key)
- Strategy:
- Start with lazy load on first access.
- Cache with a TTL (e.g., 30–120s) or with a config revision.
- Optional hot-reload: poll a
tdd_schema_version or a dedicated tdd_config_revision table.
- Conflict resolution (overrides)
- Treat rows as layered:
(tenant_id, env) most specific
(tenant_id, null)
(null, env)
(null, null) base default
- Within the same layer, higher
priority wins.
- For fields: choose the single “winning” row per
(field_key).
- For validations: you may keep multiple rules per field; you still resolve by specificity + priority.
- Validate config
- Schema validation: table constraints + allowed values (types, ops) enforced in code.
- Semantic validation (on load):
- Field types are in allow-list.
- Column names are in allow-list (or verified against actual DB schema if you want).
- Regex compiles (where applicable).
- Numeric thresholds parse.
- If invalid: fail “closed” (deny edit) or fall back to base config depending on your risk tolerance.
- Failure modes
- Missing entity/config: return “entity not configured” (safe error), no dynamic behavior.
- Invalid overrides: ignore override layer and fall back to less-specific rows; log + alert.
- Partial migrations: use
tdd_schema_version gate; if app expects version N but DB has N-1, run in safe mode.
- Migration strategy
- Increment
tdd_schema_version.
- Backfill:
- Add new columns as nullable with defaults.
- Populate base rows first (tenant/env null), then overrides.
- Optional: store a
config_schema_version column per row if you want per-row evolution.
4) Cross-language implementation (repeat for each language)