Skip to content

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:

ArtifactFolderWhat it isUse it for
Data Providermetadata/provider/a name → REST base path mappingthe thing a grid's metadata.dataSource points at
Data Viewmetadata/data_view/a declared SQL join over physical tables, read-onlyid/label option lists, multi-table reads
Data Servicemetadata/data_service/a parameterized query: count, search, get, or compositeKPI 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.jsonreal 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 its schema (must match your plugin's schemaName).
  • 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 value and label.

Data Service — the complete examples

A count (one number)

oeq-equipment-count-available.jsonreal 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.jsonreal 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.jsonreal 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 returnsbind 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-service

How to verify it worked

bash
erp schema validate spk-assembly/metadata/data_service/oeq-equipment-kpis.json --schema data-service-definition
OK — ... 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

SymptomCauseFix
lookup returns nothingData View doesn't output columns named value/labelrename the outputs
/execute 400s on a parameterized serviceargument not nested under parameters{ "params": { "parameters": { ... } } }
KPI card shows undefinedbound ${out.value} on a compositecomposite is ${out.results.<step>.value}
Data View returns 0 rowssource.schema doesn't match where the table livesset it to your plugin's schemaName