@wildo-ai/systems-of-record 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +34 -0
- package/dist/esm/microsoft-dataverse.system-of-record.d.ts +3 -0
- package/dist/esm/microsoft-dataverse.system-of-record.d.ts.map +1 -0
- package/dist/esm/microsoft-dataverse.system-of-record.js +124 -0
- package/dist/esm/microsoft-dataverse.system-of-record.js.map +1 -0
- package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.d.ts +3 -0
- package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.d.ts.map +1 -0
- package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.js +94 -0
- package/dist/esm/microsoft-dynamics-365-business-central.system-of-record.js.map +1 -0
- package/dist/esm/netsuite.system-of-record.d.ts +3 -0
- package/dist/esm/netsuite.system-of-record.d.ts.map +1 -0
- package/dist/esm/netsuite.system-of-record.js +77 -0
- package/dist/esm/netsuite.system-of-record.js.map +1 -0
- package/dist/esm/odata.system-of-record.d.ts +3 -0
- package/dist/esm/odata.system-of-record.d.ts.map +1 -0
- package/dist/esm/odata.system-of-record.js +102 -0
- package/dist/esm/odata.system-of-record.js.map +1 -0
- package/dist/esm/odoo.system-of-record.d.ts +3 -0
- package/dist/esm/odoo.system-of-record.d.ts.map +1 -0
- package/dist/esm/odoo.system-of-record.js +122 -0
- package/dist/esm/odoo.system-of-record.js.map +1 -0
- package/dist/esm/salesforce.system-of-record.d.ts +3 -0
- package/dist/esm/salesforce.system-of-record.d.ts.map +1 -0
- package/dist/esm/salesforce.system-of-record.js +80 -0
- package/dist/esm/salesforce.system-of-record.js.map +1 -0
- package/dist/esm/sap-s4hana.system-of-record.d.ts +3 -0
- package/dist/esm/sap-s4hana.system-of-record.d.ts.map +1 -0
- package/dist/esm/sap-s4hana.system-of-record.js +84 -0
- package/dist/esm/sap-s4hana.system-of-record.js.map +1 -0
- package/dist/esm/systems-of-record.exports.d.ts +78 -0
- package/dist/esm/systems-of-record.exports.d.ts.map +1 -0
- package/dist/esm/systems-of-record.exports.js +46 -0
- package/dist/esm/systems-of-record.exports.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Copyright (c) 2026 P2C Consulting, s.à r.l.-S
|
|
2
|
+
RCS Luxembourg B307591
|
|
3
|
+
6C, Porte de France, L-4360 Esch-sur-Alzette, Luxembourg
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
This software and its accompanying materials (the "Software") are the
|
|
8
|
+
confidential and proprietary property of P2C Consulting, s.à r.l.-S (the
|
|
9
|
+
"Licensor"). The Software is made available for viewing only. No licence,
|
|
10
|
+
right, or permission of any kind is granted by its publication or by its
|
|
11
|
+
availability on any package registry or source repository.
|
|
12
|
+
|
|
13
|
+
Without the Licensor's prior written permission, you may not use, copy,
|
|
14
|
+
reproduce, modify, adapt, translate, merge, publish, distribute, sublicense,
|
|
15
|
+
sell, host, or create derivative works of the Software, in whole or in part,
|
|
16
|
+
nor use it to provide a service to any third party.
|
|
17
|
+
|
|
18
|
+
Publication of the Software in any public registry is for distribution to
|
|
19
|
+
authorised recipients and for technical evaluation of its packaging only. It
|
|
20
|
+
does not place the Software in the public domain, does not constitute an open
|
|
21
|
+
source release, and does not waive any right of the Licensor.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
27
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
28
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Third-party components distributed with the Software remain governed by their
|
|
31
|
+
own licences.
|
|
32
|
+
|
|
33
|
+
Contact: P2C Consulting, s.à r.l.-S, 6C, Porte de France, L-4360
|
|
34
|
+
Esch-sur-Alzette, Luxembourg.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-dataverse.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/microsoft-dataverse.system-of-record.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAUrF,eAAO,MAAM,qCAAqC,EAAE,yBA4EnD,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Dataverse (Dynamics 365 Customer Engagement), as a SYSTEM OF RECORD.
|
|
3
|
+
*
|
|
4
|
+
* spec-cite: Microsoft Learn, "Use the Microsoft Dataverse Web API" —
|
|
5
|
+
* https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview
|
|
6
|
+
*
|
|
7
|
+
* ## It pages by CONTINUATION, and that is why this facet exists rather than `odata`
|
|
8
|
+
*
|
|
9
|
+
* The Dataverse Web API does not support `$skip`. The engine's read-through LIST compiles `$top` and
|
|
10
|
+
* `$skip` for every page after the first, so an instance declared against the generic `odata` facet
|
|
11
|
+
* would serve page one and be refused on page two — an application that looks correct in a demo and
|
|
12
|
+
* fails the first time somebody scrolls.
|
|
13
|
+
*
|
|
14
|
+
* spec-cite: Microsoft Learn, "Query data using the Web API" —
|
|
15
|
+
* https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api
|
|
16
|
+
* (that page enumerates the OData query options the Web API does not support).
|
|
17
|
+
*
|
|
18
|
+
* The consequence an author should know before choosing this vendor: page N costs N requests, and
|
|
19
|
+
* the engine refuses past a bound rather than walking indefinitely. A deep collection wants a filter.
|
|
20
|
+
*
|
|
21
|
+
* ## Deterministic order is not decoration here
|
|
22
|
+
*
|
|
23
|
+
* Its paging cookie is built from the LAST ROW of the previous page under the query's own order, and
|
|
24
|
+
* its own documentation is explicit that without a deterministic order the same record can appear on
|
|
25
|
+
* more than one page. The engine appends the binding's key components to `$orderby` under
|
|
26
|
+
* continuation paging for exactly that reason — a duplicated row is harmless, a SKIPPED one is
|
|
27
|
+
* silently absent.
|
|
28
|
+
*
|
|
29
|
+
* spec-cite: Microsoft Learn, "Page results using OData from Microsoft Dataverse Web API" —
|
|
30
|
+
* "Deterministic ordering is important" / "Without proper ordering, the same record can appear in
|
|
31
|
+
* more than one page."
|
|
32
|
+
*
|
|
33
|
+
* ## Why three Microsoft refs exist and only this one is the facet
|
|
34
|
+
*
|
|
35
|
+
* The catalogue holds `microsoft-dataverse`, `microsoft-dynamics-365` and `microsoft-dynamics-crm` —
|
|
36
|
+
* three scraped refs over one Web API. `microsoft-dataverse` is the name the API itself uses, so it
|
|
37
|
+
* is the one an instance names. The other two are deliberately not given facets: a second facet for
|
|
38
|
+
* the same API would be a second answer to "how is this read", free to disagree.
|
|
39
|
+
*
|
|
40
|
+
* ## Partitioning is absent, and that is #1058's
|
|
41
|
+
*
|
|
42
|
+
* Dataverse partitions by business unit (`_owningbusinessunit_value`). The partition vocabulary does
|
|
43
|
+
* not exist yet, and declaring one here would claim a narrowing that does not happen — so until
|
|
44
|
+
* #1058 an application reading this vendor sees every business unit its service identity can see.
|
|
45
|
+
*/
|
|
46
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
47
|
+
import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, SystemOfRecordODataListPaging, } from '@wildo-ai/saas-models/external-data';
|
|
48
|
+
export const MicrosoftDataverseSystemOfRecordFacet = {
|
|
49
|
+
vendor: 'microsoft-dataverse',
|
|
50
|
+
dialects: [HttpApiTransportDialect.ODATA],
|
|
51
|
+
odataListPaging: SystemOfRecordODataListPaging.NEXT_LINK_ONLY,
|
|
52
|
+
/*
|
|
53
|
+
* #1722 — every Dataverse table carries `modifiedon`, and the Web API tracks changes (delta links, deleted entities)
|
|
54
|
+
* for a table that has change tracking enabled. Enabling it is per table, so a pipeline on a table without it is
|
|
55
|
+
* refused at its first delta read, naming the cause.
|
|
56
|
+
* spec-cite: Microsoft Learn, "Use change tracking to synchronize data with external systems"
|
|
57
|
+
* (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-change-tracking-synchronize-data-external-systems),
|
|
58
|
+
* "Retrieve changes for a table using the Web API".
|
|
59
|
+
*/
|
|
60
|
+
incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],
|
|
61
|
+
/*
|
|
62
|
+
* #1722 — Dataverse keeps change tracking for `ExpireChangeTrackingInDays` (seven by default) and says only that "the
|
|
63
|
+
* system throws an exception" past it; its error-code reference names that exception, and it is not the protocol's
|
|
64
|
+
* `410`. Stating the code lets the pass start over with a full read instead of failing every tick. The HTTP status
|
|
65
|
+
* Dataverse pairs it with is not documented, so the code alone decides (UNPROVEN against a live table, #2009).
|
|
66
|
+
* spec-cite: Microsoft Learn, "Web service error codes" (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/web-service-error-codes)
|
|
67
|
+
* — row `0x80044352`, `ExpiredVersionStamp`: "Version stamp associated with the client has expired. Please perform a
|
|
68
|
+
* full sync."
|
|
69
|
+
*/
|
|
70
|
+
odataExpiredChangePositionErrorCodes: ['0x80044352'],
|
|
71
|
+
/*
|
|
72
|
+
* #1726 — every Dataverse row carries an entity tag, and an update or delete sent with it as `If-Match` is refused
|
|
73
|
+
* with `412 Precondition Failed` when the row changed since. So a binding may declare `VERSION_CHECKED` on a field
|
|
74
|
+
* mapped to the tag and get a real optimistic-concurrency check.
|
|
75
|
+
*
|
|
76
|
+
* spec-cite: Microsoft Learn, "Perform conditional operations using the Web API (Microsoft Dataverse)" —
|
|
77
|
+
* https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/perform-conditional-operations-using-web-api
|
|
78
|
+
* ("Dataverse generates a weakly validating `@odata.etag` property for every entity instance", and the two 412
|
|
79
|
+
* examples under "Apply optimistic concurrency").
|
|
80
|
+
*
|
|
81
|
+
* UNCONFIRMED, and stated rather than hidden: Dataverse documents optimistic concurrency as supported "on all
|
|
82
|
+
* out-of-box tables enabled for offline sync and all custom tables" (Microsoft Learn, "Optimistic concurrency"), and
|
|
83
|
+
* the SDK refuses a version-checked update on another table (`OptimisticConcurrencyNotEnabled`). What the Web API
|
|
84
|
+
* answers to an `If-Match` on such a table was not measured; a refusal there reaches the caller as a remote refusal.
|
|
85
|
+
*
|
|
86
|
+
* Dataverse also UPSERTS: a `PATCH` to a key that no longer exists creates the row. The write dialect therefore sends
|
|
87
|
+
* `If-Match` on every update (the row's tag, or `*`), which the same page documents as the way to prevent it.
|
|
88
|
+
*/
|
|
89
|
+
rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
|
|
90
|
+
addressSlots: [
|
|
91
|
+
{
|
|
92
|
+
slug: 'baseUrl',
|
|
93
|
+
required: true,
|
|
94
|
+
description: "This environment's Web API service root, version included "
|
|
95
|
+
+ '(https://<org>.crm<region>.dynamics.com/api/data/v9.2). The organization URL alone is not '
|
|
96
|
+
+ 'enough: entity-set names are appended to the versioned data path.',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
credentialOffers: [
|
|
100
|
+
{
|
|
101
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
|
|
102
|
+
// spec-cite: Microsoft Learn, "Microsoft identity platform and the OAuth 2.0 client credentials
|
|
103
|
+
// flow" — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant
|
|
104
|
+
clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
|
|
105
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
|
|
109
|
+
/*
|
|
110
|
+
* UNCONFIRMED, and left so deliberately: Entra's certificate credential is documented as
|
|
111
|
+
* identifying the signing key by an `x5t` thumbprint in the assertion header, and that is what
|
|
112
|
+
* this declares — but the page was not fetched at authoring time, so it is the plan's
|
|
113
|
+
* expectation rather than a quotation (`external-specification-citations.md` §4). An operator
|
|
114
|
+
* meeting `invalid_client` with a certificate credential should read the vendor's assertion
|
|
115
|
+
* format before assuming the engine is wrong.
|
|
116
|
+
*/
|
|
117
|
+
assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
|
|
118
|
+
assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
|
|
119
|
+
assertionLifetimeSeconds: 300,
|
|
120
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=microsoft-dataverse.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-dataverse.system-of-record.js","sourceRoot":"","sources":["../../../src/microsoft-dataverse.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,qCAAqC,EACrC,oCAAoC,EACpC,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,EAC3C,6BAA6B,GAC9B,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,qCAAqC,GAA8B;IAC9E,MAAM,EAAE,qBAAqB;IAC7B,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC,eAAe,EAAE,6BAA6B,CAAC,cAAc;IAC7D;;;;;;;OAOG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,EAAE,qCAAqC,CAAC,gBAAgB,CAAC;IAC7I;;;;;;;;OAQG;IACH,oCAAoC,EAAE,CAAC,YAAY,CAAC;IACpD;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,4DAA4D;kBAC1D,4FAA4F;kBAC5F,mEAAmE;SACxE;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,gGAAgG;YAChG,iGAAiG;YACjG,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE;;;;;;;eAOG;YACH,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * Microsoft Dataverse (Dynamics 365 Customer Engagement), as a SYSTEM OF RECORD.\n *\n * spec-cite: Microsoft Learn, \"Use the Microsoft Dataverse Web API\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview\n *\n * ## It pages by CONTINUATION, and that is why this facet exists rather than `odata`\n *\n * The Dataverse Web API does not support `$skip`. The engine's read-through LIST compiles `$top` and\n * `$skip` for every page after the first, so an instance declared against the generic `odata` facet\n * would serve page one and be refused on page two — an application that looks correct in a demo and\n * fails the first time somebody scrolls.\n *\n * spec-cite: Microsoft Learn, \"Query data using the Web API\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api\n * (that page enumerates the OData query options the Web API does not support).\n *\n * The consequence an author should know before choosing this vendor: page N costs N requests, and\n * the engine refuses past a bound rather than walking indefinitely. A deep collection wants a filter.\n *\n * ## Deterministic order is not decoration here\n *\n * Its paging cookie is built from the LAST ROW of the previous page under the query's own order, and\n * its own documentation is explicit that without a deterministic order the same record can appear on\n * more than one page. The engine appends the binding's key components to `$orderby` under\n * continuation paging for exactly that reason — a duplicated row is harmless, a SKIPPED one is\n * silently absent.\n *\n * spec-cite: Microsoft Learn, \"Page results using OData from Microsoft Dataverse Web API\" —\n * \"Deterministic ordering is important\" / \"Without proper ordering, the same record can appear in\n * more than one page.\"\n *\n * ## Why three Microsoft refs exist and only this one is the facet\n *\n * The catalogue holds `microsoft-dataverse`, `microsoft-dynamics-365` and `microsoft-dynamics-crm` —\n * three scraped refs over one Web API. `microsoft-dataverse` is the name the API itself uses, so it\n * is the one an instance names. The other two are deliberately not given facets: a second facet for\n * the same API would be a second answer to \"how is this read\", free to disagree.\n *\n * ## Partitioning is absent, and that is #1058's\n *\n * Dataverse partitions by business unit (`_owningbusinessunit_value`). The partition vocabulary does\n * not exist yet, and declaring one here would claim a narrowing that does not happen — so until\n * #1058 an application reading this vendor sees every business unit its service identity can see.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n SystemOfRecordODataListPaging,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const MicrosoftDataverseSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'microsoft-dataverse',\n dialects: [HttpApiTransportDialect.ODATA],\n odataListPaging: SystemOfRecordODataListPaging.NEXT_LINK_ONLY,\n /*\n * #1722 — every Dataverse table carries `modifiedon`, and the Web API tracks changes (delta links, deleted entities)\n * for a table that has change tracking enabled. Enabling it is per table, so a pipeline on a table without it is\n * refused at its first delta read, naming the cause.\n * spec-cite: Microsoft Learn, \"Use change tracking to synchronize data with external systems\"\n * (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-change-tracking-synchronize-data-external-systems),\n * \"Retrieve changes for a table using the Web API\".\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],\n /*\n * #1722 — Dataverse keeps change tracking for `ExpireChangeTrackingInDays` (seven by default) and says only that \"the\n * system throws an exception\" past it; its error-code reference names that exception, and it is not the protocol's\n * `410`. Stating the code lets the pass start over with a full read instead of failing every tick. The HTTP status\n * Dataverse pairs it with is not documented, so the code alone decides (UNPROVEN against a live table, #2009).\n * spec-cite: Microsoft Learn, \"Web service error codes\" (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/web-service-error-codes)\n * — row `0x80044352`, `ExpiredVersionStamp`: \"Version stamp associated with the client has expired. Please perform a\n * full sync.\"\n */\n odataExpiredChangePositionErrorCodes: ['0x80044352'],\n /*\n * #1726 — every Dataverse row carries an entity tag, and an update or delete sent with it as `If-Match` is refused\n * with `412 Precondition Failed` when the row changed since. So a binding may declare `VERSION_CHECKED` on a field\n * mapped to the tag and get a real optimistic-concurrency check.\n *\n * spec-cite: Microsoft Learn, \"Perform conditional operations using the Web API (Microsoft Dataverse)\" —\n * https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/perform-conditional-operations-using-web-api\n * (\"Dataverse generates a weakly validating `@odata.etag` property for every entity instance\", and the two 412\n * examples under \"Apply optimistic concurrency\").\n *\n * UNCONFIRMED, and stated rather than hidden: Dataverse documents optimistic concurrency as supported \"on all\n * out-of-box tables enabled for offline sync and all custom tables\" (Microsoft Learn, \"Optimistic concurrency\"), and\n * the SDK refuses a version-checked update on another table (`OptimisticConcurrencyNotEnabled`). What the Web API\n * answers to an `If-Match` on such a table was not measured; a refusal there reaches the caller as a remote refusal.\n *\n * Dataverse also UPSERTS: a `PATCH` to a key that no longer exists creates the row. The write dialect therefore sends\n * `If-Match` on every update (the row's tag, or `*`), which the same page documents as the way to prevent it.\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This environment's Web API service root, version included \"\n + '(https://<org>.crm<region>.dynamics.com/api/data/v9.2). The organization URL alone is not '\n + 'enough: entity-set names are appended to the versioned data path.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // spec-cite: Microsoft Learn, \"Microsoft identity platform and the OAuth 2.0 client credentials\n // flow\" — https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n /*\n * UNCONFIRMED, and left so deliberately: Entra's certificate credential is documented as\n * identifying the signing key by an `x5t` thumbprint in the assertion header, and that is what\n * this declares — but the page was not fetched at authoring time, so it is the plan's\n * expectation rather than a quotation (`external-specification-citations.md` §4). An operator\n * meeting `invalid_client` with a certificate credential should read the vendor's assertion\n * format before assuming the engine is wrong.\n */\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
|
|
2
|
+
export declare const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet: SystemOfRecordVendorFacet;
|
|
3
|
+
//# sourceMappingURL=microsoft-dynamics-365-business-central.system-of-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-dynamics-365-business-central.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/microsoft-dynamics-365-business-central.system-of-record.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AASrF,eAAO,MAAM,sDAAsD,EAAE,yBA6DpE,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Dynamics 365 Business Central, as a SYSTEM OF RECORD.
|
|
3
|
+
*
|
|
4
|
+
* Business Central's own API is OData v4, and an application reads it as a registered Entra
|
|
5
|
+
* application rather than as a person — which is the whole of D12 for this vendor.
|
|
6
|
+
*
|
|
7
|
+
* spec-cite: Microsoft Learn, "Use OAuth to Authorize Business Central Web Services" —
|
|
8
|
+
* https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth
|
|
9
|
+
*
|
|
10
|
+
* ## Why `baseUrl` reaches down to the environment and not merely the host
|
|
11
|
+
*
|
|
12
|
+
* A Business Central address carries a TENANT and an ENVIRONMENT
|
|
13
|
+
* (`.../v2.0/<tenant>/<environment>/api/v2.0`), and both are deployment facts: production and
|
|
14
|
+
* sandbox differ only in that path segment. Putting the whole service root in one slot is what lets
|
|
15
|
+
* one declaration serve both by pointing at different environments, which is exactly the shape
|
|
16
|
+
* `systemsOfRecord` exists for.
|
|
17
|
+
*
|
|
18
|
+
* ## Its PARTITION is a path segment, and that is why this facet declares none
|
|
19
|
+
*
|
|
20
|
+
* Business Central partitions by COMPANY, and a company is addressed as a path segment
|
|
21
|
+
* (`/companies({id})/customers`) rather than as a filterable field. That is a real difference from
|
|
22
|
+
* every other vendor here and it has no expression yet: the partition vocabulary is #1058's, and
|
|
23
|
+
* that slice supports FIELD partitions first. Declaring something now would be declaring a
|
|
24
|
+
* narrowing that does not happen — which #1055 already refuses for the instance-side `partition`
|
|
25
|
+
* key, for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* So until #1058: an application reading this vendor sees every company its service identity can
|
|
28
|
+
* see. That is stated here rather than discovered, and it is why the address slot below asks for a
|
|
29
|
+
* service root that MAY already name a company.
|
|
30
|
+
*/
|
|
31
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
32
|
+
import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
|
|
33
|
+
export const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet = {
|
|
34
|
+
vendor: 'microsoft-dynamics-365-business-central',
|
|
35
|
+
dialects: [HttpApiTransportDialect.ODATA],
|
|
36
|
+
/*
|
|
37
|
+
* #1722 — MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;
|
|
38
|
+
* whether an entity carries one is the declaration's to say, not the vendor's.
|
|
39
|
+
*/
|
|
40
|
+
incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],
|
|
41
|
+
/*
|
|
42
|
+
* #1726 — Business Central's API entities carry an entity tag, and it REQUIRES `If-Match` on an update: a request
|
|
43
|
+
* whose tag no longer matches is not applied. The write dialect sends the tag of the row it read first (or `*`) on
|
|
44
|
+
* every update and delete, so the requirement is always met.
|
|
45
|
+
*
|
|
46
|
+
* spec-cite: Microsoft Learn, "Update customers - Business Central" —
|
|
47
|
+
* https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_update
|
|
48
|
+
* (request header `If-Match`: "Required. When this request header is included and the eTag provided doesn't match
|
|
49
|
+
* the current tag on the **customers**, the **customers** won't be updated.").
|
|
50
|
+
*
|
|
51
|
+
* Writes and the company PARTITION: a Business Central company is a path segment, and this facet declares no
|
|
52
|
+
* partition (see above) — the vocabulary's PATH_SEGMENT kind is one startup refuses to confine on. A write therefore
|
|
53
|
+
* lands in whatever company its `baseUrl` or `entityRef` path names — the same reach its reads have, and nothing wider.
|
|
54
|
+
*/
|
|
55
|
+
rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
|
|
56
|
+
addressSlots: [
|
|
57
|
+
{
|
|
58
|
+
slug: 'baseUrl',
|
|
59
|
+
required: true,
|
|
60
|
+
description: 'The API service root for THIS environment, tenant and environment included '
|
|
61
|
+
+ '(https://api.businesscentral.dynamics.com/v2.0/<tenant>/<environment>/api/v2.0). It may '
|
|
62
|
+
+ "already name a company (…/companies(<id>)) — until per-company narrowing exists, that is "
|
|
63
|
+
+ 'the only way to confine a deployment to one.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
credentialOffers: [
|
|
67
|
+
{
|
|
68
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
|
|
69
|
+
// Entra's token endpoint accepts both placements; the body form is what its own examples show.
|
|
70
|
+
// spec-cite: Microsoft Learn, "Microsoft identity platform and the OAuth 2.0 client credentials flow" —
|
|
71
|
+
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant
|
|
72
|
+
clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
|
|
73
|
+
// Entra's client-credentials answer carries `expires_in`, so this fallback is a floor that is
|
|
74
|
+
// not expected to be reached rather than a guess at the real lifetime.
|
|
75
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
|
|
79
|
+
/*
|
|
80
|
+
* UNCONFIRMED: Entra's certificate credential is documented as identifying the signing key by
|
|
81
|
+
* an `x5t` thumbprint in the assertion header, and that is what this declares. The page could
|
|
82
|
+
* not be fetched at authoring time, so this is the plan's expectation rather than a quotation
|
|
83
|
+
* — `external-specification-citations.md` §4 rather than an invented citation. An operator who
|
|
84
|
+
* meets `invalid_client` with a certificate credential should read the vendor's assertion
|
|
85
|
+
* format before assuming the engine is wrong.
|
|
86
|
+
*/
|
|
87
|
+
assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
|
|
88
|
+
assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
|
|
89
|
+
assertionLifetimeSeconds: 300,
|
|
90
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=microsoft-dynamics-365-business-central.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-dynamics-365-business-central.system-of-record.js","sourceRoot":"","sources":["../../../src/microsoft-dynamics-365-business-central.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,qCAAqC,EACrC,oCAAoC,EACpC,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,sDAAsD,GAA8B;IAC/F,MAAM,EAAE,yCAAyC;IACjD,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC;;;OAGG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,CAAC;IACrF;;;;;;;;;;;;;OAaG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,6EAA6E;kBAC3E,0FAA0F;kBAC1F,2FAA2F;kBAC3F,8CAA8C;SACnD;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,+FAA+F;YAC/F,wGAAwG;YACxG,yFAAyF;YACzF,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,8FAA8F;YAC9F,uEAAuE;YACvE,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE;;;;;;;eAOG;YACH,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * Microsoft Dynamics 365 Business Central, as a SYSTEM OF RECORD.\n *\n * Business Central's own API is OData v4, and an application reads it as a registered Entra\n * application rather than as a person — which is the whole of D12 for this vendor.\n *\n * spec-cite: Microsoft Learn, \"Use OAuth to Authorize Business Central Web Services\" —\n * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth\n *\n * ## Why `baseUrl` reaches down to the environment and not merely the host\n *\n * A Business Central address carries a TENANT and an ENVIRONMENT\n * (`.../v2.0/<tenant>/<environment>/api/v2.0`), and both are deployment facts: production and\n * sandbox differ only in that path segment. Putting the whole service root in one slot is what lets\n * one declaration serve both by pointing at different environments, which is exactly the shape\n * `systemsOfRecord` exists for.\n *\n * ## Its PARTITION is a path segment, and that is why this facet declares none\n *\n * Business Central partitions by COMPANY, and a company is addressed as a path segment\n * (`/companies({id})/customers`) rather than as a filterable field. That is a real difference from\n * every other vendor here and it has no expression yet: the partition vocabulary is #1058's, and\n * that slice supports FIELD partitions first. Declaring something now would be declaring a\n * narrowing that does not happen — which #1055 already refuses for the instance-side `partition`\n * key, for the same reason.\n *\n * So until #1058: an application reading this vendor sees every company its service identity can\n * see. That is stated here rather than discovered, and it is why the address slot below asks for a\n * service root that MAY already name a company.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const MicrosoftDynamics365BusinessCentralSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'microsoft-dynamics-365-business-central',\n dialects: [HttpApiTransportDialect.ODATA],\n /*\n * #1722 — MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;\n * whether an entity carries one is the declaration's to say, not the vendor's.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],\n /*\n * #1726 — Business Central's API entities carry an entity tag, and it REQUIRES `If-Match` on an update: a request\n * whose tag no longer matches is not applied. The write dialect sends the tag of the row it read first (or `*`) on\n * every update and delete, so the requirement is always met.\n *\n * spec-cite: Microsoft Learn, \"Update customers - Business Central\" —\n * https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_customer_update\n * (request header `If-Match`: \"Required. When this request header is included and the eTag provided doesn't match\n * the current tag on the **customers**, the **customers** won't be updated.\").\n *\n * Writes and the company PARTITION: a Business Central company is a path segment, and this facet declares no\n * partition (see above) — the vocabulary's PATH_SEGMENT kind is one startup refuses to confine on. A write therefore\n * lands in whatever company its `baseUrl` or `entityRef` path names — the same reach its reads have, and nothing wider.\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n 'The API service root for THIS environment, tenant and environment included '\n + '(https://api.businesscentral.dynamics.com/v2.0/<tenant>/<environment>/api/v2.0). It may '\n + \"already name a company (…/companies(<id>)) — until per-company narrowing exists, that is \"\n + 'the only way to confine a deployment to one.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // Entra's token endpoint accepts both placements; the body form is what its own examples show.\n // spec-cite: Microsoft Learn, \"Microsoft identity platform and the OAuth 2.0 client credentials flow\" —\n // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n // Entra's client-credentials answer carries `expires_in`, so this fallback is a floor that is\n // not expected to be reached rather than a guess at the real lifetime.\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n /*\n * UNCONFIRMED: Entra's certificate credential is documented as identifying the signing key by\n * an `x5t` thumbprint in the assertion header, and that is what this declares. The page could\n * not be fetched at authoring time, so this is the plan's expectation rather than a quotation\n * — `external-specification-citations.md` §4 rather than an invented citation. An operator who\n * meets `invalid_client` with a certificate credential should read the vendor's assertion\n * format before assuming the engine is wrong.\n */\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"netsuite.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/netsuite.system-of-record.ts"],"names":[],"mappings":"AAkDA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAMrF,eAAO,MAAM,2BAA2B,EAAE,yBAyBzC,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oracle NetSuite, as a SYSTEM OF RECORD.
|
|
3
|
+
*
|
|
4
|
+
* spec-cite: Oracle NetSuite, "SuiteQL" —
|
|
5
|
+
* https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html
|
|
6
|
+
*
|
|
7
|
+
* ## Read through SuiteQL, posted rather than queried
|
|
8
|
+
*
|
|
9
|
+
* A collection read is a SuiteQL statement in the BODY of a POST to
|
|
10
|
+
* `/services/rest/query/v1/suiteql`, paged by `limit` and `offset` QUERY parameters, and the request
|
|
11
|
+
* requires a `Prefer: transient` header. All three are declared on the binding's operation — the
|
|
12
|
+
* statement, the paging parameters and the static header — which is what #1293 added.
|
|
13
|
+
*
|
|
14
|
+
* The statement-in-a-body shape is why its operation is a POST and Salesforce's is a GET: the
|
|
15
|
+
* request builder places the STATEMENT by method. Paging is the exception — NetSuite reads it from
|
|
16
|
+
* the URL even on a POST, so its placement is declared (below).
|
|
17
|
+
*
|
|
18
|
+
* ## Its app-only identity is CLIENT AUTHENTICATION, not the assertion grant
|
|
19
|
+
*
|
|
20
|
+
* NetSuite's machine-to-machine flow is `grant_type=client_credentials` with a `client_assertion`
|
|
21
|
+
* (RFC 7523 §2.2) — its documentation states the grant type is *always* `client_credentials`. That
|
|
22
|
+
* is client AUTHENTICATION by assertion rather than the assertion GRANT, which is Salesforce's, and
|
|
23
|
+
* the two are separate credential kinds because the token endpoint reads them differently.
|
|
24
|
+
*
|
|
25
|
+
* spec-cite: Oracle NetSuite, "OAuth 2.0 Client Credentials Flow" —
|
|
26
|
+
* https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162755359851.html
|
|
27
|
+
*
|
|
28
|
+
* The assertion's header carries the key id NetSuite assigns when the certificate is uploaded, which
|
|
29
|
+
* is why this facet declares the key-id policy and the instance supplies `certificateKeyId`.
|
|
30
|
+
*
|
|
31
|
+
* ## What an author gives up, stated rather than discovered
|
|
32
|
+
*
|
|
33
|
+
* No declared `sortFields` — a LIST serves the statement's own order, and the ORDER BY belongs in
|
|
34
|
+
* the SuiteQL. Its paging parameters go in the URL while the statement is in the body, so the
|
|
35
|
+
* binding declares `pagingParams.placement: 'query-string'` (#1686); placed by method, a POST's
|
|
36
|
+
* `limit` would land in the body and bound nothing.
|
|
37
|
+
*
|
|
38
|
+
* A pipeline walks a declared keyset (#1686), never offsets: the binding's `list` statement declares
|
|
39
|
+
* `keyset: { keyRemoteField: 'id' }`, and each page is `… WHERE id > <last id> ORDER BY id ASC` with
|
|
40
|
+
* `limit=<n>&offset=0`, so the 100,000-result offset ceiling is never reached.
|
|
41
|
+
*
|
|
42
|
+
* ## The account id is in the ADDRESS, and so is the token endpoint
|
|
43
|
+
*
|
|
44
|
+
* Both the REST base URL and the token URL are account-specific
|
|
45
|
+
* (`https://<accountId>.suitetalk.api.netsuite.com/…`), so neither can be a constant in this file.
|
|
46
|
+
* They are separate slots because they are separate hosts' worth of a decision: a sandbox account
|
|
47
|
+
* changes both, and an operator who changed only one would get a working token against the wrong
|
|
48
|
+
* data.
|
|
49
|
+
*/
|
|
50
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
51
|
+
import { SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, } from '@wildo-ai/saas-models/external-data';
|
|
52
|
+
export const NetsuiteSystemOfRecordFacet = {
|
|
53
|
+
vendor: 'netsuite',
|
|
54
|
+
dialects: [HttpApiTransportDialect.REST_JSON],
|
|
55
|
+
addressSlots: [
|
|
56
|
+
{
|
|
57
|
+
slug: 'baseUrl',
|
|
58
|
+
required: true,
|
|
59
|
+
description: "This account's REST services host (https://<accountId>.suitetalk.api.netsuite.com). A "
|
|
60
|
+
+ "binding's path templates are appended to it — the SuiteQL endpoint is one of them.",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
credentialOffers: [
|
|
64
|
+
{
|
|
65
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
|
|
66
|
+
// NetSuite locates the signing key by the id it assigned at certificate upload, carried as
|
|
67
|
+
// `kid` in the assertion's protected header. A thumbprint would identify nothing to it.
|
|
68
|
+
assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.KEY_ID_SUPPLIED,
|
|
69
|
+
assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
|
|
70
|
+
assertionLifetimeSeconds: 300,
|
|
71
|
+
// Its token answer states `expires_in` is always 3600, so this fallback is a floor that should
|
|
72
|
+
// never be reached rather than a guess at the real lifetime.
|
|
73
|
+
tokenLifetimeFallbackSeconds: 3600,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=netsuite.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"netsuite.system-of-record.js","sourceRoot":"","sources":["../../../src/netsuite.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,mCAAmC,EACnC,uCAAuC,GACxC,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,2BAA2B,GAA8B;IACpE,MAAM,EAAE,UAAU;IAClB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC;IAC7C,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,wFAAwF;kBACtF,oFAAoF;SACzF;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE,2FAA2F;YAC3F,wFAAwF;YACxF,qBAAqB,EAAE,mCAAmC,CAAC,eAAe;YAC1E,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,+FAA+F;YAC/F,6DAA6D;YAC7D,4BAA4B,EAAE,IAAI;SACnC;KACF;CACF,CAAC","sourcesContent":["/**\n * Oracle NetSuite, as a SYSTEM OF RECORD.\n *\n * spec-cite: Oracle NetSuite, \"SuiteQL\" —\n * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html\n *\n * ## Read through SuiteQL, posted rather than queried\n *\n * A collection read is a SuiteQL statement in the BODY of a POST to\n * `/services/rest/query/v1/suiteql`, paged by `limit` and `offset` QUERY parameters, and the request\n * requires a `Prefer: transient` header. All three are declared on the binding's operation — the\n * statement, the paging parameters and the static header — which is what #1293 added.\n *\n * The statement-in-a-body shape is why its operation is a POST and Salesforce's is a GET: the\n * request builder places the STATEMENT by method. Paging is the exception — NetSuite reads it from\n * the URL even on a POST, so its placement is declared (below).\n *\n * ## Its app-only identity is CLIENT AUTHENTICATION, not the assertion grant\n *\n * NetSuite's machine-to-machine flow is `grant_type=client_credentials` with a `client_assertion`\n * (RFC 7523 §2.2) — its documentation states the grant type is *always* `client_credentials`. That\n * is client AUTHENTICATION by assertion rather than the assertion GRANT, which is Salesforce's, and\n * the two are separate credential kinds because the token endpoint reads them differently.\n *\n * spec-cite: Oracle NetSuite, \"OAuth 2.0 Client Credentials Flow\" —\n * https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162755359851.html\n *\n * The assertion's header carries the key id NetSuite assigns when the certificate is uploaded, which\n * is why this facet declares the key-id policy and the instance supplies `certificateKeyId`.\n *\n * ## What an author gives up, stated rather than discovered\n *\n * No declared `sortFields` — a LIST serves the statement's own order, and the ORDER BY belongs in\n * the SuiteQL. Its paging parameters go in the URL while the statement is in the body, so the\n * binding declares `pagingParams.placement: 'query-string'` (#1686); placed by method, a POST's\n * `limit` would land in the body and bound nothing.\n *\n * A pipeline walks a declared keyset (#1686), never offsets: the binding's `list` statement declares\n * `keyset: { keyRemoteField: 'id' }`, and each page is `… WHERE id > <last id> ORDER BY id ASC` with\n * `limit=<n>&offset=0`, so the 100,000-result offset ceiling is never reached.\n *\n * ## The account id is in the ADDRESS, and so is the token endpoint\n *\n * Both the REST base URL and the token URL are account-specific\n * (`https://<accountId>.suitetalk.api.netsuite.com/…`), so neither can be a constant in this file.\n * They are separate slots because they are separate hosts' worth of a decision: a sandbox account\n * changes both, and an operator who changed only one would get a working token against the wrong\n * data.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const NetsuiteSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'netsuite',\n dialects: [HttpApiTransportDialect.REST_JSON],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This account's REST services host (https://<accountId>.suitetalk.api.netsuite.com). A \"\n + \"binding's path templates are appended to it — the SuiteQL endpoint is one of them.\",\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n // NetSuite locates the signing key by the id it assigned at certificate upload, carried as\n // `kid` in the assertion's protected header. A thumbprint would identify nothing to it.\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.KEY_ID_SUPPLIED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n // Its token answer states `expires_in` is always 3600, so this fallback is a floor that should\n // never be reached rather than a guess at the real lifetime.\n tokenLifetimeFallbackSeconds: 3600,\n },\n ],\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odata.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/odata.system-of-record.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAQrF,eAAO,MAAM,wBAAwB,EAAE,yBAsEtC,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GENERIC OData v4 facet — for a system of record this catalogue has never heard of.
|
|
3
|
+
*
|
|
4
|
+
* Every other facet beside this one names a vendor and encodes what that vendor is known to do.
|
|
5
|
+
* This one names none, and that is its purpose: an enterprise's own OData v4 service — a bespoke
|
|
6
|
+
* middleware, an internal ERP surface, a vendor nobody catalogued — is the ordinary case for an
|
|
7
|
+
* internal tool, and without this facet such a deployment cannot be declared a system of record at
|
|
8
|
+
* all.
|
|
9
|
+
*
|
|
10
|
+
* ## What it can and cannot know
|
|
11
|
+
*
|
|
12
|
+
* It declares the widest credential set, because it has no vendor to narrow it to; the INSTANCE
|
|
13
|
+
* chooses one kind, and boot refuses a kind this list does not offer. It declares OData v4 only,
|
|
14
|
+
* because that is the one dialect its name asserts. It declares no partition field, because a
|
|
15
|
+
* generic service has none to declare.
|
|
16
|
+
*
|
|
17
|
+
* The consequence an author should know: naming `odata` gives up every vendor-specific correction —
|
|
18
|
+
* the paging mode, the header a key travels in, the query options a Web API refuses. A vendor with
|
|
19
|
+
* its own facet is always the better answer when one exists.
|
|
20
|
+
*
|
|
21
|
+
* ## `$skip` paging is assumed, and that is a real limitation
|
|
22
|
+
*
|
|
23
|
+
* The read-through LIST compiles `$top`/`$skip` (`odata-query-compiler.backend.ts`). OData v4
|
|
24
|
+
* defines both, so a conformant service serves them — but at least one large implementation
|
|
25
|
+
* (Dataverse) refuses `$skip` outright, which is why that vendor needs its own facet rather than
|
|
26
|
+
* this one.
|
|
27
|
+
* spec-cite: OData Version 4.01 Part 1: Protocol §11.2.6 ($top and $skip system query options).
|
|
28
|
+
*/
|
|
29
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
30
|
+
import { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';
|
|
31
|
+
import { SystemOfRecordAssertionHeaderPolicy, SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
|
|
32
|
+
export const OdataSystemOfRecordFacet = {
|
|
33
|
+
vendor: 'odata',
|
|
34
|
+
dialects: [HttpApiTransportDialect.ODATA],
|
|
35
|
+
/*
|
|
36
|
+
* #1722 — both protocol mechanisms, because the generic facet names no vendor and can narrow nothing: a modified-at
|
|
37
|
+
* filter is plain OData, and change tracking is an optional protocol feature (`Prefer: track-changes`) a service
|
|
38
|
+
* may or may not offer. A service that does not track changes answers the first delta read without a delta link,
|
|
39
|
+
* and that pass is refused by name.
|
|
40
|
+
*/
|
|
41
|
+
incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],
|
|
42
|
+
/*
|
|
43
|
+
* #1678 — actions are part of the OData v4 PROTOCOL rather than of any one vendor, so the generic facet lists both
|
|
44
|
+
* invocation forms. Whether a given service exposes a given action is the declaring binding's to know; the engine
|
|
45
|
+
* only guarantees it calls it the way the protocol defines.
|
|
46
|
+
*
|
|
47
|
+
* spec-cite: OData Version 4.01 Part 1: Protocol §11.5.5.1 (Invoking an Action).
|
|
48
|
+
*/
|
|
49
|
+
actionCallShapes: [SystemOfRecordActionCallShape.ODATA_BOUND_ACTION, SystemOfRecordActionCallShape.ODATA_UNBOUND_ACTION],
|
|
50
|
+
/*
|
|
51
|
+
* #1726 — entity tags are part of the OData v4 PROTOCOL: each entity has one that "MUST change when structural
|
|
52
|
+
* properties or links from that entity have changed", and a write carrying it as `If-Match` is refused with 412 when it
|
|
53
|
+
* no longer matches. So the generic facet names the tag as its row version, which is what lets a binding declare
|
|
54
|
+
* `writeConcurrency: VERSION_CHECKED` and have the SERVICE refuse a stale update.
|
|
55
|
+
*
|
|
56
|
+
* Whether a given service serves a tag for a given entity set is the declaring binding's to know, exactly as for an
|
|
57
|
+
* action. A row served without one is refused by name (`WRITE_ROW_VERSION_NOT_SERVED`) rather than compared with
|
|
58
|
+
* nothing, and a write to such a row still carries `If-Match: *`, which forbids an upsert re-creating it.
|
|
59
|
+
*
|
|
60
|
+
* spec-cite: OData Version 4.01 Part 1: Protocol §11.4.1.1 (Use of ETags for Avoiding Update Conflicts) and §8.2.4
|
|
61
|
+
* (Header If-Match).
|
|
62
|
+
*/
|
|
63
|
+
rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,
|
|
64
|
+
addressSlots: [
|
|
65
|
+
{
|
|
66
|
+
slug: 'baseUrl',
|
|
67
|
+
required: true,
|
|
68
|
+
description: "The OData SERVICE ROOT, not the host — the URL an entity-set name is appended to "
|
|
69
|
+
+ '(e.g. https://erp.example/odata/v4). Everything this facet compiles is relative to it.',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
credentialOffers: [
|
|
73
|
+
// Bearer is the common shape for a generic service, so it leads. `valuePrefix` carries its own
|
|
74
|
+
// trailing space: the prefix is concatenated verbatim, which is what lets a vendor that wants a
|
|
75
|
+
// bare key declare none at all.
|
|
76
|
+
{ kind: SystemOfRecordCredentialKind.API_KEY_HEADER, headerName: 'Authorization', valuePrefix: 'Bearer ' },
|
|
77
|
+
{ kind: SystemOfRecordCredentialKind.BASIC },
|
|
78
|
+
{
|
|
79
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
|
|
80
|
+
// RFC 6749 §2.3.1 says a server MUST support the Basic header and MAY support the body form,
|
|
81
|
+
// so the header is the one a generic facet can rely on.
|
|
82
|
+
// spec-cite: RFC 6749 §2.3.1 (Client Password).
|
|
83
|
+
clientAuthentication: SystemOfRecordClientAuthenticationPlacement.BASIC_HEADER,
|
|
84
|
+
// RFC 6749 §4.2.2 makes `expires_in` RECOMMENDED rather than required, so a generic facet
|
|
85
|
+
// needs a fallback. Fifteen minutes: short enough that a server issuing something shorter is
|
|
86
|
+
// re-minted before it dies, long enough not to mint on every read.
|
|
87
|
+
// spec-cite: RFC 6749 §4.2.2 (Access Token Response).
|
|
88
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,
|
|
92
|
+
// A generic service cannot be assumed to issue key ids, so the thumbprint policy — derivable
|
|
93
|
+
// from the certificate the operator already supplies — is the one that needs nothing from the
|
|
94
|
+
// vendor.
|
|
95
|
+
assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,
|
|
96
|
+
assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
|
|
97
|
+
assertionLifetimeSeconds: 300,
|
|
98
|
+
tokenLifetimeFallbackSeconds: 900,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=odata.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odata.system-of-record.js","sourceRoot":"","sources":["../../../src/odata.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,qCAAqC,EAAE,oCAAoC,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACjK,OAAO,EACL,mCAAmC,EACnC,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,wBAAwB,GAA8B;IACjE,MAAM,EAAE,OAAO;IACf,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC;IACzC;;;;;OAKG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,EAAE,qCAAqC,CAAC,gBAAgB,CAAC;IAC7I;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,6BAA6B,CAAC,kBAAkB,EAAE,6BAA6B,CAAC,oBAAoB,CAAC;IACxH;;;;;;;;;;;;OAYG;IACH,eAAe,EAAE,oCAAoC;IACrD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,mFAAmF;kBACjF,wFAAwF;SAC7F;KACF;IACD,gBAAgB,EAAE;QAChB,+FAA+F;QAC/F,gGAAgG;QAChG,gCAAgC;QAChC,EAAE,IAAI,EAAE,4BAA4B,CAAC,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE;QAC1G,EAAE,IAAI,EAAE,4BAA4B,CAAC,KAAK,EAAE;QAC5C;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,6FAA6F;YAC7F,wDAAwD;YACxD,gDAAgD;YAChD,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E,0FAA0F;YAC1F,6FAA6F;YAC7F,mEAAmE;YACnE,sDAAsD;YACtD,4BAA4B,EAAE,GAAG;SAClC;QACD;YACE,IAAI,EAAE,4BAA4B,CAAC,qCAAqC;YACxE,6FAA6F;YAC7F,8FAA8F;YAC9F,UAAU;YACV,qBAAqB,EAAE,mCAAmC,CAAC,uBAAuB;YAClF,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,GAAG;SAClC;KACF;CACF,CAAC","sourcesContent":["/**\n * The GENERIC OData v4 facet — for a system of record this catalogue has never heard of.\n *\n * Every other facet beside this one names a vendor and encodes what that vendor is known to do.\n * This one names none, and that is its purpose: an enterprise's own OData v4 service — a bespoke\n * middleware, an internal ERP surface, a vendor nobody catalogued — is the ordinary case for an\n * internal tool, and without this facet such a deployment cannot be declared a system of record at\n * all.\n *\n * ## What it can and cannot know\n *\n * It declares the widest credential set, because it has no vendor to narrow it to; the INSTANCE\n * chooses one kind, and boot refuses a kind this list does not offer. It declares OData v4 only,\n * because that is the one dialect its name asserts. It declares no partition field, because a\n * generic service has none to declare.\n *\n * The consequence an author should know: naming `odata` gives up every vendor-specific correction —\n * the paging mode, the header a key travels in, the query options a Web API refuses. A vendor with\n * its own facet is always the better answer when one exists.\n *\n * ## `$skip` paging is assumed, and that is a real limitation\n *\n * The read-through LIST compiles `$top`/`$skip` (`odata-query-compiler.backend.ts`). OData v4\n * defines both, so a conformant service serves them — but at least one large implementation\n * (Dataverse) refuses `$skip` outright, which is why that vendor needs its own facet rather than\n * this one.\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.2.6 ($top and $skip system query options).\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { ExternalDataIncrementalExtractionMode, ODATA_ENTITY_TAG_CONTROL_INFORMATION, SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionHeaderPolicy,\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const OdataSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'odata',\n dialects: [HttpApiTransportDialect.ODATA],\n /*\n * #1722 — both protocol mechanisms, because the generic facet names no vendor and can narrow nothing: a modified-at\n * filter is plain OData, and change tracking is an optional protocol feature (`Prefer: track-changes`) a service\n * may or may not offer. A service that does not track changes answers the first delta read without a delta link,\n * and that pass is refused by name.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT, ExternalDataIncrementalExtractionMode.ODATA_DELTA_LINK],\n /*\n * #1678 — actions are part of the OData v4 PROTOCOL rather than of any one vendor, so the generic facet lists both\n * invocation forms. Whether a given service exposes a given action is the declaring binding's to know; the engine\n * only guarantees it calls it the way the protocol defines.\n *\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.5.5.1 (Invoking an Action).\n */\n actionCallShapes: [SystemOfRecordActionCallShape.ODATA_BOUND_ACTION, SystemOfRecordActionCallShape.ODATA_UNBOUND_ACTION],\n /*\n * #1726 — entity tags are part of the OData v4 PROTOCOL: each entity has one that \"MUST change when structural\n * properties or links from that entity have changed\", and a write carrying it as `If-Match` is refused with 412 when it\n * no longer matches. So the generic facet names the tag as its row version, which is what lets a binding declare\n * `writeConcurrency: VERSION_CHECKED` and have the SERVICE refuse a stale update.\n *\n * Whether a given service serves a tag for a given entity set is the declaring binding's to know, exactly as for an\n * action. A row served without one is refused by name (`WRITE_ROW_VERSION_NOT_SERVED`) rather than compared with\n * nothing, and a write to such a row still carries `If-Match: *`, which forbids an upsert re-creating it.\n *\n * spec-cite: OData Version 4.01 Part 1: Protocol §11.4.1.1 (Use of ETags for Avoiding Update Conflicts) and §8.2.4\n * (Header If-Match).\n */\n rowVersionField: ODATA_ENTITY_TAG_CONTROL_INFORMATION,\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"The OData SERVICE ROOT, not the host — the URL an entity-set name is appended to \"\n + '(e.g. https://erp.example/odata/v4). Everything this facet compiles is relative to it.',\n },\n ],\n credentialOffers: [\n // Bearer is the common shape for a generic service, so it leads. `valuePrefix` carries its own\n // trailing space: the prefix is concatenated verbatim, which is what lets a vendor that wants a\n // bare key declare none at all.\n { kind: SystemOfRecordCredentialKind.API_KEY_HEADER, headerName: 'Authorization', valuePrefix: 'Bearer ' },\n { kind: SystemOfRecordCredentialKind.BASIC },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n // RFC 6749 §2.3.1 says a server MUST support the Basic header and MAY support the body form,\n // so the header is the one a generic facet can rely on.\n // spec-cite: RFC 6749 §2.3.1 (Client Password).\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.BASIC_HEADER,\n // RFC 6749 §4.2.2 makes `expires_in` RECOMMENDED rather than required, so a generic facet\n // needs a fallback. Fifteen minutes: short enough that a server issuing something shorter is\n // re-minted before it dies, long enough not to mint on every read.\n // spec-cite: RFC 6749 §4.2.2 (Access Token Response).\n tokenLifetimeFallbackSeconds: 900,\n },\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_CERTIFICATE,\n // A generic service cannot be assumed to issue key ids, so the thumbprint policy — derivable\n // from the certificate the operator already supplies — is the one that needs nothing from the\n // vendor.\n assertionHeaderPolicy: SystemOfRecordAssertionHeaderPolicy.X509_THUMBPRINT_DERIVED,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 300,\n tokenLifetimeFallbackSeconds: 900,\n },\n ],\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odoo.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/odoo.system-of-record.ts"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AASrF,eAAO,MAAM,uBAAuB,EAAE,yBA+DrC,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Odoo, as a SYSTEM OF RECORD — moved here from the engine (#1057 E9).
|
|
3
|
+
*
|
|
4
|
+
* ## Why it moved
|
|
5
|
+
*
|
|
6
|
+
* #1055 shipped this facet inside `engine/external-connectors-private` by decision, with its own
|
|
7
|
+
* docstring calling the arrangement temporary and the engine's facet map one that "should shrink to
|
|
8
|
+
* empty". A vendor facet is a PACKAGE's to own: the engine has no business knowing what an Odoo
|
|
9
|
+
* needs, any more than it knows what a Salesforce needs, and an engine-resident facet is a vendor
|
|
10
|
+
* every application carries whether or not it declared one.
|
|
11
|
+
*
|
|
12
|
+
* ## Why it is AUTHORED rather than generated
|
|
13
|
+
*
|
|
14
|
+
* Odoo has no scraped source — the catalogue record has an `odoo` entry that produced nothing, and
|
|
15
|
+
* the transposer excludes engine-owned refs by construction. More fundamentally, nothing a scrape
|
|
16
|
+
* yields could produce this file: a scraped source gives a base URL and an auth kind, and never
|
|
17
|
+
* "JSON-RPC over `/jsonrpc`, credentials inside the RPC arguments". Dialect and credential kind are
|
|
18
|
+
* always authored, for every vendor.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the slots are what they are
|
|
21
|
+
*
|
|
22
|
+
* `baseUrl` is registration-scoped and never credential material: each enterprise runs its own
|
|
23
|
+
* Odoo, so the URL comes from the deployment environment.
|
|
24
|
+
*
|
|
25
|
+
* `database` and `login` are VENDOR address slots even though `ODOO_API_KEY_IN_ARGS` consumes them:
|
|
26
|
+
* they identify WHICH Odoo database and WHICH user, which stays true under any other credential
|
|
27
|
+
* kind Odoo might later offer. Only `apiKey` belongs to the kind, and it is declared there.
|
|
28
|
+
*
|
|
29
|
+
* ## Its PARTITION is `company_id`, and every part of the declaration was measured
|
|
30
|
+
*
|
|
31
|
+
* Odoo holds several COMPANIES inside one database, and `company_id` is on the row. This is the
|
|
32
|
+
* FIRST vendor facet to declare a partition: #1058 built the whole engine-side
|
|
33
|
+
* vocabulary and no facet had ever used it, so `partition: true` on an instance was refused by
|
|
34
|
+
* `PARTITION_NOT_SUPPORTED_BY_VENDOR` for every vendor we ship. The two neighbouring facets that
|
|
35
|
+
* mention partitioning (Dataverse, Business Central) say only that the vocabulary did not exist
|
|
36
|
+
* when they were written; Business Central's remains true for a different reason, its partition
|
|
37
|
+
* being a PATH SEGMENT this engine deliberately cannot confine.
|
|
38
|
+
*
|
|
39
|
+
* Measured 2026-09-22 against a REAL Odoo (a 20-company instance), which is what settles the two
|
|
40
|
+
* fields a reader would otherwise have to guess at:
|
|
41
|
+
*
|
|
42
|
+
* - `valueShape` is `MANY2ONE_PAIR` because a many2one reads back as `[1, "Odoo S.A."]`, never as
|
|
43
|
+
* a bare id. A reader taking the whole value matches nothing; one coercing it to a scalar gets
|
|
44
|
+
* `"1,Odoo S.A."`. The id is the first element.
|
|
45
|
+
* - `codec` is `INTEGER` because the domain is compared by id and Odoo is STRICT about the type:
|
|
46
|
+
* `["company_id", "in", [1]]` matches, and `["company_id", "in", ["1"]]` matches NOTHING and
|
|
47
|
+
* raises no error. A string key would therefore confine a member to an empty result set while
|
|
48
|
+
* looking like a correct narrowing — silence being the dangerous half.
|
|
49
|
+
*
|
|
50
|
+
* spec-cite: N/A — both bullets are this repository's own measurement against a live instance, not
|
|
51
|
+
* a quotation. Odoo's External API documentation states the many2one read shape; it does not state
|
|
52
|
+
* the domain's type strictness, which is why that half was probed rather than cited.
|
|
53
|
+
*
|
|
54
|
+
* An unset `company_id` reads back as `false` (Odoo's spelling of "no value"), which the engine
|
|
55
|
+
* already reads as the SHARED row that belongs to every partition
|
|
56
|
+
* (`http-api-partition-confinement.backend.ts:266-273`).
|
|
57
|
+
*/
|
|
58
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
59
|
+
import { SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';
|
|
60
|
+
import { ExternalDataIncrementalExtractionMode, HttpApiKeyComponentCodec, SystemOfRecordPartitionKind, SystemOfRecordPartitionValueShape, } from '@wildo-ai/saas-models/external-data';
|
|
61
|
+
export const OdooSystemOfRecordFacet = {
|
|
62
|
+
vendor: 'odoo',
|
|
63
|
+
dialects: [HttpApiTransportDialect.ODOO_JSONRPC],
|
|
64
|
+
addressSlots: [
|
|
65
|
+
{
|
|
66
|
+
slug: 'baseUrl',
|
|
67
|
+
required: true,
|
|
68
|
+
description: "This instance's Odoo URL (e.g. https://mycompany.odoo.com). Deployment-targeted: each "
|
|
69
|
+
+ 'environment names ITS Odoo, and there is no fixed SaaS host to fall back on.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
slug: 'database',
|
|
73
|
+
required: true,
|
|
74
|
+
description: 'The Odoo database name on that instance — the first positional argument of every '
|
|
75
|
+
+ '`execute_kw` call, and part of the `common.authenticate` handshake.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
slug: 'login',
|
|
79
|
+
required: true,
|
|
80
|
+
description: 'The Odoo user login the API key belongs to. `common.authenticate` resolves it to the uid '
|
|
81
|
+
+ 'that every `execute_kw` call then acts as, so it decides what the application can read.',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
credentialOffers: [{ kind: SystemOfRecordCredentialKind.ODOO_API_KEY_IN_ARGS }],
|
|
85
|
+
/*
|
|
86
|
+
* #1678 — Odoo's external API calls ANY public model method through `execute_kw`, so a declared action
|
|
87
|
+
* (`account.move` `action_post`, `sale.order` `action_confirm`, a model-level `create`) is one call shape.
|
|
88
|
+
* Odoo offers no idempotency mechanism for such a call, so no Odoo action is ever retry-safe.
|
|
89
|
+
*
|
|
90
|
+
* spec-cite: Odoo 18.0 developer documentation, External API — "Calling methods".
|
|
91
|
+
*/
|
|
92
|
+
actionCallShapes: [SystemOfRecordActionCallShape.ODOO_EXECUTE_KW],
|
|
93
|
+
partition: {
|
|
94
|
+
kind: SystemOfRecordPartitionKind.FIELD,
|
|
95
|
+
remoteField: 'company_id',
|
|
96
|
+
codec: HttpApiKeyComponentCodec.INTEGER,
|
|
97
|
+
valueShape: SystemOfRecordPartitionValueShape.MANY2ONE_PAIR,
|
|
98
|
+
},
|
|
99
|
+
/*
|
|
100
|
+
* #1682 — every Odoo model that logs access carries `write_date` ("Last Updated on"), maintained by
|
|
101
|
+
* the ORM on every write, so a pipeline can read what changed since a watermark. A model declared
|
|
102
|
+
* `_log_access = False` has no such column; a pipeline over one is refused at its first incremental
|
|
103
|
+
* page, which names the missing field, rather than here — this facet describes the vendor, not
|
|
104
|
+
* each of its models.
|
|
105
|
+
*/
|
|
106
|
+
incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODOO_WRITE_DATE],
|
|
107
|
+
/*
|
|
108
|
+
* #1724 — the same ORM-maintained `write_date` is the row's VERSION: every `write` stamps it, and no caller can
|
|
109
|
+
* forget to. It is what a `writeConcurrency: VERSION_CHECKED` binding compares before sending an update.
|
|
110
|
+
*
|
|
111
|
+
* Best-effort, and stated where the vendor is described: Odoo 17's `BaseModel.write` takes no precondition, so the
|
|
112
|
+
* engine can only read the version and then write — two calls, with a race between them. Odoo also SERVES the value
|
|
113
|
+
* cut to the second (`Datetime.to_string`) while storing microseconds, so two changes inside one second read as one
|
|
114
|
+
* version. The check narrows the lost-update window; it never closes it.
|
|
115
|
+
*
|
|
116
|
+
* spec-cite: Odoo 17.0 `odoo/models.py` — `BaseModel.write(self, vals)` ("Updates all records in ``self`` with the
|
|
117
|
+
* provided values", no version argument) and `_write` (`vals.setdefault('write_date', self.env.cr.now())`);
|
|
118
|
+
* `odoo/fields.py` `Datetime.to_string` for the served precision (https://github.com/odoo/odoo/tree/17.0/odoo).
|
|
119
|
+
*/
|
|
120
|
+
rowVersionField: 'write_date',
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=odoo.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odoo.system-of-record.js","sourceRoot":"","sources":["../../../src/odoo.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EACL,qCAAqC,EACrC,wBAAwB,EACxB,2BAA2B,EAC3B,iCAAiC,GAClC,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,uBAAuB,GAA8B;IAChE,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,CAAC,uBAAuB,CAAC,YAAY,CAAC;IAChD,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,wFAAwF;kBACtF,8EAA8E;SACnF;QACD;YACE,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,mFAAmF;kBACjF,qEAAqE;SAC1E;QACD;YACE,IAAI,EAAE,OAAO;YACb,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,2FAA2F;kBACzF,yFAAyF;SAC9F;KACF;IACD,gBAAgB,EAAE,CAAC,EAAE,IAAI,EAAE,4BAA4B,CAAC,oBAAoB,EAAE,CAAC;IAC/E;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,6BAA6B,CAAC,eAAe,CAAC;IACjE,SAAS,EAAE;QACT,IAAI,EAAE,2BAA2B,CAAC,KAAK;QACvC,WAAW,EAAE,YAAY;QACzB,KAAK,EAAE,wBAAwB,CAAC,OAAO;QACvC,UAAU,EAAE,iCAAiC,CAAC,aAAa;KAC5D;IACD;;;;;;OAMG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,eAAe,CAAC;IACnF;;;;;;;;;;;;OAYG;IACH,eAAe,EAAE,YAAY;CAC9B,CAAC","sourcesContent":["/**\n * Odoo, as a SYSTEM OF RECORD — moved here from the engine (#1057 E9).\n *\n * ## Why it moved\n *\n * #1055 shipped this facet inside `engine/external-connectors-private` by decision, with its own\n * docstring calling the arrangement temporary and the engine's facet map one that \"should shrink to\n * empty\". A vendor facet is a PACKAGE's to own: the engine has no business knowing what an Odoo\n * needs, any more than it knows what a Salesforce needs, and an engine-resident facet is a vendor\n * every application carries whether or not it declared one.\n *\n * ## Why it is AUTHORED rather than generated\n *\n * Odoo has no scraped source — the catalogue record has an `odoo` entry that produced nothing, and\n * the transposer excludes engine-owned refs by construction. More fundamentally, nothing a scrape\n * yields could produce this file: a scraped source gives a base URL and an auth kind, and never\n * \"JSON-RPC over `/jsonrpc`, credentials inside the RPC arguments\". Dialect and credential kind are\n * always authored, for every vendor.\n *\n * ## Why the slots are what they are\n *\n * `baseUrl` is registration-scoped and never credential material: each enterprise runs its own\n * Odoo, so the URL comes from the deployment environment.\n *\n * `database` and `login` are VENDOR address slots even though `ODOO_API_KEY_IN_ARGS` consumes them:\n * they identify WHICH Odoo database and WHICH user, which stays true under any other credential\n * kind Odoo might later offer. Only `apiKey` belongs to the kind, and it is declared there.\n *\n * ## Its PARTITION is `company_id`, and every part of the declaration was measured\n *\n * Odoo holds several COMPANIES inside one database, and `company_id` is on the row. This is the\n * FIRST vendor facet to declare a partition: #1058 built the whole engine-side\n * vocabulary and no facet had ever used it, so `partition: true` on an instance was refused by\n * `PARTITION_NOT_SUPPORTED_BY_VENDOR` for every vendor we ship. The two neighbouring facets that\n * mention partitioning (Dataverse, Business Central) say only that the vocabulary did not exist\n * when they were written; Business Central's remains true for a different reason, its partition\n * being a PATH SEGMENT this engine deliberately cannot confine.\n *\n * Measured 2026-09-22 against a REAL Odoo (a 20-company instance), which is what settles the two\n * fields a reader would otherwise have to guess at:\n *\n * - `valueShape` is `MANY2ONE_PAIR` because a many2one reads back as `[1, \"Odoo S.A.\"]`, never as\n * a bare id. A reader taking the whole value matches nothing; one coercing it to a scalar gets\n * `\"1,Odoo S.A.\"`. The id is the first element.\n * - `codec` is `INTEGER` because the domain is compared by id and Odoo is STRICT about the type:\n * `[\"company_id\", \"in\", [1]]` matches, and `[\"company_id\", \"in\", [\"1\"]]` matches NOTHING and\n * raises no error. A string key would therefore confine a member to an empty result set while\n * looking like a correct narrowing — silence being the dangerous half.\n *\n * spec-cite: N/A — both bullets are this repository's own measurement against a live instance, not\n * a quotation. Odoo's External API documentation states the many2one read shape; it does not state\n * the domain's type strictness, which is why that half was probed rather than cited.\n *\n * An unset `company_id` reads back as `false` (Odoo's spelling of \"no value\"), which the engine\n * already reads as the SHARED row that belongs to every partition\n * (`http-api-partition-confinement.backend.ts:266-273`).\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { SystemOfRecordActionCallShape } from '@wildo-ai/saas-models/external-data';\nimport {\n ExternalDataIncrementalExtractionMode,\n HttpApiKeyComponentCodec,\n SystemOfRecordPartitionKind,\n SystemOfRecordPartitionValueShape,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const OdooSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'odoo',\n dialects: [HttpApiTransportDialect.ODOO_JSONRPC],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This instance's Odoo URL (e.g. https://mycompany.odoo.com). Deployment-targeted: each \"\n + 'environment names ITS Odoo, and there is no fixed SaaS host to fall back on.',\n },\n {\n slug: 'database',\n required: true,\n description:\n 'The Odoo database name on that instance — the first positional argument of every '\n + '`execute_kw` call, and part of the `common.authenticate` handshake.',\n },\n {\n slug: 'login',\n required: true,\n description:\n 'The Odoo user login the API key belongs to. `common.authenticate` resolves it to the uid '\n + 'that every `execute_kw` call then acts as, so it decides what the application can read.',\n },\n ],\n credentialOffers: [{ kind: SystemOfRecordCredentialKind.ODOO_API_KEY_IN_ARGS }],\n /*\n * #1678 — Odoo's external API calls ANY public model method through `execute_kw`, so a declared action\n * (`account.move` `action_post`, `sale.order` `action_confirm`, a model-level `create`) is one call shape.\n * Odoo offers no idempotency mechanism for such a call, so no Odoo action is ever retry-safe.\n *\n * spec-cite: Odoo 18.0 developer documentation, External API — \"Calling methods\".\n */\n actionCallShapes: [SystemOfRecordActionCallShape.ODOO_EXECUTE_KW],\n partition: {\n kind: SystemOfRecordPartitionKind.FIELD,\n remoteField: 'company_id',\n codec: HttpApiKeyComponentCodec.INTEGER,\n valueShape: SystemOfRecordPartitionValueShape.MANY2ONE_PAIR,\n },\n /*\n * #1682 — every Odoo model that logs access carries `write_date` (\"Last Updated on\"), maintained by\n * the ORM on every write, so a pipeline can read what changed since a watermark. A model declared\n * `_log_access = False` has no such column; a pipeline over one is refused at its first incremental\n * page, which names the missing field, rather than here — this facet describes the vendor, not\n * each of its models.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODOO_WRITE_DATE],\n /*\n * #1724 — the same ORM-maintained `write_date` is the row's VERSION: every `write` stamps it, and no caller can\n * forget to. It is what a `writeConcurrency: VERSION_CHECKED` binding compares before sending an update.\n *\n * Best-effort, and stated where the vendor is described: Odoo 17's `BaseModel.write` takes no precondition, so the\n * engine can only read the version and then write — two calls, with a race between them. Odoo also SERVES the value\n * cut to the second (`Datetime.to_string`) while storing microseconds, so two changes inside one second read as one\n * version. The check narrows the lost-update window; it never closes it.\n *\n * spec-cite: Odoo 17.0 `odoo/models.py` — `BaseModel.write(self, vals)` (\"Updates all records in ``self`` with the\n * provided values\", no version argument) and `_write` (`vals.setdefault('write_date', self.env.cr.now())`);\n * `odoo/fields.py` `Datetime.to_string` for the served precision (https://github.com/odoo/odoo/tree/17.0/odoo).\n */\n rowVersionField: 'write_date',\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"salesforce.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/salesforce.system-of-record.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAMrF,eAAO,MAAM,6BAA6B,EAAE,yBA0C3C,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Salesforce, as a SYSTEM OF RECORD.
|
|
3
|
+
*
|
|
4
|
+
* spec-cite: Salesforce Developers, "REST API Developer Guide" —
|
|
5
|
+
* https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/
|
|
6
|
+
*
|
|
7
|
+
* ## Read through SOQL, which is why this is REST_JSON and not a dialect
|
|
8
|
+
*
|
|
9
|
+
* A collection read is a SOQL statement at `/services/data/vXX.X/query?q=…`, and a page is followed
|
|
10
|
+
* by the answer's own `nextRecordsUrl`. Neither is expressible as a set of filter parameters, which
|
|
11
|
+
* is what #1293 added the statement declaration for. A BINDING supplies the object and its fields;
|
|
12
|
+
* this facet says the vendor speaks REST_JSON and how its service identity authenticates.
|
|
13
|
+
*
|
|
14
|
+
* spec-cite: Salesforce Developers, "SOQL and SOSL Reference" —
|
|
15
|
+
* https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/
|
|
16
|
+
*
|
|
17
|
+
* ## What an author gives up by choosing it, stated rather than discovered
|
|
18
|
+
*
|
|
19
|
+
* **Pipelines walk a declared keyset (#1686).** A Salesforce object feeds a pipeline when the
|
|
20
|
+
* binding's `list` statement declares `keyset: { keyRemoteField: 'Id' }` and its `cursorPath`
|
|
21
|
+
* (`nextRecordsUrl`): the engine reads `… WHERE Id > <last Id> ORDER BY Id ASC LIMIT <n>`, follows
|
|
22
|
+
* the answer's batches to the end of each page, and persists the last Id — never the query locator.
|
|
23
|
+
* Without a keyset the object is read-through only: offset paging is refused for pipelines.
|
|
24
|
+
*
|
|
25
|
+
* **No declared sort.** Resource registration refuses `sortFields` on a REST_JSON binding, so a LIST
|
|
26
|
+
* serves the statement's own order. A binding may still order inside its SOQL, which is the honest
|
|
27
|
+
* place for it — the ordering belongs to the query, not to an advertised capability.
|
|
28
|
+
*
|
|
29
|
+
* ## The instance URL is an ADDRESS, not an account fact
|
|
30
|
+
*
|
|
31
|
+
* An OAuth answer carries `instance_url`, and it is deliberately not read: a system of record is a
|
|
32
|
+
* deployment the operator names in this environment, and taking its address from a token response
|
|
33
|
+
* would make the address a property of the credential. Sandbox and production differ by that URL
|
|
34
|
+
* alone, which is exactly what an address slot is for.
|
|
35
|
+
*/
|
|
36
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
37
|
+
import { SystemOfRecordAssertionSigningAlgorithm, SystemOfRecordClientAuthenticationPlacement, } from '@wildo-ai/saas-models/external-data';
|
|
38
|
+
export const SalesforceSystemOfRecordFacet = {
|
|
39
|
+
vendor: 'salesforce',
|
|
40
|
+
dialects: [HttpApiTransportDialect.REST_JSON],
|
|
41
|
+
addressSlots: [
|
|
42
|
+
{
|
|
43
|
+
slug: 'baseUrl',
|
|
44
|
+
required: true,
|
|
45
|
+
description: "This org's instance URL (https://<myDomain>.my.salesforce.com). A binding's path templates "
|
|
46
|
+
+ 'are appended to it, so it carries no API version — the version belongs to the operation.',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
credentialOffers: [
|
|
50
|
+
{
|
|
51
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,
|
|
52
|
+
clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,
|
|
53
|
+
/*
|
|
54
|
+
* R3's case, and the reason the fallback lives on the OFFER. Salesforce's client-credentials
|
|
55
|
+
* answer is reported to omit `expires_in` while NetSuite's always carries it — two vendors of
|
|
56
|
+
* ONE kind, so a per-kind engine constant would have to be wrong for one of them.
|
|
57
|
+
*
|
|
58
|
+
* Two hours: a Salesforce session lives far longer, and this is the interval after which the
|
|
59
|
+
* engine re-mints rather than a claim about the token's real life. Erring short costs a token
|
|
60
|
+
* request; erring long costs every read until the token dies.
|
|
61
|
+
*/
|
|
62
|
+
tokenLifetimeFallbackSeconds: 7200,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
/*
|
|
66
|
+
* The assertion GRANT (RFC 7523 §2.1), which among the vendors surveyed is Salesforce alone —
|
|
67
|
+
* NetSuite uses the same assertion for client AUTHENTICATION under `client_credentials`. The
|
|
68
|
+
* two differ in one form field and are two credential kinds for exactly that reason.
|
|
69
|
+
*
|
|
70
|
+
* Its `subject` is the integration user the assertion is made FOR, which is why that slot
|
|
71
|
+
* becomes required under this kind and under no other.
|
|
72
|
+
*/
|
|
73
|
+
kind: SystemOfRecordCredentialKind.OAUTH2_JWT_BEARER,
|
|
74
|
+
assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,
|
|
75
|
+
assertionLifetimeSeconds: 180,
|
|
76
|
+
tokenLifetimeFallbackSeconds: 7200,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=salesforce.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"salesforce.system-of-record.js","sourceRoot":"","sources":["../../../src/salesforce.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,qCAAqC,CAAC;AAE7C,MAAM,CAAC,MAAM,6BAA6B,GAA8B;IACtE,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,SAAS,CAAC;IAC7C,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,6FAA6F;kBAC3F,0FAA0F;SAC/F;KACF;IACD,gBAAgB,EAAE;QAChB;YACE,IAAI,EAAE,4BAA4B,CAAC,gCAAgC;YACnE,oBAAoB,EAAE,2CAA2C,CAAC,YAAY;YAC9E;;;;;;;;eAQG;YACH,4BAA4B,EAAE,IAAI;SACnC;QACD;YACE;;;;;;;eAOG;YACH,IAAI,EAAE,4BAA4B,CAAC,iBAAiB;YACpD,yBAAyB,EAAE,uCAAuC,CAAC,KAAK;YACxE,wBAAwB,EAAE,GAAG;YAC7B,4BAA4B,EAAE,IAAI;SACnC;KACF;CACF,CAAC","sourcesContent":["/**\n * Salesforce, as a SYSTEM OF RECORD.\n *\n * spec-cite: Salesforce Developers, \"REST API Developer Guide\" —\n * https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/\n *\n * ## Read through SOQL, which is why this is REST_JSON and not a dialect\n *\n * A collection read is a SOQL statement at `/services/data/vXX.X/query?q=…`, and a page is followed\n * by the answer's own `nextRecordsUrl`. Neither is expressible as a set of filter parameters, which\n * is what #1293 added the statement declaration for. A BINDING supplies the object and its fields;\n * this facet says the vendor speaks REST_JSON and how its service identity authenticates.\n *\n * spec-cite: Salesforce Developers, \"SOQL and SOSL Reference\" —\n * https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/\n *\n * ## What an author gives up by choosing it, stated rather than discovered\n *\n * **Pipelines walk a declared keyset (#1686).** A Salesforce object feeds a pipeline when the\n * binding's `list` statement declares `keyset: { keyRemoteField: 'Id' }` and its `cursorPath`\n * (`nextRecordsUrl`): the engine reads `… WHERE Id > <last Id> ORDER BY Id ASC LIMIT <n>`, follows\n * the answer's batches to the end of each page, and persists the last Id — never the query locator.\n * Without a keyset the object is read-through only: offset paging is refused for pipelines.\n *\n * **No declared sort.** Resource registration refuses `sortFields` on a REST_JSON binding, so a LIST\n * serves the statement's own order. A binding may still order inside its SOQL, which is the honest\n * place for it — the ordering belongs to the query, not to an advertised capability.\n *\n * ## The instance URL is an ADDRESS, not an account fact\n *\n * An OAuth answer carries `instance_url`, and it is deliberately not read: a system of record is a\n * deployment the operator names in this environment, and taking its address from a token response\n * would make the address a property of the credential. Sandbox and production differ by that URL\n * alone, which is exactly what an address slot is for.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport {\n SystemOfRecordAssertionSigningAlgorithm,\n SystemOfRecordClientAuthenticationPlacement,\n} from '@wildo-ai/saas-models/external-data';\n\nexport const SalesforceSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'salesforce',\n dialects: [HttpApiTransportDialect.REST_JSON],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n \"This org's instance URL (https://<myDomain>.my.salesforce.com). A binding's path templates \"\n + 'are appended to it, so it carries no API version — the version belongs to the operation.',\n },\n ],\n credentialOffers: [\n {\n kind: SystemOfRecordCredentialKind.OAUTH2_CLIENT_CREDENTIALS_SECRET,\n clientAuthentication: SystemOfRecordClientAuthenticationPlacement.REQUEST_BODY,\n /*\n * R3's case, and the reason the fallback lives on the OFFER. Salesforce's client-credentials\n * answer is reported to omit `expires_in` while NetSuite's always carries it — two vendors of\n * ONE kind, so a per-kind engine constant would have to be wrong for one of them.\n *\n * Two hours: a Salesforce session lives far longer, and this is the interval after which the\n * engine re-mints rather than a claim about the token's real life. Erring short costs a token\n * request; erring long costs every read until the token dies.\n */\n tokenLifetimeFallbackSeconds: 7200,\n },\n {\n /*\n * The assertion GRANT (RFC 7523 §2.1), which among the vendors surveyed is Salesforce alone —\n * NetSuite uses the same assertion for client AUTHENTICATION under `client_credentials`. The\n * two differ in one form field and are two credential kinds for exactly that reason.\n *\n * Its `subject` is the integration user the assertion is made FOR, which is why that slot\n * becomes required under this kind and under no other.\n */\n kind: SystemOfRecordCredentialKind.OAUTH2_JWT_BEARER,\n assertionSigningAlgorithm: SystemOfRecordAssertionSigningAlgorithm.RS256,\n assertionLifetimeSeconds: 180,\n tokenLifetimeFallbackSeconds: 7200,\n },\n ],\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sap-s4hana.system-of-record.d.ts","sourceRoot":"","sources":["../../../src/sap-s4hana.system-of-record.ts"],"names":[],"mappings":"AAsDA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAGrF,eAAO,MAAM,4BAA4B,EAAE,yBA4B1C,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAP S/4HANA, as a SYSTEM OF RECORD.
|
|
3
|
+
*
|
|
4
|
+
* Named `sap-s4hana` rather than `sap-s4hana-cloud`, and that is a measured choice rather than a
|
|
5
|
+
* preference: `sap-s4hana` already exists as a catalogue-record ref and is the `providerRef` three
|
|
6
|
+
* OData fixtures in `saas-models` already use. A second spelling would make the same vendor two
|
|
7
|
+
* vendors, which is what `mergedCatalogueEntries` exists to prevent.
|
|
8
|
+
*
|
|
9
|
+
* ## Only BASIC, and the honest reason
|
|
10
|
+
*
|
|
11
|
+
* S/4HANA's app-only identity is a COMMUNICATION USER, and SAP's own guidance prefers an X.509
|
|
12
|
+
* client certificate over a password for one. That preference cannot be honoured here: presenting a
|
|
13
|
+
* client certificate is a TRANSPORT credential, and the outbound seam carries no channel for one
|
|
14
|
+
* (#1263). So this facet offers the password half only, and an operator whose communication
|
|
15
|
+
* arrangement is certificate-based cannot use it yet.
|
|
16
|
+
*
|
|
17
|
+
* Stated rather than omitted, because the two failure modes read identically from outside: a vendor
|
|
18
|
+
* this facet cannot serve and a vendor nobody has authored look the same to an author holding a
|
|
19
|
+
* refusal.
|
|
20
|
+
*
|
|
21
|
+
* spec-cite: N/A — SAP's certificate preference for communication users is recorded as UNCONFIRMED
|
|
22
|
+
* in epic #1049 §4.6 and is deliberately not quoted here; what is asserted above is only what this
|
|
23
|
+
* facet does, which is a fact about this file.
|
|
24
|
+
*
|
|
25
|
+
* ## OData v2 and v4, one instance per service root (#1687)
|
|
26
|
+
*
|
|
27
|
+
* S/4HANA's most-used API surface (`API_BUSINESS_PARTNER`) is OData **v2**, published beside newer v4
|
|
28
|
+
* services, so this facet speaks both: an instance declares `dialect: ODATA_V2` for a classic Gateway root
|
|
29
|
+
* (`/sap/opu/odata/sap/<SERVICE>`) and `ODATA` for a v4 root (`/sap/opu/odata4/…`). The binding's dialect
|
|
30
|
+
* must match its instance's. A system reached over both is two instances, one per root, each with its own
|
|
31
|
+
* `maxRequestsPerMinute` — the reasoning is at the declaration's `dialect` field.
|
|
32
|
+
*
|
|
33
|
+
* A v2 root declared as `ODATA` is still refused BY NAME at the first read, because #1055 forbids a
|
|
34
|
+
* boot-time probe of the remote and a path heuristic could only ever warn.
|
|
35
|
+
*
|
|
36
|
+
* A v2 instance READS only: the engine takes each property's type from the service's `$metadata`, decodes
|
|
37
|
+
* `/Date(…)/`, decimals and 64-bit integers from their string forms, and a pipeline walks the entity's key
|
|
38
|
+
* rather than `__next` (v2 promises no stability across a partial listing).
|
|
39
|
+
*
|
|
40
|
+
* ## Writes are REFUSED at startup: SAP requires a CSRF-token handshake the engine does not perform (#1726)
|
|
41
|
+
*
|
|
42
|
+
* SAP Gateway protects every modifying request with a token: the client first sends a non-modifying request carrying
|
|
43
|
+
* `X-CSRF-Token: Fetch`, then sends the returned token and the session cookie with the write. The engine's read client
|
|
44
|
+
* keeps no cookie jar and fetches no token, so every S/4HANA write would be answered `403` — hence
|
|
45
|
+
* `writePrerequisites`, and a binding opening writes against this vendor is refused at startup
|
|
46
|
+
* (`WRITE_PREREQUISITE_NOT_SUPPORTED`) instead of failing at its first request.
|
|
47
|
+
* spec-cite: SAP Help Portal, SAP Gateway Foundation — "Cross-Site Request Forgery Protection"
|
|
48
|
+
* (https://help.sap.com/saphelp_gateway20sp12/helpdata/en/e6/cae27d5e8d4996add4067280c8714e/frameset.htm).
|
|
49
|
+
*
|
|
50
|
+
* No row version is declared either: SAP's OData v4 services may serve entity tags per entity type, and nobody here
|
|
51
|
+
* has read that for a service this facet would reach. Both are for whoever performs the handshake; proof against a real
|
|
52
|
+
* service is #1474's.
|
|
53
|
+
*/
|
|
54
|
+
import { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';
|
|
55
|
+
import { ExternalDataIncrementalExtractionMode, SystemOfRecordWritePrerequisite } from '@wildo-ai/saas-models/external-data';
|
|
56
|
+
export const SapS4hanaSystemOfRecordFacet = {
|
|
57
|
+
vendor: 'sap-s4hana',
|
|
58
|
+
dialects: [HttpApiTransportDialect.ODATA, HttpApiTransportDialect.ODATA_V2],
|
|
59
|
+
/*
|
|
60
|
+
* #1722 — v4 roots only (the mode is spoken over ODATA). MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;
|
|
61
|
+
* whether an entity carries one is the declaration's to say, not the vendor's.
|
|
62
|
+
*/
|
|
63
|
+
incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],
|
|
64
|
+
// #1726 — see "Writes are REFUSED at startup" above.
|
|
65
|
+
writePrerequisites: [SystemOfRecordWritePrerequisite.CSRF_TOKEN_HANDSHAKE],
|
|
66
|
+
addressSlots: [
|
|
67
|
+
{
|
|
68
|
+
slug: 'baseUrl',
|
|
69
|
+
required: true,
|
|
70
|
+
description: 'The OData service root for this instance: https://<host>/sap/opu/odata4/sap/<service>/… for a v4 service '
|
|
71
|
+
+ '(declare dialect ODATA), or https://<host>/sap/opu/odata/sap/<SERVICE> for a classic v2 Gateway service '
|
|
72
|
+
+ '(declare dialect ODATA_V2). One root per instance; a system serving both is two instances.',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
credentialOffers: [
|
|
76
|
+
/*
|
|
77
|
+
* The communication user's password. Its USERNAME is an address slot rather than a secret — it
|
|
78
|
+
* names WHICH user rather than proving anything — and it may not contain `:`, which RFC 7617
|
|
79
|
+
* makes the delimiter; boot refuses one that does.
|
|
80
|
+
*/
|
|
81
|
+
{ kind: SystemOfRecordCredentialKind.BASIC },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=sap-s4hana.system-of-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sap-s4hana.system-of-record.js","sourceRoot":"","sources":["../../../src/sap-s4hana.system-of-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EAAE,qCAAqC,EAAE,+BAA+B,EAAE,MAAM,qCAAqC,CAAC;AAE7H,MAAM,CAAC,MAAM,4BAA4B,GAA8B;IACrE,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,CAAC,uBAAuB,CAAC,KAAK,EAAE,uBAAuB,CAAC,QAAQ,CAAC;IAC3E;;;OAGG;IACH,0BAA0B,EAAE,CAAC,qCAAqC,CAAC,iBAAiB,CAAC;IACrF,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,+BAA+B,CAAC,oBAAoB,CAAC;IAC1E,YAAY,EAAE;QACZ;YACE,IAAI,EAAE,SAAS;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EACT,2GAA2G;kBACzG,0GAA0G;kBAC1G,4FAA4F;SACjG;KACF;IACD,gBAAgB,EAAE;QAChB;;;;WAIG;QACH,EAAE,IAAI,EAAE,4BAA4B,CAAC,KAAK,EAAE;KAC7C;CACF,CAAC","sourcesContent":["/**\n * SAP S/4HANA, as a SYSTEM OF RECORD.\n *\n * Named `sap-s4hana` rather than `sap-s4hana-cloud`, and that is a measured choice rather than a\n * preference: `sap-s4hana` already exists as a catalogue-record ref and is the `providerRef` three\n * OData fixtures in `saas-models` already use. A second spelling would make the same vendor two\n * vendors, which is what `mergedCatalogueEntries` exists to prevent.\n *\n * ## Only BASIC, and the honest reason\n *\n * S/4HANA's app-only identity is a COMMUNICATION USER, and SAP's own guidance prefers an X.509\n * client certificate over a password for one. That preference cannot be honoured here: presenting a\n * client certificate is a TRANSPORT credential, and the outbound seam carries no channel for one\n * (#1263). So this facet offers the password half only, and an operator whose communication\n * arrangement is certificate-based cannot use it yet.\n *\n * Stated rather than omitted, because the two failure modes read identically from outside: a vendor\n * this facet cannot serve and a vendor nobody has authored look the same to an author holding a\n * refusal.\n *\n * spec-cite: N/A — SAP's certificate preference for communication users is recorded as UNCONFIRMED\n * in epic #1049 §4.6 and is deliberately not quoted here; what is asserted above is only what this\n * facet does, which is a fact about this file.\n *\n * ## OData v2 and v4, one instance per service root (#1687)\n *\n * S/4HANA's most-used API surface (`API_BUSINESS_PARTNER`) is OData **v2**, published beside newer v4\n * services, so this facet speaks both: an instance declares `dialect: ODATA_V2` for a classic Gateway root\n * (`/sap/opu/odata/sap/<SERVICE>`) and `ODATA` for a v4 root (`/sap/opu/odata4/…`). The binding's dialect\n * must match its instance's. A system reached over both is two instances, one per root, each with its own\n * `maxRequestsPerMinute` — the reasoning is at the declaration's `dialect` field.\n *\n * A v2 root declared as `ODATA` is still refused BY NAME at the first read, because #1055 forbids a\n * boot-time probe of the remote and a path heuristic could only ever warn.\n *\n * A v2 instance READS only: the engine takes each property's type from the service's `$metadata`, decodes\n * `/Date(…)/`, decimals and 64-bit integers from their string forms, and a pipeline walks the entity's key\n * rather than `__next` (v2 promises no stability across a partial listing).\n *\n * ## Writes are REFUSED at startup: SAP requires a CSRF-token handshake the engine does not perform (#1726)\n *\n * SAP Gateway protects every modifying request with a token: the client first sends a non-modifying request carrying\n * `X-CSRF-Token: Fetch`, then sends the returned token and the session cookie with the write. The engine's read client\n * keeps no cookie jar and fetches no token, so every S/4HANA write would be answered `403` — hence\n * `writePrerequisites`, and a binding opening writes against this vendor is refused at startup\n * (`WRITE_PREREQUISITE_NOT_SUPPORTED`) instead of failing at its first request.\n * spec-cite: SAP Help Portal, SAP Gateway Foundation — \"Cross-Site Request Forgery Protection\"\n * (https://help.sap.com/saphelp_gateway20sp12/helpdata/en/e6/cae27d5e8d4996add4067280c8714e/frameset.htm).\n *\n * No row version is declared either: SAP's OData v4 services may serve entity tags per entity type, and nobody here\n * has read that for a service this facet would reach. Both are for whoever performs the handshake; proof against a real\n * service is #1474's.\n */\nimport { HttpApiTransportDialect, SystemOfRecordCredentialKind } from '@wildo-ai/saas-models';\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\nimport { ExternalDataIncrementalExtractionMode, SystemOfRecordWritePrerequisite } from '@wildo-ai/saas-models/external-data';\n\nexport const SapS4hanaSystemOfRecordFacet: SystemOfRecordVendorFacet = {\n vendor: 'sap-s4hana',\n dialects: [HttpApiTransportDialect.ODATA, HttpApiTransportDialect.ODATA_V2],\n /*\n * #1722 — v4 roots only (the mode is spoken over ODATA). MODIFIED_AT is a plain OData `$filter … ge` on a timestamp property the pipeline declaration names per entity;\n * whether an entity carries one is the declaration's to say, not the vendor's.\n */\n incrementalExtractionModes: [ExternalDataIncrementalExtractionMode.ODATA_MODIFIED_AT],\n // #1726 — see \"Writes are REFUSED at startup\" above.\n writePrerequisites: [SystemOfRecordWritePrerequisite.CSRF_TOKEN_HANDSHAKE],\n addressSlots: [\n {\n slug: 'baseUrl',\n required: true,\n description:\n 'The OData service root for this instance: https://<host>/sap/opu/odata4/sap/<service>/… for a v4 service '\n + '(declare dialect ODATA), or https://<host>/sap/opu/odata/sap/<SERVICE> for a classic v2 Gateway service '\n + '(declare dialect ODATA_V2). One root per instance; a system serving both is two instances.',\n },\n ],\n credentialOffers: [\n /*\n * The communication user's password. Its USERNAME is an address slot rather than a secret — it\n * names WHICH user rather than proving anything — and it may not contain `:`, which RFC 7617\n * makes the delimiter; boot refuses one that does.\n */\n { kind: SystemOfRecordCredentialKind.BASIC },\n ],\n};\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's SYSTEM-OF-RECORD FACETS — the public entrypoint behind
|
|
3
|
+
* `@wildo-ai/systems-of-record/systems-of-record` (#1057, moved into its own library by #2013).
|
|
4
|
+
*
|
|
5
|
+
* ## Why a library of its own
|
|
6
|
+
*
|
|
7
|
+
* These facets were born inside the transposed provider catalogue (now archived under `_code_archive/providers/`),
|
|
8
|
+
* under an underscore folder that its regeneration skipped. That kept them alive and nothing more: a system of
|
|
9
|
+
* record is not a provider an application CALLS, it is a remote system that is the authoritative store
|
|
10
|
+
* of some of the application's own resources — declared under `systemsOfRecord`, reached through its
|
|
11
|
+
* instance's service identity only, and never copied into an application (#1049). Paul decided on
|
|
12
|
+
* 2026-09-29 that the lane is delivered as a classical library, and the catalogue was archived
|
|
13
|
+
* (#2012, #2019), so the facets moved here.
|
|
14
|
+
*
|
|
15
|
+
* It is an OPT-IN framework package (`optInFrameworkPackages`, #1836): an application links it only by
|
|
16
|
+
* declaring it, and `wildo config sync` declares it on the backend the moment `systemsOfRecord` is
|
|
17
|
+
* non-empty. An application that declares no system of record never receives it.
|
|
18
|
+
*
|
|
19
|
+
* Discovery admits this package's `./systems-of-record` by its exact NAME; any other facet module is
|
|
20
|
+
* admitted only when an instance opts into it through `facetAugmentations`. The engine code that reads
|
|
21
|
+
* a facet (dialects, pipelines, actions) stays in `@wildo-ai/saas-backend-lib`.
|
|
22
|
+
*
|
|
23
|
+
* ## Why ONE subpath rather than one per vendor
|
|
24
|
+
*
|
|
25
|
+
* The first plan called for a subpath per vendor, `./<ref>/facet`. It does not survive contact with
|
|
26
|
+
* what a facet IS: every field that makes a facet a facet — the dialect, the credential kinds, the
|
|
27
|
+
* partition — is AUTHORED vendor knowledge, and a facet is inert data with no transitive graph. One
|
|
28
|
+
* module holding a record of seven object literals is the whole of it.
|
|
29
|
+
*
|
|
30
|
+
* ## Why a flat record rather than a loader map
|
|
31
|
+
*
|
|
32
|
+
* The engine's map — the one this replaces — used thunks (`vendor -> () => import(...)`) so nothing
|
|
33
|
+
* was evaluated until a vendor was asked for. That cost is real when the map sits in an engine
|
|
34
|
+
* everybody loads. Here it does not apply: this module is reached only when an application declared a
|
|
35
|
+
* system of record at all.
|
|
36
|
+
*
|
|
37
|
+
* ## The export NAMES are the facet-module contract
|
|
38
|
+
*
|
|
39
|
+
* `PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS` and its siblings keep the names they had in the
|
|
40
|
+
* catalogue because they are what `SYSTEM_OF_RECORD_FACET_RECORD_EXPORT` (saas-models) names, and every
|
|
41
|
+
* augmentation package exports its record under the matching `…_FACET_AUGMENTATIONS`. Renaming them is a
|
|
42
|
+
* contract change for every facet module, not a tidy-up of this file.
|
|
43
|
+
*
|
|
44
|
+
* ## Adding one
|
|
45
|
+
*
|
|
46
|
+
* Author `<vendor>.system-of-record.ts` beside this file and add its row below. The key and the
|
|
47
|
+
* facet's own `vendor` must agree; `systems-of-record.exports.test.ts` asserts it, because a
|
|
48
|
+
* hand-written key beside a declared id is exactly the shape that drifts.
|
|
49
|
+
*/
|
|
50
|
+
import type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';
|
|
51
|
+
/**
|
|
52
|
+
* Every vendor the framework can serve as a system of record, keyed by the id an instance names.
|
|
53
|
+
*
|
|
54
|
+
* Far fewer vendors than there are providers, because being reachable over HTTP is not the same as
|
|
55
|
+
* being a deployment an application reads its own rows out of. A vendor absent from here is not
|
|
56
|
+
* refused by this record — it is a vendor the framework does not describe; the generic `odata` facet
|
|
57
|
+
* serves any OData v4 service.
|
|
58
|
+
*/
|
|
59
|
+
export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS: Readonly<Record<string, SystemOfRecordVendorFacet>>;
|
|
60
|
+
/**
|
|
61
|
+
* Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.
|
|
62
|
+
*
|
|
63
|
+
* Not misspellings. Each of these is a real provider id, and an author naming one has made a
|
|
64
|
+
* reasonable mistake: the provider catalogue held `microsoft-dataverse`,
|
|
65
|
+
* `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they
|
|
66
|
+
* are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says
|
|
67
|
+
* "no facet" without saying which sibling has it leaves the author to guess between the other two.
|
|
68
|
+
*
|
|
69
|
+
* Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same
|
|
70
|
+
* product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.
|
|
71
|
+
*
|
|
72
|
+
* Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,
|
|
73
|
+
* because an alias pointing at a vendor that has no facet sends the author from one refusal to another.
|
|
74
|
+
*/
|
|
75
|
+
export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES: Readonly<Record<string, string>>;
|
|
76
|
+
/** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */
|
|
77
|
+
export declare const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS: readonly string[];
|
|
78
|
+
//# sourceMappingURL=systems-of-record.exports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systems-of-record.exports.d.ts","sourceRoot":"","sources":["../../../src/systems-of-record.exports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAUrF;;;;;;;GAOG;AACH,eAAO,MAAM,0CAA0C,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAQ1G,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kDAAkD,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/F,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,2CAA2C,EAAE,SAAS,MAAM,EACT,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MicrosoftDataverseSystemOfRecordFacet } from './microsoft-dataverse.system-of-record.js';
|
|
2
|
+
import { MicrosoftDynamics365BusinessCentralSystemOfRecordFacet } from './microsoft-dynamics-365-business-central.system-of-record.js';
|
|
3
|
+
import { NetsuiteSystemOfRecordFacet } from './netsuite.system-of-record.js';
|
|
4
|
+
import { OdataSystemOfRecordFacet } from './odata.system-of-record.js';
|
|
5
|
+
import { OdooSystemOfRecordFacet } from './odoo.system-of-record.js';
|
|
6
|
+
import { SalesforceSystemOfRecordFacet } from './salesforce.system-of-record.js';
|
|
7
|
+
import { SapS4hanaSystemOfRecordFacet } from './sap-s4hana.system-of-record.js';
|
|
8
|
+
/**
|
|
9
|
+
* Every vendor the framework can serve as a system of record, keyed by the id an instance names.
|
|
10
|
+
*
|
|
11
|
+
* Far fewer vendors than there are providers, because being reachable over HTTP is not the same as
|
|
12
|
+
* being a deployment an application reads its own rows out of. A vendor absent from here is not
|
|
13
|
+
* refused by this record — it is a vendor the framework does not describe; the generic `odata` facet
|
|
14
|
+
* serves any OData v4 service.
|
|
15
|
+
*/
|
|
16
|
+
export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS = {
|
|
17
|
+
'microsoft-dataverse': MicrosoftDataverseSystemOfRecordFacet,
|
|
18
|
+
'microsoft-dynamics-365-business-central': MicrosoftDynamics365BusinessCentralSystemOfRecordFacet,
|
|
19
|
+
netsuite: NetsuiteSystemOfRecordFacet,
|
|
20
|
+
odata: OdataSystemOfRecordFacet,
|
|
21
|
+
odoo: OdooSystemOfRecordFacet,
|
|
22
|
+
salesforce: SalesforceSystemOfRecordFacet,
|
|
23
|
+
'sap-s4hana': SapS4hanaSystemOfRecordFacet,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.
|
|
27
|
+
*
|
|
28
|
+
* Not misspellings. Each of these is a real provider id, and an author naming one has made a
|
|
29
|
+
* reasonable mistake: the provider catalogue held `microsoft-dataverse`,
|
|
30
|
+
* `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they
|
|
31
|
+
* are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says
|
|
32
|
+
* "no facet" without saying which sibling has it leaves the author to guess between the other two.
|
|
33
|
+
*
|
|
34
|
+
* Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same
|
|
35
|
+
* product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.
|
|
36
|
+
*
|
|
37
|
+
* Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,
|
|
38
|
+
* because an alias pointing at a vendor that has no facet sends the author from one refusal to another.
|
|
39
|
+
*/
|
|
40
|
+
export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES = {
|
|
41
|
+
'microsoft-dynamics-365': 'microsoft-dataverse',
|
|
42
|
+
'microsoft-dynamics-crm': 'microsoft-dataverse',
|
|
43
|
+
};
|
|
44
|
+
/** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */
|
|
45
|
+
export const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS = Object.keys(PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS).sort();
|
|
46
|
+
//# sourceMappingURL=systems-of-record.exports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systems-of-record.exports.js","sourceRoot":"","sources":["../../../src/systems-of-record.exports.ts"],"names":[],"mappings":"AAmDA,OAAO,EAAE,qCAAqC,EAAE,MAAM,wCAAwC,CAAC;AAC/F,OAAO,EAAE,sDAAsD,EAAE,MAAM,4DAA4D,CAAC;AACpI,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,0CAA0C,GAAwD;IAC7G,qBAAqB,EAAE,qCAAqC;IAC5D,yCAAyC,EAAE,sDAAsD;IACjG,QAAQ,EAAE,2BAA2B;IACrC,KAAK,EAAE,wBAAwB;IAC/B,IAAI,EAAE,uBAAuB;IAC7B,UAAU,EAAE,6BAA6B;IACzC,YAAY,EAAE,4BAA4B;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,kDAAkD,GAAqC;IAClG,wBAAwB,EAAE,qBAAqB;IAC/C,wBAAwB,EAAE,qBAAqB;CAChD,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,2CAA2C,GACtD,MAAM,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC,IAAI,EAAE,CAAC","sourcesContent":["/**\n * The framework's SYSTEM-OF-RECORD FACETS — the public entrypoint behind\n * `@wildo-ai/systems-of-record/systems-of-record` (#1057, moved into its own library by #2013).\n *\n * ## Why a library of its own\n *\n * These facets were born inside the transposed provider catalogue (now archived under `_code_archive/providers/`),\n * under an underscore folder that its regeneration skipped. That kept them alive and nothing more: a system of\n * record is not a provider an application CALLS, it is a remote system that is the authoritative store\n * of some of the application's own resources — declared under `systemsOfRecord`, reached through its\n * instance's service identity only, and never copied into an application (#1049). Paul decided on\n * 2026-09-29 that the lane is delivered as a classical library, and the catalogue was archived\n * (#2012, #2019), so the facets moved here.\n *\n * It is an OPT-IN framework package (`optInFrameworkPackages`, #1836): an application links it only by\n * declaring it, and `wildo config sync` declares it on the backend the moment `systemsOfRecord` is\n * non-empty. An application that declares no system of record never receives it.\n *\n * Discovery admits this package's `./systems-of-record` by its exact NAME; any other facet module is\n * admitted only when an instance opts into it through `facetAugmentations`. The engine code that reads\n * a facet (dialects, pipelines, actions) stays in `@wildo-ai/saas-backend-lib`.\n *\n * ## Why ONE subpath rather than one per vendor\n *\n * The first plan called for a subpath per vendor, `./<ref>/facet`. It does not survive contact with\n * what a facet IS: every field that makes a facet a facet — the dialect, the credential kinds, the\n * partition — is AUTHORED vendor knowledge, and a facet is inert data with no transitive graph. One\n * module holding a record of seven object literals is the whole of it.\n *\n * ## Why a flat record rather than a loader map\n *\n * The engine's map — the one this replaces — used thunks (`vendor -> () => import(...)`) so nothing\n * was evaluated until a vendor was asked for. That cost is real when the map sits in an engine\n * everybody loads. Here it does not apply: this module is reached only when an application declared a\n * system of record at all.\n *\n * ## The export NAMES are the facet-module contract\n *\n * `PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS` and its siblings keep the names they had in the\n * catalogue because they are what `SYSTEM_OF_RECORD_FACET_RECORD_EXPORT` (saas-models) names, and every\n * augmentation package exports its record under the matching `…_FACET_AUGMENTATIONS`. Renaming them is a\n * contract change for every facet module, not a tidy-up of this file.\n *\n * ## Adding one\n *\n * Author `<vendor>.system-of-record.ts` beside this file and add its row below. The key and the\n * facet's own `vendor` must agree; `systems-of-record.exports.test.ts` asserts it, because a\n * hand-written key beside a declared id is exactly the shape that drifts.\n */\nimport type { SystemOfRecordVendorFacet } from '@wildo-ai/saas-models/external-data';\n\nimport { MicrosoftDataverseSystemOfRecordFacet } from './microsoft-dataverse.system-of-record';\nimport { MicrosoftDynamics365BusinessCentralSystemOfRecordFacet } from './microsoft-dynamics-365-business-central.system-of-record';\nimport { NetsuiteSystemOfRecordFacet } from './netsuite.system-of-record';\nimport { OdataSystemOfRecordFacet } from './odata.system-of-record';\nimport { OdooSystemOfRecordFacet } from './odoo.system-of-record';\nimport { SalesforceSystemOfRecordFacet } from './salesforce.system-of-record';\nimport { SapS4hanaSystemOfRecordFacet } from './sap-s4hana.system-of-record';\n\n/**\n * Every vendor the framework can serve as a system of record, keyed by the id an instance names.\n *\n * Far fewer vendors than there are providers, because being reachable over HTTP is not the same as\n * being a deployment an application reads its own rows out of. A vendor absent from here is not\n * refused by this record — it is a vendor the framework does not describe; the generic `odata` facet\n * serves any OData v4 service.\n */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS: Readonly<Record<string, SystemOfRecordVendorFacet>> = {\n 'microsoft-dataverse': MicrosoftDataverseSystemOfRecordFacet,\n 'microsoft-dynamics-365-business-central': MicrosoftDynamics365BusinessCentralSystemOfRecordFacet,\n netsuite: NetsuiteSystemOfRecordFacet,\n odata: OdataSystemOfRecordFacet,\n odoo: OdooSystemOfRecordFacet,\n salesforce: SalesforceSystemOfRecordFacet,\n 'sap-s4hana': SapS4hanaSystemOfRecordFacet,\n};\n\n/**\n * Vendor ids that are ANOTHER vendor's, and which one — so a refusal can point rather than just say no.\n *\n * Not misspellings. Each of these is a real provider id, and an author naming one has made a\n * reasonable mistake: the provider catalogue held `microsoft-dataverse`,\n * `microsoft-dynamics-365` and `microsoft-dynamics-crm` as three separate connector refs, and they\n * are three names for ONE Web API. Only one of them can carry the facet, and a refusal that says\n * \"no facet\" without saying which sibling has it leaves the author to guess between the other two.\n *\n * Lives HERE rather than in the engine for the reason the facets do: which vendor ids are the same\n * product is vendor knowledge, and adding a vendor tomorrow adds its aliases in the same edit.\n *\n * Every value MUST be a key of the facet record above — asserted by `systems-of-record.exports.test.ts`,\n * because an alias pointing at a vendor that has no facet sends the author from one refusal to another.\n */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDOR_ALIASES: Readonly<Record<string, string>> = {\n 'microsoft-dynamics-365': 'microsoft-dataverse',\n 'microsoft-dynamics-crm': 'microsoft-dataverse',\n};\n\n/** The vendor ids above, sorted — what a sync-time discovery records without loading a facet's shape. */\nexport const PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_VENDORS: readonly string[] =\n Object.keys(PROVIDER_CATALOGUE_SYSTEM_OF_RECORD_FACETS).sort();\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wildo-ai/systems-of-record",
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"description": "Wildo system-of-record vendor facets (Odoo, SAP S/4HANA, Microsoft Dataverse, Business Central, NetSuite, Salesforce, generic OData): an opt-in library an application links when it declares systemsOfRecord",
|
|
6
|
+
"author": "Wildo AI",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
"./systems-of-record": {
|
|
11
|
+
"types": "./dist/esm/systems-of-record.exports.d.ts",
|
|
12
|
+
"import": "./dist/esm/systems-of-record.exports.js",
|
|
13
|
+
"default": "./dist/esm/systems-of-record.exports.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm",
|
|
21
|
+
"dev": "node ../../scripts/run-tsc-watch-single-writer.mjs --dist dist/esm --exec tsc-watch -- -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm\"",
|
|
22
|
+
"rebuild": "pnpm clean && pnpm build",
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"watch": "node ../../scripts/run-tsc-watch-single-writer.mjs --dist dist/esm --exec tsc-watch -- -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ../../scripts/run-tsc-alias-safe.mjs --project tsconfig.build.json --dist dist/esm\"",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@wildo-ai/saas-models": "1.1.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"rimraf": "^6.0.1",
|
|
34
|
+
"tsc-watch": "7.2.1",
|
|
35
|
+
"typescript": "5.9.3",
|
|
36
|
+
"vitest": "^1.6.1"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/wildo-ai/wildo-ai.git",
|
|
41
|
+
"directory": "engine/systems-of-record"
|
|
42
|
+
}
|
|
43
|
+
}
|