Power Apps Code Apps Documentation

repository·main·Indexed 19 days ago

https://github.com/microsoft/powerappscodeapps

Guidance for building custom web applications that run natively within the Power Apps environment using React, TypeScript, and Vite. Includes instructions for scaffolding projects with starter templates, integrating Power Platform connectors, and performing CRUD operations with Native Dataverse and the Dataverse Connector. Features documentation on using the PAC CLI for authentication, data source generation, and deploying apps via the Power Platform SDK.

Tokens
40.3K
Snippets
115
Records
152
Agent score
66%

What's inside Power Apps Code Apps

  1. Overview of Power Apps Template - starter

    main
    The power-apps-template-starter is an opinionated starter template designed for building Power Apps code apps. It uses a modern stack of Vite, TypeScript, and React to provide a foundation for common app scenarios with minimal setup and high extensibility. The template is optimized for use with AI coding agents and follows industry-standard patterns.
  2. Use the React + Vite template

    main

    This template provides a minimal setup for running React within Vite, featuring Hot Module Replacement (HMR) and pre-configured ESLint rules. It offers two primary plugin options for Fast Refresh:

    1. @vitejs/plugin-react: Uses Babel (or oxc when used in rolldown-vite) for Fast Refresh.
    2. @vitejs/plugin-react-swc: Uses SWC for Fast Refresh.
  3. Compare Native Dataverse vs. Dataverse Connector

    main

    There are two distinct ways to interact with Dataverse in a Power Apps Code App. This sample uses the Dataverse connector (shared_commondataserviceforapps), which is useful if you specifically need connector-based access, but for most new projects, Native Dataverse is preferred because it is strongly typed and first-class.

    FeatureNative DataverseDataverse Connector (this sample)
    CLI Commandpower-apps add-data-source -a dataverse -t contactpower-apps add-data-source -a shared_commondataserviceforapps -c <connectionId> -t contact -d <orgUrl>
    Connection Required?NoYes
    Code GenerationOne typed service per table (e.g., ContactsService)A single, untyped MicrosoftDataverseService
    TypingStrongly typed rowsUntyped OData rows (Record<string, unknown>)
    Org TargetingImplicitRequires an org URL for each operation via ...WithOrganization variants
  4. Use Dataverse Functions and Actions

    main

    While the PAC CLI generates CRUD services, you must use the @microsoft/power-apps Node CLI (npx power-apps) to discover and generate services for Dataverse Functions and Actions.

    Discover and Generate

    # Search for available actions and functions
    npx power-apps find-dataverse-api --search "<name>"
    
    # Generate a typed service
    npx power-apps add-dataverse-api --api-name <OperationName>

    API Patterns

    1. Unbound Function

    Not bound to any table. Takes no parameters and returns data.

    const result = await WhoAmIService.WhoAmI();
    const user = result.data; // { UserId, BusinessUnitId, OrganizationId }

    2. Unbound Action

    Not bound to any table. Accepts typed scalar parameters and performs a write (returns no data).

    const result = await SetAutoNumberSeedService.SetAutoNumberSeed(
      "contact",       // EntityName
      "cr123_num",     // AttributeName
      1000             // Value
    );
    // result.success is true on 204 No Content

    3. Bound Action

    Bound to a specific table. Requires the record GUID (id) to operate on a specific record.

    const result = await ConvertOwnerTeamToAccessTeamService.ConvertOwnerTeamToAccessTeam(teamId);
    // result.success is true on 204 No Content
    npx power-apps find-dataverse-api --search "<name>"
    npx power-apps add-dataverse-api --api-name <OperationName>
  5. Executing Dataverse Actions vs. Functions

    main

    The Dataverse connector handles operations differently based on their HTTP method:

    • Actions (POST): Supported. Use PerformUnboundAction (unbound) or PerformBoundAction (bound to a specific record). These are used for operations that may have side effects.
    • Functions (GET): Not supported via the generic action endpoints. Attempting to call a read-only function (like WhoAmI) as an action will result in a 404 error: "No HTTP resource was found that matches the request URI '.../api/data/v9.1.0/WhoAmI'".

    To access data typically provided by functions, use dedicated connector operations such as GetEntities, GetOrganizations, or GetCatalogs.

  6. Handle Lookup Fields in Dataverse

    main

    The demo demonstrates how to manage relationships using OData bind syntax for writing and on-demand resolution for reading.

    Writing a Lookup (Create/Update)

    To link a record to another via a relationship, use the @odata.bind syntax on the relationship property name. To clear a lookup, set the property to null.

    // Link contact to account using OData bind syntax
    contact['parentcustomerid_account@odata.bind'] = `/accounts(${accountId})`;
    
    // Clear a lookup by setting it to null
    updates['parentcustomerid_account@odata.bind'] = null;

    Reading a Lookup (On-demand Resolution)

    When reading, Dataverse returns the GUID of the related record. To display a human-readable name, you must perform a second lookup using the related service.

    // Step 1: Load contacts with the lookup GUID field
    const contacts = await ContactsService.getAll({
      select: ['contactid', 'firstname', '_msa_managingpartnerid_value']
    });
    
    // Step 2: Resolve the GUID to a display name when needed
    const partner = await AccountsService.get(contact._msa_managingpartnerid_value, {
      select: ['accountid', 'name']
    });
    // Writing
    contact['parentcustomerid_account@odata.bind'] = `/accounts(${accountId})`;
    
    // Reading
    const partner = await AccountsService.get(contact._msa_managingpartnerid_value, {
      select: ['accountid', 'name']
    });
  7. Implement the 'Props Down, Events Up' pattern

    main

    To maintain a unidirectional data flow, components should receive data through props and communicate user actions upward to hooks via callback functions. This prevents components from needing to manage global state or direct API calls.

    // App passes data and callbacks to children
    <ContactList
      contacts={contacts}        // data down
      loading={loading}          // data down
      onEdit={startEdit}         // event up
      onDelete={deleteContact}   // event up
      onCreateNew={startCreate}  // event up
    />
  8. Read and Write Dataverse Lookup Fields

    main

    Dataverse lookup fields store a reference (GUID) to a record in another table. The naming convention for these fields differs depending on whether you are performing a GET or a POST/PATCH operation.

    Reading (GET responses)

    When reading data, lookup values are returned using the format _<schemaname>_value, which contains the GUID of the related record.

    Writing (POST / PATCH requests)

    To set or update a lookup relationship, use the OData bind syntax. The format is "<SchemaName>@odata.bind": "/<entitysetname>(<guid>)". To clear a lookup, set the bound property to null.

    Note: Do not use $expand to eagerly load related records; the on-demand get() pattern is preferred for performance.

    // Writing: Set a lookup relationship
    contact['parentcustomerid_account@odata.bind'] = `/accounts(${accountId})`;
    contact['TransactionCurrencyId@odata.bind'] = `/transactioncurrencies(${currencyId})`;
    
    // Writing: Clear a lookup
    updates['parentcustomerid_account@odata.bind'] = null;
    
    // Reading: Access the GUID
    const contactId = contact._parentcontactid_value;
  9. Optimize Dataverse queries and lookups

    main

    To ensure high performance in the Dataverse Demo App, follow these two patterns:

    1. Query Optimization: When calling getAll(), always use select to limit fields, top to limit the number of records, and orderBy to control sorting. This reduces the payload size from the Dataverse Web API.
    2. On-Demand Lookup Resolution: Instead of loading entire related tables (which can be heavy), resolve lookup GUIDs to display names on-demand using individual Service.get() calls. This is managed by the useLookupResolver hook.
  10. Limitations of File and Image columns in the Dataverse connector

    main

    When using the Dataverse connector, be aware of the following differences regarding media columns compared to native Dataverse:

    • File columns: You can perform PUT (upload) and GET (download), but DELETE is not possible. Dataverse ignores null in a PATCH for File columns, so you cannot clear them via the connector.
    • Image columns: You can perform PUT (upload) and GET (download). Unlike File columns, Image columns can be cleared by patching them to null.
    • Full-size images: The built-in entityimage column only stores a ~144px thumbnail. To download full-resolution images, you must use a custom Image column with the "Can store full-size image" setting enabled. Full-size images are served via the file-content endpoint (GET /entity(id)/column/$value?size=full).
  11. Understand the Dataverse Demo App architecture

    main

    The Dataverse Demo App follows a three-layer architecture designed to separate UI, business logic, and data access. This separation ensures that each layer is independently testable and easy to maintain.

    1. Presentation Layer (Components): Located in src/components/. These are pure, presentational components that receive data via props and emit events via callbacks. They contain no business logic or direct service calls.
    2. Business Logic Layer (Hooks): Located in src/hooks/. Custom hooks encapsulate all state management and asynchronous operations. They are the only layer permitted to call the generated services.
    3. Data Layer (Generated Services): Located in src/generated/. These services are auto-generated by the PAC CLI and handle authentication and Dataverse Web API communication. Do not edit these files manually; regenerate them if the schema changes.
    ┌──────────────────────────────────────┐
    │  Presentation Layer  (Components)    │  UI only — no business logic
    ├──────────────────────────────────────┤
    │  Business Logic Layer  (Hooks)       │  State, async ops, orchestration
    ├──────────────────────────────────────┤
    │  Data Layer  (Generated Services)   │  Dataverse Web API — auto-generated
    └──────────────────────────────────────┘
  12. Understand the Dataverse Connector Architecture

    main

    The Dataverse Connector sample uses a three-layer architecture to manage the untyped nature of the connector service:

    1. Components (UI): Presentation layer (e.g., ContactList, AccountForm).
    2. Hooks (State/Orchestration): Business logic (e.g., useContacts.ts) that calls the client wrapper.
    3. src/dataverse/client.ts (Wrapper): A helper that wraps the auto-generated MicrosoftDataverseService. It handles three critical requirements of the connector:
      • Org URL: Automatically resolves the URL via getContext().app.dataverseOrgUrl and passes it to ...WithOrganization operations.
      • Headers: Injects odata.include-annotations="*" and application/json to ensure lookup display names and JSON parsing work correctly.
      • Result Envelope: Unwraps the connector's { success, data, error } response format into a usable interface.
    4. MicrosoftDataverseService: The auto-generated, untyped service layer.
    5. Dataverse: The backend storage.