Appearance
Add a data provider, data view, or data service
What you're doing
Three related artifacts feed data to your pages. They sound alike; here's the difference in one line each:
| Artifact | Folder | What it is | Use it for |
|---|---|---|---|
| Data Provider | metadata/provider/ | a name → REST base path mapping | the thing a grid's metadata.dataSource points at |
| Data View | metadata/data_view/ | a declared SQL join over physical tables, read-only | id/label option lists, multi-table reads |
| Data Service | metadata/data_service/ | a parameterized query: count, search, get, or composite | KPI counts, typeahead lookups, dashboard fan-outs |
Rule of thumb: prefer a Data View or Data Service over hand-writing a REST endpoint. You never write a controller.
Data Provider — the complete example
Covered in Wire a page's data. For an entity you own it is always this, with the name and entity swapped:
json
{
"name": "oeq-checkout-provider",
"description": "Data Provider for oeq_checkout (grid + CRUD).",
"definition": {
"kind": "rest",
"connectionRef": "self",
"basePath": "/api/v1/entities/oeq_checkout/records",
"supports": ["search", "get", "create", "update"]
},
"metadata": {}, "modules": []
}Data View — the complete example
spk-assembly/metadata/data_view/oeq-category-select-options-view.json — real file. It produces { value, label } pairs for a category picker:
json
{
"name": "oeq-category-select-options-view",
"description": "oeq_category id/label pairs for pickers.",
"definition": {
"source": { "table": "oeq_category", "alias": "t", "excludeDeleted": false, "schema": "erp_core" },
"joins": [],
"fields": [ { "ref": "t.id", "outputName": "value" } ],
"calculatedFields": [
{ "outputName": "label", "expression": { "op": "concat", "args": [
{ "op": "field", "fieldRef": "t.category_code" },
{ "op": "literal", "literalValue": " - " },
{ "op": "coalesce", "args": [ { "op": "field", "fieldRef": "t.category_name" }, { "op": "literal", "literalValue": "" } ] }
] } }
],
"filter": null, "groupBy": [], "aggregations": [],
"sort": [ { "ref": "t.id", "descending": true } ],
"pagination": { "defaultPageSize": 200, "maxPageSize": 500 },
"permissionKey": null
},
"metadata": {}, "modules": []
}Line by line
source— the driving table, its alias, and itsschema(must match your plugin'sschemaName).fields— plain column selections,{ ref, outputName }.calculatedFields— expression columns. The expression tree ops you'll use:field(a column),literal(a constant),concat,coalesce.sort/pagination— read defaults.- A picker view must output columns literally named
valueandlabel.
Data Service — the complete examples
A count (one number)
oeq-equipment-count-available.json — real file:
json
{
"name": "oeq-equipment-count-available",
"description": "Available equipment.",
"definition": {
"operation": "count",
"source": { "kind": "entity", "entityName": "oeq_equipment" },
"filters": [ { "field": "status", "operator": "eq", "value": "AVAILABLE" } ],
"parameters": []
},
"metadata": {}, "modules": []
}POST /api/v1/data-services/oeq-equipment-count-available/execute returns { "value": 3 }. Bind it as ${out.value}.
A search (typeahead over a Data View)
oeq-category-search.json — real file:
json
{
"name": "oeq-category-search",
"description": "Typeahead category search for the category lookup field.",
"definition": {
"operation": "search",
"source": { "kind": "dataView", "dataViewName": "oeq-category-select-options-view" },
"filters": [ { "field": "t.category_name", "operator": "contains", "value": "${param.search}" } ],
"parameters": [ { "name": "search", "type": "string" } ]
},
"metadata": {}, "modules": []
}A core.lookup block points its optionsSourceKey at "oeq-category-search". See the lookup recipe.
A composite (KPI fan-out)
oeq-equipment-kpis.json — real file:
json
{
"name": "oeq-equipment-kpis",
"description": "Composite KPI fan-out for the Equipment Register page.",
"definition": {
"operation": "composite",
"source": { "kind": "entity", "entityName": "oeq_equipment" },
"steps": [
{ "as": "total", "service": "oeq-equipment-count-total", "parameters": {} },
{ "as": "available", "service": "oeq-equipment-count-available", "parameters": {} },
{ "as": "assigned", "service": "oeq-equipment-count-assigned", "parameters": {} }
]
},
"metadata": {}, "modules": []
}POST .../oeq-equipment-kpis/execute returns { "results": { "total": { "value": 12 }, "available": { "value": 8 }, "assigned": { "value": 4 } } }. Bind each as ${out.results.total.value}. Verified live against the tutorial module.
The /execute wire-shape trap
A Data Service that declares parameters must be called with the arguments nested one level deeper than a normal entity call:
json
{ "params": { "parameters": { "search": "lap" } } }not { "params": { "search": "lap" } }. A core.lookup block does this for you; a hand-written callApi action must nest it.
Response envelope by operation
operation | /execute returns | bind as |
|---|---|---|
count | { "value": <n> } | ${out.value} |
search / get | { "items": [...] } or { "records": [...] } | check a shipped example |
composite | { "results": { "<step.as>": <that step's envelope> } } | ${out.results.<as>.value} |
Ground yourself first
bash
erp schema pull data-service-definition
erp schema pull data-view-definition
erp examples search employee-search --kind data-serviceHow to verify it worked
bash
erp schema validate spk-assembly/metadata/data_service/oeq-equipment-kpis.json --schema data-service-definitionOK — ... matches schema "data-service-definition"After publishing:
bash
erp api post "/api/v1/data-services/oeq-equipment-kpis/execute" --body "{}"json
{ "results": { "total": { "value": 0 }, "available": { "value": 0 }, "assigned": { "value": 0 } } }Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| lookup returns nothing | Data View doesn't output columns named value/label | rename the outputs |
/execute 400s on a parameterized service | argument not nested under parameters | { "params": { "parameters": { ... } } } |
KPI card shows undefined | bound ${out.value} on a composite | composite is ${out.results.<step>.value} |
| Data View returns 0 rows | source.schema doesn't match where the table lives | set it to your plugin's schemaName |