Detailed reference for YrestAPI configuration, request semantics, post-processing, import tooling, and runtime behavior.
make test runs both unit and integration tests (go test -v ./...).
Before running tests:
- Ensure PostgreSQL is available on local host (
localhostor127.0.0.1). - Ensure
POSTGRES_DSNis valid. - Ensure
APP_ENVis notproduction.
Run:
make testIntegration test behavior:
- test bootstrap derives test DSN from
POSTGRES_DSN - creates DB
test - applies migrations from
migrations/ - drops DB
testafter tests - rejects non-local DB hosts for safety
All requests are POST with JSON bodies.
Fetch a list of records using a model preset.
Payload:
{
"model": "Person",
"preset": "card",
"filters": {
"name__cnt": "John",
"org.name_or_org.full_name__cnt": "IBM"
},
"sorts": ["org.name DESC", "id ASC"],
"offset": 0,
"limit": 50
}Rules:
modelis the logical model name fromdb/*.ymlpresetis the preset name inside the modelfiltersis a map offield__op: value
Supported filter operators:
__eqdefault equality__cntcontains__startprefix__endsuffix__lt__lte__gt__gte__in
String behavior:
__eq,__cnt,__start,__endare case-insensitive by default- case-sensitive variants:
__eq_cs,__cnt_cs,__start_cs,__end_cs
Null behavior:
field__null: true->IS NULLfield__null: false->IS NOT NULL- aliases:
field__is_null,field__not_null
Grouping:
{
"or": { "id__in": [0, 1], "id__null": true },
"status_id__null": false
}Composite fields:
- join multiple paths with
_or_and_and_ - example:
org.name_or_org.full_name__cnt
Aliases and computable fields:
- aliases declared in
aliases:can be used in filters and sorts - computable fields declared under
computable:can also be used directly
Sort syntax:
- array of strings like
["path ASC", "other DESC"] - supports aliases and computable fields the same way as filters
Response:
- success: HTTP
200with JSON array - invalid JSON / unknown model / unknown preset: HTTP
400 - SQL/build/runtime issues: HTTP
500
Example success response:
[
{
"id": 1,
"name": "John Smith",
"org": { "name": "IBM" }
}
]Returns a single integer ({"count": N}) for the same filter semantics.
Payload:
{
"model": "Person",
"filters": { "org.name__cnt": "IBM" }
}Notes:
- filters and sorts can traverse relations with dotted paths
- path resolution goes through the alias map
- alias maps are cached in memory
- query execution hits PostgreSQL directly
Configuration is read from environment variables.
| Env var | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP port for the API server |
POSTGRES_DSN |
postgres://postgres:postgres@localhost:5432/app?sslmode=disable |
PostgreSQL connection string |
MODELS_DIR |
./db |
Path to directory with YAML model files |
LOCALE |
en |
Default locale for localization |
AUTH_ENABLED |
false |
Enable JWT auth middleware |
AUTH_JWT_VALIDATION_TYPE |
HS256 |
JWT signature algorithm: HS256 / RS256 / ES256 |
AUTH_JWT_ISSUER |
empty | Required iss claim value |
AUTH_JWT_AUDIENCE |
empty | Required aud claim value, single or CSV |
AUTH_JWT_HMAC_SECRET |
empty | Shared secret for HS256 |
AUTH_JWT_PUBLIC_KEY |
empty | PEM public key for RS256 / ES256 |
AUTH_JWT_PUBLIC_KEY_PATH |
empty | Path to PEM public key for RS256 / ES256 |
AUTH_JWT_CLOCK_SKEW_SEC |
60 |
Allowed clock skew for exp / nbf / iat |
CORS_ALLOW_ORIGIN |
* |
Value for Access-Control-Allow-Origin |
CORS_ALLOW_CREDENTIALS |
false |
Set Access-Control-Allow-Credentials: true |
ALIAS_CACHE_MAX_BYTES |
0 |
Max bytes for in-memory alias cache, 0 means unlimited |
Resolution of MODELS_DIR:
- if
MODELS_DIRis explicitly set, that path is used as-is - otherwise the service first tries
./db - if
./dbis missing or contains no model.ymlfiles, it falls back to./test_db
The repository keeps db/ as an intentionally empty primary model directory placeholder.
For Docker DX, the image copies both /app/db and /app/test_db.
GET /healthzreturns200 OKwhile the HTTP loop is aliveGET /readyzreturns200 OKonly when the model registry is initialized and PostgreSQL is reachable- both endpoints are unauthenticated and intended for liveness/readiness probes
When AUTH_ENABLED=true, each API request must include Authorization: Bearer <token>.
Token validation is fully local:
- signature
issaudexpnbfiat
Example HS256:
AUTH_ENABLED=true
AUTH_JWT_VALIDATION_TYPE=HS256
AUTH_JWT_ISSUER=auth-service
AUTH_JWT_AUDIENCE=yrest-api
AUTH_JWT_HMAC_SECRET=replace-with-strong-shared-secret
AUTH_JWT_CLOCK_SKEW_SEC=60Multiple audiences may be passed as CSV:
AUTH_JWT_AUDIENCE=service-a,service-bExample RS256:
AUTH_ENABLED=true
AUTH_JWT_VALIDATION_TYPE=RS256
AUTH_JWT_ISSUER=auth-service
AUTH_JWT_AUDIENCE=yrest-api
AUTH_JWT_PUBLIC_KEY_PATH=/etc/yrestapi/keys/auth_public.pem
AUTH_JWT_CLOCK_SKEW_SEC=60Generate YAML models from a PostgreSQL schema via POSTGRES_DSN:
make import ARGS="-help"
make import ARGS="-dry-run"
make import ARGS="-dry-run -only-simple"
make import ARGS="-out ./db_imported"
make import ARGS="-dsn 'postgres://user:pass@localhost:5432/app?sslmode=disable' -out ./db_imported"
make import ARGS="-prisma-schema ./prisma/schema.prisma -out ./db_imported"
make import ARGS="-graphql-queries ./gateway/queries -models-dir ./db -dry-run"Supported SQL import modes:
-only-simple: phase one, tables without outgoing relations- without
-only-simple: imports models withbelongs_toand reversehas_many; also adds relateditempresets intofull_infoforbelongs_to
Generated has_many relations receive a helper preset:
presets:
with_project_members:
fields:
- source: project_members
type: preset
preset: item- pass
-prisma-schema <path>to read models fromschema.prisma - in this mode
-dsnis optional and unused - output keeps the same YAML shape as SQL mode
- reverse
has_manyrelations are generated automatically - helper presets
with_<relation>are generated for eachhas_many - Prisma enum fields are generated as
type: intwithlocalize: true - enum dictionaries are merged into
cfg/locales/<LOCALE>.ymlor fallbackcfg/locales/en.yml
- pass
-graphql-queries <path>to read GraphQL documents and update presets in existing YAML models - this mode does not create models or relations
- model lookup uses
root field -> model name - preset names are generated from root field, operation name, and shape hash
- nested GraphQL selections are imported only when the relation already exists in YAML
sourceis taken directly from the GraphQL field name
- at startup the service loads
.ymlmodel files fromMODELS_DIR - it builds a registry of models, relations, presets, computable fields, and aliases
- it validates the graph
- the registry stays in memory and is reused for all requests
Validation checks:
- all referenced models, relations, and presets exist
- relation types are valid
- FK/PK defaults are applied correctly
throughchains are consistent- polymorphic
belongs_tois allowed only withpolymorphic: true type: presetfields reference an existing relation and nested presettype: formatterfields define an aliastype: computablefields reference an existing computable definition- unknown YAML keys and invalid types fail startup
If validation fails, startup is aborted.
table: people
aliases:
org: "contragent.organization"
computable:
fio:
source: "(select concat({surname}, ' ', {name}, ' ', {patrname}))"
type: string
relations:
person_name:
model: PersonName
type: has_one
where: .used = true
presets:
card:
fields:
- source: id
type: int
- source: person_name
type: preset
preset: item
- source: fio
type: computable
alias: full_nameKey points:
tableis mandatoryrelationsdefine the graph, optionally withthrough,where,order,polymorphicpresetsdescribe fields to select and returntype: presetwalks relationstype: computableinserts expressionstype: formatterpost-processes valuestype: nested_fieldcopies nested JSON branchescomputableandaliasesare global per model
Example:
table: contracts
relations:
next:
model: Contract
type: has_one
fk: prev_contract_id
reentrant: true
max_depth: 3
presets:
chain:
fields:
- source: id
type: int
- source: next
type: preset
preset: chainRules:
reentrant: trueis required to return to an already visited modelmax_depthlimits repeated traversal on one pathfield.max_depthoverrides relationmax_depth- if
reentrant: false, cyclic re-entry fails startup validation - if
max_depthis exceeded, traversal is capped - if omitted for a reentrant cycle, default
max_depth=3is applied with a warning
- dictionaries live in
cfg/locales/<locale>.yml - the active locale is loaded into a tree structure
- date/time formats can be customized via
layoutSettings - lookup order is
model -> preset -> field, then fallback to more global matches - if nothing is found, the original value is returned
- to localize a field set
localize: true - numeric codes are matched as numbers when used with
type: int
Example dictionary:
Person:
list:
status:
0: "Inactive"
1: "Active"
gender:
male: "Male"
female: "Female"Example fields:
fields:
- source: status
type: int
localize: true
- source: gender
type: string
localize: trueExample locale layouts:
layoutSettings:
date: "02.01.2006"
ttime: "15:04:05"
datetime: "02.01.2006 15:04:05"Declare a polymorphic belongs_to like this:
relations:
auditable:
model: "*"
type: belongs_to
polymorphic: trueRules:
- parent table must have
<relation>_idand<relation>_type type_columnmay override the default type column name- resolver batches child queries by type
- nested preset name must exist on each target model
You can pull relations and presets from db/templates/*.yml:
include: shared_relationsOr:
include: [shared_relations, auditable]Rules:
- relations from templates are added if missing
- if a relation exists in the model, empty fields are filled from the template
- template preset fields are applied first
- model fields override or extend by alias/source
- fields marked with
alias: skipin templates are ignored
A leading . in a condition is replaced with the unique SQL alias of that relation.
Example:
relations:
phone:
model: Contact
type: has_one
through: PersonContact
where: .type = 'Phone'
through_where: .used = trueSQL shape:
LEFT JOIN person_contacts AS pc
ON (main.id = pc.person_id)
AND (pc.used = true)
LEFT JOIN contacts AS c
ON (pc.contact_id = c.id)
AND (c.type = 'Phone')Meaning:
wherefilters the final relation tablethrough_wherefilters the intermediate table
Formatters transform or combine field values after SQL execution and after merging related data.
They are useful when you want to:
- collapse a nested preset into a string
- build computed display text
- derive compact labels from nested objects
- apply conditional display logic without controller code
Inline computed field:
- source: "{surname} {name}[0] {patronymic}[0..1]"
type: formatter
alias: full_nameFormatter on a relation:
- source: contacts
type: preset
alias: phones
formatter: "{type}: {value}"
preset: phone_listInside {...} you can use:
{field}{relation.field}{name}[0]{name}[0..1]
| Relation type | Result |
|---|---|
belongs_to |
string from related object |
has_one |
string from child object |
has_many |
array of strings |
| simple field | string from current row |
presets:
card:
fields:
- source: id
type: int
alias: id
- source: "{person_name.value}"
type: formatter
alias: name
- source: contacts
type: preset
alias: contacts
formatter: "{type}: {value}"
preset: contact_shortResult:
[
{
"id": 64,
"name": "Ivanov A V",
"contacts": ["Phone: +7 923 331 49 55", "Email: example@mail.com"]
}
]Syntax:
{ <condition> ? <then> : <else> }Condition forms:
- full form:
<field> <op> <value> - shorthand: just
<field>for truthy/falsy
Supported operators:
===!=>>=<<=
Supported literals:
- numbers
- booleans
null- quoted strings
Examples:
- source: `{? used ? "+" : "-"}`
type: formatter
alias: used_flag
- source: `{? age >= 18 ? "adult" : "minor"}`
type: formatter
alias: age_group
- source: `{? status == "ok" ? "✔" : "✖"}`
type: formatter
alias: status_iconNested ternaries:
- source: `{? used ? "{? age >= 18 ? "adult" : "minor"}" : "-"}`
type: formatter
alias: nested_exampleCombining conditionals and substitutions:
- source: '{? used ? "+" : "-"} {naming.surname} {naming.name}[0].'
type: formatter
alias: short_nameNotes:
- formatter fields must define an alias
- formatter fields are not included in SQL queries
- they are resolved only at post-processing stage
Use type: nested_field with a path in {...} to lift nested data into the current preset without SQL joins.
Example:
- source: "{person.contacts}"
type: nested_field
alias: contactsThe contacts array from nested person will be copied to the current item even if person itself is not exposed.
Calculated fields are declared at model level and are available in all presets.
computable:
fio:
source: "(select concat({surname}, ' ', {name}, ' ', {patrname}))"
type: string
stages_sum:
source: "(select sum({stages}.amount))"
where: "(select sum({stages}.amount))"
type: float
presets:
card:
fields:
- source: fio
alias: full_name
type: computable
- source: stages_sum
type: computableRules:
{path}placeholders are replaced with SQL aliases from alias map- wrap subqueries in parentheses so they can be safely used in
SELECT - for filters and sorts, refer to the computable field by name
presets:
base:
fields:
- source: id
type: int
- source: name
type: string
alias: name
head:
fields:
- source: full_name
type: string
alias: name
- source: head_only
type: string
alias: head_only
item:
extends: base, head
fields:
- source: okopf
type: int
alias: okopf
- source: item_only
type: string
alias: item_only- the service is read-only by design: only
/api/indexand/api/countare provided - PostgreSQL is the only supported database backend
- model configuration is loaded and validated on startup; changing YAML files requires restart
- polymorphic relation resolution is based on
<relation>_typevalues present in data - integration tests are safety-scoped to local PostgreSQL hosts and create/drop a temporary
testdatabase
YrestAPI is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later).
See LICENSE.txt.