mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-07 01:46:53 +01:00
Add P0 integration and control add-in guidance
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02baffe8-0600-430d-81fa-a9993685e7cb
This commit is contained in:
parent
9214f73819
commit
cf55246ecf
16 changed files with 526 additions and 12 deletions
|
|
@ -0,0 +1,79 @@
|
|||
page 50353 "WS Order API Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'order';
|
||||
EntitySetName = 'orders';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = "Sales Header";
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
part(lines; "WS Order Line API Bad")
|
||||
{
|
||||
EntityName = 'orderLine';
|
||||
EntitySetName = 'orderLines';
|
||||
SubPageLink = "Order No." = Field("No.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table 50353 "WS Order Line Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Entry No."; Integer)
|
||||
{
|
||||
AutoIncrement = true;
|
||||
}
|
||||
field(2; "Order No."; Code[20])
|
||||
{
|
||||
TableRelation = "Sales Header"."No.";
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "Entry No.")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 50354 "WS Order Line API Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'orderLine';
|
||||
EntitySetName = 'orderLines';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = "WS Order Line Bad";
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
field(id; Rec.SystemId)
|
||||
{
|
||||
Editable = false;
|
||||
}
|
||||
field(orderNumber; Rec."Order No.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
page 50350 "WS Order API"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'order';
|
||||
EntitySetName = 'orders';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = "Sales Header";
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
field(id; Rec.SystemId)
|
||||
{
|
||||
Editable = false;
|
||||
}
|
||||
part(lines; "WS Order Line API")
|
||||
{
|
||||
EntityName = 'orderLine';
|
||||
EntitySetName = 'orderLines';
|
||||
Multiplicity = Many;
|
||||
SubPageLink = "Order Id" = Field(SystemId);
|
||||
}
|
||||
part(summary; "WS Order Summary API")
|
||||
{
|
||||
EntityName = 'orderSummary';
|
||||
Multiplicity = ZeroOrOne;
|
||||
SubPageLink = "Order Id" = Field(SystemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table 50350 "WS Order Line"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Entry No."; Integer)
|
||||
{
|
||||
AutoIncrement = true;
|
||||
}
|
||||
field(2; "Order Id"; Guid)
|
||||
{
|
||||
TableRelation = "Sales Header".SystemId;
|
||||
}
|
||||
field(3; Description; Text[100])
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "Entry No.")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table 50351 "WS Order Summary"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Order Id"; Guid)
|
||||
{
|
||||
TableRelation = "Sales Header".SystemId;
|
||||
}
|
||||
field(2; Summary; Text[100])
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "Order Id")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 50351 "WS Order Line API"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'orderLine';
|
||||
EntitySetName = 'orderLines';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = "WS Order Line";
|
||||
DelayedInsert = true;
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
field(id; Rec.SystemId)
|
||||
{
|
||||
Editable = false;
|
||||
}
|
||||
field(orderId; Rec."Order Id")
|
||||
{
|
||||
}
|
||||
field(description; Rec.Description)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 50352 "WS Order Summary API"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'orderSummary';
|
||||
EntitySetName = 'orderSummaries';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = "WS Order Summary";
|
||||
DelayedInsert = true;
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
field(id; Rec.SystemId)
|
||||
{
|
||||
Editable = false;
|
||||
}
|
||||
field(orderId; Rec."Order Id")
|
||||
{
|
||||
}
|
||||
field(summary; Rec.Summary)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [18..]
|
||||
domain: web-services
|
||||
keywords: [api-page, page-part, subpagelink, systemid, multiplicity, deep-insert, navigation-property]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Link API parts on SystemId and declare their multiplicity
|
||||
|
||||
## Description
|
||||
|
||||
An API page part creates an OData navigation property and, for `Multiplicity = Many`, enables deep insert of child entities. When a custom parent API is keyed by its immutable `SystemId`, its child should carry a related GUID foreign key so the navigation constraint uses that same stable external identity. `Multiplicity` also controls whether metadata exposes an object (`ZeroOrOne`) or a collection (`Many`), so declare it deliberately instead of relying on the default 1:N relationship.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Define the child foreign key as `Guid` with a `TableRelation` to the parent table's `SystemId`, then use `SubPageLink = "<Parent Id>" = Field(SystemId)` on the parent API page. Set `Multiplicity = Many` for child collections and deep insert, or `Multiplicity = ZeroOrOne` for a singleton navigation property.
|
||||
|
||||
See sample: `link-api-parts-on-systemid-and-set-multiplicity.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
On a parent API with `ODataKeyFields = SystemId`, linking a child business field such as `"Order No."` to the parent's `"No."`, or omitting `Multiplicity` because the current default happens to produce a collection. The first creates a second identity scheme for navigation instead of using the contract's stable GUID; the second hides whether the contract intentionally exposes a singleton or collection.
|
||||
|
||||
See sample: `link-api-parts-on-systemid-and-set-multiplicity.bad.al`.
|
||||
|
||||
## Source
|
||||
|
||||
[Developing a custom API](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-develop-custom-api) and [Multiplicity property](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-multiplicity-property).
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
query 50355 "WS Webhook Customer Query"
|
||||
{
|
||||
QueryType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'webhookCustomer';
|
||||
EntitySetName = 'webhookCustomers';
|
||||
|
||||
elements
|
||||
{
|
||||
dataitem(customer; Customer)
|
||||
{
|
||||
column(id; SystemId)
|
||||
{
|
||||
}
|
||||
column(displayName; Name)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
function receiveBusinessCentralWebhook(request, response) {
|
||||
processNotifications(request.body.value);
|
||||
response.sendStatus(200);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
page 50354 "WS Webhook Customer API"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'sales';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'webhookCustomer';
|
||||
EntitySetName = 'webhookCustomers';
|
||||
ODataKeyFields = SystemId;
|
||||
SourceTable = Customer;
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(records)
|
||||
{
|
||||
field(id; Rec.SystemId)
|
||||
{
|
||||
Editable = false;
|
||||
}
|
||||
field(displayName; Rec.Name)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
function receiveBusinessCentralWebhook(request, response) {
|
||||
const validationToken = request.query.validationToken;
|
||||
|
||||
if (typeof validationToken === "string") {
|
||||
response.status(200).type("text/plain").send(validationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
processNotifications(request.body.value);
|
||||
response.sendStatus(200);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: web-services
|
||||
keywords: [webhook, subscription, validationtoken, expirationdatetime, webhook-supported-resources, api-page, sourcetabletemporary, querytype]
|
||||
technologies: [al, javascript]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Verify webhook eligibility and complete every validationToken handshake
|
||||
|
||||
## Description
|
||||
|
||||
Business Central can subscribe only to eligible API pages, not every endpoint that can be read through an API. Webhooks exclude API queries, temporary API pages, pages with composite OData keys, pages over system tables, and pages over Job Queue Entry (table 472); the environment's `webhookSupportedResources` endpoint is authoritative. Creating and renewing a subscription both call the `notificationUrl` with `validationToken`, and both fail unless the subscriber returns that token in the response body with `200 OK`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Before creating a subscription, confirm the resource appears in `webhookSupportedResources` and that a custom endpoint is an API page with a single stable key over an eligible persistent table. Use one validation path that echoes `validationToken` for both create (`POST`) and renew (`PATCH`) handshakes. Track `expirationDateTime` and renew before expiry: online subscriptions expire after three days, while on-premises lifetime defaults to three days and can be changed with `ApiSubscriptionExpiration`.
|
||||
|
||||
See samples: `webhook-eligibility-and-validationtoken-renewal.good.al` and `webhook-eligibility-and-validationtoken-renewal.good.js`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Attempting to subscribe to an API query, temporary/composite/system-table/Job Queue Entry API page, or assuming a successful create handshake makes renewal automatic. Composite includes an explicit multi-field `ODataKeyFields` and a missing `ODataKeyFields` when the source table's primary key has multiple fields. A renewal issues the same validation challenge; a notification handler that ignores the query-string token cannot create or renew the subscription.
|
||||
|
||||
See samples: `webhook-eligibility-and-validationtoken-renewal.bad.al` and `webhook-eligibility-and-validationtoken-renewal.bad.js`.
|
||||
|
||||
## Source
|
||||
|
||||
[Working with webhooks](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-subscriptions) and [Update subscriptions](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_subscriptions_update).
|
||||
Loading…
Add table
Add a link
Reference in a new issue