Selecto Domain Schema v1
Copy MarkdownSelecto domain schema v1 is the first small, documented contract for authored
domain maps. It is intentionally compatibility-safe: Selecto.Domain.normalize/1
and Selecto.Domain.validate/1 expose diagnostics and normalized projections,
but Selecto.configure/3 does not consume the normalized contract yet.
Version
Generated domains should declare the current schema version:
%{
schema_version: 1,
domain_version: "0.1.0",
domain_fingerprint: "sha256:9f5d...",
name: "Orders",
source: %{
source_table: "orders",
primary_key: :id,
fields: [:id],
columns: %{id: %{type: :integer}}
},
schemas: %{},
joins: %{}
}When schema_version is missing, Selecto.Domain.normalize/1 infers version
1 and returns a :schema_version_inferred warning. Invalid versions fall back
to the current version with an :invalid_schema_version warning. Newer positive
integer versions are preserved and receive an :unsupported_schema_version
warning.
schema_version is the machine compatibility version for the canonical Selecto
domain schema, and should remain a positive integer.
domain_version is optional authored-domain metadata. It is an opaque
non-empty atom, string, or integer that a host can use for semantic versions,
date-based releases, or generated build ids:
domain_version: "0.5.0"
domain_version: "2026-05-12"domain_fingerprint is optional authored-domain identity metadata. It is an
opaque non-empty string, usually a content hash or stable generated-artifact
fingerprint:
domain_fingerprint: "sha256:9f5d..."Selecto core preserves a supplied fingerprint but does not compute one during normalization.
Top-Level Sections
The normalizer classifies authored top-level keys into four categories.
Canonical
Canonical sections are part of the current domain contract:
schema_versiondomain_versiondomain_fingerprintnamesourceschemasjoinsdefault_selectedrequired_selectedrequired_filtersrequired_order_byrequired_group_byfiltersfunctionsquery_memberspublished_viewsdetail_actionsdomain_dataextensions
Projection
Projection sections are recognized implementation or consumer-facing sections. They are not unknown, but diagnostics call them out because future normalized projections may reshape them:
columnscustom_columnsjsonb_schemassubfilterswindow_functionspaginationretargetredact_fields
Proposed
Proposed sections are reserved for the write/action/reference contract that is still being formalized:
writesactionscapabilitiessource_relationshipschoice_sources
Unknown
Any other top-level key is unknown and appears in diagnostics. Unknown keys are
not preserved as a legacy contract. Selecto is still pre-0.5 and has no shipped
domain compatibility burden; old experimental write-like keys should be migrated
or removed rather than carried forward as legacy support.
Core Relation Shape
The first strict contract validates source and every entry in schemas as
relation maps. A relation map uses this shape:
%{
source_table: "orders",
primary_key: :id,
fields: [:id, :status, :total],
columns: %{
id: %{type: :integer},
status: %{type: :string},
total: %{type: :decimal}
},
associations: %{
customer: %{
queryable: :customers,
owner_key: :customer_id,
related_key: :id
}
}
}Validation checks:
sourceandschemasmust be present in the authored domain.source_tablemust be an atom or string.primary_keymust be an atom or string and must appear infields.fieldsmust be a list.columnsmust be a map.- Every listed field must have a matching column definition.
joins must be a map when present. Each join key must be declared as an
association on its parent relation, and each association must point at a schema
available in schemas unless it explicitly targets :source.
Query Field Lists
The normalized contract validates query field-list metadata before runtime query execution:
default_selectedrequired_selectedrequired_order_byrequired_group_by
Each section must be a list when present. Direct atom/string field references
must refer to known fields from the root source, joined schemas using
"schema.field" paths, or custom_columns.
Explicit UDF references using {:udf, function_id, args} are checked against
the function registry when they appear in selected, ordered, or grouped query
field lists. Aliased selectors such as {:field, {:udf, function_id, args}, alias} are checked the same way. The validator checks that the function id is
a non-empty atom or string, exists in functions, and is allowed for the query
call site when allowed_in is declared. It does not inspect UDF argument
values or compile SQL.
For registered UDFs with args metadata, query-list validation also checks
argument count. Arguments declared with source: :selector get static field
reference validation for direct atom/string selectors and nested UDF references.
Arguments declared as :value or :literal are left to runtime execution
validation.
Order entries may use a direct field, {field, direction}, or
{direction, field}. Supported directions are :asc, :desc,
:asc_nulls_first, :asc_nulls_last, :desc_nulls_first, and
:desc_nulls_last.
Group entries may use direct fields or wrapper tuples such as
{:rollup, fields} and {:grouping_set, fields}. Tuple/map expressions that
are not direct field references are left permissive in this slice.
Invalid query list metadata produces :invalid_section_shape,
:invalid_query_field_reference, :query_field_not_found,
:invalid_query_order_direction, :invalid_query_group_wrapper,
:invalid_query_function_id, :query_function_not_found, or
:query_function_call_site_not_allowed, or
:query_function_arg_count_mismatch diagnostics.
Filter References
The first contract also validates filter registry metadata and filter
references. filters must be a map. Each filter id must be a non-empty atom or
string, and each filter config must be a map. Virtual filters may omit field.
When present, field must be a non-empty atom or dotted string path and type
must be a non-empty atom or string.
Registered filters with a field and expressions in required_filters must
refer to known fields from:
- the root
source - entries in
schemas, addressed as"schema.field" custom_columns
Unknown filter fields produce :filter_field_not_found diagnostics.
Invalid filter registry metadata produces :invalid_filter_id,
:invalid_filter_config, :invalid_filter_field, or :invalid_filter_type
diagnostics.
Function Registry
functions must be a map when present. Each function id must be a non-empty
atom or string, and each function spec must be a map. Function specs validate
the current UDF metadata contract:
kindmust be:scalar,:predicate, or:table.sql_namemust be a safe SQL function identifier such as"lower"or"public.similarity".- optional
allowed_inmust be a list of supported call sites. - optional
argsmust be a list of arg maps with non-emptyname, declaredtype, andsourceset to:selector,:value, or:literal. - predicate functions must return
:boolean. - table functions must return
%{columns: %{...}}. - scalar function returns may be omitted or declared as an atom or
{:array, type}tuple.
Invalid function metadata produces diagnostics such as :invalid_function_id,
:invalid_function_spec, :invalid_function_kind,
:invalid_function_sql_name, :invalid_function_call_site,
:invalid_function_arg_source, or :invalid_function_returns.
Query Members
query_members must be a map when present. The normalized contract recognizes
the current named member groups:
ctesvaluessubquerieslateralsunnests
Each group must be a map of non-empty atom or string ids to member specs. Specs must be maps. The first query-member contract validates metadata shape only; it does not execute member functions or compile SQL.
Current member checks:
- CTE members require
queryorquery_builderas a function with arity0or1; recursive CTE members requirebase_queryarity0or1andrecursive_queryarity1or2. - VALUES members require
rowsordataas a list; optionalcolumnsmust be a list. - Subquery members require
queryorquery_builderas a function with arity0or1; optionalkindmust be:join, optionalonmust be a list, optionaltypemust be:left,:inner,:right, or:full, and optionaljoin_idmust be a non-empty atom or string. - LATERAL members require
query,source, orlateral_sourceas a tuple or function with arity0,1, or2; optionaljoin_typeortypemust be:left,:inner,:right, or:full. - UNNEST members require
array_fieldorfieldas a non-empty atom, string, or tuple expression; optionalordinalitymust be a non-empty atom or string. - CTE and VALUES optional
joinmetadata must betrue,false,nil, a list, or a map. - VALUES, LATERAL, and UNNEST aliases via
as,alias, oralias_namemust be non-empty atoms or strings when provided. - LATERAL and UNNEST optional
optionsmust be a list or map.
Invalid query-member metadata produces diagnostics such as
:invalid_query_member_group, :invalid_query_member_id,
:invalid_query_member_spec, :invalid_query_member_query,
:invalid_query_member_rows, :invalid_query_member_join_type,
:invalid_query_member_source, or :invalid_query_member_field.
Published Views
published_views must be a map when present. The normalized contract validates
published-view metadata shape only; it does not compile the query or generate
DDL.
Each published view id must be a non-empty atom or string, and each spec must be a map with:
database_nameas a non-empty stringkindas:viewor:materialized_viewqueryas a function with arity1columnsas a non-empty map of non-empty atom/string ids to column spec maps
Optional metadata:
indexesmust be a list when present. Each index spec must be a map withcolumnsas a non-empty list of atom/string names. Optionaluniqueandconcurrentlyflags must be booleans.refreshmust be a map when present.
Invalid published-view metadata produces diagnostics such as
:invalid_published_view_id, :invalid_published_view_spec,
:invalid_published_view_database_name, :invalid_published_view_kind,
:invalid_published_view_query, :invalid_published_view_columns,
:invalid_published_view_index_columns, or :invalid_published_view_refresh.
Detail Actions
detail_actions must be a map when present. The normalized contract validates
detail-row action metadata only; it does not render modals, resolve LiveView
components, or execute links.
Each detail action id must be a non-empty atom or string, and each action spec must be a map with:
nameas a non-empty stringtypeas:modal,:iframe_modal,:external_link, or:live_component
Optional metadata:
payloadmust be a map when provided.required_fieldsmust be a list when provided. Each entry must be a non-empty atom or string and must refer to a known source, schema, or custom column field.
Type-specific payload checks:
:external_linkand:iframe_modalrequirepayload.url_templateas a non-empty string.:live_componentrequirespayload.moduleas an atom.
Invalid detail-action metadata produces diagnostics such as
:invalid_detail_action_id, :invalid_detail_action_spec,
:invalid_detail_action_name, :invalid_detail_action_type,
:invalid_detail_action_payload, :missing_detail_action_url_template,
:missing_detail_action_module, or :detail_action_field_not_found.
Write Transitions
writes.transitions is the first proposed write contract section with strict
validation. It is a direct state graph keyed by a known domain field:
%{
writes: %{
transitions: %{
status: %{
"pending" => ["ready", "cancelled"],
"ready" => [:complete, "cancelled"],
complete: []
}
}
}
}Validation checks:
writesmust be a map when present.writes.transitionsmust be a map when present.- each transition field key must be an atom or string
- each transition field must exist in the source, schemas, or custom columns
- each transition graph must be a map
- each source state must be an atom or string
- each target list must be a list of atoms or strings
This validation does not execute writes and does not make Selecto.configure/3
depend on the write contract.
Capability Catalog
capabilities declares the stable capability names a domain can reference. It
does not decide which actors have those capabilities; host applications and
future resolver adapters own that policy decision.
%{
capabilities: %{
"order.view" => %{
label: "View orders",
operations: [:select, :detail],
target: :order
},
"order.approve" => %{
label: "Approve order",
operations: [:action],
action: :approve_order
},
"order.export" => %{
label: "Export orders",
operations: [:export],
sensitivity: :high
}
}
}Validation checks:
capabilitiesmust be a map when present.- capability ids must be atoms or strings.
- each capability entry must be a map.
- each capability must declare a non-empty
operationslist. - each operation must be an atom or string.
The domain contract also validates optional capability references on
query-facing metadata:
- filters
- functions
- query members
- published views
- detail actions
When present, the value must be an atom or string and must exist in the domain
capability catalog. These checks only validate metadata references; they do not
perform authorization or alter Selecto.configure/3 behavior.
Runtime capability checks use a shared request/decision value shape:
request =
Selecto.Capabilities.request(
actor: current_user,
tenant: tenant_context,
domain: :orders,
capability: "order.approve",
operation: :execute_action,
target: %{type: :row, id: order_id},
context: %{surface: :components}
)
decision =
Selecto.Capabilities.allow(:role_allowed,
effects: [{:required_filter, "tenant_id", {:eq, tenant_id}}],
obligations: [:audit_action]
)Decision statuses are :allow, :deny, :conditional, and
:not_applicable. Visibility recommendations are :enabled, :disabled,
:hidden, and :preview_only.
Direct Transition Actions
actions declares named business commands. The first strict action shape is a
row action that directly references a writes.transitions edge:
%{
actions: %{
complete_order: %{
target: :order,
scope: :row,
capability: "order.complete",
transition: %{
field: :status,
from: "ready",
to: "complete"
},
execution: %{
kind: :updato,
operation: :update,
set: %{status: "complete"}
}
}
}
}Validation checks:
actionsmust be a map when present.- action ids must be atoms or strings.
- each action entry must be a map.
capability, when present, must be an atom or string and must exist in the domain capability catalog.- actions with
type: :transitionmust declare a direct transition map. transitionmust be a map withfield,from, andto.- the transition field must exist in the source, schemas, or custom columns.
- the transition edge must exist in
writes.transitions. - optional direct execution metadata currently supports only
%{kind: :updato, operation: :update}. - optional execution
setmust set the transition field to the target state.
This validates that preview and execution can ask the same domain question; it does not execute actions.
Source Relationships And Choice Sources
source_relationships declares the first compact working-domain to source-domain
binding shape. It is used by choice_sources to describe context-safe option
providers.
%{
source: %{
columns: %{
customer_id: %{
type: :integer,
reference: %{
choice_source: :customer_choices,
value_source: "customers.id",
caption_source: "customers.name"
}
}
}
},
source_relationships: %{
customer: %{
target_domain: :customers,
source_field: :customer_id,
target_field: :id,
source_path: "customers",
virtual_join: [
%{working_field: :customer_id, source_field: "customers.id", required: true}
],
filters: [
{:eq, "customers.active", true}
]
}
},
choice_sources: %{
customer_choices: %{
domain: :customers,
value_field: :id,
label_field: :name,
source_path: "customers",
value_source: "customers.id",
caption_source: "customers.name",
description_source: "customers.description",
filters: [{:eq, "customers.active", true}],
order_by: ["customers.name", {"customers.id", :desc}],
presentation: %{
control: :autocomplete,
mode: :searchable,
cardinality: :one
},
source_relationship: :customer,
capability: "customer.choose"
}
}
}Source relationship validation checks:
source_relationshipsmust be a map when present.- source relationship ids must be atoms or strings.
- each source relationship entry must be a map.
- each source relationship must declare
target_domain,source_field, andtarget_field. target_domain,source_field, andtarget_fieldmust be atoms or strings.source_fieldmust exist in the working domain source, schemas, or custom columns.- optional
source_pathmust be a non-empty atom or dotted string path. - optional
virtual_joinmust be a list of maps withworking_fieldandsource_field;working_fieldmust exist in the working domain,source_fieldmust be a non-empty atom or dotted string path, and optionalrequiredmust be a boolean. - optional
filtersmust be a list of static filter expressions using the same operator and path syntax as choice-source filters.
Choice source validation checks:
choice_sourcesmust be a map when present.- choice source ids must be atoms or strings.
- each choice source entry must be a map.
- each choice source must declare
domain,value_field, andlabel_field. domain,value_field, andlabel_fieldmust be atoms or strings.- optional
source_relationshipmust reference a declared source relationship. - optional
capabilitymust reference a declared capability. - optional
source_path,value_source,caption_source, anddescription_sourcemust be non-empty atom or dotted string paths. - optional
filtersmust be a list of static filter expressions. Field operators such as:eq,:gt,:between, and:in, plus logical:and,:or, and:not, may be atoms or strings. - choice-source filter field operands must be non-empty atom or dotted string paths. Literal, context, and runtime values are preserved without evaluation.
- optional
order_bymust be a list of paths or{path, direction}entries; direction must be:ascor:desc. - optional
presentationmust be a map. Known presentation hints are:control: :select | :autocomplete | :table_picker,mode: :static | :searchable | :async | :inline, andcardinality: :one | :many.
Field binding validation checks:
- source, schema, and projection column metadata may use
choice_source: choice_source_idas compact field binding. - source, schema, and projection column metadata may use
reference: %{choice_source: choice_source_id}for richer bindings. - field-level
choice_sourcereferences must be atoms or strings and must reference a declared choice source. reference, when present, must be a map.reference.choice_source, when present, must be an atom or string and must reference a declared choice source.- optional
reference.value_sourceandreference.caption_sourcemust be atoms or strings. - optional
reference.caption_fieldmust be an atom or string and must refer to a known working-domain field.
Authoring Shorthand
For authoring ergonomics, a field may declare choice_source: %{...} directly.
Selecto.Domain.normalize/1 expands that supported shorthand into the canonical
registries without changing Selecto.configure/3 behavior:
customer_id: %{
type: :integer,
choice_source: %{
id: :customer_choices,
domain: :customers,
source_relationship: %{
id: :customer,
virtual_join: [
%{working_field: :customer_id, source_field: "customers.id", required: true}
]
},
value_source: "customers.id",
caption_source: "customers.name",
filters: [{:eq, "customers.active", true}],
presentation: :select
}
}The normalized form contains:
source_relationships.customerchoice_sources.customer_choicesreference: %{choice_source: :customer_choices, ...}on the field- compact
choice_source: :customer_choiceson the field
If id values are omitted, the normalizer generates deterministic string ids
from the field path. This is canonical shorthand only; pre-0.5 legacy sections
are still not preserved or expanded.
The current slice validates static choice-source metadata and filter expression syntax plus static source-relationship metadata. It does not resolve external source-domain schemas, apply filters, fetch options, or execute membership checks.
Domain Composition
Selecto.Domain.compose/2 is the opt-in Stage 2 boundary for combining an
authored domain with overlays before projecting or validating it. It does not
change Selecto.configure/3 behavior.
{:ok, normalized, diagnostics} =
Selecto.Domain.compose(base_domain, [
%{
source: %{
columns: %{total: %{label: "Total", format: :currency}},
redact_fields: [:tenant_secret]
},
filters: %{"status" => %{field: :status}}
}
])Composition semantics are deterministic:
- maps deep-merge by section.
redact_fields, includingsource.redact_fields, are unioned.extensionsare appended uniquely.- other lists and scalar values are replaced by later overlays.
- governance/reference registry collisions, such as
choice_sourcesorsource_relationships, produce:domain_composition_collisionwarnings.
After overlays merge, declared extension merge_domain/2 callbacks run in
declaration order and the result is normalized again.
Domain Projections
Selecto.Domain.project/2 turns a normalized domain into read-only consumer
views. These projections are opt-in and do not change Selecto.configure/3
behavior.
Supported projections are:
:queryfor query/runtime-facing sections:writefor write/action/reference metadata:uifor display defaults, choices, actions, and detail actions:apifor combined read/write/action API-style consumers:query_contractfor constrained query metadata used by tools, Components, and AI query contracts
The :query_contract projection is intentionally summary-only. It exposes:
- source table and primary key
- selectable fields from
source,schemas, andcustom_columns - join summaries with target schemas and target field ids
- query defaults and required query lists
- filter, function, query-member, and published-view summaries
- source relationship and choice-source summaries
- field-to-choice-source bindings
- declared capability ids
It does not include write/action/detail-action sections, raw authored unknown keys, or function captures from query members and published views.
For consumers that do not need the lower-level projection API,
Selecto.Domain.query_contract/1 accepts either an authored domain or an
already-normalized domain and returns {:ok, query_contract, diagnostics}.
Domain Inspection
Selecto.Domain.describe/1 returns a compact structured inspection map for an
authored or normalized domain. The output is intended for generators, docs,
Studio, tests, and other tools that need stable metadata without walking the
full normalized domain shape.
{:ok, inspection, diagnostics} = Selecto.Domain.describe(domain)
inspection.counts.choice_sources
inspection.registries.source_fields
inspection.source_relationships
inspection.field_choice_bindingsThe inspection output includes:
- section categories and normalization diagnostics summary
- counts for source fields, registries, writes, actions, capabilities, source relationships, choice sources, and field choice bindings
- sorted registry ids for filters, functions, query members, joins, schemas, actions, capabilities, source relationships, and choice sources
- compact summaries of
writes, actions, capabilities, source relationships, choice sources, and field-to-choice-source bindings
Choice Membership API
Selecto.Domain.Choices is the first shared API for asking whether a submitted
value belongs to a field's declared choice source. In this slice it resolves
domain metadata and builds a membership request, but it does not query source
domains or databases unless a caller supplies an explicit resolver.
{:ok, request} =
Selecto.Domain.Choices.request(domain, :customer_id, 42,
actor: current_user,
tenant: tenant_context,
record: %{customer_id: 42},
context: %{surface: :components}
)
request.choice_source
#=> :customer_choices
{:error, result} =
Selecto.Domain.Choices.validate_choice(domain, :customer_id, 42)
result.status
#=> :unknown
result.reason_code
#=> :resolver_requiredCallers that have a membership implementation can pass a resolver function:
resolver = fn request ->
if source_member?(request) do
Selecto.Domain.Choices.valid(:source_member)
else
Selecto.Domain.Choices.invalid(:not_in_choice_source)
end
end
{:ok, result} =
Selecto.Domain.Choices.validate_choice(domain, :customer_id, 42,
resolver: resolver
)The request/result shape lets Components, API, GraphQL, AI, actions, and Updato
share one membership question later. Core Selecto remains conservative: without
a resolver, membership is :unknown, not assumed valid.
Choice sources may declare resolver-facing constraint policy metadata:
choice_sources: %{
customer_choices: %{
domain: :customers,
value_field: :id,
label_field: :name,
constraint_policy: %{
domain_of_interest: :fail_closed
}
}
}The policy is carried on membership and option-list requests. A resolver can
use :fail_closed to reject a request when server-owned Domain-of-Interest
filters are present but cannot be enforced. The default is best-effort when no
policy is declared.
Choice Option Lists
Selecto.Domain.Choices also exposes the sibling option-list request shape for
surfaces that need to ask "what options should this field show?" The request can
be built from a field binding or directly from a declared choice source.
{:ok, request} =
Selecto.Domain.Choices.options_request(domain, :customer_id,
search: "acme",
limit: 20,
offset: 0,
actor: current_user,
tenant: tenant_context,
record: %{customer_id: 42},
context: %{surface: :components}
)
request.choice_source
#=> :customer_choices
{:ok, direct_request} =
Selecto.Domain.Choices.options_request(domain, :customer_choices,
by: :choice_source,
search: "acme"
)As with membership checks, core Selecto does not fetch option rows without an explicit resolver:
{:error, result} =
Selecto.Domain.Choices.list_options(domain, :customer_id, search: "acme")
result.status
#=> :unknown
resolver = fn request ->
options =
fetch_options(request)
|> Enum.map(&%{value: &1.id, label: &1.name})
Selecto.Domain.Choices.options_resolved(options)
end
{:ok, result} =
Selecto.Domain.Choices.list_options(domain, :customer_id,
search: "acme",
resolver: resolver
)The option-list API is a projection contract for future Components, API,
GraphQL, AI, operations, and Updato integrations. It does not change
Selecto.configure/3 behavior.
Elixir Example
domain = %{
schema_version: 1,
domain_version: "0.5.0",
domain_fingerprint: "sha256:9f5d...",
name: "Orders",
source: %{
source_table: "orders",
primary_key: :id,
fields: [:id, :status, :customer_id],
columns: %{
id: %{type: :integer},
status: %{type: :string},
customer_id: %{
type: :integer,
reference: %{
choice_source: :customer_choices,
value_source: "customers.id",
caption_source: "customers.name"
}
}
},
associations: %{
customer: %{queryable: :customers}
}
},
schemas: %{
customers: %{
source_table: "customers",
primary_key: :id,
fields: [:id, :name],
columns: %{
id: %{type: :integer},
name: %{type: :string}
},
associations: %{}
}
},
joins: %{
customer: %{}
},
filters: %{
"customer_name" => %{field: "customers.name"}
},
source_relationships: %{
customer: %{
target_domain: :customers,
source_field: :customer_id,
target_field: :id,
source_path: "customers",
virtual_join: [
%{working_field: :customer_id, source_field: "customers.id", required: true}
],
filters: [{:eq, "customers.active", true}]
}
},
choice_sources: %{
customer_choices: %{
domain: :customers,
value_field: :id,
label_field: :name,
source_path: "customers",
value_source: "customers.id",
caption_source: "customers.name",
filters: [{:eq, "customers.active", true}],
order_by: ["customers.name"],
presentation: %{
control: :autocomplete,
mode: :searchable,
cardinality: :one
},
source_relationship: :customer,
capability: "customer.choose"
}
},
capabilities: %{
"order.view" => %{operations: [:select, :detail]},
"order.approve" => %{operations: [:action], action: :approve_order},
"customer.choose" => %{operations: [:choice_source]}
},
writes: %{
transitions: %{
status: %{
"pending" => ["ready", "cancelled"],
"ready" => ["complete", "cancelled"],
"complete" => []
}
}
},
actions: %{
complete_order: %{
target: :order,
scope: :row,
capability: "order.approve",
transition: %{field: :status, from: "ready", to: "complete"},
execution: %{kind: :updato, operation: :update, set: %{status: "complete"}}
}
}
}
{:ok, normalized, diagnostics} = Selecto.Domain.validate(domain)JSON Equivalent
JSON domains use string keys and string field identifiers:
{
"schema_version": 1,
"domain_version": "0.5.0",
"domain_fingerprint": "sha256:9f5d...",
"name": "Orders",
"source": {
"source_table": "orders",
"primary_key": "id",
"fields": ["id", "status", "customer_id"],
"columns": {
"id": {"type": "integer"},
"status": {"type": "string"},
"customer_id": {
"type": "integer",
"reference": {
"choice_source": "customer_choices",
"value_source": "customers.id",
"caption_source": "customers.name"
}
}
},
"associations": {
"customer": {"queryable": "customers"}
}
},
"schemas": {
"customers": {
"source_table": "customers",
"primary_key": "id",
"fields": ["id", "name"],
"columns": {
"id": {"type": "integer"},
"name": {"type": "string"}
},
"associations": {}
}
},
"joins": {
"customer": {}
},
"filters": {
"customer_name": {"field": "customers.name"}
},
"source_relationships": {
"customer": {
"target_domain": "customers",
"source_field": "customer_id",
"target_field": "id",
"source_path": "customers",
"virtual_join": [
{"working_field": "customer_id", "source_field": "customers.id", "required": true}
],
"filters": [["eq", "customers.active", true]]
}
},
"choice_sources": {
"customer_choices": {
"domain": "customers",
"value_field": "id",
"label_field": "name",
"source_path": "customers",
"value_source": "customers.id",
"caption_source": "customers.name",
"filters": [["eq", "customers.active", true]],
"order_by": ["customers.name"],
"presentation": {
"control": "autocomplete",
"mode": "searchable",
"cardinality": "one"
},
"source_relationship": "customer",
"capability": "customer.choose"
}
},
"capabilities": {
"order.view": {"operations": ["select", "detail"]},
"order.approve": {"operations": ["action"], "action": "approve_order"},
"customer.choose": {"operations": ["choice_source"]}
},
"writes": {
"transitions": {
"status": {
"pending": ["ready", "cancelled"],
"ready": ["complete", "cancelled"],
"complete": []
}
}
},
"actions": {
"complete_order": {
"target": "order",
"scope": "row",
"capability": "order.approve",
"transition": {"field": "status", "from": "ready", "to": "complete"},
"execution": {
"kind": "updato",
"operation": "update",
"set": {"status": "complete"}
}
}
}
}Diagnostics Example
{:ok, _normalized, diagnostics} = Selecto.Domain.normalize(%{
source: %{
source_table: "orders",
primary_key: :id,
fields: [:id],
columns: %{id: %{type: :integer}}
},
schemas: %{},
joins: %{},
custom_columns: %{},
writes: %{},
old_write_flag: true
})
diagnostics.schema_version_inferred
#=> true
Enum.map(diagnostics.warnings, & &1.code)
#=> [:schema_version_inferred, :projection_sections, :proposed_sections, :unknown_sections]
diagnostics.unknown_sections
#=> [:old_write_flag]Use Selecto.Domain.validate/1 when callers want contract errors in addition to
normalization diagnostics.