mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(windchill): add document integration (#6577)
* feat(windchill): add document integration * fix(windchill): align tool contracts and docs * fix(windchill): use official integration icon * fix(windchill): correct response and paging semantics * fix(windchill): align execution and API contracts * refactor(windchill): inline route authentication * fix(windchill): correct OData query encoding, content download, and cleared-field handling Validated the integration end to end against PTC Windchill REST Services 2.7 documentation and fixed every divergence found. Protocol correctness: - Encode OData query spaces as %20 rather than the form-encoded `+` that URLSearchParams emits. Every multi-token $filter and $orderby reached Windchill as a literal `+` and could not match. - Download content through the documented typed navigation `<content>/PTC.ApplicationData/Content/URL`, which returns a signed vault URL, instead of a `$value` segment that WRS does not implement. The resolved URL is pinned to the configured HTTPS origin. - Terminate every Stage 2 CacheDescriptor_array entry with `;` to match the documented grammar. - Raise the $top bound to Windchill's documented 2000 maximum, keeping 200 as the default page size. Cleared-field handling: - The executor merges raw block inputs before the block's param transform, so omitting a key could not clear it. A cleared numeric or boolean field reached the URL builder as '' and threw, and cleared optional strings failed contract validation. Coercions now emit an explicit undefined, and the internal-route body strips blanks centrally. Robustness and contracts: - Bound the document-structure walk to the depth actually requested. - Loosen response schemas that re-applied request-side bounds to provider-returned values, which turned committed mutations into opaque parse failures. - Return contract-shaped bodies for oversized, malformed, and unhandled request failures. - Normalize downloaded content types and drop charset parameters. Presentation and docs: - Square the icon to a centred tile on white. - Replace WT.Document and PATCH-compatible jargon with plain language. - Fix canvas sentence noun stutters on the bulk operations. - Correct the revision skill's unverified working-copy claim to read the OID back rather than assume it, and add retirement and stale-checkout skills. - Add a manual intro section to the integration docs page. * fix(windchill): align tool copy with the docs page and rebase the route baseline Tool descriptions feed both the integration catalog and the generated docs page, so the plain-language pass had to reach them too: drop WT.Document and PATCH-compatible from the operation copy, and correct the $top bound the descriptions still advertised as 200. Correct the docs intro's attachment wording, gloss OData on first use, and attribute the bulk-atomicity claim to PTC's documented behavior. Raise the API route-count baseline, which staging advanced while this branch was behind. * feat(windchill): add update common properties Name, Number, and Organization are rejected by the PATCH-based update operation, and the rejection message told users to reach for Windchill's UpdateCommonProperties action that the integration did not expose. Add it. PTC documents UpdateCommonProperties as a bound DocMgmt action taking an Updates wrapper, available when hasCommonProperties is set on the Documents entity, and refused while the document is checked out. The subblock and param descriptions carry that constraint, and the rejection message now names the operation that does the job. * test(windchill): assert block and tool params stay aligned for every operation Validating the new operation surfaced that nothing enforced the block-to-tool alignment the review process had been checking by hand. Assert it for all 27 operations instead: every required tool param has a required, non-advanced input under that operation's condition, and no operation shows an input its tool cannot accept. Both fail on a deliberately broken condition or a dropped required flag. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
co-authored by
Bill Leoutsakos
Waleed Latif
parent
9aa16e381c
commit
2805a8def9
@@ -2494,6 +2494,32 @@ export function DocumentIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function WindchillIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='79 57 88 88'
|
||||
role='img'
|
||||
aria-label='Windchill icon'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<rect x='79' y='57' width='88' height='88' fill='#FFFFFF' />
|
||||
<g fill='#3D4647'>
|
||||
<polygon points='137.2,86.6 137.2,68.5 122.8,60.2 107.1,69.2' />
|
||||
<polygon points='142.7,106.4 158.4,97.3 158.4,80.7 142.7,71.6' />
|
||||
<polygon points='108.5,115.8 108.5,133.9 122.8,142.2 138.5,133.2' />
|
||||
<polygon points='128.2,121 143.9,130.1 158.4,121.7 158.4,103.6' />
|
||||
<polygon points='117.4,81.4 101.7,72.4 87.3,80.7 87.3,98.8' />
|
||||
<polygon points='103,96 87.3,105.1 87.3,121.7 103,130.8' />
|
||||
</g>
|
||||
<polygon
|
||||
fill='#40AA1D'
|
||||
points='137.2,109.5 122.8,117.8 108.4,109.5 108.4,92.9 122.8,84.6 137.2,92.9'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MintlifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 19' fill='none'>
|
||||
|
||||
@@ -245,6 +245,7 @@ import {
|
||||
WebhookIcon,
|
||||
WhatsAppIcon,
|
||||
WikipediaIcon,
|
||||
WindchillIcon,
|
||||
WizaIcon,
|
||||
WordpressIcon,
|
||||
WorkdayIcon,
|
||||
@@ -538,6 +539,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
webflow: WebflowIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
wikipedia: WikipediaIcon,
|
||||
windchill: WindchillIcon,
|
||||
wiza: WizaIcon,
|
||||
wordpress: WordpressIcon,
|
||||
workday: WorkdayIcon,
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
"webflow-service-account",
|
||||
"whatsapp",
|
||||
"wikipedia",
|
||||
"windchill",
|
||||
"wiza",
|
||||
"wordpress",
|
||||
"workday",
|
||||
|
||||
@@ -0,0 +1,937 @@
|
||||
---
|
||||
title: Windchill
|
||||
description: Manage documents, revisions, and content in PTC Windchill
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="windchill"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[PTC Windchill](https://www.ptc.com/en/products/windchill) is the product lifecycle management system manufacturers use as the system of record for engineering data. Documents in Windchill are controlled objects: each one carries a number, a revision and iteration, a lifecycle state, folder placement, security labels, and a checkout status that decides who is allowed to change it right now.
|
||||
|
||||
This integration talks to Windchill REST Services (WRS) 2.7 over OData — the query protocol Windchill exposes its data through — using a Basic-authenticated service account. Point it at a complete versioned service root — `https://your-host/Windchill/servlet/odata/v6` — and your agents can:
|
||||
|
||||
- **Find and read documents**: list documents with an OData filter, sort order, field selection, page size, and a latest-version-only switch; fetch a single document by its object identifier (OID); and walk a document's structure through its usage links to see child documents with their versions and states.
|
||||
- **Create and update**: create one document or a batch of them in a container and optional folder, and patch editable attributes on one or many documents. Name, Number, and Organization are rejected here and have their own operation, because Windchill changes those through a separate action and refuses it while a document is checked out.
|
||||
- **Run the version and lifecycle cycle**: check documents out and back in with notes, undo a checkout, revise to the next revision, read the lifecycle states a document is actually allowed to move to, and transition it to one of them.
|
||||
- **Move files**: download a document's primary content, or a specific attachment by its OID, into a Sim file — and upload files as primary content or attachments. Sim handles Windchill's CSRF token and its multi-step upload handshake for you.
|
||||
|
||||
Bulk actions are atomic on Windchill's side: PTC documents that if the action fails for any object in the collection, the entire action is rolled back and nothing changes.
|
||||
|
||||
Two limits are worth knowing before you build. Windchill identifies everything by OID (`OR:wt.doc.WTDocument:48796581`), so most operations need an OID you got from a list or get call rather than a document number. And this integration supports Basic authentication only — Windchill deployments fronted by OAuth are not currently supported.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate PTC Windchill REST Services 2.7 document management into your workflow using Basic authentication. Read and update document metadata, perform version and lifecycle actions, and transfer primary content and attachments. Windchill OAuth deployments are not currently supported.
|
||||
|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
### Windchill List Documents
|
||||
|
||||
List documents with an OData query, sorting, and pagination
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `select` | string | No | Comma-separated normalized document properties to return |
|
||||
| `filter` | string | No | OData $filter expression |
|
||||
| `orderBy` | string | No | OData $orderby expression |
|
||||
| `top` | number | No | Maximum documents in the OData result set \($top\), from 1 to 2000 |
|
||||
| `skip` | number | No | Documents to skip |
|
||||
| `count` | boolean | No | Ask Windchill to include the total matching count |
|
||||
| `latestVersion` | boolean | No | Return only the latest version of matching documents |
|
||||
| `nextLink` | string | No | Verified @odata.nextLink from a previous list response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `documents` | array | Windchill documents |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
| `pageInfo` | object | OData pagination information |
|
||||
| ↳ `count` | number | Number of items returned in this page |
|
||||
| ↳ `totalCount` | number | Total matching items |
|
||||
| ↳ `nextLink` | string | URL returned by Windchill for the next page |
|
||||
|
||||
### Windchill Get Document
|
||||
|
||||
Get a WT.Document by OID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `select` | string | No | Comma-separated normalized document properties to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `document` | object | Windchill document |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Get Document Structure
|
||||
|
||||
Retrieve recursive document usage links and their parent and child documents
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `structureDepth` | number | No | Document structure expansion depth, from 1 to 3 |
|
||||
| `nextLink` | string | No | Verified @odata.nextLink from a previous structure response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `structure` | array | Document usage links, including recursively expanded child links |
|
||||
| ↳ `id` | string | Document usage link OID |
|
||||
| ↳ `parent` | object | Parent document |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
| ↳ `child` | object | Child document |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
| ↳ `children` | array | Nested child usage links with the same recursive shape |
|
||||
| `pageInfo` | object | OData pagination information |
|
||||
| ↳ `count` | number | Number of items returned in this page |
|
||||
| ↳ `totalCount` | number | Total matching items |
|
||||
| ↳ `nextLink` | string | URL returned by Windchill for the next page |
|
||||
|
||||
### Windchill Get Valid State Transitions
|
||||
|
||||
Get lifecycle states a document can transition to from its current state
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `states` | array | Valid lifecycle transitions |
|
||||
| ↳ `value` | string | Internal state value |
|
||||
| ↳ `display` | string | Displayed state value |
|
||||
|
||||
### Windchill Get Primary Content
|
||||
|
||||
Get primary-content metadata for a document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `content` | object | Primary-content metadata |
|
||||
| ↳ `id` | string | Content object identifier |
|
||||
| ↳ `fileName` | string | Content file name |
|
||||
| ↳ `description` | string | Content description |
|
||||
| ↳ `format` | string | Windchill content format |
|
||||
| ↳ `mimeType` | string | Content MIME type |
|
||||
| ↳ `fileSize` | number | Content size in bytes |
|
||||
| ↳ `contentType` | string | Windchill OData content entity type |
|
||||
| ↳ `displayName` | string | Displayed content name |
|
||||
| ↳ `urlLocation` | string | URL-data location |
|
||||
| ↳ `externalLocation` | string | External-storage location |
|
||||
|
||||
### Windchill List Attachments
|
||||
|
||||
List attachment metadata for a document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `nextLink` | string | No | Verified @odata.nextLink from a previous attachment response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `attachments` | array | Document attachments |
|
||||
| ↳ `id` | string | Content object identifier |
|
||||
| ↳ `fileName` | string | Content file name |
|
||||
| ↳ `description` | string | Content description |
|
||||
| ↳ `format` | string | Windchill content format |
|
||||
| ↳ `mimeType` | string | Content MIME type |
|
||||
| ↳ `fileSize` | number | Content size in bytes |
|
||||
| ↳ `contentType` | string | Windchill OData content entity type |
|
||||
| ↳ `displayName` | string | Displayed content name |
|
||||
| ↳ `urlLocation` | string | URL-data location |
|
||||
| ↳ `externalLocation` | string | External-storage location |
|
||||
| `pageInfo` | object | OData pagination information |
|
||||
| ↳ `count` | number | Number of items returned in this page |
|
||||
| ↳ `totalCount` | number | Total matching items |
|
||||
| ↳ `nextLink` | string | URL returned by Windchill for the next page |
|
||||
|
||||
### Windchill Create Document
|
||||
|
||||
Create one WT.Document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `name` | string | Yes | Document name |
|
||||
| `containerOid` | string | Yes | Container OID in which to create the document |
|
||||
| `number` | string | No | Optional document number when manual numbering is enabled |
|
||||
| `title` | string | No | Document title |
|
||||
| `description` | string | No | Document description |
|
||||
| `folderOid` | string | No | Optional folder OID for the new document |
|
||||
| `attributes` | json | No | Optional installed Windchill document attributes as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Create Documents
|
||||
|
||||
Create several documents in one atomic Windchill request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documents` | array | Yes | Document inputs as a JSON array; each item requires name and containerOid |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Update Document
|
||||
|
||||
Update one document's editable attributes
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `attributes` | json | Yes | Editable attributes as a JSON object. Name, Number, and Organization require the Update Common Properties operation and are not supported here. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Update Common Properties
|
||||
|
||||
Update a document's Name, Number, and other common properties
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581. The document must not be checked out. |
|
||||
| `commonProperties` | json | Yes | Common properties as a JSON object, for example \{"Name":"New name","Number":"NEW-001"\}. Enumerated properties take a value/display pair. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Update Documents
|
||||
|
||||
Update several documents' editable attributes in one atomic request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documents` | array | Yes | Document updates as a JSON array; each item requires id and the editable attributes to set. Name, Number, and Organization are not supported. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Delete Document
|
||||
|
||||
Delete one document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
|
||||
### Windchill Delete Documents
|
||||
|
||||
Delete multiple documents atomically
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
|
||||
### Windchill Check Out Document
|
||||
|
||||
Check out one document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `checkOutNote` | string | No | Checkout note |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Check Out Documents
|
||||
|
||||
Check out multiple documents atomically
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
|
||||
| `checkOutNote` | string | No | Checkout note |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Check In Document
|
||||
|
||||
Check in one document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `checkInNote` | string | No | Check-in note |
|
||||
| `keepCheckedOut` | boolean | No | Keep the document checked out after checking it in |
|
||||
| `checkOutNote` | string | No | Checkout note |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Check In Documents
|
||||
|
||||
Check in multiple documents atomically
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
|
||||
| `checkInNote` | string | No | Check-in note |
|
||||
| `keepCheckedOut` | boolean | No | Keep the document checked out after checking it in |
|
||||
| `checkOutNote` | string | No | Checkout note |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Undo Check Out Document
|
||||
|
||||
Undo checkout for one document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Undo Check Out Documents
|
||||
|
||||
Undo checkout for multiple documents atomically
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Revise Document
|
||||
|
||||
Create a new revision of one document
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `versionId` | string | No | Optional target revision identifier when override-on-revise is enabled |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Revise Documents
|
||||
|
||||
Create new revisions of multiple documents atomically
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Set Lifecycle State
|
||||
|
||||
Transition a document to a valid lifecycle state
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `stateValue` | string | Yes | Internal value of the target lifecycle state |
|
||||
| `stateDisplay` | string | Yes | Display value of the target lifecycle state |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `document` | object | Document returned by Windchill when the operation returns one |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Update Document Security Labels
|
||||
|
||||
Update installed security-label attributes for one or more documents
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `securityLabelUpdates` | array | Yes | Array of document IDs and installed security-label values |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the operation |
|
||||
| `documents` | array | Documents returned by Windchill when the operation returns them |
|
||||
| ↳ `id` | string | Windchill object identifier |
|
||||
| ↳ `name` | string | Document name |
|
||||
| ↳ `number` | string | Document number |
|
||||
| ↳ `title` | string | Document title |
|
||||
| ↳ `description` | string | Document description |
|
||||
| ↳ `state` | string | Internal life cycle state value |
|
||||
| ↳ `stateDisplay` | string | Displayed life cycle state value |
|
||||
| ↳ `versionId` | string | Version identifier |
|
||||
| ↳ `revision` | string | Revision identifier |
|
||||
| ↳ `version` | string | Version and iteration |
|
||||
| ↳ `latest` | boolean | Whether this is the latest version |
|
||||
| ↳ `checkoutState` | string | Checkout state |
|
||||
| ↳ `folderName` | string | Folder name |
|
||||
| ↳ `folderLocation` | string | Folder path |
|
||||
|
||||
### Windchill Download Primary Content
|
||||
|
||||
Download primary content into a canonical UserFile
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `fileName` | string | No | Optional downloaded file name override |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `file` | file | Downloaded content stored as a canonical UserFile |
|
||||
| `fileName` | string | Downloaded file name |
|
||||
| `mimeType` | string | Downloaded content MIME type |
|
||||
|
||||
### Windchill Upload Primary Content
|
||||
|
||||
Upload a primary-content file to a document that has none
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `primaryFile` | file | Yes | Primary content file to upload |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the upload |
|
||||
| `uploadedFileNames` | array | Names of files accepted by Windchill |
|
||||
|
||||
### Windchill Download Attachment
|
||||
|
||||
Download a document attachment into a canonical UserFile
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `attachmentOid` | string | Yes | Windchill attachment content OID |
|
||||
| `fileName` | string | No | Optional downloaded file name override |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `file` | file | Downloaded content stored as a canonical UserFile |
|
||||
| `fileName` | string | Downloaded file name |
|
||||
| `mimeType` | string | Downloaded content MIME type |
|
||||
|
||||
### Windchill Upload Attachments
|
||||
|
||||
Upload one or more files as document attachments
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
|
||||
| `username` | string | Yes | Windchill service-account username |
|
||||
| `password` | string | Yes | Windchill service-account password |
|
||||
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
|
||||
| `attachmentFiles` | file[] | Yes | Attachment files to upload |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `operation` | string | Windchill operation that was executed |
|
||||
| `affectedIds` | array | Document identifiers affected by the upload |
|
||||
| `uploadedFileNames` | array | Names of files accepted by Windchill |
|
||||
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest as createTestingRequest, resetEnvMock } from '@sim/testing'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
|
||||
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
|
||||
|
||||
const {
|
||||
MockInvalidBindingError,
|
||||
MockWindchillProviderError,
|
||||
mockAssertToolFileAccess,
|
||||
mockBindDelegation,
|
||||
mockCreateWindchillSession,
|
||||
mockDownloadServableFileFromStorage,
|
||||
mockDownloadWindchillContent,
|
||||
mockGetSession,
|
||||
mockResolveWindchillContentUrl,
|
||||
mockProcessFilesToUserFiles,
|
||||
mockUploadCopilotFile,
|
||||
mockUploadExecutionFile,
|
||||
mockUploadWindchillContent,
|
||||
mockWindchillMutationRequest,
|
||||
} = vi.hoisted(() => {
|
||||
class MockInvalidBindingError extends Error {}
|
||||
class MockWindchillProviderError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'WindchillProviderError'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MockInvalidBindingError,
|
||||
MockWindchillProviderError,
|
||||
mockAssertToolFileAccess: vi.fn(),
|
||||
mockBindDelegation: vi.fn(),
|
||||
mockCreateWindchillSession: vi.fn(),
|
||||
mockDownloadServableFileFromStorage: vi.fn(),
|
||||
mockDownloadWindchillContent: vi.fn(),
|
||||
mockGetSession: vi.fn(),
|
||||
mockResolveWindchillContentUrl: vi.fn(),
|
||||
mockProcessFilesToUserFiles: vi.fn(),
|
||||
mockUploadCopilotFile: vi.fn(),
|
||||
mockUploadExecutionFile: vi.fn(),
|
||||
mockUploadWindchillContent: vi.fn(),
|
||||
mockWindchillMutationRequest: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
||||
vi.mock('@/lib/auth/internal-delegation', () => ({
|
||||
bindInternalExecutorDelegation: mockBindDelegation,
|
||||
InvalidInternalDelegationBindingError: MockInvalidBindingError,
|
||||
}))
|
||||
vi.unmock('@/lib/auth/internal')
|
||||
|
||||
vi.mock('@/app/api/files/authorization', () => ({
|
||||
assertToolFileAccess: mockAssertToolFileAccess,
|
||||
}))
|
||||
vi.mock('@/lib/uploads/utils/file-utils', () => ({
|
||||
processFilesToUserFiles: mockProcessFilesToUserFiles,
|
||||
}))
|
||||
vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
|
||||
downloadServableFileFromStorage: mockDownloadServableFileFromStorage,
|
||||
}))
|
||||
vi.mock('@/lib/uploads/utils/servable-file-response', () => ({
|
||||
docNotReadyResponse: vi.fn().mockReturnValue(null),
|
||||
}))
|
||||
vi.mock('@/lib/uploads/contexts/copilot', () => ({
|
||||
uploadCopilotFile: mockUploadCopilotFile,
|
||||
}))
|
||||
vi.mock('@/lib/uploads/contexts/execution', () => ({
|
||||
uploadExecutionFile: mockUploadExecutionFile,
|
||||
}))
|
||||
vi.mock('@/tools/windchill/utils.server', () => ({
|
||||
createWindchillSession: mockCreateWindchillSession,
|
||||
downloadWindchillContent: mockDownloadWindchillContent,
|
||||
resolveWindchillContentUrl: mockResolveWindchillContentUrl,
|
||||
sanitizeWindchillError: (message: string) => message.replace(/https?:\/\/\S+/g, '[redacted URL]'),
|
||||
uploadWindchillContent: mockUploadWindchillContent,
|
||||
windchillDocumentUrl: (baseUrl: string, documentOid: string) =>
|
||||
`${baseUrl}/DocMgmt/Documents('${encodeURIComponent(documentOid)}')`,
|
||||
windchillMutationRequest: mockWindchillMutationRequest,
|
||||
WindchillProviderError: MockWindchillProviderError,
|
||||
}))
|
||||
|
||||
import { generateInternalDelegationToken, generateInternalToken } from '@/lib/auth/internal'
|
||||
import { POST } from '@/app/api/tools/windchill/route'
|
||||
|
||||
const BASE_BODY = {
|
||||
baseUrl: 'https://windchill.example.com/Windchill/servlet/odata/v6',
|
||||
username: 'windchill-user',
|
||||
password: 'not-a-real-password',
|
||||
}
|
||||
|
||||
const DOCUMENT_OID = 'OR:wt.doc.WTDocument:1'
|
||||
const SECOND_DOCUMENT_OID = 'OR:wt.doc.WTDocument:2'
|
||||
let delegationToken = ''
|
||||
let legacyInternalToken = ''
|
||||
|
||||
function createMockRequest(method: string, body: unknown, headers: Record<string, string> = {}) {
|
||||
return createTestingRequest(method, body, {
|
||||
authorization: `Bearer ${delegationToken}`,
|
||||
...headers,
|
||||
})
|
||||
}
|
||||
|
||||
const MUTATION_CASES = [
|
||||
{
|
||||
operation: 'windchill_create_document',
|
||||
input: { name: 'Specification', containerOid: 'OR:wt.pdmlink.PDMLinkProduct:1' },
|
||||
url: '/DocMgmt/Documents',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_create_documents',
|
||||
input: {
|
||||
documents: [{ name: 'Specification', containerOid: 'OR:wt.pdmlink.PDMLinkProduct:1' }],
|
||||
},
|
||||
url: '/DocMgmt/CreateDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_document',
|
||||
input: { documentOid: DOCUMENT_OID, attributes: { Title: 'Updated' } },
|
||||
url: '/DocMgmt/Documents(',
|
||||
method: 'PATCH',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_documents',
|
||||
input: { documents: [{ id: DOCUMENT_OID, attributes: { Title: 'Updated' } }] },
|
||||
url: '/DocMgmt/UpdateDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_common_properties',
|
||||
input: { documentOid: DOCUMENT_OID, commonProperties: { Name: 'Renamed' } },
|
||||
url: '/PTC.DocMgmt.UpdateCommonProperties',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_delete_document',
|
||||
input: { documentOid: DOCUMENT_OID },
|
||||
url: '/DocMgmt/Documents(',
|
||||
method: 'DELETE',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_delete_documents',
|
||||
input: { documentOids: [DOCUMENT_OID, SECOND_DOCUMENT_OID] },
|
||||
url: '/DocMgmt/DeleteDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_check_out_document',
|
||||
input: { documentOid: DOCUMENT_OID, checkOutNote: 'Editing' },
|
||||
url: '/PTC.DocMgmt.CheckOut',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_check_out_documents',
|
||||
input: { documentOids: [DOCUMENT_OID], checkOutNote: 'Editing' },
|
||||
url: '/DocMgmt/CheckOutDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_check_in_document',
|
||||
input: { documentOid: DOCUMENT_OID, checkInNote: 'Done', keepCheckedOut: false },
|
||||
url: '/PTC.DocMgmt.CheckIn',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_check_in_documents',
|
||||
input: { documentOids: [DOCUMENT_OID], checkInNote: 'Done' },
|
||||
url: '/DocMgmt/CheckInDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_undo_check_out_document',
|
||||
input: { documentOid: DOCUMENT_OID },
|
||||
url: '/PTC.DocMgmt.UndoCheckOut',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_undo_check_out_documents',
|
||||
input: { documentOids: [DOCUMENT_OID] },
|
||||
url: '/DocMgmt/UndoCheckOutDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_revise_document',
|
||||
input: { documentOid: DOCUMENT_OID, versionId: 'B' },
|
||||
url: '/PTC.DocMgmt.Revise',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_revise_documents',
|
||||
input: { documentOids: [DOCUMENT_OID] },
|
||||
url: '/DocMgmt/ReviseDocuments',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_set_lifecycle_state',
|
||||
input: { documentOid: DOCUMENT_OID, stateValue: 'RELEASED', stateDisplay: 'Released' },
|
||||
url: '/PTC.DocMgmt.SetState',
|
||||
method: 'POST',
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_document_security_labels',
|
||||
input: {
|
||||
securityLabelUpdates: [{ id: DOCUMENT_OID, labels: { EXPORT_CONTROL: 'L1' } }],
|
||||
},
|
||||
url: '/DocMgmt/EditDocumentsSecurityLabels',
|
||||
method: 'POST',
|
||||
},
|
||||
] as const
|
||||
|
||||
const MUTATION_PAYLOAD_CASES = [
|
||||
{
|
||||
operation: 'windchill_check_out_documents',
|
||||
input: { documentOids: [DOCUMENT_OID], checkOutNote: 'Editing' },
|
||||
body: { Documents: [{ ID: DOCUMENT_OID }], CheckOutNote: 'Editing' },
|
||||
},
|
||||
{
|
||||
operation: 'windchill_check_in_document',
|
||||
input: {
|
||||
documentOid: DOCUMENT_OID,
|
||||
checkInNote: 'Done',
|
||||
keepCheckedOut: false,
|
||||
checkOutNote: 'Continue editing',
|
||||
},
|
||||
body: {
|
||||
CheckInNote: 'Done',
|
||||
KeepCheckedOut: false,
|
||||
CheckOutNote: 'Continue editing',
|
||||
},
|
||||
},
|
||||
{
|
||||
operation: 'windchill_revise_document',
|
||||
input: { documentOid: DOCUMENT_OID, versionId: 'B' },
|
||||
body: { VersionId: 'B' },
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_common_properties',
|
||||
input: {
|
||||
documentOid: DOCUMENT_OID,
|
||||
commonProperties: { Name: 'Renamed', Number: 'DOC-001' },
|
||||
},
|
||||
body: { Updates: { Name: 'Renamed', Number: 'DOC-001' } },
|
||||
},
|
||||
{
|
||||
operation: 'windchill_revise_documents',
|
||||
input: { documentOids: [DOCUMENT_OID] },
|
||||
body: { Documents: [{ ID: DOCUMENT_OID }] },
|
||||
},
|
||||
{
|
||||
operation: 'windchill_set_lifecycle_state',
|
||||
input: { documentOid: DOCUMENT_OID, stateValue: 'RELEASED', stateDisplay: 'Released' },
|
||||
body: { State: { Display: 'Released', Value: 'RELEASED' } },
|
||||
},
|
||||
{
|
||||
operation: 'windchill_update_document_security_labels',
|
||||
input: {
|
||||
securityLabelUpdates: [{ id: DOCUMENT_OID, labels: { EXPORT_CONTROL: 'L1' } }],
|
||||
},
|
||||
body: { Documents: [{ EXPORT_CONTROL: 'L1', ID: DOCUMENT_OID }] },
|
||||
},
|
||||
] as const
|
||||
|
||||
beforeAll(async () => {
|
||||
delegationToken = await generateInternalDelegationToken({
|
||||
subjectUserId: 'user-1',
|
||||
workflowId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
})
|
||||
legacyInternalToken = await generateInternalToken()
|
||||
})
|
||||
|
||||
afterAll(resetEnvMock)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue(null)
|
||||
mockBindDelegation.mockImplementation(async (delegation, options) => ({
|
||||
kind: 'delegated',
|
||||
serviceId: 'executor',
|
||||
subjectUserId: delegation.subjectUserId,
|
||||
workspaceId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
delegationId: delegation.delegationId,
|
||||
audience: options.audience,
|
||||
issuedAt: delegation.issuedAt,
|
||||
expiresAt: delegation.expiresAt,
|
||||
delegationContext: {
|
||||
kind: 'workflow_execution',
|
||||
workflowId: delegation.workflowId,
|
||||
executionId: delegation.executionId,
|
||||
},
|
||||
}))
|
||||
mockCreateWindchillSession.mockResolvedValue({
|
||||
nonceHeader: 'CSRF_NONCE',
|
||||
nonceValue: 'nonce-value',
|
||||
cookie: 'JSESSIONID=session-value',
|
||||
})
|
||||
mockWindchillMutationRequest.mockResolvedValue({ value: [{ ID: DOCUMENT_OID }] })
|
||||
mockAssertToolFileAccess.mockResolvedValue(null)
|
||||
mockProcessFilesToUserFiles.mockReturnValue([
|
||||
{
|
||||
key: 'workspace/workspace-1/specification.pdf',
|
||||
name: 'specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
])
|
||||
mockDownloadServableFileFromStorage.mockResolvedValue({
|
||||
buffer: Buffer.from('pdf'),
|
||||
contentType: 'application/pdf',
|
||||
})
|
||||
mockUploadWindchillContent.mockResolvedValue(['specification.pdf'])
|
||||
mockResolveWindchillContentUrl.mockImplementation(
|
||||
async ({ contentPath }: { contentPath: string }) =>
|
||||
`https://windchill.example.com/Windchill/servlet/WindchillGW/download?from=${encodeURIComponent(contentPath)}`
|
||||
)
|
||||
mockDownloadWindchillContent.mockResolvedValue({
|
||||
buffer: Buffer.from('pdf'),
|
||||
contentType: 'application/pdf',
|
||||
contentDisposition: 'attachment; filename="specification.pdf"',
|
||||
})
|
||||
mockUploadCopilotFile.mockResolvedValue({
|
||||
id: 'file-1',
|
||||
name: 'specification.pdf',
|
||||
url: '/api/files/serve?key=copilot/specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
key: 'copilot/specification.pdf',
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/tools/windchill', () => {
|
||||
it('authenticates before parsing the request body', async () => {
|
||||
const response = await POST(createTestingRequest('POST', { operation: 'not-valid' }))
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(await response.json()).toEqual({ success: false, error: 'Unauthorized' })
|
||||
expect(mockCreateWindchillSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('binds executor identity and scope through the canonical delegation path', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_update_document',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attributes: { Title: 'Updated' },
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockBindDelegation).toHaveBeenCalledWith(expect.any(Object), {
|
||||
audience: 'sim:windchill',
|
||||
resourceScope: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects browser sessions and legacy internal tokens', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-1' },
|
||||
session: { id: 'session-1' },
|
||||
})
|
||||
|
||||
const sessionResponse = await POST(createTestingRequest('POST', BASE_BODY))
|
||||
const legacyResponse = await POST(
|
||||
createTestingRequest('POST', BASE_BODY, {
|
||||
authorization: `Bearer ${legacyInternalToken}`,
|
||||
})
|
||||
)
|
||||
|
||||
expect(sessionResponse.status).toBe(401)
|
||||
expect(legacyResponse.status).toBe(401)
|
||||
expect(mockBindDelegation).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects malformed operation inputs at the shared contract boundary', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_update_document',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attributes: {},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(mockCreateWindchillSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an invalid service root before reading a protected upload', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
baseUrl: `${BASE_BODY.baseUrl}?token=secret`,
|
||||
operation: 'windchill_upload_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
primaryFile: {
|
||||
key: 'workspace/workspace-1/specification.pdf',
|
||||
name: 'specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(mockAssertToolFileAccess).not.toHaveBeenCalled()
|
||||
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(MUTATION_CASES)(
|
||||
'dispatches $operation through one CSRF-protected transaction',
|
||||
async ({ operation, input, url, method }) => {
|
||||
const response = await POST(createMockRequest('POST', { ...BASE_BODY, operation, ...input }))
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect((await response.json()).success).toBe(true)
|
||||
expect(mockCreateWindchillSession).toHaveBeenCalledTimes(1)
|
||||
expect(mockWindchillMutationRequest).toHaveBeenCalledTimes(1)
|
||||
expect(mockWindchillMutationRequest.mock.calls[0][0].url).toContain(url)
|
||||
expect(mockWindchillMutationRequest.mock.calls[0][0].method).toBe(method)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(MUTATION_PAYLOAD_CASES)(
|
||||
'encodes the exact $operation action payload',
|
||||
async ({ operation, input, body }) => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation,
|
||||
...input,
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockWindchillMutationRequest.mock.calls[0][0].body).toEqual(body)
|
||||
}
|
||||
)
|
||||
|
||||
it('maps create bindings and custom attributes without allowing them to replace bindings', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_create_document',
|
||||
name: 'Specification',
|
||||
containerOid: 'OR:wt.pdmlink.PDMLinkProduct:1',
|
||||
folderOid: 'OR:wt.folder.SubFolder:2',
|
||||
attributes: { CustomString: 'value' },
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect((await response.json()).output.affectedIds).toEqual([DOCUMENT_OID])
|
||||
expect(mockWindchillMutationRequest.mock.calls[0][0].body).toEqual({
|
||||
CustomString: 'value',
|
||||
Name: 'Specification',
|
||||
'Context@odata.bind': "Containers('OR%3Awt.pdmlink.PDMLinkProduct%3A1')",
|
||||
'Folder@odata.bind': "Folders('OR%3Awt.folder.SubFolder%3A2')",
|
||||
})
|
||||
})
|
||||
|
||||
it('returns operation-specific single, bulk, and delete mutation shapes', async () => {
|
||||
const singleResponse = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_update_document',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attributes: { Title: 'Updated' },
|
||||
})
|
||||
)
|
||||
const singleOutput = (await singleResponse.json()).output
|
||||
expect(singleOutput.document).toMatchObject({ id: DOCUMENT_OID })
|
||||
expect(singleOutput).not.toHaveProperty('documents')
|
||||
|
||||
const bulkResponse = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_update_documents',
|
||||
documents: [{ id: DOCUMENT_OID, attributes: { Title: 'Updated' } }],
|
||||
})
|
||||
)
|
||||
const bulkOutput = (await bulkResponse.json()).output
|
||||
expect(bulkOutput.documents).toEqual([expect.objectContaining({ id: DOCUMENT_OID })])
|
||||
expect(bulkOutput).not.toHaveProperty('document')
|
||||
|
||||
const deleteResponse = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_delete_document',
|
||||
documentOid: DOCUMENT_OID,
|
||||
})
|
||||
)
|
||||
const deleteOutput = (await deleteResponse.json()).output
|
||||
expect(deleteOutput.affectedIds).toEqual([DOCUMENT_OID])
|
||||
expect(deleteOutput).not.toHaveProperty('document')
|
||||
expect(deleteOutput).not.toHaveProperty('documents')
|
||||
})
|
||||
|
||||
it('authorizes and reads a UserFile before starting the upload transaction', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
primaryFile: {
|
||||
key: 'workspace/workspace-1/specification.pdf',
|
||||
name: 'specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockAssertToolFileAccess).toHaveBeenCalledWith(
|
||||
'workspace/workspace-1/specification.pdf',
|
||||
'user-1',
|
||||
expect.any(String),
|
||||
expect.anything()
|
||||
)
|
||||
expect(mockDownloadServableFileFromStorage).toHaveBeenCalledTimes(1)
|
||||
expect(mockUploadWindchillContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
documentOid: DOCUMENT_OID,
|
||||
primaryContent: true,
|
||||
files: [
|
||||
expect.objectContaining({
|
||||
name: 'specification.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 3,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('uploads multiple authorized files as attachments', async () => {
|
||||
mockProcessFilesToUserFiles.mockReturnValueOnce([
|
||||
{
|
||||
key: 'workspace/workspace-1/one.txt',
|
||||
name: 'one.txt',
|
||||
size: 3,
|
||||
type: 'text/plain',
|
||||
},
|
||||
{
|
||||
key: 'workspace/workspace-1/two.txt',
|
||||
name: 'two.txt',
|
||||
size: 3,
|
||||
type: 'text/plain',
|
||||
},
|
||||
])
|
||||
mockDownloadServableFileFromStorage.mockResolvedValue({
|
||||
buffer: Buffer.from('txt'),
|
||||
contentType: 'text/plain',
|
||||
})
|
||||
mockUploadWindchillContent.mockResolvedValueOnce(['one.txt', 'two.txt'])
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_attachments',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attachmentFiles: [
|
||||
{ key: 'workspace/workspace-1/one.txt', name: 'one.txt', size: 3 },
|
||||
{ key: 'workspace/workspace-1/two.txt', name: 'two.txt', size: 3 },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockAssertToolFileAccess).toHaveBeenCalledTimes(2)
|
||||
expect(mockDownloadServableFileFromStorage).toHaveBeenCalledTimes(2)
|
||||
expect(mockUploadWindchillContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ primaryContent: false })
|
||||
)
|
||||
expect(mockDownloadServableFileFromStorage.mock.calls[0][3]).toEqual({
|
||||
maxBytes: MAX_FILE_SIZE,
|
||||
})
|
||||
expect(mockDownloadServableFileFromStorage.mock.calls[1][3]).toEqual({
|
||||
maxBytes: MAX_FILE_SIZE - 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects attachment counts above the contract limit before reading storage', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_attachments',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attachmentFiles: Array.from({ length: 11 }, (_, index) => ({
|
||||
key: `workspace/workspace-1/${index}.txt`,
|
||||
name: `${index}.txt`,
|
||||
size: 1,
|
||||
})),
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(mockAssertToolFileAccess).not.toHaveBeenCalled()
|
||||
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects declared aggregate upload size before reading storage', async () => {
|
||||
mockProcessFilesToUserFiles.mockReturnValueOnce([
|
||||
{
|
||||
key: 'workspace/workspace-1/oversized.bin',
|
||||
name: 'oversized.bin',
|
||||
size: MAX_FILE_SIZE + 1,
|
||||
type: 'application/octet-stream',
|
||||
},
|
||||
])
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
primaryFile: {
|
||||
key: 'workspace/workspace-1/oversized.bin',
|
||||
name: 'oversized.bin',
|
||||
size: MAX_FILE_SIZE + 1,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(413)
|
||||
expect(mockAssertToolFileAccess).not.toHaveBeenCalled()
|
||||
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stops an under-reported upload at the remaining aggregate byte budget', async () => {
|
||||
mockProcessFilesToUserFiles.mockReturnValueOnce([
|
||||
{ key: 'workspace/workspace-1/one.txt', name: 'one.txt', size: 1, type: 'text/plain' },
|
||||
{ key: 'workspace/workspace-1/two.txt', name: 'two.txt', size: 1, type: 'text/plain' },
|
||||
{
|
||||
key: 'workspace/workspace-1/three.txt',
|
||||
name: 'three.txt',
|
||||
size: 1,
|
||||
type: 'text/plain',
|
||||
},
|
||||
])
|
||||
mockDownloadServableFileFromStorage
|
||||
.mockResolvedValueOnce({ buffer: Buffer.from('one'), contentType: 'text/plain' })
|
||||
.mockRejectedValueOnce(
|
||||
new PayloadSizeLimitError({
|
||||
label: 'Uploaded file',
|
||||
maxBytes: MAX_FILE_SIZE - 3,
|
||||
observedBytes: MAX_FILE_SIZE - 2,
|
||||
})
|
||||
)
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_attachments',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attachmentFiles: [
|
||||
{ key: 'workspace/workspace-1/one.txt', name: 'one.txt', size: 1 },
|
||||
{ key: 'workspace/workspace-1/two.txt', name: 'two.txt', size: 1 },
|
||||
{ key: 'workspace/workspace-1/three.txt', name: 'three.txt', size: 1 },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(413)
|
||||
expect(mockDownloadServableFileFromStorage).toHaveBeenCalledTimes(2)
|
||||
expect(mockDownloadServableFileFromStorage.mock.calls[1][3]).toEqual({
|
||||
maxBytes: MAX_FILE_SIZE - 3,
|
||||
})
|
||||
expect(mockUploadWindchillContent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stops before storage or Windchill when file ownership is denied', async () => {
|
||||
mockAssertToolFileAccess.mockResolvedValueOnce(
|
||||
NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
)
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_upload_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
primaryFile: {
|
||||
key: 'workspace/other/specification.pdf',
|
||||
name: 'specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
|
||||
expect(mockUploadWindchillContent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stores downloads as a UserFile instead of returning inline bytes', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_download_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
})
|
||||
)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockResolveWindchillContentUrl).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contentPath: expect.stringContaining('/PrimaryContent'),
|
||||
})
|
||||
)
|
||||
expect(mockResolveWindchillContentUrl.mock.calls[0][0].contentPath).not.toContain('$value')
|
||||
expect(mockDownloadWindchillContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: expect.stringContaining('/WindchillGW/download'),
|
||||
})
|
||||
)
|
||||
expect(mockUploadCopilotFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buffer: Buffer.from('pdf'),
|
||||
fileName: 'specification.pdf',
|
||||
contentType: 'application/pdf',
|
||||
userId: 'user-1',
|
||||
})
|
||||
)
|
||||
expect(data.output.file).toMatchObject({ key: 'copilot/specification.pdf' })
|
||||
expect(data.output.content).toBeUndefined()
|
||||
})
|
||||
|
||||
it('downloads an attachment through its document-scoped content path', async () => {
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_download_attachment',
|
||||
documentOid: DOCUMENT_OID,
|
||||
attachmentOid: 'OR:wt.content.ApplicationData:2',
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockResolveWindchillContentUrl).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contentPath: expect.stringContaining("/Attachments('OR%3Awt.content.ApplicationData%3A2')"),
|
||||
})
|
||||
)
|
||||
expect(mockDownloadWindchillContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: expect.stringContaining('/WindchillGW/download'),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('uses execution storage derived from the bound delegation principal', async () => {
|
||||
mockBindDelegation.mockResolvedValueOnce({
|
||||
kind: 'delegated',
|
||||
serviceId: 'executor',
|
||||
subjectUserId: 'user-1',
|
||||
workspaceId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
delegationId: 'delegation-1',
|
||||
audience: 'sim:windchill',
|
||||
issuedAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
expiresAt: new Date('2027-01-01T00:00:00.000Z'),
|
||||
delegationContext: {
|
||||
kind: 'workflow_execution',
|
||||
workflowId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
executionId: 'execution-1',
|
||||
},
|
||||
})
|
||||
mockUploadExecutionFile.mockResolvedValueOnce({
|
||||
id: 'file-2',
|
||||
name: 'specification.pdf',
|
||||
url: '/api/files/serve?key=execution/specification.pdf',
|
||||
size: 3,
|
||||
type: 'application/pdf',
|
||||
key: 'execution/specification.pdf',
|
||||
})
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_download_primary_content',
|
||||
documentOid: DOCUMENT_OID,
|
||||
workspaceId: 'forged-workspace',
|
||||
workflowId: 'forged-workflow',
|
||||
executionId: 'forged-execution',
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockUploadExecutionFile).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
workflowId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
executionId: 'execution-1',
|
||||
},
|
||||
Buffer.from('pdf'),
|
||||
'specification.pdf',
|
||||
'application/pdf',
|
||||
'user-1'
|
||||
)
|
||||
expect(mockUploadCopilotFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves sanitized provider status codes', async () => {
|
||||
mockWindchillMutationRequest.mockRejectedValueOnce(
|
||||
new MockWindchillProviderError('Windchill rejected the transition', 409)
|
||||
)
|
||||
|
||||
const response = await POST(
|
||||
createMockRequest('POST', {
|
||||
...BASE_BODY,
|
||||
operation: 'windchill_set_lifecycle_state',
|
||||
documentOid: DOCUMENT_OID,
|
||||
stateValue: 'RELEASED',
|
||||
stateDisplay: 'Released',
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(await response.json()).toEqual({
|
||||
success: false,
|
||||
error: 'Windchill rejected the transition',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,641 @@
|
||||
import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type {
|
||||
WindchillOperationBody,
|
||||
WindchillOperationResponse,
|
||||
} from '@/lib/api/contracts/tools/windchill'
|
||||
import { windchillOperationContract } from '@/lib/api/contracts/tools/windchill'
|
||||
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
|
||||
import {
|
||||
createInternalSessionOrExecutorAuth,
|
||||
InternalUnauthenticatedError,
|
||||
} from '@/lib/api/server/routes'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { uploadCopilotFile } from '@/lib/uploads/contexts/copilot'
|
||||
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
||||
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
|
||||
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { sanitizeFileName } from '@/executor/constants'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import {
|
||||
encodeWindchillOid,
|
||||
normalizeServiceRoot,
|
||||
normalizeWindchillDocument,
|
||||
normalizeWindchillDocuments,
|
||||
sanitizeWindchillError,
|
||||
} from '@/tools/windchill/utils'
|
||||
import {
|
||||
createWindchillSession,
|
||||
downloadWindchillContent,
|
||||
resolveWindchillContentUrl,
|
||||
uploadWindchillContent,
|
||||
WindchillProviderError,
|
||||
type WindchillUploadFile,
|
||||
windchillDocumentUrl,
|
||||
windchillMutationRequest,
|
||||
} from '@/tools/windchill/utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 900
|
||||
|
||||
const logger = createLogger('WindchillAPI')
|
||||
const windchillSessionOrExecutorAuth = createInternalSessionOrExecutorAuth({
|
||||
audience: 'sim:windchill',
|
||||
})
|
||||
|
||||
async function authenticateWindchillExecutor(
|
||||
request: NextRequest
|
||||
): Promise<WorkflowExecutionDelegatedPrincipal> {
|
||||
const principal = await windchillSessionOrExecutorAuth.authenticate(request, {})
|
||||
if (
|
||||
principal.kind !== 'delegated' ||
|
||||
principal.serviceId !== 'executor' ||
|
||||
!('delegationContext' in principal)
|
||||
) {
|
||||
throw new InternalUnauthenticatedError('Authentication required')
|
||||
}
|
||||
return principal
|
||||
}
|
||||
|
||||
type WindchillRouteOutput = Extract<WindchillOperationResponse, { success: true }>['output']
|
||||
type MutationOperation = Exclude<
|
||||
WindchillRouteOutput['operation'],
|
||||
| 'windchill_download_attachment'
|
||||
| 'windchill_download_primary_content'
|
||||
| 'windchill_upload_attachments'
|
||||
| 'windchill_upload_primary_content'
|
||||
>
|
||||
|
||||
const BULK_RESULT_OPERATIONS = [
|
||||
'windchill_create_documents',
|
||||
'windchill_update_documents',
|
||||
'windchill_check_out_documents',
|
||||
'windchill_check_in_documents',
|
||||
'windchill_undo_check_out_documents',
|
||||
'windchill_revise_documents',
|
||||
'windchill_update_document_security_labels',
|
||||
] as const satisfies readonly MutationOperation[]
|
||||
|
||||
const DELETE_OPERATIONS = [
|
||||
'windchill_delete_document',
|
||||
'windchill_delete_documents',
|
||||
] as const satisfies readonly MutationOperation[]
|
||||
|
||||
type BulkResultOperation = (typeof BULK_RESULT_OPERATIONS)[number]
|
||||
type DeleteOperation = (typeof DELETE_OPERATIONS)[number]
|
||||
|
||||
function isBulkResultOperation(operation: MutationOperation): operation is BulkResultOperation {
|
||||
return BULK_RESULT_OPERATIONS.includes(operation as BulkResultOperation)
|
||||
}
|
||||
|
||||
function isDeleteOperation(operation: MutationOperation): operation is DeleteOperation {
|
||||
return DELETE_OPERATIONS.includes(operation as DeleteOperation)
|
||||
}
|
||||
|
||||
function successResponse(output: WindchillRouteOutput) {
|
||||
const body = { success: true, output } satisfies WindchillOperationResponse
|
||||
return NextResponse.json(body)
|
||||
}
|
||||
|
||||
function failureResponse(error: string, status: number) {
|
||||
const body = {
|
||||
success: false,
|
||||
error: sanitizeWindchillError(error),
|
||||
} satisfies WindchillOperationResponse
|
||||
return NextResponse.json(body, { status })
|
||||
}
|
||||
|
||||
function documentsById(documentOids: string[]) {
|
||||
return documentOids.map((ID) => ({ ID }))
|
||||
}
|
||||
|
||||
/** Keeps the media type and drops any `; charset=...` parameters Windchill cannot use. */
|
||||
function safeMimeType(value: string | undefined): string {
|
||||
const mediaType = value?.split(';', 1)[0]?.trim()
|
||||
if (mediaType && /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(mediaType)) {
|
||||
return mediaType
|
||||
}
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
function mutationOutput(
|
||||
operation: MutationOperation,
|
||||
data: unknown,
|
||||
fallbackIds: string[]
|
||||
): WindchillRouteOutput {
|
||||
const documents = normalizeWindchillDocuments(data)
|
||||
const document = documents[0] ?? normalizeWindchillDocument(data)
|
||||
const collectionIds = documents
|
||||
.map((item) => item.id)
|
||||
.filter((id): id is string => typeof id === 'string')
|
||||
const returnedIds =
|
||||
document?.id && !collectionIds.includes(document.id)
|
||||
? [document.id, ...collectionIds]
|
||||
: collectionIds
|
||||
const affectedIds = returnedIds.length > 0 ? returnedIds : fallbackIds
|
||||
if (isDeleteOperation(operation)) return { operation, affectedIds: fallbackIds }
|
||||
if (isBulkResultOperation(operation)) {
|
||||
return {
|
||||
operation,
|
||||
affectedIds,
|
||||
...(documents.length > 0 ? { documents } : {}),
|
||||
}
|
||||
}
|
||||
return {
|
||||
operation,
|
||||
affectedIds,
|
||||
...(document ? { document } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMutation(
|
||||
body: Exclude<
|
||||
WindchillOperationBody,
|
||||
| { operation: 'windchill_download_primary_content' }
|
||||
| { operation: 'windchill_upload_primary_content' }
|
||||
| { operation: 'windchill_download_attachment' }
|
||||
| { operation: 'windchill_upload_attachments' }
|
||||
>,
|
||||
signal: AbortSignal
|
||||
): Promise<WindchillRouteOutput> {
|
||||
const session = await createWindchillSession(body, signal)
|
||||
const root = normalizeServiceRoot(body.baseUrl)
|
||||
|
||||
switch (body.operation) {
|
||||
case 'windchill_create_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/Documents`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
...(body.attributes ?? {}),
|
||||
Name: body.name,
|
||||
...(body.number ? { Number: body.number } : {}),
|
||||
...(body.title ? { Title: body.title } : {}),
|
||||
...(body.description ? { Description: body.description } : {}),
|
||||
'Context@odata.bind': `Containers('${encodeWindchillOid(body.containerOid)}')`,
|
||||
...(body.folderOid
|
||||
? { 'Folder@odata.bind': `Folders('${encodeWindchillOid(body.folderOid)}')` }
|
||||
: {}),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [])
|
||||
}
|
||||
case 'windchill_create_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/CreateDocuments`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: body.documents.map((document) => ({
|
||||
...(document.attributes ?? {}),
|
||||
Name: document.name,
|
||||
...(document.number ? { Number: document.number } : {}),
|
||||
...(document.title ? { Title: document.title } : {}),
|
||||
...(document.description ? { Description: document.description } : {}),
|
||||
'Context@odata.bind': `Containers('${encodeWindchillOid(document.containerOid)}')`,
|
||||
...(document.folderOid
|
||||
? { 'Folder@odata.bind': `Folders('${encodeWindchillOid(document.folderOid)}')` }
|
||||
: {}),
|
||||
})),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [])
|
||||
}
|
||||
case 'windchill_update_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: windchillDocumentUrl(root, body.documentOid),
|
||||
method: 'PATCH',
|
||||
body: body.attributes,
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_update_common_properties': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.UpdateCommonProperties`,
|
||||
method: 'POST',
|
||||
body: { Updates: body.commonProperties },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_update_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/UpdateDocuments`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: body.documents.map((document) => ({
|
||||
...document.attributes,
|
||||
ID: document.id,
|
||||
})),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(
|
||||
body.operation,
|
||||
data,
|
||||
body.documents.map((document) => document.id)
|
||||
)
|
||||
}
|
||||
case 'windchill_delete_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: windchillDocumentUrl(root, body.documentOid),
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_delete_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/DeleteDocuments`,
|
||||
method: 'POST',
|
||||
body: { Documents: documentsById(body.documentOids) },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, body.documentOids)
|
||||
}
|
||||
case 'windchill_check_out_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.CheckOut`,
|
||||
method: 'POST',
|
||||
body: { ...(body.checkOutNote ? { CheckOutNote: body.checkOutNote } : {}) },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_check_out_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/CheckOutDocuments`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: documentsById(body.documentOids),
|
||||
...(body.checkOutNote ? { CheckOutNote: body.checkOutNote } : {}),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, body.documentOids)
|
||||
}
|
||||
case 'windchill_check_in_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.CheckIn`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
...(body.checkInNote ? { CheckInNote: body.checkInNote } : {}),
|
||||
...(body.keepCheckedOut !== undefined ? { KeepCheckedOut: body.keepCheckedOut } : {}),
|
||||
...(body.checkOutNote ? { CheckOutNote: body.checkOutNote } : {}),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_check_in_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/CheckInDocuments`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: documentsById(body.documentOids),
|
||||
...(body.checkInNote ? { CheckInNote: body.checkInNote } : {}),
|
||||
...(body.keepCheckedOut !== undefined ? { KeepCheckedOut: body.keepCheckedOut } : {}),
|
||||
...(body.checkOutNote ? { CheckOutNote: body.checkOutNote } : {}),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, body.documentOids)
|
||||
}
|
||||
case 'windchill_undo_check_out_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.UndoCheckOut`,
|
||||
method: 'POST',
|
||||
body: {},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_undo_check_out_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/UndoCheckOutDocuments`,
|
||||
method: 'POST',
|
||||
body: { Documents: documentsById(body.documentOids) },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, body.documentOids)
|
||||
}
|
||||
case 'windchill_revise_document': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.Revise`,
|
||||
method: 'POST',
|
||||
body: { ...(body.versionId ? { VersionId: body.versionId } : {}) },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_revise_documents': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/ReviseDocuments`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: documentsById(body.documentOids),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, body.documentOids)
|
||||
}
|
||||
case 'windchill_set_lifecycle_state': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${windchillDocumentUrl(root, body.documentOid)}/PTC.DocMgmt.SetState`,
|
||||
method: 'POST',
|
||||
body: { State: { Display: body.stateDisplay, Value: body.stateValue } },
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(body.operation, data, [body.documentOid])
|
||||
}
|
||||
case 'windchill_update_document_security_labels': {
|
||||
const data = await windchillMutationRequest({
|
||||
params: body,
|
||||
session,
|
||||
url: `${root}/DocMgmt/EditDocumentsSecurityLabels`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
Documents: body.securityLabelUpdates.map((update) => ({
|
||||
...update.labels,
|
||||
ID: update.id,
|
||||
})),
|
||||
},
|
||||
signal,
|
||||
})
|
||||
return mutationOutput(
|
||||
body.operation,
|
||||
data,
|
||||
body.securityLabelUpdates.map((update) => update.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUploadFiles(
|
||||
inputs: RawFileInput[],
|
||||
userId: string,
|
||||
requestId: string
|
||||
): Promise<WindchillUploadFile[] | NextResponse> {
|
||||
let userFiles: UserFile[]
|
||||
try {
|
||||
userFiles = processFilesToUserFiles(inputs, requestId, logger)
|
||||
} catch (error) {
|
||||
return failureResponse(getErrorMessage(error, 'Invalid file input'), 400)
|
||||
}
|
||||
if (userFiles.length !== inputs.length) return failureResponse('Invalid file input', 400)
|
||||
|
||||
const declaredTotal = userFiles.reduce((total, file) => total + file.size, 0)
|
||||
if (declaredTotal > MAX_FILE_SIZE) {
|
||||
return failureResponse('Combined Windchill upload exceeds the maximum file size', 413)
|
||||
}
|
||||
|
||||
const files: WindchillUploadFile[] = []
|
||||
let actualTotal = 0
|
||||
for (const userFile of userFiles) {
|
||||
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
try {
|
||||
const servable = await downloadServableFileFromStorage(userFile, requestId, logger, {
|
||||
maxBytes: MAX_FILE_SIZE - actualTotal,
|
||||
})
|
||||
actualTotal += servable.buffer.length
|
||||
if (actualTotal > MAX_FILE_SIZE) {
|
||||
return failureResponse('Combined Windchill upload exceeds the maximum file size', 413)
|
||||
}
|
||||
files.push({
|
||||
name: sanitizeFileName(userFile.name),
|
||||
mimeType: safeMimeType(servable.contentType || userFile.type),
|
||||
size: servable.buffer.length,
|
||||
buffer: servable.buffer,
|
||||
})
|
||||
} catch (error) {
|
||||
const notReady = docNotReadyResponse(error)
|
||||
if (notReady) return notReady
|
||||
return failureResponse(
|
||||
getErrorMessage(error, 'Failed to read uploaded file'),
|
||||
isPayloadSizeLimitError(error) ? 413 : 400
|
||||
)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function contentDispositionFileName(value: string | null): string | null {
|
||||
if (!value) return null
|
||||
const encoded = value.match(/filename\*=UTF-8''([^;]+)/i)?.[1]
|
||||
if (encoded) {
|
||||
try {
|
||||
return decodeURIComponent(encoded)
|
||||
} catch {
|
||||
return encoded
|
||||
}
|
||||
}
|
||||
return (
|
||||
value.match(/filename\s*=\s*"([^"]+)"/i)?.[1] ??
|
||||
value.match(/filename\s*=\s*([^;]+)/i)?.[1]?.trim() ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async function storeDownloadedFile({
|
||||
principal,
|
||||
buffer,
|
||||
fileName,
|
||||
contentType,
|
||||
}: {
|
||||
principal: WorkflowExecutionDelegatedPrincipal
|
||||
buffer: Buffer
|
||||
fileName: string
|
||||
contentType: string
|
||||
}): Promise<UserFile> {
|
||||
const { workflowId, executionId } = principal.delegationContext
|
||||
if (executionId) {
|
||||
return uploadExecutionFile(
|
||||
{
|
||||
workspaceId: principal.workspaceId,
|
||||
workflowId,
|
||||
executionId,
|
||||
},
|
||||
buffer,
|
||||
fileName,
|
||||
contentType,
|
||||
principal.subjectUserId
|
||||
)
|
||||
}
|
||||
return uploadCopilotFile({
|
||||
buffer,
|
||||
fileName,
|
||||
contentType,
|
||||
userId: principal.subjectUserId,
|
||||
})
|
||||
}
|
||||
|
||||
async function executeDownload(
|
||||
body: Extract<
|
||||
WindchillOperationBody,
|
||||
| { operation: 'windchill_download_primary_content' }
|
||||
| { operation: 'windchill_download_attachment' }
|
||||
>,
|
||||
principal: WorkflowExecutionDelegatedPrincipal,
|
||||
signal: AbortSignal
|
||||
): Promise<WindchillRouteOutput> {
|
||||
const documentUrl = windchillDocumentUrl(body.baseUrl, body.documentOid)
|
||||
const contentPath =
|
||||
body.operation === 'windchill_download_primary_content'
|
||||
? `${documentUrl}/PrimaryContent`
|
||||
: `${documentUrl}/Attachments('${encodeWindchillOid(body.attachmentOid)}')`
|
||||
const contentUrl = await resolveWindchillContentUrl({
|
||||
params: body,
|
||||
contentPath,
|
||||
signal,
|
||||
})
|
||||
const downloaded = await downloadWindchillContent({
|
||||
params: body,
|
||||
url: contentUrl,
|
||||
maxBytes: MAX_FILE_SIZE,
|
||||
signal,
|
||||
})
|
||||
const fallback =
|
||||
body.operation === 'windchill_download_primary_content'
|
||||
? 'windchill-primary-content.bin'
|
||||
: 'windchill-attachment.bin'
|
||||
const fileName = sanitizeFileName(
|
||||
body.fileName || contentDispositionFileName(downloaded.contentDisposition) || fallback
|
||||
)
|
||||
const mimeType = safeMimeType(downloaded.contentType)
|
||||
const file = await storeDownloadedFile({
|
||||
principal,
|
||||
buffer: downloaded.buffer,
|
||||
fileName,
|
||||
contentType: mimeType,
|
||||
})
|
||||
return {
|
||||
operation: body.operation,
|
||||
file: { ...file },
|
||||
fileName,
|
||||
mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withRouteHandler(
|
||||
async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
let principal: WorkflowExecutionDelegatedPrincipal
|
||||
try {
|
||||
principal = await authenticateWindchillExecutor(request)
|
||||
} catch (error) {
|
||||
if (error instanceof InternalUnauthenticatedError) {
|
||||
return failureResponse(error.message, 401)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(
|
||||
windchillOperationContract,
|
||||
request,
|
||||
{},
|
||||
{
|
||||
validationErrorResponse: (error) =>
|
||||
failureResponse(getValidationErrorMessage(error, 'Invalid Windchill request'), 400),
|
||||
invalidJsonResponse: () =>
|
||||
failureResponse('Windchill request body must be valid JSON', 400),
|
||||
payloadTooLargeResponse: () => failureResponse('Windchill request body is too large', 413),
|
||||
}
|
||||
)
|
||||
if (!parsed.success) return parsed.response
|
||||
const body = parsed.data.body
|
||||
|
||||
try {
|
||||
if (
|
||||
body.operation === 'windchill_download_primary_content' ||
|
||||
body.operation === 'windchill_download_attachment'
|
||||
) {
|
||||
return successResponse(await executeDownload(body, principal, request.signal))
|
||||
}
|
||||
|
||||
if (
|
||||
body.operation === 'windchill_upload_primary_content' ||
|
||||
body.operation === 'windchill_upload_attachments'
|
||||
) {
|
||||
const inputs =
|
||||
body.operation === 'windchill_upload_primary_content'
|
||||
? [body.primaryFile]
|
||||
: body.attachmentFiles
|
||||
const files = await loadUploadFiles(inputs, principal.subjectUserId, requestId)
|
||||
if (files instanceof NextResponse) return files
|
||||
const uploadedFileNames = await uploadWindchillContent({
|
||||
params: body,
|
||||
documentOid: body.documentOid,
|
||||
files,
|
||||
primaryContent: body.operation === 'windchill_upload_primary_content',
|
||||
signal: request.signal,
|
||||
})
|
||||
return successResponse({
|
||||
operation: body.operation,
|
||||
affectedIds: [body.documentOid],
|
||||
uploadedFileNames,
|
||||
})
|
||||
}
|
||||
|
||||
return successResponse(await executeMutation(body, request.signal))
|
||||
} catch (error) {
|
||||
logger.error('Windchill operation failed', {
|
||||
operation: body.operation,
|
||||
error: sanitizeWindchillError(getErrorMessage(error, 'Windchill operation failed')),
|
||||
})
|
||||
if (error instanceof WindchillProviderError) {
|
||||
const status = error.status >= 400 && error.status <= 599 ? error.status : 502
|
||||
return failureResponse(error.message, status)
|
||||
}
|
||||
return failureResponse(
|
||||
getErrorMessage(error, 'Windchill operation failed'),
|
||||
isPayloadSizeLimitError(error) ? 413 : 500
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
unhandledErrorResponse: () => failureResponse('Windchill operation failed', 500),
|
||||
}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -333,6 +333,7 @@ import { WebflowBlock, WebflowBlockMeta } from '@/blocks/blocks/webflow'
|
||||
import { WebhookRequestBlock } from '@/blocks/blocks/webhook_request'
|
||||
import { WhatsAppBlock, WhatsAppBlockMeta } from '@/blocks/blocks/whatsapp'
|
||||
import { WikipediaBlock, WikipediaBlockMeta } from '@/blocks/blocks/wikipedia'
|
||||
import { WindchillBlock, WindchillBlockMeta } from '@/blocks/blocks/windchill'
|
||||
import { WizaBlock, WizaBlockMeta } from '@/blocks/blocks/wiza'
|
||||
import { WordPressBlock, WordPressBlockMeta } from '@/blocks/blocks/wordpress'
|
||||
import { WorkdayBlock, WorkdayBlockMeta } from '@/blocks/blocks/workday'
|
||||
@@ -659,6 +660,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
|
||||
webhook_request: WebhookRequestBlock,
|
||||
whatsapp: WhatsAppBlock,
|
||||
wikipedia: WikipediaBlock,
|
||||
windchill: WindchillBlock,
|
||||
wiza: WizaBlock,
|
||||
wordpress: WordPressBlock,
|
||||
workday: WorkdayBlock,
|
||||
@@ -931,6 +933,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
|
||||
webflow: WebflowBlockMeta,
|
||||
whatsapp: WhatsAppBlockMeta,
|
||||
wikipedia: WikipediaBlockMeta,
|
||||
windchill: WindchillBlockMeta,
|
||||
wiza: WizaBlockMeta,
|
||||
wordpress: WordPressBlockMeta,
|
||||
workday: WorkdayBlockMeta,
|
||||
|
||||
@@ -2494,6 +2494,32 @@ export function DocumentIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function WindchillIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='79 57 88 88'
|
||||
role='img'
|
||||
aria-label='Windchill icon'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<rect x='79' y='57' width='88' height='88' fill='#FFFFFF' />
|
||||
<g fill='#3D4647'>
|
||||
<polygon points='137.2,86.6 137.2,68.5 122.8,60.2 107.1,69.2' />
|
||||
<polygon points='142.7,106.4 158.4,97.3 158.4,80.7 142.7,71.6' />
|
||||
<polygon points='108.5,115.8 108.5,133.9 122.8,142.2 138.5,133.2' />
|
||||
<polygon points='128.2,121 143.9,130.1 158.4,121.7 158.4,103.6' />
|
||||
<polygon points='117.4,81.4 101.7,72.4 87.3,80.7 87.3,98.8' />
|
||||
<polygon points='103,96 87.3,105.1 87.3,121.7 103,130.8' />
|
||||
</g>
|
||||
<polygon
|
||||
fill='#40AA1D'
|
||||
points='137.2,109.5 122.8,117.8 108.4,109.5 108.4,92.9 122.8,84.6 137.2,92.9'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MintlifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 19' fill='none'>
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
import { z } from 'zod'
|
||||
import { userFileSchema } from '@/lib/api/contracts/primitives'
|
||||
import type { ContractBodyInput, ContractJsonResponse } from '@/lib/api/contracts/types'
|
||||
import { defineRouteContract } from '@/lib/api/contracts/types'
|
||||
import { RawFileInputArraySchema, RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
|
||||
const MAX_CREDENTIAL_LENGTH = 8192
|
||||
const MAX_OID_LENGTH = 512
|
||||
const MAX_TEXT_LENGTH = 4096
|
||||
const MAX_ATTRIBUTES = 100
|
||||
const MAX_ATTRIBUTE_DEPTH = 8
|
||||
const MAX_ATTRIBUTE_NODES = 1000
|
||||
const MAX_BULK_DOCUMENTS = 100
|
||||
const MAX_ATTACHMENT_FILES = 10
|
||||
|
||||
function parseUrl(value: string): URL | null {
|
||||
try {
|
||||
return new URL(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const credentialSchema = z
|
||||
.string({ error: 'Credential is required' })
|
||||
.min(1, 'Credential is required')
|
||||
.max(MAX_CREDENTIAL_LENGTH, 'Credential is too long')
|
||||
|
||||
const baseUrlSchema = z
|
||||
.url('Windchill base URL must be a valid URL')
|
||||
.max(2048, 'Windchill base URL is too long')
|
||||
.refine((value) => value.startsWith('https://'), 'Windchill base URL must use HTTPS')
|
||||
.refine((value) => {
|
||||
const parsed = parseUrl(value)
|
||||
return Boolean(parsed && !parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
||||
}, 'Windchill base URL must not include credentials, query parameters, or a hash')
|
||||
.refine((value) => {
|
||||
const parsed = parseUrl(value)
|
||||
return Boolean(parsed && /\/servlet\/odata\/v\d+\/?$/i.test(parsed.pathname))
|
||||
}, 'Windchill base URL must end with a versioned /servlet/odata/vN path')
|
||||
|
||||
const oidSchema = z
|
||||
.string({ error: 'Windchill OID is required' })
|
||||
.trim()
|
||||
.min(1, 'Windchill OID is required')
|
||||
.max(MAX_OID_LENGTH, 'Windchill OID is too long')
|
||||
.regex(/^[A-Za-z0-9_.:-]+$/, 'Windchill OID contains unsupported characters')
|
||||
|
||||
const optionalTextSchema = z.string().max(MAX_TEXT_LENGTH, 'Value is too long').optional()
|
||||
const requiredTextSchema = z
|
||||
.string({ error: 'Value is required' })
|
||||
.trim()
|
||||
.min(1, 'Value is required')
|
||||
.max(MAX_TEXT_LENGTH, 'Value is too long')
|
||||
|
||||
type WindchillAttributeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| WindchillAttributeValue[]
|
||||
| { [key: string]: WindchillAttributeValue }
|
||||
|
||||
function isBoundedAttributeValue(value: unknown): value is WindchillAttributeValue {
|
||||
const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
|
||||
let nodes = 0
|
||||
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop()
|
||||
if (!current || current.depth > MAX_ATTRIBUTE_DEPTH || ++nodes > MAX_ATTRIBUTE_NODES) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
current.value === null ||
|
||||
typeof current.value === 'boolean' ||
|
||||
(typeof current.value === 'number' && Number.isFinite(current.value))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (typeof current.value === 'string') {
|
||||
if (current.value.length > MAX_TEXT_LENGTH) return false
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(current.value)) {
|
||||
if (current.value.length > MAX_ATTRIBUTES) return false
|
||||
for (const item of current.value) pending.push({ value: item, depth: current.depth + 1 })
|
||||
continue
|
||||
}
|
||||
if (typeof current.value !== 'object') return false
|
||||
|
||||
const entries = Object.entries(current.value)
|
||||
if (entries.length > MAX_ATTRIBUTES) return false
|
||||
for (const [key, nested] of entries) {
|
||||
if (
|
||||
key.length === 0 ||
|
||||
key.length > 256 ||
|
||||
['__proto__', 'constructor', 'prototype'].includes(key)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
pending.push({ value: nested, depth: current.depth + 1 })
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const attributeValueSchema = z.custom<WindchillAttributeValue>(isBoundedAttributeValue, {
|
||||
message: `Attribute values must be bounded JSON with at most ${MAX_ATTRIBUTE_DEPTH} nested levels`,
|
||||
})
|
||||
const attributeNameSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(256)
|
||||
.regex(/^[A-Za-z_][A-Za-z0-9_.]*$/, 'Attribute name contains unsupported characters')
|
||||
.refine(
|
||||
(value) => !['__proto__', 'constructor', 'prototype'].includes(value),
|
||||
'Attribute name is reserved'
|
||||
)
|
||||
|
||||
const attributesSchema = z
|
||||
.record(attributeNameSchema, attributeValueSchema)
|
||||
.refine((attributes) => Object.keys(attributes).length <= MAX_ATTRIBUTES, {
|
||||
message: `Attributes cannot contain more than ${MAX_ATTRIBUTES} fields`,
|
||||
})
|
||||
|
||||
const COMMON_DOCUMENT_PROPERTIES = new Set(['Name', 'Number', 'Organization'])
|
||||
const patchAttributesSchema = attributesSchema
|
||||
.refine((attributes) => Object.keys(attributes).length > 0, {
|
||||
message: 'At least one document attribute is required',
|
||||
})
|
||||
.refine(
|
||||
(attributes) => !Object.keys(attributes).some((key) => COMMON_DOCUMENT_PROPERTIES.has(key)),
|
||||
'Name, Number, and Organization must be changed with the Update Common Properties operation and are not supported here'
|
||||
)
|
||||
|
||||
const commonPropertiesSchema = attributesSchema.refine(
|
||||
(properties) => Object.keys(properties).length > 0,
|
||||
{ message: 'At least one common property is required' }
|
||||
)
|
||||
|
||||
const credentialsSchema = z.object({
|
||||
baseUrl: baseUrlSchema,
|
||||
username: credentialSchema,
|
||||
password: credentialSchema,
|
||||
})
|
||||
|
||||
const createDocumentInputSchema = z.object({
|
||||
name: requiredTextSchema,
|
||||
containerOid: oidSchema,
|
||||
number: optionalTextSchema,
|
||||
title: optionalTextSchema,
|
||||
description: optionalTextSchema,
|
||||
folderOid: oidSchema.optional(),
|
||||
attributes: attributesSchema.optional(),
|
||||
})
|
||||
|
||||
const updateDocumentInputSchema = z.object({
|
||||
id: oidSchema,
|
||||
attributes: patchAttributesSchema,
|
||||
})
|
||||
|
||||
const documentOidsSchema = z
|
||||
.array(oidSchema)
|
||||
.min(1, 'At least one document OID is required')
|
||||
.max(MAX_BULK_DOCUMENTS, `At most ${MAX_BULK_DOCUMENTS} documents can be processed at once`)
|
||||
.refine((ids) => new Set(ids).size === ids.length, 'Document OIDs must be unique')
|
||||
|
||||
const commonMutationShapes = {
|
||||
createDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_create_document'),
|
||||
name: requiredTextSchema,
|
||||
containerOid: oidSchema,
|
||||
number: optionalTextSchema,
|
||||
title: optionalTextSchema,
|
||||
description: optionalTextSchema,
|
||||
folderOid: oidSchema.optional(),
|
||||
attributes: attributesSchema.optional(),
|
||||
}),
|
||||
createDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_create_documents'),
|
||||
documents: z
|
||||
.array(createDocumentInputSchema)
|
||||
.min(1, 'At least one document is required')
|
||||
.max(MAX_BULK_DOCUMENTS),
|
||||
}),
|
||||
updateDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_update_document'),
|
||||
documentOid: oidSchema,
|
||||
attributes: patchAttributesSchema,
|
||||
}),
|
||||
updateCommonProperties: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_update_common_properties'),
|
||||
documentOid: oidSchema,
|
||||
commonProperties: commonPropertiesSchema,
|
||||
}),
|
||||
updateDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_update_documents'),
|
||||
documents: z
|
||||
.array(updateDocumentInputSchema)
|
||||
.min(1, 'At least one document is required')
|
||||
.max(MAX_BULK_DOCUMENTS),
|
||||
}),
|
||||
deleteDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_delete_document'),
|
||||
documentOid: oidSchema,
|
||||
}),
|
||||
deleteDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_delete_documents'),
|
||||
documentOids: documentOidsSchema,
|
||||
}),
|
||||
checkOutDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_check_out_document'),
|
||||
documentOid: oidSchema,
|
||||
checkOutNote: optionalTextSchema,
|
||||
}),
|
||||
checkOutDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_check_out_documents'),
|
||||
documentOids: documentOidsSchema,
|
||||
checkOutNote: optionalTextSchema,
|
||||
}),
|
||||
checkInDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_check_in_document'),
|
||||
documentOid: oidSchema,
|
||||
checkInNote: optionalTextSchema,
|
||||
keepCheckedOut: z.boolean().optional(),
|
||||
checkOutNote: optionalTextSchema,
|
||||
}),
|
||||
checkInDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_check_in_documents'),
|
||||
documentOids: documentOidsSchema,
|
||||
checkInNote: optionalTextSchema,
|
||||
keepCheckedOut: z.boolean().optional(),
|
||||
checkOutNote: optionalTextSchema,
|
||||
}),
|
||||
undoCheckOutDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_undo_check_out_document'),
|
||||
documentOid: oidSchema,
|
||||
}),
|
||||
undoCheckOutDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_undo_check_out_documents'),
|
||||
documentOids: documentOidsSchema,
|
||||
}),
|
||||
reviseDocument: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_revise_document'),
|
||||
documentOid: oidSchema,
|
||||
versionId: optionalTextSchema,
|
||||
}),
|
||||
reviseDocuments: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_revise_documents'),
|
||||
documentOids: documentOidsSchema,
|
||||
}),
|
||||
setLifecycleState: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_set_lifecycle_state'),
|
||||
documentOid: oidSchema,
|
||||
stateValue: requiredTextSchema,
|
||||
stateDisplay: requiredTextSchema,
|
||||
}),
|
||||
updateSecurityLabels: credentialsSchema.extend({
|
||||
operation: z.literal('windchill_update_document_security_labels'),
|
||||
securityLabelUpdates: z
|
||||
.array(
|
||||
z.object({
|
||||
id: oidSchema,
|
||||
labels: z
|
||||
.record(attributeNameSchema, z.string().max(MAX_TEXT_LENGTH))
|
||||
.refine((labels) => Object.keys(labels).length > 0, {
|
||||
message: 'At least one security label is required',
|
||||
})
|
||||
.refine((labels) => Object.keys(labels).length <= MAX_ATTRIBUTES, {
|
||||
message: `Security labels cannot contain more than ${MAX_ATTRIBUTES} fields`,
|
||||
}),
|
||||
})
|
||||
)
|
||||
.min(1, 'At least one document security-label update is required')
|
||||
.max(MAX_BULK_DOCUMENTS),
|
||||
}),
|
||||
} as const
|
||||
|
||||
const downloadPrimarySchema = credentialsSchema.extend({
|
||||
operation: z.literal('windchill_download_primary_content'),
|
||||
documentOid: oidSchema,
|
||||
fileName: z.string().trim().min(1).max(255).optional(),
|
||||
})
|
||||
|
||||
const uploadPrimarySchema = credentialsSchema.extend({
|
||||
operation: z.literal('windchill_upload_primary_content'),
|
||||
documentOid: oidSchema,
|
||||
primaryFile: RawFileInputSchema,
|
||||
})
|
||||
|
||||
const downloadAttachmentSchema = credentialsSchema.extend({
|
||||
operation: z.literal('windchill_download_attachment'),
|
||||
documentOid: oidSchema,
|
||||
attachmentOid: oidSchema,
|
||||
fileName: z.string().trim().min(1).max(255).optional(),
|
||||
})
|
||||
|
||||
const uploadAttachmentsSchema = credentialsSchema.extend({
|
||||
operation: z.literal('windchill_upload_attachments'),
|
||||
documentOid: oidSchema,
|
||||
attachmentFiles: RawFileInputArraySchema.min(1, 'At least one attachment file is required').max(
|
||||
MAX_ATTACHMENT_FILES,
|
||||
`At most ${MAX_ATTACHMENT_FILES} attachments can be uploaded at once`
|
||||
),
|
||||
})
|
||||
|
||||
export const windchillOperationBodySchema = z.discriminatedUnion('operation', [
|
||||
commonMutationShapes.createDocument,
|
||||
commonMutationShapes.createDocuments,
|
||||
commonMutationShapes.updateDocument,
|
||||
commonMutationShapes.updateCommonProperties,
|
||||
commonMutationShapes.updateDocuments,
|
||||
commonMutationShapes.deleteDocument,
|
||||
commonMutationShapes.deleteDocuments,
|
||||
commonMutationShapes.checkOutDocument,
|
||||
commonMutationShapes.checkOutDocuments,
|
||||
commonMutationShapes.checkInDocument,
|
||||
commonMutationShapes.checkInDocuments,
|
||||
commonMutationShapes.undoCheckOutDocument,
|
||||
commonMutationShapes.undoCheckOutDocuments,
|
||||
commonMutationShapes.reviseDocument,
|
||||
commonMutationShapes.reviseDocuments,
|
||||
commonMutationShapes.setLifecycleState,
|
||||
commonMutationShapes.updateSecurityLabels,
|
||||
downloadPrimarySchema,
|
||||
uploadPrimarySchema,
|
||||
downloadAttachmentSchema,
|
||||
uploadAttachmentsSchema,
|
||||
])
|
||||
|
||||
/**
|
||||
* Response primitives are deliberately looser than their request counterparts: these values are
|
||||
* whatever the customer's Windchill returned, so re-applying the request-side OID regex or text
|
||||
* bounds would turn an already-committed mutation into an opaque parse failure.
|
||||
*/
|
||||
const returnedOidSchema = z.string().min(1)
|
||||
const nullableId = z.string().nullable()
|
||||
const nullableText = z.string().nullable()
|
||||
const documentSchema = z.object({
|
||||
id: nullableId,
|
||||
name: nullableText,
|
||||
number: nullableText,
|
||||
title: nullableText,
|
||||
description: nullableText,
|
||||
state: nullableText,
|
||||
stateDisplay: nullableText,
|
||||
versionId: nullableText,
|
||||
revision: nullableText,
|
||||
version: nullableText,
|
||||
latest: z.boolean().nullable(),
|
||||
checkoutState: nullableText,
|
||||
folderName: nullableText,
|
||||
folderLocation: nullableText,
|
||||
})
|
||||
|
||||
const affectedIdsSchema = z.array(returnedOidSchema)
|
||||
const singleMutationOperationSchema = z.enum([
|
||||
'windchill_create_document',
|
||||
'windchill_update_document',
|
||||
'windchill_update_common_properties',
|
||||
'windchill_check_out_document',
|
||||
'windchill_check_in_document',
|
||||
'windchill_undo_check_out_document',
|
||||
'windchill_revise_document',
|
||||
'windchill_set_lifecycle_state',
|
||||
])
|
||||
const bulkMutationOperationSchema = z.enum([
|
||||
'windchill_create_documents',
|
||||
'windchill_update_documents',
|
||||
'windchill_check_out_documents',
|
||||
'windchill_check_in_documents',
|
||||
'windchill_undo_check_out_documents',
|
||||
'windchill_revise_documents',
|
||||
'windchill_update_document_security_labels',
|
||||
])
|
||||
const deleteOperationSchema = z.enum(['windchill_delete_document', 'windchill_delete_documents'])
|
||||
const downloadOperationSchema = z.enum([
|
||||
'windchill_download_primary_content',
|
||||
'windchill_download_attachment',
|
||||
])
|
||||
const uploadOperationSchema = z.enum([
|
||||
'windchill_upload_primary_content',
|
||||
'windchill_upload_attachments',
|
||||
])
|
||||
|
||||
export const windchillOperationOutputSchema = z.discriminatedUnion('operation', [
|
||||
z.object({
|
||||
operation: singleMutationOperationSchema,
|
||||
affectedIds: affectedIdsSchema,
|
||||
document: documentSchema.optional(),
|
||||
}),
|
||||
z.object({
|
||||
operation: bulkMutationOperationSchema,
|
||||
affectedIds: affectedIdsSchema,
|
||||
documents: z.array(documentSchema).optional(),
|
||||
}),
|
||||
z.object({ operation: deleteOperationSchema, affectedIds: affectedIdsSchema }),
|
||||
z.object({
|
||||
operation: downloadOperationSchema,
|
||||
file: userFileSchema,
|
||||
fileName: z.string().min(1),
|
||||
mimeType: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
operation: uploadOperationSchema,
|
||||
affectedIds: affectedIdsSchema,
|
||||
uploadedFileNames: z.array(z.string().min(1)),
|
||||
}),
|
||||
])
|
||||
|
||||
export const windchillOperationResponseSchema = z.discriminatedUnion('success', [
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
output: windchillOperationOutputSchema,
|
||||
}),
|
||||
z.object({
|
||||
success: z.literal(false),
|
||||
error: z.string().min(1),
|
||||
}),
|
||||
])
|
||||
|
||||
export const windchillOperationContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/windchill',
|
||||
body: windchillOperationBodySchema,
|
||||
response: { mode: 'json', schema: windchillOperationResponseSchema },
|
||||
})
|
||||
|
||||
export type WindchillOperationBody = ContractBodyInput<typeof windchillOperationContract>
|
||||
export type WindchillOperationResponse = ContractJsonResponse<typeof windchillOperationContract>
|
||||
@@ -242,6 +242,7 @@ import {
|
||||
WebhookIcon,
|
||||
WhatsAppIcon,
|
||||
WikipediaIcon,
|
||||
WindchillIcon,
|
||||
WizaIcon,
|
||||
WordpressIcon,
|
||||
WorkdayIcon,
|
||||
@@ -512,6 +513,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
webflow: WebflowIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
wikipedia: WikipediaIcon,
|
||||
windchill: WindchillIcon,
|
||||
wiza: WizaIcon,
|
||||
wordpress: WordpressIcon,
|
||||
workday: WorkdayIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updatedAt": "2026-08-11",
|
||||
"updatedAt": "2026-08-12",
|
||||
"integrations": [
|
||||
{
|
||||
"type": "onepassword",
|
||||
@@ -21940,6 +21940,133 @@
|
||||
"integrationType": "search",
|
||||
"tags": ["knowledge-base"]
|
||||
},
|
||||
{
|
||||
"type": "windchill",
|
||||
"slug": "windchill",
|
||||
"name": "Windchill",
|
||||
"description": "Manage documents, revisions, and content in PTC Windchill",
|
||||
"longDescription": "Integrate PTC Windchill REST Services 2.7 document management into your workflow using Basic authentication. Read and update document metadata, perform version and lifecycle actions, and transfer primary content and attachments. Windchill OAuth deployments are not currently supported.",
|
||||
"bgColor": "#FFFFFF",
|
||||
"iconName": "WindchillIcon",
|
||||
"docsUrl": "https://docs.sim.ai/integrations/windchill",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Documents",
|
||||
"description": "List documents with an OData query, sorting, and pagination"
|
||||
},
|
||||
{
|
||||
"name": "Get Document",
|
||||
"description": "Get a WT.Document by OID"
|
||||
},
|
||||
{
|
||||
"name": "Get Document Structure",
|
||||
"description": "Retrieve recursive document usage links and their parent and child documents"
|
||||
},
|
||||
{
|
||||
"name": "Get Valid State Transitions",
|
||||
"description": "Get lifecycle states a document can transition to from its current state"
|
||||
},
|
||||
{
|
||||
"name": "Get Primary Content",
|
||||
"description": "Get primary-content metadata for a document"
|
||||
},
|
||||
{
|
||||
"name": "List Attachments",
|
||||
"description": "List attachment metadata for a document"
|
||||
},
|
||||
{
|
||||
"name": "Create Document",
|
||||
"description": "Create one WT.Document"
|
||||
},
|
||||
{
|
||||
"name": "Create Documents",
|
||||
"description": "Create several documents in one atomic Windchill request"
|
||||
},
|
||||
{
|
||||
"name": "Update Document",
|
||||
"description": "Update one document's editable attributes"
|
||||
},
|
||||
{
|
||||
"name": "Update Common Properties",
|
||||
"description": "Update a document's Name, Number, and other common properties"
|
||||
},
|
||||
{
|
||||
"name": "Update Documents",
|
||||
"description": "Update several documents' editable attributes in one atomic request"
|
||||
},
|
||||
{
|
||||
"name": "Delete Document",
|
||||
"description": "Delete one document"
|
||||
},
|
||||
{
|
||||
"name": "Delete Documents",
|
||||
"description": "Delete multiple documents atomically"
|
||||
},
|
||||
{
|
||||
"name": "Check Out Document",
|
||||
"description": "Check out one document"
|
||||
},
|
||||
{
|
||||
"name": "Check Out Documents",
|
||||
"description": "Check out multiple documents atomically"
|
||||
},
|
||||
{
|
||||
"name": "Check In Document",
|
||||
"description": "Check in one document"
|
||||
},
|
||||
{
|
||||
"name": "Check In Documents",
|
||||
"description": "Check in multiple documents atomically"
|
||||
},
|
||||
{
|
||||
"name": "Undo Check Out Document",
|
||||
"description": "Undo checkout for one document"
|
||||
},
|
||||
{
|
||||
"name": "Undo Check Out Documents",
|
||||
"description": "Undo checkout for multiple documents atomically"
|
||||
},
|
||||
{
|
||||
"name": "Revise Document",
|
||||
"description": "Create a new revision of one document"
|
||||
},
|
||||
{
|
||||
"name": "Revise Documents",
|
||||
"description": "Create new revisions of multiple documents atomically"
|
||||
},
|
||||
{
|
||||
"name": "Set Lifecycle State",
|
||||
"description": "Transition a document to a valid lifecycle state"
|
||||
},
|
||||
{
|
||||
"name": "Update Document Security Labels",
|
||||
"description": "Update installed security-label attributes for one or more documents"
|
||||
},
|
||||
{
|
||||
"name": "Download Primary Content",
|
||||
"description": "Download primary content into a canonical UserFile"
|
||||
},
|
||||
{
|
||||
"name": "Upload Primary Content",
|
||||
"description": "Upload a primary-content file to a document that has none"
|
||||
},
|
||||
{
|
||||
"name": "Download Attachment",
|
||||
"description": "Download a document attachment into a canonical UserFile"
|
||||
},
|
||||
{
|
||||
"name": "Upload Attachments",
|
||||
"description": "Upload one or more files as document attachments"
|
||||
}
|
||||
],
|
||||
"operationCount": 27,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
"category": "tools",
|
||||
"integrationType": "documents",
|
||||
"tags": ["content-management", "document-processing"]
|
||||
},
|
||||
{
|
||||
"type": "wiza",
|
||||
"slug": "wiza",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4768,6 +4768,35 @@ import {
|
||||
wikipediaRandomPageTool,
|
||||
wikipediaSearchTool,
|
||||
} from '@/tools/wikipedia'
|
||||
import {
|
||||
windchillCheckInDocumentsTool,
|
||||
windchillCheckInDocumentTool,
|
||||
windchillCheckOutDocumentsTool,
|
||||
windchillCheckOutDocumentTool,
|
||||
windchillCreateDocumentsTool,
|
||||
windchillCreateDocumentTool,
|
||||
windchillDeleteDocumentsTool,
|
||||
windchillDeleteDocumentTool,
|
||||
windchillDownloadAttachmentTool,
|
||||
windchillDownloadPrimaryContentTool,
|
||||
windchillGetDocumentStructureTool,
|
||||
windchillGetDocumentTool,
|
||||
windchillGetPrimaryContentTool,
|
||||
windchillGetValidStateTransitionsTool,
|
||||
windchillListAttachmentsTool,
|
||||
windchillListDocumentsTool,
|
||||
windchillReviseDocumentsTool,
|
||||
windchillReviseDocumentTool,
|
||||
windchillSetLifecycleStateTool,
|
||||
windchillUndoCheckOutDocumentsTool,
|
||||
windchillUndoCheckOutDocumentTool,
|
||||
windchillUpdateCommonPropertiesTool,
|
||||
windchillUpdateDocumentSecurityLabelsTool,
|
||||
windchillUpdateDocumentsTool,
|
||||
windchillUpdateDocumentTool,
|
||||
windchillUploadAttachmentsTool,
|
||||
windchillUploadPrimaryContentTool,
|
||||
} from '@/tools/windchill'
|
||||
import {
|
||||
wizaCompanyEnrichmentTool,
|
||||
wizaGetCreditsTool,
|
||||
@@ -8633,6 +8662,33 @@ export const tools: Record<string, ToolConfig> = {
|
||||
wikipedia_search: wikipediaSearchTool,
|
||||
wikipedia_content: wikipediaPageContentTool,
|
||||
wikipedia_random: wikipediaRandomPageTool,
|
||||
windchill_check_in_document: windchillCheckInDocumentTool,
|
||||
windchill_check_in_documents: windchillCheckInDocumentsTool,
|
||||
windchill_check_out_document: windchillCheckOutDocumentTool,
|
||||
windchill_check_out_documents: windchillCheckOutDocumentsTool,
|
||||
windchill_create_document: windchillCreateDocumentTool,
|
||||
windchill_create_documents: windchillCreateDocumentsTool,
|
||||
windchill_delete_document: windchillDeleteDocumentTool,
|
||||
windchill_delete_documents: windchillDeleteDocumentsTool,
|
||||
windchill_download_attachment: windchillDownloadAttachmentTool,
|
||||
windchill_download_primary_content: windchillDownloadPrimaryContentTool,
|
||||
windchill_get_document: windchillGetDocumentTool,
|
||||
windchill_get_document_structure: windchillGetDocumentStructureTool,
|
||||
windchill_get_primary_content: windchillGetPrimaryContentTool,
|
||||
windchill_get_valid_state_transitions: windchillGetValidStateTransitionsTool,
|
||||
windchill_list_attachments: windchillListAttachmentsTool,
|
||||
windchill_list_documents: windchillListDocumentsTool,
|
||||
windchill_revise_document: windchillReviseDocumentTool,
|
||||
windchill_revise_documents: windchillReviseDocumentsTool,
|
||||
windchill_set_lifecycle_state: windchillSetLifecycleStateTool,
|
||||
windchill_undo_check_out_document: windchillUndoCheckOutDocumentTool,
|
||||
windchill_undo_check_out_documents: windchillUndoCheckOutDocumentsTool,
|
||||
windchill_update_common_properties: windchillUpdateCommonPropertiesTool,
|
||||
windchill_update_document: windchillUpdateDocumentTool,
|
||||
windchill_update_document_security_labels: windchillUpdateDocumentSecurityLabelsTool,
|
||||
windchill_update_documents: windchillUpdateDocumentsTool,
|
||||
windchill_upload_attachments: windchillUploadAttachmentsTool,
|
||||
windchill_upload_primary_content: windchillUploadPrimaryContentTool,
|
||||
wiza_company_enrichment: wizaCompanyEnrichmentTool,
|
||||
wiza_get_credits: wizaGetCreditsTool,
|
||||
wiza_individual_reveal: wizaIndividualRevealTool,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCheckInDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_check_in_document',
|
||||
name: 'Windchill Check In Document',
|
||||
description: 'Check in one document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
checkInNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Check-in note',
|
||||
},
|
||||
keepCheckedOut: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Keep the document checked out after checking it in',
|
||||
},
|
||||
checkOutNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Checkout note',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_check_in_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_check_in_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCheckInDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_check_in_documents',
|
||||
name: 'Windchill Check In Documents',
|
||||
description: 'Check in multiple documents atomically',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOids: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OIDs to process atomically',
|
||||
},
|
||||
checkInNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Check-in note',
|
||||
},
|
||||
keepCheckedOut: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Keep the document checked out after checking it in',
|
||||
},
|
||||
checkOutNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Checkout note',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_check_in_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_check_in_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCheckOutDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_check_out_document',
|
||||
name: 'Windchill Check Out Document',
|
||||
description: 'Check out one document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
checkOutNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Checkout note',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_check_out_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_check_out_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCheckOutDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_check_out_documents',
|
||||
name: 'Windchill Check Out Documents',
|
||||
description: 'Check out multiple documents atomically',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOids: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OIDs to process atomically',
|
||||
},
|
||||
checkOutNote: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Checkout note',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_check_out_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_check_out_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCreateDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_create_document',
|
||||
name: 'Windchill Create Document',
|
||||
description: 'Create one WT.Document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document name',
|
||||
},
|
||||
containerOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Container OID in which to create the document',
|
||||
},
|
||||
number: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional document number when manual numbering is enabled',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document description',
|
||||
},
|
||||
folderOid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional folder OID for the new document',
|
||||
},
|
||||
attributes: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional installed Windchill document attributes as a JSON object',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_create_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_create_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillCreateDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_create_documents',
|
||||
name: 'Windchill Create Documents',
|
||||
description: 'Create several documents in one atomic Windchill request',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documents: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document inputs as a JSON array; each item requires name and containerOid',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_create_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_create_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_AFFECTED_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillDeleteDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_delete_document',
|
||||
name: 'Windchill Delete Document',
|
||||
description: 'Delete one document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_delete_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_delete_document', response),
|
||||
outputs: WINDCHILL_AFFECTED_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_AFFECTED_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillDeleteDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_delete_documents',
|
||||
name: 'Windchill Delete Documents',
|
||||
description: 'Delete multiple documents atomically',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOids: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OIDs to process atomically',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_delete_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_delete_documents', response),
|
||||
outputs: WINDCHILL_AFFECTED_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_FILE_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillDownloadAttachmentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_download_attachment',
|
||||
name: 'Windchill Download Attachment',
|
||||
description: 'Download a document attachment into a canonical UserFile',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
attachmentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Windchill attachment content OID',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional downloaded file name override',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_download_attachment', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_download_attachment', response),
|
||||
outputs: WINDCHILL_FILE_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_FILE_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillDownloadPrimaryContentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_download_primary_content',
|
||||
name: 'Windchill Download Primary Content',
|
||||
description: 'Download primary content into a canonical UserFile',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional downloaded file name override',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_download_primary_content', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_download_primary_content', response),
|
||||
outputs: WINDCHILL_FILE_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_DOCUMENT_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillGetDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_get_document',
|
||||
name: 'Windchill Get Document',
|
||||
description: 'Get a WT.Document by OID',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated normalized document properties to return',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_get_document', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) => transformWindchillDirectRead('windchill_get_document', response),
|
||||
outputs: WINDCHILL_DOCUMENT_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_STRUCTURE_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillGetDocumentStructureTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_get_document_structure',
|
||||
name: 'Windchill Get Document Structure',
|
||||
description: 'Retrieve recursive document usage links and their parent and child documents',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
structureDepth: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document structure expansion depth, from 1 to 3',
|
||||
},
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Verified @odata.nextLink from a previous structure response',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_get_document_structure', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillDirectRead('windchill_get_document_structure', response),
|
||||
outputs: WINDCHILL_STRUCTURE_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_PRIMARY_CONTENT_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillGetPrimaryContentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_get_primary_content',
|
||||
name: 'Windchill Get Primary Content',
|
||||
description: 'Get primary-content metadata for a document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_get_primary_content', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillDirectRead('windchill_get_primary_content', response),
|
||||
outputs: WINDCHILL_PRIMARY_CONTENT_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_STATE_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillGetValidStateTransitionsTool: ToolConfig<WindchillParams, WindchillResponse> =
|
||||
{
|
||||
id: 'windchill_get_valid_state_transitions',
|
||||
name: 'Windchill Get Valid State Transitions',
|
||||
description: 'Get lifecycle states a document can transition to from its current state',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_get_valid_state_transitions', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillDirectRead('windchill_get_valid_state_transitions', response),
|
||||
outputs: WINDCHILL_STATE_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export { windchillCheckInDocumentTool } from '@/tools/windchill/check_in_document'
|
||||
export { windchillCheckInDocumentsTool } from '@/tools/windchill/check_in_documents'
|
||||
export { windchillCheckOutDocumentTool } from '@/tools/windchill/check_out_document'
|
||||
export { windchillCheckOutDocumentsTool } from '@/tools/windchill/check_out_documents'
|
||||
export { windchillCreateDocumentTool } from '@/tools/windchill/create_document'
|
||||
export { windchillCreateDocumentsTool } from '@/tools/windchill/create_documents'
|
||||
export { windchillDeleteDocumentTool } from '@/tools/windchill/delete_document'
|
||||
export { windchillDeleteDocumentsTool } from '@/tools/windchill/delete_documents'
|
||||
export { windchillDownloadAttachmentTool } from '@/tools/windchill/download_attachment'
|
||||
export { windchillDownloadPrimaryContentTool } from '@/tools/windchill/download_primary_content'
|
||||
export { windchillGetDocumentTool } from '@/tools/windchill/get_document'
|
||||
export { windchillGetDocumentStructureTool } from '@/tools/windchill/get_document_structure'
|
||||
export { windchillGetPrimaryContentTool } from '@/tools/windchill/get_primary_content'
|
||||
export { windchillGetValidStateTransitionsTool } from '@/tools/windchill/get_valid_state_transitions'
|
||||
export { windchillListAttachmentsTool } from '@/tools/windchill/list_attachments'
|
||||
export { windchillListDocumentsTool } from '@/tools/windchill/list_documents'
|
||||
export { windchillReviseDocumentTool } from '@/tools/windchill/revise_document'
|
||||
export { windchillReviseDocumentsTool } from '@/tools/windchill/revise_documents'
|
||||
export { windchillSetLifecycleStateTool } from '@/tools/windchill/set_lifecycle_state'
|
||||
export * from '@/tools/windchill/types'
|
||||
export { windchillUndoCheckOutDocumentTool } from '@/tools/windchill/undo_check_out_document'
|
||||
export { windchillUndoCheckOutDocumentsTool } from '@/tools/windchill/undo_check_out_documents'
|
||||
export { windchillUpdateCommonPropertiesTool } from '@/tools/windchill/update_common_properties'
|
||||
export { windchillUpdateDocumentTool } from '@/tools/windchill/update_document'
|
||||
export { windchillUpdateDocumentSecurityLabelsTool } from '@/tools/windchill/update_document_security_labels'
|
||||
export { windchillUpdateDocumentsTool } from '@/tools/windchill/update_documents'
|
||||
export { windchillUploadAttachmentsTool } from '@/tools/windchill/upload_attachments'
|
||||
export { windchillUploadPrimaryContentTool } from '@/tools/windchill/upload_primary_content'
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_ATTACHMENTS_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillListAttachmentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_list_attachments',
|
||||
name: 'Windchill List Attachments',
|
||||
description: 'List attachment metadata for a document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Verified @odata.nextLink from a previous attachment response',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_list_attachments', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillDirectRead('windchill_list_attachments', response),
|
||||
outputs: WINDCHILL_ATTACHMENTS_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_LIST_DOCUMENTS_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillReadUrl,
|
||||
transformWindchillDirectRead,
|
||||
windchillReadHeaders,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillListDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_list_documents',
|
||||
name: 'Windchill List Documents',
|
||||
description: 'List documents with an OData query, sorting, and pagination',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated normalized document properties to return',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $filter expression',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $orderby expression',
|
||||
},
|
||||
top: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum documents in the OData result set ($top), from 1 to 2000',
|
||||
},
|
||||
skip: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Documents to skip',
|
||||
},
|
||||
count: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Ask Windchill to include the total matching count',
|
||||
},
|
||||
latestVersion: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return only the latest version of matching documents',
|
||||
},
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Verified @odata.nextLink from a previous list response',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => buildWindchillReadUrl('windchill_list_documents', params),
|
||||
method: 'GET',
|
||||
headers: windchillReadHeaders,
|
||||
stripAuthOnRedirect: true,
|
||||
retry: { enabled: true, maxRetries: 2, retryIdempotentOnly: true },
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillDirectRead('windchill_list_documents', response),
|
||||
outputs: WINDCHILL_LIST_DOCUMENTS_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.unmock('@/tools/registry')
|
||||
|
||||
import { tools } from '@/tools/registry'
|
||||
import { WINDCHILL_OPERATIONS } from '@/tools/windchill/types'
|
||||
|
||||
describe('Windchill registry', () => {
|
||||
it('registers every operation in the global tool registry', () => {
|
||||
for (const operation of WINDCHILL_OPERATIONS) {
|
||||
expect(tools[operation]?.id).toBe(operation)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillReviseDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_revise_document',
|
||||
name: 'Windchill Revise Document',
|
||||
description: 'Create a new revision of one document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
versionId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional target revision identifier when override-on-revise is enabled',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_revise_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_revise_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillReviseDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_revise_documents',
|
||||
name: 'Windchill Revise Documents',
|
||||
description: 'Create new revisions of multiple documents atomically',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOids: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OIDs to process atomically',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_revise_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_revise_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillSetLifecycleStateTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_set_lifecycle_state',
|
||||
name: 'Windchill Set Lifecycle State',
|
||||
description: 'Transition a document to a valid lifecycle state',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
stateValue: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Internal value of the target lifecycle state',
|
||||
},
|
||||
stateDisplay: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Display value of the target lifecycle state',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_set_lifecycle_state', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_set_lifecycle_state', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolOutputProperty, ToolResponse, WorkflowToolExecutionContext } from '@/tools/types'
|
||||
|
||||
export const WINDCHILL_DOCUMENT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Windchill object identifier', nullable: true },
|
||||
name: { type: 'string', description: 'Document name', nullable: true },
|
||||
number: { type: 'string', description: 'Document number', nullable: true },
|
||||
title: { type: 'string', description: 'Document title', nullable: true },
|
||||
description: { type: 'string', description: 'Document description', nullable: true },
|
||||
state: { type: 'string', description: 'Internal life cycle state value', nullable: true },
|
||||
stateDisplay: { type: 'string', description: 'Displayed life cycle state value', nullable: true },
|
||||
versionId: { type: 'string', description: 'Version identifier', nullable: true },
|
||||
revision: { type: 'string', description: 'Revision identifier', nullable: true },
|
||||
version: { type: 'string', description: 'Version and iteration', nullable: true },
|
||||
latest: { type: 'boolean', description: 'Whether this is the latest version', nullable: true },
|
||||
checkoutState: { type: 'string', description: 'Checkout state', nullable: true },
|
||||
folderName: { type: 'string', description: 'Folder name', nullable: true },
|
||||
folderLocation: { type: 'string', description: 'Folder path', nullable: true },
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_CONTENT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Content object identifier', nullable: true },
|
||||
fileName: { type: 'string', description: 'Content file name', nullable: true },
|
||||
description: { type: 'string', description: 'Content description', nullable: true },
|
||||
format: { type: 'string', description: 'Windchill content format', nullable: true },
|
||||
mimeType: { type: 'string', description: 'Content MIME type', nullable: true },
|
||||
fileSize: { type: 'number', description: 'Content size in bytes', nullable: true },
|
||||
contentType: {
|
||||
type: 'string',
|
||||
description: 'Windchill OData content entity type',
|
||||
nullable: true,
|
||||
},
|
||||
displayName: { type: 'string', description: 'Displayed content name', nullable: true },
|
||||
urlLocation: { type: 'string', description: 'URL-data location', nullable: true },
|
||||
externalLocation: { type: 'string', description: 'External-storage location', nullable: true },
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_DOCUMENT_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
document: {
|
||||
type: 'object',
|
||||
description: 'Windchill document',
|
||||
properties: WINDCHILL_DOCUMENT_PROPERTIES,
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_LIST_DOCUMENTS_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
documents: {
|
||||
type: 'array',
|
||||
description: 'Windchill documents',
|
||||
items: { type: 'object', properties: WINDCHILL_DOCUMENT_PROPERTIES },
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'OData pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of items returned in this page' },
|
||||
totalCount: { type: 'number', description: 'Total matching items', nullable: true },
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
description: 'URL returned by Windchill for the next page',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_USAGE_LINK_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Document usage link OID', nullable: true },
|
||||
parent: {
|
||||
type: 'object',
|
||||
description: 'Parent document',
|
||||
nullable: true,
|
||||
properties: WINDCHILL_DOCUMENT_PROPERTIES,
|
||||
},
|
||||
child: {
|
||||
type: 'object',
|
||||
description: 'Child document',
|
||||
nullable: true,
|
||||
properties: WINDCHILL_DOCUMENT_PROPERTIES,
|
||||
},
|
||||
children: {
|
||||
type: 'array',
|
||||
description: 'Nested child usage links with the same recursive shape',
|
||||
items: { type: 'json' },
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_USAGE_LINK_OUTPUT = {
|
||||
type: 'object',
|
||||
description: 'Document usage link',
|
||||
properties: WINDCHILL_USAGE_LINK_PROPERTIES,
|
||||
} as const satisfies ToolOutputProperty
|
||||
|
||||
export const WINDCHILL_STRUCTURE_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
structure: {
|
||||
type: 'array',
|
||||
description: 'Document usage links, including recursively expanded child links',
|
||||
items: WINDCHILL_USAGE_LINK_OUTPUT,
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'OData pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of items returned in this page' },
|
||||
totalCount: { type: 'number', description: 'Total matching items', nullable: true },
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
description: 'URL returned by Windchill for the next page',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_STATE_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
states: {
|
||||
type: 'array',
|
||||
description: 'Valid lifecycle transitions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: { type: 'string', description: 'Internal state value', nullable: true },
|
||||
display: { type: 'string', description: 'Displayed state value', nullable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_PRIMARY_CONTENT_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
content: {
|
||||
type: 'object',
|
||||
description: 'Primary-content metadata',
|
||||
nullable: true,
|
||||
properties: WINDCHILL_CONTENT_PROPERTIES,
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_ATTACHMENTS_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
attachments: {
|
||||
type: 'array',
|
||||
description: 'Document attachments',
|
||||
items: { type: 'object', properties: WINDCHILL_CONTENT_PROPERTIES },
|
||||
},
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
description: 'OData pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of items returned in this page' },
|
||||
totalCount: { type: 'number', description: 'Total matching items', nullable: true },
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
description: 'URL returned by Windchill for the next page',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_AFFECTED_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
affectedIds: {
|
||||
type: 'array',
|
||||
description: 'Document identifiers affected by the operation',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_SINGLE_MUTATION_OUTPUTS = {
|
||||
...WINDCHILL_AFFECTED_OUTPUTS,
|
||||
document: {
|
||||
type: 'object',
|
||||
description: 'Document returned by Windchill when the operation returns one',
|
||||
optional: true,
|
||||
properties: WINDCHILL_DOCUMENT_PROPERTIES,
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_BULK_MUTATION_OUTPUTS = {
|
||||
...WINDCHILL_AFFECTED_OUTPUTS,
|
||||
documents: {
|
||||
type: 'array',
|
||||
description: 'Documents returned by Windchill when the operation returns them',
|
||||
optional: true,
|
||||
items: { type: 'object', properties: WINDCHILL_DOCUMENT_PROPERTIES },
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_FILE_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
file: { type: 'file', description: 'Downloaded content stored as a canonical UserFile' },
|
||||
fileName: { type: 'string', description: 'Downloaded file name' },
|
||||
mimeType: { type: 'string', description: 'Downloaded content MIME type' },
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_UPLOAD_OUTPUTS = {
|
||||
operation: { type: 'string', description: 'Windchill operation that was executed' },
|
||||
affectedIds: {
|
||||
type: 'array',
|
||||
description: 'Document identifiers affected by the upload',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
uploadedFileNames: {
|
||||
type: 'array',
|
||||
description: 'Names of files accepted by Windchill',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
} as const satisfies Record<string, ToolOutputProperty>
|
||||
|
||||
export const WINDCHILL_OPERATIONS = [
|
||||
'windchill_list_documents',
|
||||
'windchill_get_document',
|
||||
'windchill_get_document_structure',
|
||||
'windchill_get_valid_state_transitions',
|
||||
'windchill_get_primary_content',
|
||||
'windchill_list_attachments',
|
||||
'windchill_create_document',
|
||||
'windchill_create_documents',
|
||||
'windchill_update_document',
|
||||
'windchill_update_common_properties',
|
||||
'windchill_update_documents',
|
||||
'windchill_delete_document',
|
||||
'windchill_delete_documents',
|
||||
'windchill_check_out_document',
|
||||
'windchill_check_out_documents',
|
||||
'windchill_check_in_document',
|
||||
'windchill_check_in_documents',
|
||||
'windchill_undo_check_out_document',
|
||||
'windchill_undo_check_out_documents',
|
||||
'windchill_revise_document',
|
||||
'windchill_revise_documents',
|
||||
'windchill_set_lifecycle_state',
|
||||
'windchill_update_document_security_labels',
|
||||
'windchill_download_primary_content',
|
||||
'windchill_upload_primary_content',
|
||||
'windchill_download_attachment',
|
||||
'windchill_upload_attachments',
|
||||
] as const
|
||||
|
||||
export type WindchillOperation = (typeof WINDCHILL_OPERATIONS)[number]
|
||||
|
||||
export interface WindchillDocument {
|
||||
id: string | null
|
||||
name: string | null
|
||||
number: string | null
|
||||
title: string | null
|
||||
description: string | null
|
||||
state: string | null
|
||||
stateDisplay: string | null
|
||||
versionId: string | null
|
||||
revision: string | null
|
||||
version: string | null
|
||||
latest: boolean | null
|
||||
checkoutState: string | null
|
||||
folderName: string | null
|
||||
folderLocation: string | null
|
||||
}
|
||||
|
||||
export interface WindchillContent {
|
||||
id: string | null
|
||||
fileName: string | null
|
||||
description: string | null
|
||||
format: string | null
|
||||
mimeType: string | null
|
||||
fileSize: number | null
|
||||
contentType: string | null
|
||||
displayName: string | null
|
||||
urlLocation: string | null
|
||||
externalLocation: string | null
|
||||
}
|
||||
|
||||
export interface WindchillStateTransition {
|
||||
value: string | null
|
||||
display: string | null
|
||||
}
|
||||
|
||||
export interface WindchillDocumentUsageLink {
|
||||
id: string | null
|
||||
parent: WindchillDocument | null
|
||||
child: WindchillDocument | null
|
||||
children: WindchillDocumentUsageLink[]
|
||||
}
|
||||
|
||||
export interface WindchillPageInfo {
|
||||
count: number
|
||||
totalCount: number | null
|
||||
nextLink: string | null
|
||||
}
|
||||
|
||||
export type WindchillAttributeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| WindchillAttributeValue[]
|
||||
| { [key: string]: WindchillAttributeValue }
|
||||
|
||||
export interface WindchillCreateDocumentInput {
|
||||
name: string
|
||||
containerOid: string
|
||||
number?: string
|
||||
title?: string
|
||||
description?: string
|
||||
folderOid?: string
|
||||
attributes?: Record<string, WindchillAttributeValue>
|
||||
}
|
||||
|
||||
export interface WindchillUpdateDocumentInput {
|
||||
id: string
|
||||
attributes: Record<string, WindchillAttributeValue>
|
||||
}
|
||||
|
||||
export interface WindchillSecurityLabelInput {
|
||||
id: string
|
||||
labels: Record<string, string>
|
||||
}
|
||||
|
||||
export interface WindchillParams {
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
documentOid?: string
|
||||
documentOids?: string[]
|
||||
attachmentOid?: string
|
||||
name?: string
|
||||
number?: string
|
||||
title?: string
|
||||
description?: string
|
||||
containerOid?: string
|
||||
folderOid?: string
|
||||
attributes?: Record<string, WindchillAttributeValue>
|
||||
commonProperties?: Record<string, WindchillAttributeValue>
|
||||
documents?: WindchillCreateDocumentInput[] | WindchillUpdateDocumentInput[]
|
||||
checkOutNote?: string
|
||||
checkInNote?: string
|
||||
keepCheckedOut?: boolean
|
||||
versionId?: string
|
||||
stateValue?: string
|
||||
stateDisplay?: string
|
||||
securityLabelUpdates?: WindchillSecurityLabelInput[]
|
||||
primaryFile?: RawFileInput
|
||||
attachmentFiles?: RawFileInput[]
|
||||
fileName?: string
|
||||
select?: string
|
||||
filter?: string
|
||||
orderBy?: string
|
||||
top?: number
|
||||
skip?: number
|
||||
count?: boolean
|
||||
latestVersion?: boolean
|
||||
nextLink?: string
|
||||
structureDepth?: number
|
||||
_context?: WorkflowToolExecutionContext
|
||||
}
|
||||
|
||||
export interface WindchillOutput {
|
||||
operation: WindchillOperation
|
||||
document?: WindchillDocument
|
||||
documents?: WindchillDocument[]
|
||||
structure?: WindchillDocumentUsageLink[]
|
||||
states?: WindchillStateTransition[]
|
||||
content?: WindchillContent | null
|
||||
attachments?: WindchillContent[]
|
||||
pageInfo?: WindchillPageInfo
|
||||
affectedIds?: string[]
|
||||
uploadedFileNames?: string[]
|
||||
file?: UserFile
|
||||
fileName?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export interface WindchillResponse extends ToolResponse {
|
||||
output: WindchillOutput
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUndoCheckOutDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_undo_check_out_document',
|
||||
name: 'Windchill Undo Check Out Document',
|
||||
description: 'Undo checkout for one document',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_undo_check_out_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_undo_check_out_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUndoCheckOutDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_undo_check_out_documents',
|
||||
name: 'Windchill Undo Check Out Documents',
|
||||
description: 'Undo checkout for multiple documents atomically',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOids: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OIDs to process atomically',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_undo_check_out_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_undo_check_out_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUpdateCommonPropertiesTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_update_common_properties',
|
||||
name: 'Windchill Update Common Properties',
|
||||
description: "Update a document's Name, Number, and other common properties",
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'WT.Document OID, for example OR:wt.doc.WTDocument:48796581. The document must not be checked out.',
|
||||
},
|
||||
commonProperties: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Common properties as a JSON object, for example {"Name":"New name","Number":"NEW-001"}. Enumerated properties take a value/display pair.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_update_common_properties', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_update_common_properties', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUpdateDocumentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_update_document',
|
||||
name: 'Windchill Update Document',
|
||||
description: "Update one document's editable attributes",
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
attributes: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Editable attributes as a JSON object. Name, Number, and Organization require the Update Common Properties operation and are not supported here.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_update_document', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_update_document', response),
|
||||
outputs: WINDCHILL_SINGLE_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUpdateDocumentSecurityLabelsTool: ToolConfig<
|
||||
WindchillParams,
|
||||
WindchillResponse
|
||||
> = {
|
||||
id: 'windchill_update_document_security_labels',
|
||||
name: 'Windchill Update Document Security Labels',
|
||||
description: 'Update installed security-label attributes for one or more documents',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
securityLabelUpdates: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of document IDs and installed security-label values',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) =>
|
||||
buildWindchillInternalBody('windchill_update_document_security_labels', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_update_document_security_labels', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUpdateDocumentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_update_documents',
|
||||
name: 'Windchill Update Documents',
|
||||
description: "Update several documents' editable attributes in one atomic request",
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documents: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Document updates as a JSON array; each item requires id and the editable attributes to set. Name, Number, and Organization are not supported.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_update_documents', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_update_documents', response),
|
||||
outputs: WINDCHILL_BULK_MUTATION_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_UPLOAD_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUploadAttachmentsTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_upload_attachments',
|
||||
name: 'Windchill Upload Attachments',
|
||||
description: 'Upload one or more files as document attachments',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
attachmentFiles: {
|
||||
type: 'file[]',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Attachment files to upload',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_upload_attachments', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_upload_attachments', response),
|
||||
outputs: WINDCHILL_UPLOAD_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
WINDCHILL_UPLOAD_OUTPUTS,
|
||||
type WindchillParams,
|
||||
type WindchillResponse,
|
||||
} from '@/tools/windchill/types'
|
||||
import {
|
||||
buildWindchillInternalBody,
|
||||
transformWindchillInternalResponse,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
export const windchillUploadPrimaryContentTool: ToolConfig<WindchillParams, WindchillResponse> = {
|
||||
id: 'windchill_upload_primary_content',
|
||||
name: 'Windchill Upload Primary Content',
|
||||
description: 'Upload a primary-content file to a document that has none',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Windchill service-account password',
|
||||
},
|
||||
documentOid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'WT.Document OID, for example OR:wt.doc.WTDocument:48796581',
|
||||
},
|
||||
primaryFile: {
|
||||
type: 'file',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Primary content file to upload',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/windchill',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => buildWindchillInternalBody('windchill_upload_primary_content', params),
|
||||
internalAuth: 'executor_delegation',
|
||||
},
|
||||
transformResponse: (response) =>
|
||||
transformWindchillInternalResponse('windchill_upload_primary_content', response),
|
||||
outputs: WINDCHILL_UPLOAD_OUTPUTS,
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import { generateShortId } from '@sim/utils/id'
|
||||
import { isRecordLike } from '@sim/utils/object'
|
||||
import {
|
||||
MAX_JSON_API_RESPONSE_BYTES,
|
||||
type SecureFetchResponse,
|
||||
secureFetchWithValidation,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import type { WindchillParams } from '@/tools/windchill/types'
|
||||
import {
|
||||
createBasicAuthHeader,
|
||||
encodeWindchillOid,
|
||||
normalizeServiceRoot,
|
||||
sanitizeWindchillError,
|
||||
} from '@/tools/windchill/utils'
|
||||
|
||||
const WINDCHILL_CONTROL_RESPONSE_BYTES = 2 * 1024 * 1024
|
||||
const WINDCHILL_TIMEOUT_MS = 60_000
|
||||
|
||||
interface WindchillSession {
|
||||
nonceHeader: string
|
||||
nonceValue: string
|
||||
cookie: string | null
|
||||
}
|
||||
|
||||
export interface WindchillUploadFile {
|
||||
name: string
|
||||
mimeType: string
|
||||
size: number
|
||||
buffer: Buffer
|
||||
}
|
||||
|
||||
export class WindchillProviderError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'WindchillProviderError'
|
||||
}
|
||||
}
|
||||
|
||||
function cookieHeader(response: SecureFetchResponse): string | null {
|
||||
const cookies = response.headers
|
||||
.getSetCookie()
|
||||
.map((cookie) => cookie.split(';', 1)[0]?.trim())
|
||||
.filter((cookie): cookie is string => Boolean(cookie))
|
||||
return cookies.length > 0 ? cookies.join('; ') : null
|
||||
}
|
||||
|
||||
async function responseBody(
|
||||
response: SecureFetchResponse
|
||||
): Promise<{ data: unknown; invalidJson: boolean }> {
|
||||
const text = await response.text()
|
||||
if (!text.trim()) return { data: null, invalidJson: false }
|
||||
try {
|
||||
return { data: JSON.parse(text) as unknown, invalidJson: false }
|
||||
} catch {
|
||||
return { data: text, invalidJson: true }
|
||||
}
|
||||
}
|
||||
|
||||
function providerMessage(data: unknown, response: SecureFetchResponse): string {
|
||||
if (typeof data === 'string' && data.trim()) return data.trim()
|
||||
if (isRecordLike(data)) {
|
||||
if (typeof data.message === 'string' && data.message.trim()) return data.message.trim()
|
||||
if (isRecordLike(data.error)) {
|
||||
if (typeof data.error.message === 'string' && data.error.message.trim()) {
|
||||
return data.error.message.trim()
|
||||
}
|
||||
if (
|
||||
isRecordLike(data.error.message) &&
|
||||
typeof data.error.message.value === 'string' &&
|
||||
data.error.message.value.trim()
|
||||
) {
|
||||
return data.error.message.value.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
return `Windchill request failed with status ${response.status}`
|
||||
}
|
||||
|
||||
async function checkedBody(response: SecureFetchResponse): Promise<unknown> {
|
||||
const { data, invalidJson } = await responseBody(response)
|
||||
if (!response.ok) {
|
||||
throw new WindchillProviderError(
|
||||
sanitizeWindchillError(providerMessage(data, response)),
|
||||
response.status
|
||||
)
|
||||
}
|
||||
if (invalidJson) {
|
||||
throw new WindchillProviderError(
|
||||
`Windchill returned invalid JSON with status ${response.status}`,
|
||||
502
|
||||
)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
function ptcRoot(baseUrl: string): string {
|
||||
return normalizeServiceRoot(baseUrl).replace(/\/v\d+$/i, '')
|
||||
}
|
||||
|
||||
export function windchillDocumentUrl(baseUrl: string, documentOid: string): string {
|
||||
return `${normalizeServiceRoot(baseUrl)}/DocMgmt/Documents('${encodeWindchillOid(documentOid)}')`
|
||||
}
|
||||
|
||||
export async function createWindchillSession(
|
||||
params: Pick<WindchillParams, 'baseUrl' | 'username' | 'password'>,
|
||||
signal?: AbortSignal
|
||||
): Promise<WindchillSession> {
|
||||
const response = await secureFetchWithValidation(
|
||||
`${ptcRoot(params.baseUrl)}/PTC/GetCSRFToken()`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: createBasicAuthHeader(params.username, params.password),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
maxRedirects: 0,
|
||||
maxResponseBytes: WINDCHILL_CONTROL_RESPONSE_BYTES,
|
||||
timeout: WINDCHILL_TIMEOUT_MS,
|
||||
signal,
|
||||
},
|
||||
'baseUrl'
|
||||
)
|
||||
const data = await checkedBody(response)
|
||||
if (!isRecordLike(data)) {
|
||||
throw new WindchillProviderError('Windchill returned an invalid CSRF response', 502)
|
||||
}
|
||||
const nonceHeader = data.NonceKey
|
||||
const nonceValue = data.NonceValue
|
||||
if (
|
||||
typeof nonceHeader !== 'string' ||
|
||||
!/^[A-Za-z0-9_-]{1,128}$/.test(nonceHeader) ||
|
||||
typeof nonceValue !== 'string' ||
|
||||
nonceValue.length === 0 ||
|
||||
nonceValue.length > 8192
|
||||
) {
|
||||
throw new WindchillProviderError('Windchill returned an invalid CSRF token', 502)
|
||||
}
|
||||
return { nonceHeader, nonceValue, cookie: cookieHeader(response) }
|
||||
}
|
||||
|
||||
export async function windchillMutationRequest({
|
||||
params,
|
||||
session,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
signal,
|
||||
}: {
|
||||
params: Pick<WindchillParams, 'username' | 'password'>
|
||||
session: WindchillSession
|
||||
url: string
|
||||
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
body?: unknown
|
||||
signal?: AbortSignal
|
||||
}): Promise<unknown> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: createBasicAuthHeader(params.username, params.password),
|
||||
Accept: 'application/json',
|
||||
[session.nonceHeader]: session.nonceValue,
|
||||
}
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||||
if (session.cookie) headers.Cookie = session.cookie
|
||||
|
||||
const response = await secureFetchWithValidation(
|
||||
url,
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
maxRedirects: 0,
|
||||
maxResponseBytes: MAX_JSON_API_RESPONSE_BYTES,
|
||||
timeout: WINDCHILL_TIMEOUT_MS,
|
||||
signal,
|
||||
},
|
||||
'baseUrl'
|
||||
)
|
||||
return checkedBody(response)
|
||||
}
|
||||
|
||||
function stageOneDescriptor(data: unknown): {
|
||||
replicaUrl: string
|
||||
masterUrl: string
|
||||
streamIds: Array<string | number>
|
||||
fileNames: Array<string | number>
|
||||
} {
|
||||
const descriptor = isRecordLike(data) && Array.isArray(data.value) ? data.value[0] : null
|
||||
if (!isRecordLike(descriptor)) {
|
||||
throw new WindchillProviderError('Windchill upload Stage 1 returned no cache descriptor', 502)
|
||||
}
|
||||
if (
|
||||
typeof descriptor.ReplicaUrl !== 'string' ||
|
||||
typeof descriptor.MasterUrl !== 'string' ||
|
||||
!Array.isArray(descriptor.StreamIds) ||
|
||||
!Array.isArray(descriptor.FileNames)
|
||||
) {
|
||||
throw new WindchillProviderError(
|
||||
'Windchill upload Stage 1 returned an invalid cache descriptor',
|
||||
502
|
||||
)
|
||||
}
|
||||
if (descriptor.StreamIds.length !== descriptor.FileNames.length) {
|
||||
throw new WindchillProviderError(
|
||||
'Windchill upload Stage 1 returned mismatched file identifiers',
|
||||
502
|
||||
)
|
||||
}
|
||||
return {
|
||||
replicaUrl: descriptor.ReplicaUrl,
|
||||
masterUrl: descriptor.MasterUrl,
|
||||
streamIds: descriptor.StreamIds.filter(
|
||||
(value): value is string | number => typeof value === 'string' || typeof value === 'number'
|
||||
),
|
||||
fileNames: descriptor.FileNames.filter(
|
||||
(value): value is string | number => typeof value === 'string' || typeof value === 'number'
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function multipartBody(
|
||||
descriptor: ReturnType<typeof stageOneDescriptor>,
|
||||
files: WindchillUploadFile[]
|
||||
): { contentType: string; body: Buffer } {
|
||||
if (
|
||||
descriptor.streamIds.length !== files.length ||
|
||||
descriptor.fileNames.length !== files.length
|
||||
) {
|
||||
throw new WindchillProviderError('Windchill upload Stage 1 returned the wrong file count', 502)
|
||||
}
|
||||
const boundary = `sim-windchill-${generateShortId()}`
|
||||
const chunks: Buffer[] = []
|
||||
const appendField = (name: string, value: string) => {
|
||||
chunks.push(
|
||||
Buffer.from(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
|
||||
)
|
||||
)
|
||||
}
|
||||
appendField('Master_URL', descriptor.masterUrl)
|
||||
appendField(
|
||||
'CacheDescriptor_array',
|
||||
descriptor.streamIds
|
||||
.map(
|
||||
(streamId, index) =>
|
||||
`${streamId}:${descriptor.fileNames[index]}:${streamId}:${files[index].size};`
|
||||
)
|
||||
.join(' ')
|
||||
)
|
||||
files.forEach((file, index) => {
|
||||
chunks.push(
|
||||
Buffer.from(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="${descriptor.streamIds[index]}"; filename="${file.name.replace(/["\r\n]/g, '_')}"\r\nContent-Type: ${file.mimeType}\r\n\r\n`
|
||||
),
|
||||
file.buffer,
|
||||
Buffer.from('\r\n')
|
||||
)
|
||||
})
|
||||
chunks.push(Buffer.from(`--${boundary}--\r\n`))
|
||||
return { contentType: `multipart/form-data; boundary=${boundary}`, body: Buffer.concat(chunks) }
|
||||
}
|
||||
|
||||
function stageTwoContent(data: unknown): Array<{
|
||||
streamId: string | number
|
||||
fileSize: number
|
||||
encodedInfo: string
|
||||
}> {
|
||||
if (!isRecordLike(data) || !Array.isArray(data.contentInfos)) {
|
||||
throw new WindchillProviderError('Windchill upload Stage 2 returned invalid content info', 502)
|
||||
}
|
||||
return data.contentInfos.map((item) => {
|
||||
if (
|
||||
!isRecordLike(item) ||
|
||||
(typeof item.streamId !== 'string' && typeof item.streamId !== 'number') ||
|
||||
typeof item.fileSize !== 'number' ||
|
||||
typeof item.encodedInfo !== 'string'
|
||||
) {
|
||||
throw new WindchillProviderError(
|
||||
'Windchill upload Stage 2 returned invalid content info',
|
||||
502
|
||||
)
|
||||
}
|
||||
return { streamId: item.streamId, fileSize: item.fileSize, encodedInfo: item.encodedInfo }
|
||||
})
|
||||
}
|
||||
|
||||
export async function uploadWindchillContent({
|
||||
params,
|
||||
documentOid,
|
||||
files,
|
||||
primaryContent,
|
||||
signal,
|
||||
}: {
|
||||
params: Pick<WindchillParams, 'baseUrl' | 'username' | 'password'>
|
||||
documentOid: string
|
||||
files: WindchillUploadFile[]
|
||||
primaryContent: boolean
|
||||
signal?: AbortSignal
|
||||
}): Promise<string[]> {
|
||||
const session = await createWindchillSession(params, signal)
|
||||
const documentUrl = windchillDocumentUrl(params.baseUrl, documentOid)
|
||||
const stageOne = await windchillMutationRequest({
|
||||
params,
|
||||
session,
|
||||
url: `${documentUrl}/PTC.DocMgmt.UploadStage1Action`,
|
||||
method: 'POST',
|
||||
body: { NoOfFiles: files.length },
|
||||
signal,
|
||||
})
|
||||
const descriptor = stageOneDescriptor(stageOne)
|
||||
const multipart = multipartBody(descriptor, files)
|
||||
const stageTwoResponse = await secureFetchWithValidation(
|
||||
descriptor.replicaUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': multipart.contentType, Accept: 'application/json' },
|
||||
body: multipart.body,
|
||||
maxRedirects: 0,
|
||||
maxResponseBytes: WINDCHILL_CONTROL_RESPONSE_BYTES,
|
||||
timeout: WINDCHILL_TIMEOUT_MS,
|
||||
signal,
|
||||
},
|
||||
'ReplicaUrl'
|
||||
)
|
||||
const uploaded = stageTwoContent(await checkedBody(stageTwoResponse))
|
||||
if (uploaded.length !== files.length) {
|
||||
throw new WindchillProviderError('Windchill upload Stage 2 returned the wrong file count', 502)
|
||||
}
|
||||
const byStreamId = new Map(uploaded.map((item) => [String(item.streamId), item]))
|
||||
const contentInfo = descriptor.streamIds.map((streamId, index) => {
|
||||
const uploadedItem = byStreamId.get(String(streamId))
|
||||
if (!uploadedItem) {
|
||||
throw new WindchillProviderError('Windchill upload Stage 2 omitted a file', 502)
|
||||
}
|
||||
return {
|
||||
StreamId: streamId,
|
||||
EncodedInfo: uploadedItem.encodedInfo,
|
||||
FileName: files[index].name,
|
||||
PrimaryContent: primaryContent,
|
||||
MimeType: files[index].mimeType,
|
||||
FileSize: uploadedItem.fileSize,
|
||||
}
|
||||
})
|
||||
await windchillMutationRequest({
|
||||
params,
|
||||
session,
|
||||
url: `${documentUrl}/PTC.DocMgmt.UploadStage3Action`,
|
||||
method: 'POST',
|
||||
body: { ContentInfo: contentInfo },
|
||||
signal,
|
||||
})
|
||||
return files.map((file) => file.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the signed vault URL that serves a content item's bytes.
|
||||
*
|
||||
* Windchill has no OData media-stream segment for content. The documented path is a typed
|
||||
* navigation to `Content/URL`, which returns a short-lived signed WindchillGW/WindchillAuthGW
|
||||
* download URL on the same origin as the service root.
|
||||
*/
|
||||
export async function resolveWindchillContentUrl({
|
||||
params,
|
||||
contentPath,
|
||||
signal,
|
||||
}: {
|
||||
params: Pick<WindchillParams, 'baseUrl' | 'username' | 'password'>
|
||||
contentPath: string
|
||||
signal?: AbortSignal
|
||||
}): Promise<string> {
|
||||
const response = await secureFetchWithValidation(
|
||||
`${contentPath}/PTC.ApplicationData/Content/URL`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: createBasicAuthHeader(params.username, params.password),
|
||||
Accept: 'application/json',
|
||||
},
|
||||
maxRedirects: 0,
|
||||
maxResponseBytes: WINDCHILL_CONTROL_RESPONSE_BYTES,
|
||||
timeout: WINDCHILL_TIMEOUT_MS,
|
||||
signal,
|
||||
},
|
||||
'baseUrl'
|
||||
)
|
||||
const data = await checkedBody(response)
|
||||
const value = isRecordLike(data) ? data.value : null
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new WindchillProviderError('Windchill did not return a content download URL', 502)
|
||||
}
|
||||
|
||||
const serviceRoot = new URL(normalizeServiceRoot(params.baseUrl))
|
||||
let resolved: URL
|
||||
try {
|
||||
resolved = new URL(value, `${serviceRoot.toString()}/`)
|
||||
} catch {
|
||||
throw new WindchillProviderError('Windchill returned an invalid content download URL', 502)
|
||||
}
|
||||
if (
|
||||
resolved.protocol !== 'https:' ||
|
||||
resolved.origin !== serviceRoot.origin ||
|
||||
resolved.username ||
|
||||
resolved.password ||
|
||||
resolved.hash
|
||||
) {
|
||||
throw new WindchillProviderError(
|
||||
'Windchill content download URL must remain on the configured HTTPS origin',
|
||||
502
|
||||
)
|
||||
}
|
||||
return resolved.toString()
|
||||
}
|
||||
|
||||
export async function downloadWindchillContent({
|
||||
params,
|
||||
url,
|
||||
maxBytes,
|
||||
signal,
|
||||
}: {
|
||||
params: Pick<WindchillParams, 'username' | 'password'>
|
||||
url: string
|
||||
maxBytes: number
|
||||
signal?: AbortSignal
|
||||
}): Promise<{
|
||||
buffer: Buffer
|
||||
contentType: string
|
||||
contentDisposition: string | null
|
||||
}> {
|
||||
const response = await secureFetchWithValidation(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Authorization: createBasicAuthHeader(params.username, params.password) },
|
||||
stripAuthOnRedirect: true,
|
||||
maxResponseBytes: maxBytes,
|
||||
timeout: WINDCHILL_TIMEOUT_MS,
|
||||
signal,
|
||||
},
|
||||
'contentUrl'
|
||||
)
|
||||
if (!response.ok) await checkedBody(response)
|
||||
return {
|
||||
buffer: Buffer.from(await response.arrayBuffer()),
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
contentDisposition: response.headers.get('content-disposition'),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { isRecordLike, omit } from '@sim/utils/object'
|
||||
import {
|
||||
type WindchillOperationResponse,
|
||||
windchillOperationResponseSchema,
|
||||
} from '@/lib/api/contracts/tools/windchill'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type {
|
||||
WindchillContent,
|
||||
WindchillDocument,
|
||||
WindchillDocumentUsageLink,
|
||||
WindchillOperation,
|
||||
WindchillOutput,
|
||||
WindchillParams,
|
||||
WindchillResponse,
|
||||
WindchillStateTransition,
|
||||
} from '@/tools/windchill/types'
|
||||
|
||||
/**
|
||||
* Deepest `DocUsageLinks` expansion this integration ever requests, and therefore the deepest
|
||||
* nesting it will normalize. Bounding the walk keeps a pathologically nested provider response
|
||||
* from recursing without limit.
|
||||
*/
|
||||
const MAX_STRUCTURE_DEPTH = 3
|
||||
|
||||
/** Windchill's documented maximum server page size (`Prefer: odata.maxpagesize`). */
|
||||
const MAX_PAGE_SIZE = 2000
|
||||
|
||||
/** Page size requested when the caller does not choose one. */
|
||||
const DEFAULT_PAGE_SIZE = 200
|
||||
|
||||
function stringValue(record: Record<string, unknown>, key: string): string | null {
|
||||
return typeof record[key] === 'string' ? record[key] : null
|
||||
}
|
||||
|
||||
function numberValue(record: Record<string, unknown>, key: string): number | null {
|
||||
const value = record[key]
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function booleanValue(record: Record<string, unknown>, key: string): boolean | null {
|
||||
return typeof record[key] === 'boolean' ? record[key] : null
|
||||
}
|
||||
|
||||
const DOCUMENT_RESPONSE_PROPERTIES = [
|
||||
'ID',
|
||||
'Name',
|
||||
'Number',
|
||||
'Title',
|
||||
'Description',
|
||||
'State',
|
||||
'VersionID',
|
||||
'Revision',
|
||||
'Version',
|
||||
'Latest',
|
||||
'CheckoutState',
|
||||
'FolderName',
|
||||
'FolderLocation',
|
||||
] as const
|
||||
|
||||
const CONTENT_RESPONSE_PROPERTIES = [
|
||||
'ID',
|
||||
'FileName',
|
||||
'Description',
|
||||
'Format',
|
||||
'MimeType',
|
||||
'FileSize',
|
||||
'@odata.type',
|
||||
'DisplayName',
|
||||
'UrlLocation',
|
||||
'ExternalLocation',
|
||||
] as const
|
||||
|
||||
function hasResponseProperty(
|
||||
value: Record<string, unknown>,
|
||||
properties: readonly string[]
|
||||
): boolean {
|
||||
return properties.some((property) => Object.hasOwn(value, property))
|
||||
}
|
||||
|
||||
export function normalizeWindchillDocument(value: unknown): WindchillDocument | null {
|
||||
if (!isRecordLike(value) || !hasResponseProperty(value, DOCUMENT_RESPONSE_PROPERTIES)) return null
|
||||
const state = value.State
|
||||
return {
|
||||
id: stringValue(value, 'ID'),
|
||||
name: stringValue(value, 'Name'),
|
||||
number: stringValue(value, 'Number'),
|
||||
title: stringValue(value, 'Title'),
|
||||
description: stringValue(value, 'Description'),
|
||||
state:
|
||||
(isRecordLike(state) ? stringValue(state, 'Value') : null) ?? stringValue(value, 'State'),
|
||||
stateDisplay: isRecordLike(state) ? stringValue(state, 'Display') : null,
|
||||
versionId: stringValue(value, 'VersionID'),
|
||||
revision: stringValue(value, 'Revision'),
|
||||
version: stringValue(value, 'Version'),
|
||||
latest: booleanValue(value, 'Latest'),
|
||||
checkoutState: stringValue(value, 'CheckoutState'),
|
||||
folderName: stringValue(value, 'FolderName'),
|
||||
folderLocation: stringValue(value, 'FolderLocation'),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWindchillContent(value: unknown): WindchillContent | null {
|
||||
if (!isRecordLike(value) || !hasResponseProperty(value, CONTENT_RESPONSE_PROPERTIES)) return null
|
||||
return {
|
||||
id: stringValue(value, 'ID'),
|
||||
fileName: stringValue(value, 'FileName'),
|
||||
description: stringValue(value, 'Description'),
|
||||
format: stringValue(value, 'Format'),
|
||||
mimeType: stringValue(value, 'MimeType'),
|
||||
fileSize: numberValue(value, 'FileSize'),
|
||||
contentType: stringValue(value, '@odata.type'),
|
||||
displayName: stringValue(value, 'DisplayName'),
|
||||
urlLocation: stringValue(value, 'UrlLocation'),
|
||||
externalLocation: stringValue(value, 'ExternalLocation'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts this integration's own transport credentials out of an error string.
|
||||
*
|
||||
* Targets exactly three artifacts Sim itself puts on the wire: the `Basic` header built by
|
||||
* {@link createBasicAuthHeader}, the CSRF nonce pair from `createWindchillSession`, and the
|
||||
* short-lived signed vault URL from `resolveWindchillContentUrl`, whose bearer token lives in the
|
||||
* query string. None of these can reach the shared resolved-secret registry — it matches
|
||||
* `{{...}}`-resolved plaintext, never a base64 derivative or a provider-issued token.
|
||||
*
|
||||
* Applied to error strings only. Successful provider payloads are returned untouched.
|
||||
*/
|
||||
export function sanitizeWindchillError(message: string): string {
|
||||
return message
|
||||
.replace(/https?:\/\/[^\s"'<>]+/gi, (url) => sanitizeUrlForLog(url, 512))
|
||||
.replace(/\b(?:CSRF_NONCE|NonceValue|NonceKey)\b\s*[:=]\s*\S+/gi, '[redacted nonce]')
|
||||
.replace(/\bBasic\s+[A-Za-z0-9+/=]+/gi, 'Basic [redacted]')
|
||||
.slice(0, 4096)
|
||||
}
|
||||
|
||||
function collection(value: unknown): unknown[] {
|
||||
if (Array.isArray(value)) return value
|
||||
if (isRecordLike(value) && Array.isArray(value.value)) return value.value
|
||||
return []
|
||||
}
|
||||
|
||||
function requiredCollection(value: unknown, operation: WindchillOperation): unknown[] {
|
||||
if (Array.isArray(value)) return value
|
||||
if (isRecordLike(value) && Array.isArray(value.value)) return value.value
|
||||
throw new Error(`Windchill returned an incompatible response for ${operation}`)
|
||||
}
|
||||
|
||||
function requireAllNormalized<T>(
|
||||
values: unknown[],
|
||||
normalize: (value: unknown) => T | null,
|
||||
operation: WindchillOperation
|
||||
): T[] {
|
||||
const normalized = values.map(normalize)
|
||||
if (normalized.some((value) => value === null)) {
|
||||
throw new Error(`Windchill returned an incompatible response for ${operation}`)
|
||||
}
|
||||
return normalized as T[]
|
||||
}
|
||||
|
||||
function normalizeState(value: unknown): WindchillStateTransition | null {
|
||||
if (
|
||||
!isRecordLike(value) ||
|
||||
(!Object.hasOwn(value, 'Value') && !Object.hasOwn(value, 'Display'))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
value: stringValue(value, 'Value'),
|
||||
display: stringValue(value, 'Display'),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUsageLink(
|
||||
value: unknown,
|
||||
parentFallback: WindchillDocument | null = null,
|
||||
depth = 0
|
||||
): WindchillDocumentUsageLink | null {
|
||||
if (
|
||||
!isRecordLike(value) ||
|
||||
(!Object.hasOwn(value, 'ID') &&
|
||||
!Object.hasOwn(value, 'DocUsedBy') &&
|
||||
!Object.hasOwn(value, 'DocUses'))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const childValue = value.DocUses
|
||||
const child = normalizeWindchillDocument(childValue)
|
||||
const children =
|
||||
isRecordLike(childValue) && depth < MAX_STRUCTURE_DEPTH
|
||||
? collection(childValue.DocUsageLinks)
|
||||
.map((link) => normalizeUsageLink(link, child, depth + 1))
|
||||
.filter((link): link is WindchillDocumentUsageLink => link !== null)
|
||||
: []
|
||||
return {
|
||||
id: stringValue(value, 'ID'),
|
||||
parent: normalizeWindchillDocument(value.DocUsedBy) ?? parentFallback,
|
||||
child,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWindchillDocuments(value: unknown): WindchillDocument[] {
|
||||
return collection(value)
|
||||
.map(normalizeWindchillDocument)
|
||||
.filter((document): document is WindchillDocument => document !== null)
|
||||
}
|
||||
|
||||
export function windchillPageInfo(value: unknown, pageCount: number) {
|
||||
const record = isRecordLike(value) ? value : {}
|
||||
return {
|
||||
count: pageCount,
|
||||
totalCount: numberValue(record, '@odata.count'),
|
||||
nextLink: stringValue(record, '@odata.nextLink'),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeServiceRoot(baseUrl: string): string {
|
||||
const trimmed = baseUrl.trim().replace(/\/+$/, '')
|
||||
const parsed = new URL(trimmed)
|
||||
if (parsed.protocol !== 'https:') throw new Error('Windchill base URL must use HTTPS')
|
||||
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
||||
throw new Error('Windchill base URL must not include credentials, query parameters, or a hash')
|
||||
}
|
||||
if (!/\/servlet\/odata\/v\d+$/i.test(parsed.pathname)) {
|
||||
throw new Error('Windchill base URL must end with a versioned /servlet/odata/vN path')
|
||||
}
|
||||
return parsed.toString().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function createBasicAuthHeader(username: string, password: string): string {
|
||||
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
|
||||
}
|
||||
|
||||
export function encodeWindchillOid(oid: string): string {
|
||||
const trimmed = oid.trim()
|
||||
if (!/^[A-Za-z0-9_.:-]+$/.test(trimmed)) {
|
||||
throw new Error('Windchill OID contains unsupported characters')
|
||||
}
|
||||
return encodeURIComponent(trimmed)
|
||||
}
|
||||
|
||||
function documentPath(baseUrl: string, oid: string): string {
|
||||
return `${normalizeServiceRoot(baseUrl)}/DocMgmt/Documents('${encodeWindchillOid(oid)}')`
|
||||
}
|
||||
|
||||
function decodedPathname(pathname: string): string {
|
||||
try {
|
||||
return decodeURIComponent(pathname)
|
||||
} catch {
|
||||
return pathname
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWindchillNextLink(
|
||||
baseUrl: string,
|
||||
nextLink: string,
|
||||
expectedCollectionUrl: string
|
||||
): string {
|
||||
const serviceRoot = new URL(normalizeServiceRoot(baseUrl))
|
||||
const resolved = new URL(nextLink, `${serviceRoot.toString()}/`)
|
||||
const expected = new URL(expectedCollectionUrl)
|
||||
if (
|
||||
resolved.protocol !== 'https:' ||
|
||||
resolved.origin !== serviceRoot.origin ||
|
||||
resolved.username ||
|
||||
resolved.password ||
|
||||
resolved.hash
|
||||
) {
|
||||
throw new Error('Windchill nextLink must remain on the configured HTTPS origin')
|
||||
}
|
||||
if (
|
||||
expected.origin !== serviceRoot.origin ||
|
||||
decodedPathname(resolved.pathname) !== decodedPathname(expected.pathname)
|
||||
) {
|
||||
throw new Error('Windchill nextLink must continue the originating collection')
|
||||
}
|
||||
return resolved.toString()
|
||||
}
|
||||
|
||||
function structureExpand(depth: number): string {
|
||||
let child = 'DocUses'
|
||||
for (let level = 1; level < depth; level += 1) {
|
||||
child = `DocUses($expand=DocUsageLinks($expand=${child}))`
|
||||
}
|
||||
return `DocUsedBy,${child}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a built OData URL.
|
||||
*
|
||||
* `URLSearchParams` uses the form-urlencoded serializer, which writes a space as `+`. OData
|
||||
* readers percent-decode but never form-decode, so `$orderby=Name desc` would reach Windchill as
|
||||
* the literal `Name+desc`. Re-encoding the query's `+` as `%20` keeps multi-token `$filter` and
|
||||
* `$orderby` expressions intact.
|
||||
*/
|
||||
function serializeODataUrl(url: URL): string {
|
||||
if (!url.search) return url.toString()
|
||||
return `${url.origin}${url.pathname}${url.search.replace(/\+/g, '%20')}`
|
||||
}
|
||||
|
||||
/** Treats a cleared subblock (`''`) exactly like an absent one. */
|
||||
function isBlank(value: unknown): boolean {
|
||||
return value === undefined || value === null || value === ''
|
||||
}
|
||||
|
||||
function integerInRange(value: number, field: string, minimum: number, maximum?: number): number {
|
||||
if (!Number.isInteger(value) || value < minimum || (maximum !== undefined && value > maximum)) {
|
||||
const range =
|
||||
maximum === undefined ? `at least ${minimum}` : `between ${minimum} and ${maximum}`
|
||||
throw new Error(`Windchill ${field} must be an integer ${range}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const DOCUMENT_SELECT_PROPERTIES = new Set([
|
||||
'ID',
|
||||
'Name',
|
||||
'Number',
|
||||
'Title',
|
||||
'Description',
|
||||
'State',
|
||||
'VersionID',
|
||||
'Revision',
|
||||
'Version',
|
||||
'Latest',
|
||||
'CheckoutState',
|
||||
'FolderName',
|
||||
'FolderLocation',
|
||||
])
|
||||
|
||||
function normalizedSelect(select: string): string {
|
||||
const properties = select
|
||||
.split(',')
|
||||
.map((property) => property.trim())
|
||||
.filter(Boolean)
|
||||
if (
|
||||
properties.length === 0 ||
|
||||
properties.some((property) => !DOCUMENT_SELECT_PROPERTIES.has(property))
|
||||
) {
|
||||
throw new Error(
|
||||
`Windchill select supports only normalized document properties: ${[...DOCUMENT_SELECT_PROPERTIES].join(', ')}`
|
||||
)
|
||||
}
|
||||
return [...new Set(properties)].join(',')
|
||||
}
|
||||
|
||||
export function buildWindchillReadUrl(
|
||||
operation: WindchillOperation,
|
||||
params: WindchillParams
|
||||
): string {
|
||||
const root = normalizeServiceRoot(params.baseUrl)
|
||||
if (operation === 'windchill_list_documents') {
|
||||
const url = new URL(`${root}/DocMgmt/Documents`)
|
||||
if (params.nextLink) return resolveWindchillNextLink(root, params.nextLink, url.toString())
|
||||
if (params.select) url.searchParams.set('$select', normalizedSelect(params.select))
|
||||
if (params.filter) url.searchParams.set('$filter', params.filter.trim())
|
||||
if (params.orderBy) url.searchParams.set('$orderby', params.orderBy.trim())
|
||||
if (!isBlank(params.top)) {
|
||||
url.searchParams.set(
|
||||
'$top',
|
||||
String(integerInRange(params.top as number, 'top', 1, MAX_PAGE_SIZE))
|
||||
)
|
||||
}
|
||||
if (!isBlank(params.skip)) {
|
||||
url.searchParams.set('$skip', String(integerInRange(params.skip as number, 'skip', 0)))
|
||||
}
|
||||
if (!isBlank(params.count)) url.searchParams.set('$count', String(params.count))
|
||||
if (!isBlank(params.latestVersion)) {
|
||||
url.searchParams.set('ptc.search.latestversion', String(params.latestVersion))
|
||||
}
|
||||
return serializeODataUrl(url)
|
||||
}
|
||||
|
||||
if (!params.documentOid) throw new Error('Document OID is required')
|
||||
const path = documentPath(root, params.documentOid)
|
||||
if (operation === 'windchill_get_document') {
|
||||
const url = new URL(path)
|
||||
if (params.select) url.searchParams.set('$select', normalizedSelect(params.select))
|
||||
return serializeODataUrl(url)
|
||||
}
|
||||
if (operation === 'windchill_get_document_structure') {
|
||||
const depth = integerInRange(
|
||||
isBlank(params.structureDepth) ? 1 : (params.structureDepth as number),
|
||||
'structureDepth',
|
||||
1,
|
||||
MAX_STRUCTURE_DEPTH
|
||||
)
|
||||
const url = new URL(`${path}/DocUsageLinks`)
|
||||
if (params.nextLink) return resolveWindchillNextLink(root, params.nextLink, url.toString())
|
||||
url.searchParams.set('$expand', structureExpand(depth))
|
||||
return serializeODataUrl(url)
|
||||
}
|
||||
if (operation === 'windchill_get_valid_state_transitions') {
|
||||
return `${path}/PTC.DocMgmt.GetValidStateTransitions()`
|
||||
}
|
||||
if (operation === 'windchill_get_primary_content') return `${path}/PrimaryContent`
|
||||
if (operation === 'windchill_list_attachments') {
|
||||
const url = `${path}/Attachments`
|
||||
return params.nextLink ? resolveWindchillNextLink(root, params.nextLink, url) : url
|
||||
}
|
||||
throw new Error(`Operation ${operation} is not a direct Windchill read`)
|
||||
}
|
||||
|
||||
export function normalizeWindchillReadOutput(
|
||||
operation: WindchillOperation,
|
||||
value: unknown
|
||||
): WindchillOutput {
|
||||
if (operation === 'windchill_list_documents') {
|
||||
const documents = requireAllNormalized(
|
||||
requiredCollection(value, operation),
|
||||
normalizeWindchillDocument,
|
||||
operation
|
||||
)
|
||||
return { operation, documents, pageInfo: windchillPageInfo(value, documents.length) }
|
||||
}
|
||||
if (operation === 'windchill_get_document') {
|
||||
const document = normalizeWindchillDocument(value)
|
||||
if (!document) throw new Error(`Windchill returned an incompatible response for ${operation}`)
|
||||
return { operation, document }
|
||||
}
|
||||
if (operation === 'windchill_get_document_structure') {
|
||||
const structure = requireAllNormalized(
|
||||
requiredCollection(value, operation),
|
||||
(link) => normalizeUsageLink(link),
|
||||
operation
|
||||
)
|
||||
return { operation, structure, pageInfo: windchillPageInfo(value, structure.length) }
|
||||
}
|
||||
if (operation === 'windchill_get_valid_state_transitions') {
|
||||
const states = requireAllNormalized(
|
||||
requiredCollection(value, operation),
|
||||
normalizeState,
|
||||
operation
|
||||
)
|
||||
return { operation, states }
|
||||
}
|
||||
if (operation === 'windchill_get_primary_content') {
|
||||
const contentValues =
|
||||
Array.isArray(value) || (isRecordLike(value) && Array.isArray(value.value))
|
||||
? requiredCollection(value, operation)
|
||||
: null
|
||||
if (contentValues?.length === 0) return { operation, content: null }
|
||||
const content = normalizeWindchillContent(contentValues ? contentValues[0] : value)
|
||||
if (!content) throw new Error(`Windchill returned an incompatible response for ${operation}`)
|
||||
return { operation, content }
|
||||
}
|
||||
if (operation === 'windchill_list_attachments') {
|
||||
const attachments = requireAllNormalized(
|
||||
requiredCollection(value, operation),
|
||||
normalizeWindchillContent,
|
||||
operation
|
||||
)
|
||||
return { operation, attachments, pageInfo: windchillPageInfo(value, attachments.length) }
|
||||
}
|
||||
throw new Error(`Operation ${operation} does not have a direct-read response transform`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the internal-route body.
|
||||
*
|
||||
* Cleared subblocks arrive as `''`. The executor merges the raw block inputs before the block's
|
||||
* own param transform, so a cleared optional field cannot be dropped upstream — strip blanks here,
|
||||
* where every internal-route tool passes through, rather than trusting the caller.
|
||||
*/
|
||||
export function buildWindchillInternalBody(operation: WindchillOperation, params: WindchillParams) {
|
||||
const supplied = Object.entries(omit(params, ['_context'])).filter(([, value]) => value !== '')
|
||||
return { ...Object.fromEntries(supplied), operation }
|
||||
}
|
||||
|
||||
function providerError(value: unknown, fallback: string): string {
|
||||
if (!isRecordLike(value)) return fallback
|
||||
if (typeof value.message === 'string') return value.message
|
||||
if (isRecordLike(value.error)) {
|
||||
if (typeof value.error.message === 'string') return value.error.message
|
||||
if (isRecordLike(value.error.message) && typeof value.error.message.value === 'string') {
|
||||
return value.error.message.value
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function windchillReadHeaders(params: WindchillParams) {
|
||||
return {
|
||||
Authorization: createBasicAuthHeader(params.username, params.password),
|
||||
Accept: 'application/json',
|
||||
Prefer: `odata.maxpagesize=${isBlank(params.top) ? DEFAULT_PAGE_SIZE : params.top}`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function transformWindchillDirectRead(
|
||||
operation: WindchillOperation,
|
||||
response: Response
|
||||
): Promise<WindchillResponse> {
|
||||
let data: unknown
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch {
|
||||
if (response.ok) {
|
||||
throw new Error(`Windchill returned invalid JSON with status ${response.status}`)
|
||||
}
|
||||
data = null
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
sanitizeWindchillError(
|
||||
providerError(data, `Windchill request failed with status ${response.status}`)
|
||||
)
|
||||
)
|
||||
}
|
||||
return { success: true, output: normalizeWindchillReadOutput(operation, data) }
|
||||
}
|
||||
|
||||
export async function transformWindchillInternalResponse(
|
||||
operation: WindchillOperation,
|
||||
response: Response
|
||||
): Promise<WindchillResponse> {
|
||||
try {
|
||||
const data: unknown = await response.json()
|
||||
const parsed = windchillOperationResponseSchema.safeParse(data)
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { operation },
|
||||
error: 'Windchill route returned an invalid response',
|
||||
}
|
||||
}
|
||||
if (!response.ok || !parsed.data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { operation },
|
||||
error:
|
||||
parsed.data.success === false
|
||||
? sanitizeWindchillError(parsed.data.error)
|
||||
: `Windchill request failed with status ${response.status}`,
|
||||
}
|
||||
}
|
||||
const result: WindchillOperationResponse = parsed.data
|
||||
if (result.output.operation !== operation) {
|
||||
return {
|
||||
success: false,
|
||||
output: { operation },
|
||||
error: 'Windchill route returned a response for a different operation',
|
||||
}
|
||||
}
|
||||
return { success: true, output: result.output }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: { operation },
|
||||
error: sanitizeWindchillError(
|
||||
getErrorMessage(error, 'Failed to read Windchill route response')
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
|
||||
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
|
||||
|
||||
const BASELINE = {
|
||||
totalRoutes: 1099,
|
||||
zodRoutes: 1099,
|
||||
totalRoutes: 1100,
|
||||
zodRoutes: 1100,
|
||||
nonZodRoutes: 0,
|
||||
} as const
|
||||
|
||||
|
||||
Reference in New Issue
Block a user