Skip to content

Latest commit

 

History

History

DataverseConnector

README.md

Dataverse Connector Demo

A Power Apps Code App that talks to Microsoft Dataverse through the Microsoft Dataverse connector (shared_commondataserviceforapps) instead of native Dataverse.

The UI is intentionally identical to the native Dataverse sample — same components, same styling, same three tabs. Only the data layer differs. The goal is to show that a line-of-business app built on native Dataverse can also be built on the Dataverse connector, and to document exactly where the two approaches diverge.

Important

This sample is a connector counterpart to samples/Dataverse. If you're starting fresh and don't specifically need the connector, prefer native Dataverse — it is first‑class, strongly typed, and has fewer limitations (see Connector vs. native Dataverse).

What this demo shows

  • Contacts (CRUD) — Create, Read, Update, Delete on the contact table, with a lookup that links a contact to a parent account via OData bind syntax.
  • Accounts (File & Image Attachments) — Upload and download files and images on Dataverse file/image columns.
  • Functions & Actions — Three live connector capabilities: an OData query, a FetchXML query, and a metadata GetEntities listing.
  • Connector-only data layer — Every call goes through the connector's auto‑generated MicrosoftDataverseService; no native Dataverse services are used.

Native Dataverse vs. the Dataverse connector

There are two ways to reach Dataverse from a Code App, and they produce different generated code:

Native Dataverse (samples/Dataverse) Dataverse connector (this sample)
Add command pa app add data-source --connector dataverse --table contact pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table contact --dataset <orgUrl>
Needs a connection? No Yes
Generated code One typed service per table (ContactsService, AccountsService, …) + typed models A single, untyped MicrosoftDataverseService + MicrosoftDataverseModel
Typing Strongly typed rows Untyped OData rows (Record<string, unknown>)
Methods getAll / get / create / update / delete ListRecords, GetItem, CreateRecord, UpdateRecord, DeleteRecord, PerformUnboundAction, GetEntities, UpdateEntityFileImageFieldContent, …
Org targeting Implicit Each op has a …WithOrganization variant that takes the Dataverse org URL

Because the connector service is untyped and requires the org URL and explicit headers on every call, this sample wraps it in a small helper — src/dataverse/client.ts — that exposes simple listRows / getRow / createRow / updateRow / deleteRow (plus file and metadata helpers). Everything above that wrapper (hooks, components) mirrors the native sample.

Prerequisites

  • Node.js LTS (Download)
  • Power Apps CLI for code apps — the pa command from @microsoft/power-apps-cli
  • A Power Platform environment with Dataverse and Code Apps enabled
  • A Microsoft Dataverse connection in that environment

Quick start

1. Install dependencies

npm install

2. Initialize the code app

pa app init --display-name "Dataverse Connector Demo" --environment-id <your-environment-id>

3. Find (or create) a Dataverse connection and your org URL

# List connections — copy the Connection ID for the "Microsoft Dataverse" (shared_commondataserviceforapps) connection
pa connection list

The connector needs your environment's Dataverse org URL (e.g. https://orgXXXXXXXX.crm.dynamics.com/) as the -d dataset value. You can find it in the Power Platform admin center under the environment's details, or read it at runtime from getContext().app.dataverseOrgUrl (which is what the app itself uses).

4. Add the data sources

pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table account --dataset "https://orgXXXXXXXX.crm.dynamics.com/"
pa app add data-source --connector shared_commondataserviceforapps --connection-id <connectionId> --table contact --dataset "https://orgXXXXXXXX.crm.dynamics.com/"

Each add-data-source call generates the connector's typed service and models under src/generated/ (and a .power/ schema folder), with the operations available for the connector.

Note

src/generated/, .power/, and power.config.json are not committed to this repo — they are environment-specific and generated by pa app init / pa app add data-source. After cloning, run the steps above to create them for your own environment. Everything the app is built from (the src/dataverse/client.ts wrapper, hooks, and components) is committed and works once the data sources are added.

5. Configure your file / image column names

The Accounts tab uploads and downloads to a File column and an Image column on the account table. Add both in the maker portal (Tables → Account → Columns → New column), then set each column's logical name in src/types.ts:

// src/types.ts — set these to your columns' logical names
export const ACCOUNT_FILE_COLUMN = 'crdac_filecol' as const   // your File column
export const ACCOUNT_IMAGE_COLUMN = 'crdac_imagecol' as const // your Image column

The values above are examples from the environment this sample was built in — replace them with the logical names the maker portal shows for your columns. For the image full-size download to work, enable "Can store full-size image" on the Image column (see Full-size images).

6. Run locally

pa app run

Open the Local Play URL in the same browser profile as your Power Platform tenant.

7. Deploy

npm run build
pa app push

Project structure

src/
├── dataverse/
│   └── client.ts             # The ONLY data layer — wraps the connector's MicrosoftDataverseService
├── types.ts                  # Hand-authored Accounts/Contacts types (connector rows are untyped)
├── generated/                # Auto-generated by `pa app add data-source` (not committed)
│   ├── services/MicrosoftDataverseService.ts
│   └── models/MicrosoftDataverseModel.ts
├── hooks/                    # Business logic, built on src/dataverse/client
│   ├── useContacts.ts        # Contact CRUD + form state
│   ├── useAccountsCrud.ts    # Account CRUD for the Accounts tab
│   ├── useAccounts.ts        # Read-only accounts for the lookup dropdown
│   └── useLookupResolver.ts  # Resolves lookup GUIDs to display names on demand
├── components/               # Presentation only — same look as the native sample
│   ├── ContactList / ContactCard / ContactForm
│   ├── AccountList / AccountCard / AccountForm   # AccountForm includes the file/image attachment sub-form
│   ├── ApiActionsPanel.tsx   # Functions & Actions tab (OData query, FetchXML, GetEntities)
│   ├── Header / Footer / ErrorMessage
│   └── index.ts
├── App.tsx                   # Composition + tab navigation
└── App.css                   # Styling (identical to the native sample)

Architecture (three layers)

Components (UI)  →  Hooks (state + orchestration)  →  src/dataverse/client  →  MicrosoftDataverseService  →  Dataverse

The wrapper handles three things the connector requires but the native service does not:

  1. Org URL — resolved once from getContext().app.dataverseOrgUrl and passed to every …WithOrganization operation. (Plain operations without the org fail with "Invalid organization URL 'null'".)
  2. Headers — reads request odata.include-annotations="*" (so lookup display names come back as formatted-value annotations) and application/json.
  3. Result envelope — connector operations return { success, data, error }; the wrapper unwraps it and surfaces a readable error message.

Key patterns

CRUD (Contacts)

// Read — list rows with OData options
await listRows('contacts', { select: ['contactid', 'firstname', 'lastname'], orderBy: ['createdon desc'], top: 50 });

// Create — plain object; lookups use OData bind syntax
await createRow('contacts', {
  firstname: 'Ada',
  lastname: 'Lovelace',
  'parentcustomerid_account@odata.bind': `/accounts(${accountId})`,
});

// Update / Delete
await updateRow('contacts', id, { jobtitle: 'Engineer' });
await deleteRow('contacts', id);

File & image columns (Accounts)

// Column logical names come from src/types.ts (ACCOUNT_FILE_COLUMN / ACCOUNT_IMAGE_COLUMN)
// Upload (files and images use the same operation)
await uploadFileImage('accounts', id, ACCOUNT_FILE_COLUMN, file);

// Download a file — connector returns raw bytes (Uint8Array) + fileName on the envelope
const { blob, fileName } = await downloadFile('accounts', id, ACCOUNT_FILE_COLUMN);

// Download an image — full-size via the file-content endpoint, thumbnail inline from the record
const base64 = await downloadFileImage('accounts', id, ACCOUNT_IMAGE_COLUMN, /* fullSize */ true);

Functions & Actions

// OData / FetchXML queries via ListRecords
await listRows('accounts', { filter: "contains(name, 'a')", top: 5 });
await listRows('contacts', { fetchXml: '<fetch top="5">…</fetch>' });

// List all tables (metadata)
await listEntities(); // MicrosoftDataverseService.GetEntities

Connector vs. native Dataverse — differences we hit

Building this connector twin of the native sample surfaced several real differences. They're worth knowing before choosing the connector for a production app.

Scenario Native Dataverse Dataverse connector
CRUD ✅ typed services ✅ untyped, via MicrosoftDataverseService
Lookups (read/write) ✅ ✅ (@odata.bind write, formatted-value annotations on read)
File column upload/download ✅ ✅
Image column upload/download ✅ ✅ (full-size needs a column with full-size storage enabled)
File column delete ✅ (DELETE /entity(id)/column) ❌ Not possible — see below
Image column delete ✅ ✅ (null‑PATCH clears image columns)
Dataverse actions (POST) ✅ ✅ via PerformUnboundAction / PerformBoundAction
Dataverse functions (GET, e.g. WhoAmI) ✅ (add-dataverse-api) ❌ no GET path — see below
Relevance Search (GetRelevantRows) n/a ❌ generated without an org binding → can't target the current env
FetchXML queries – (typed getAll exposes $select/$filter/$orderby/$top only) ✅ via ListRecords fetchXml — see below

FetchXML queries work through the connector

A frequently requested capability: the connector's ListRecords accepts a fetchXml parameter, so you can run advanced FetchXML — aggregations, link-entity joins, grouping — directly from a Code App. The native sample's typed getAll only surfaces the common OData options ($select, $filter, $orderby, $top), so FetchXML is a genuine reason to reach for the connector.

// src/dataverse/client.ts wraps ListRecords; pass a fetchXml string:
await listRows('contacts', {
  fetchXml: `
    <fetch top="5">
      <entity name="contact">
        <attribute name="fullname" />
        <attribute name="emailaddress1" />
        <order attribute="createdon" descending="true" />
      </entity>
    </fetch>`,
});

Try it live on the Functions & Actions tab → FetchXML Query card.

File column delete is not possible through the connector

The connector exposes only PUT (upload) and GET (download) for file/image fields — there is no DELETE operation. And Dataverse deliberately ignores null in a PATCH for File columns (the record only holds a reference to blob storage, not the bytes). So:

  • Image columns (e.g. entityimage, or a custom Image column) store the value inline and do clear when PATCHed to null — image delete works.
  • File columns cannot be cleared through the connector at all. Native Dataverse can, because its generated service issues a real DELETE /account(id)/column request.

The app keeps the file × button and shows an honest message explaining the limitation rather than a fake success.

Actions work; GET functions don't

Dataverse has two kinds of operations: actions (POST, may have side effects) and functions (GET, read-only). The connector invokes actions through PerformUnboundAction (unbound) and PerformBoundAction (bound to a record) — both POST. It does not expose a GET path, so calling a read-only function like WhoAmI as an action returns:

404 "No HTTP resource was found that matches the request URI '.../api/data/v9.1.0/WhoAmI'"

Some function-style data is still available through dedicated connector operations (for example GetEntities, GetOrganizations, GetCatalogs) rather than the generic action endpoint.

Full-size images need a column that stores them

The built-in entityimage column stores only a ~144px thumbnail, so a "full size" download returns the same small image. To demo full resolution, add a custom Image column with "Can store full-size image" enabled. Full-size images are then served as raw bytes by the file-content endpoint (GET /entity(id)/column/$value?size=full); the thumbnail is read inline from the record.

Other operational notes

  • Connection & DLP — the connector runs under a user connection, so Data Loss Prevention policies apply. If a policy blocks the Dataverse connector's write actions, create/update/delete return HTTP 442 "Request blocked due to DLP policies" even though reads succeed.
  • No schema refresh — if a table's schema changes, delete and re‑add the data source to regenerate the service.

Learn more