Skip to content

Commit 4c8a60a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(windchill): correct response and paging semantics
1 parent 639a838 commit 4c8a60a

14 files changed

Lines changed: 429 additions & 69 deletions

File tree

apps/docs/content/docs/en/integrations/windchill.mdx

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ List WT.Document objects with bounded OData query and pagination controls
3232
| `select` | string | No | Comma-separated normalized document properties to return |
3333
| `filter` | string | No | OData $filter expression |
3434
| `orderBy` | string | No | OData $orderby expression |
35-
| `top` | number | No | Maximum documents to return, from 1 to 200 |
35+
| `top` | number | No | Maximum documents in the OData result set \($top\), from 1 to 200 |
3636
| `skip` | number | No | Documents to skip |
3737
| `count` | boolean | No | Ask Windchill to include the total matching count |
3838
| `latestVersion` | boolean | No | Return only the latest version of matching documents |
@@ -49,7 +49,8 @@ List WT.Document objects with bounded OData query and pagination controls
4949
|`number` | string | Document number |
5050
|`title` | string | Document title |
5151
|`description` | string | Document description |
52-
|`state` | string | Life cycle state |
52+
|`state` | string | Internal life cycle state value |
53+
|`stateDisplay` | string | Displayed life cycle state value |
5354
|`versionId` | string | Version identifier |
5455
|`revision` | string | Revision identifier |
5556
|`version` | string | Version and iteration |
@@ -58,9 +59,9 @@ List WT.Document objects with bounded OData query and pagination controls
5859
|`folderName` | string | Folder name |
5960
|`folderLocation` | string | Folder path |
6061
| `pageInfo` | object | OData pagination information |
61-
|`count` | number | Number of documents in this page |
62-
|`totalCount` | number | Total matching documents |
63-
|`nextLink` | string | URL for the next page |
62+
|`count` | number | Number of items returned in this page |
63+
|`totalCount` | number | Total matching items |
64+
|`nextLink` | string | URL returned by Windchill for the next page |
6465

6566
### Windchill Get Document
6667

@@ -87,7 +88,8 @@ Get a WT.Document by OID
8788
|`number` | string | Document number |
8889
|`title` | string | Document title |
8990
|`description` | string | Document description |
90-
|`state` | string | Life cycle state |
91+
|`state` | string | Internal life cycle state value |
92+
|`stateDisplay` | string | Displayed life cycle state value |
9193
|`versionId` | string | Version identifier |
9294
|`revision` | string | Revision identifier |
9395
|`version` | string | Version and iteration |
@@ -109,6 +111,7 @@ Retrieve recursive document usage links and their parent and child documents
109111
| `password` | string | Yes | Windchill service-account password |
110112
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
111113
| `structureDepth` | number | No | Document structure expansion depth, from 1 to 3 |
114+
| `nextLink` | string | No | Verified @odata.nextLink from a previous structure response |
112115

113116
#### Output
114117

@@ -123,7 +126,8 @@ Retrieve recursive document usage links and their parent and child documents
123126
|`number` | string | Document number |
124127
|`title` | string | Document title |
125128
|`description` | string | Document description |
126-
|`state` | string | Life cycle state |
129+
|`state` | string | Internal life cycle state value |
130+
|`stateDisplay` | string | Displayed life cycle state value |
127131
|`versionId` | string | Version identifier |
128132
|`revision` | string | Revision identifier |
129133
|`version` | string | Version and iteration |
@@ -137,7 +141,8 @@ Retrieve recursive document usage links and their parent and child documents
137141
|`number` | string | Document number |
138142
|`title` | string | Document title |
139143
|`description` | string | Document description |
140-
|`state` | string | Life cycle state |
144+
|`state` | string | Internal life cycle state value |
145+
|`stateDisplay` | string | Displayed life cycle state value |
141146
|`versionId` | string | Version identifier |
142147
|`revision` | string | Revision identifier |
143148
|`version` | string | Version and iteration |
@@ -146,6 +151,10 @@ Retrieve recursive document usage links and their parent and child documents
146151
|`folderName` | string | Folder name |
147152
|`folderLocation` | string | Folder path |
148153
|`children` | array | Nested child usage links with the same recursive shape |
154+
| `pageInfo` | object | OData pagination information |
155+
|`count` | number | Number of items returned in this page |
156+
|`totalCount` | number | Total matching items |
157+
|`nextLink` | string | URL returned by Windchill for the next page |
149158

150159
### Windchill Get Valid State Transitions
151160

@@ -194,6 +203,10 @@ Get primary-content metadata for a document
194203
|`format` | string | Windchill content format |
195204
|`mimeType` | string | Content MIME type |
196205
|`fileSize` | number | Content size in bytes |
206+
|`contentType` | string | Windchill OData content entity type |
207+
|`displayName` | string | Displayed content name |
208+
|`urlLocation` | string | URL-data location |
209+
|`externalLocation` | string | External-storage location |
197210

198211
### Windchill List Attachments
199212

@@ -207,6 +220,7 @@ List attachment metadata for a document
207220
| `username` | string | Yes | Windchill service-account username |
208221
| `password` | string | Yes | Windchill service-account password |
209222
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
223+
| `nextLink` | string | No | Verified @odata.nextLink from a previous attachment response |
210224

211225
#### Output
212226

@@ -220,6 +234,14 @@ List attachment metadata for a document
220234
|`format` | string | Windchill content format |
221235
|`mimeType` | string | Content MIME type |
222236
|`fileSize` | number | Content size in bytes |
237+
|`contentType` | string | Windchill OData content entity type |
238+
|`displayName` | string | Displayed content name |
239+
|`urlLocation` | string | URL-data location |
240+
|`externalLocation` | string | External-storage location |
241+
| `pageInfo` | object | OData pagination information |
242+
|`count` | number | Number of items returned in this page |
243+
|`totalCount` | number | Total matching items |
244+
|`nextLink` | string | URL returned by Windchill for the next page |
223245

224246
### Windchill Create Document
225247

@@ -252,7 +274,8 @@ Create one WT.Document
252274
|`number` | string | Document number |
253275
|`title` | string | Document title |
254276
|`description` | string | Document description |
255-
|`state` | string | Life cycle state |
277+
|`state` | string | Internal life cycle state value |
278+
|`stateDisplay` | string | Displayed life cycle state value |
256279
|`versionId` | string | Version identifier |
257280
|`revision` | string | Revision identifier |
258281
|`version` | string | Version and iteration |
@@ -286,7 +309,8 @@ Create multiple WT.Document objects atomically
286309
|`number` | string | Document number |
287310
|`title` | string | Document title |
288311
|`description` | string | Document description |
289-
|`state` | string | Life cycle state |
312+
|`state` | string | Internal life cycle state value |
313+
|`stateDisplay` | string | Displayed life cycle state value |
290314
|`versionId` | string | Version identifier |
291315
|`revision` | string | Revision identifier |
292316
|`version` | string | Version and iteration |
@@ -321,7 +345,8 @@ Update attributes on one document
321345
|`number` | string | Document number |
322346
|`title` | string | Document title |
323347
|`description` | string | Document description |
324-
|`state` | string | Life cycle state |
348+
|`state` | string | Internal life cycle state value |
349+
|`stateDisplay` | string | Displayed life cycle state value |
325350
|`versionId` | string | Version identifier |
326351
|`revision` | string | Revision identifier |
327352
|`version` | string | Version and iteration |
@@ -355,7 +380,8 @@ Update multiple documents atomically
355380
|`number` | string | Document number |
356381
|`title` | string | Document title |
357382
|`description` | string | Document description |
358-
|`state` | string | Life cycle state |
383+
|`state` | string | Internal life cycle state value |
384+
|`stateDisplay` | string | Displayed life cycle state value |
359385
|`versionId` | string | Version identifier |
360386
|`revision` | string | Revision identifier |
361387
|`version` | string | Version and iteration |
@@ -430,7 +456,8 @@ Check out one document
430456
|`number` | string | Document number |
431457
|`title` | string | Document title |
432458
|`description` | string | Document description |
433-
|`state` | string | Life cycle state |
459+
|`state` | string | Internal life cycle state value |
460+
|`stateDisplay` | string | Displayed life cycle state value |
434461
|`versionId` | string | Version identifier |
435462
|`revision` | string | Revision identifier |
436463
|`version` | string | Version and iteration |
@@ -465,7 +492,8 @@ Check out multiple documents atomically
465492
|`number` | string | Document number |
466493
|`title` | string | Document title |
467494
|`description` | string | Document description |
468-
|`state` | string | Life cycle state |
495+
|`state` | string | Internal life cycle state value |
496+
|`stateDisplay` | string | Displayed life cycle state value |
469497
|`versionId` | string | Version identifier |
470498
|`revision` | string | Revision identifier |
471499
|`version` | string | Version and iteration |
@@ -502,7 +530,8 @@ Check in one document
502530
|`number` | string | Document number |
503531
|`title` | string | Document title |
504532
|`description` | string | Document description |
505-
|`state` | string | Life cycle state |
533+
|`state` | string | Internal life cycle state value |
534+
|`stateDisplay` | string | Displayed life cycle state value |
506535
|`versionId` | string | Version identifier |
507536
|`revision` | string | Revision identifier |
508537
|`version` | string | Version and iteration |
@@ -539,7 +568,8 @@ Check in multiple documents atomically
539568
|`number` | string | Document number |
540569
|`title` | string | Document title |
541570
|`description` | string | Document description |
542-
|`state` | string | Life cycle state |
571+
|`state` | string | Internal life cycle state value |
572+
|`stateDisplay` | string | Displayed life cycle state value |
543573
|`versionId` | string | Version identifier |
544574
|`revision` | string | Revision identifier |
545575
|`version` | string | Version and iteration |
@@ -573,7 +603,8 @@ Undo checkout for one document
573603
|`number` | string | Document number |
574604
|`title` | string | Document title |
575605
|`description` | string | Document description |
576-
|`state` | string | Life cycle state |
606+
|`state` | string | Internal life cycle state value |
607+
|`stateDisplay` | string | Displayed life cycle state value |
577608
|`versionId` | string | Version identifier |
578609
|`revision` | string | Revision identifier |
579610
|`version` | string | Version and iteration |
@@ -607,7 +638,8 @@ Undo checkout for multiple documents atomically
607638
|`number` | string | Document number |
608639
|`title` | string | Document title |
609640
|`description` | string | Document description |
610-
|`state` | string | Life cycle state |
641+
|`state` | string | Internal life cycle state value |
642+
|`stateDisplay` | string | Displayed life cycle state value |
611643
|`versionId` | string | Version identifier |
612644
|`revision` | string | Revision identifier |
613645
|`version` | string | Version and iteration |
@@ -642,7 +674,8 @@ Create a new revision of one document
642674
|`number` | string | Document number |
643675
|`title` | string | Document title |
644676
|`description` | string | Document description |
645-
|`state` | string | Life cycle state |
677+
|`state` | string | Internal life cycle state value |
678+
|`stateDisplay` | string | Displayed life cycle state value |
646679
|`versionId` | string | Version identifier |
647680
|`revision` | string | Revision identifier |
648681
|`version` | string | Version and iteration |
@@ -677,7 +710,8 @@ Create new revisions of multiple documents atomically
677710
|`number` | string | Document number |
678711
|`title` | string | Document title |
679712
|`description` | string | Document description |
680-
|`state` | string | Life cycle state |
713+
|`state` | string | Internal life cycle state value |
714+
|`stateDisplay` | string | Displayed life cycle state value |
681715
|`versionId` | string | Version identifier |
682716
|`revision` | string | Revision identifier |
683717
|`version` | string | Version and iteration |
@@ -713,7 +747,8 @@ Transition a document to a valid lifecycle state
713747
|`number` | string | Document number |
714748
|`title` | string | Document title |
715749
|`description` | string | Document description |
716-
|`state` | string | Life cycle state |
750+
|`state` | string | Internal life cycle state value |
751+
|`stateDisplay` | string | Displayed life cycle state value |
717752
|`versionId` | string | Version identifier |
718753
|`revision` | string | Revision identifier |
719754
|`version` | string | Version and iteration |
@@ -747,7 +782,8 @@ Update installed security-label attributes for one or more documents
747782
|`number` | string | Document number |
748783
|`title` | string | Document title |
749784
|`description` | string | Document description |
750-
|`state` | string | Life cycle state |
785+
|`state` | string | Internal life cycle state value |
786+
|`stateDisplay` | string | Displayed life cycle state value |
751787
|`versionId` | string | Version identifier |
752788
|`revision` | string | Revision identifier |
753789
|`version` | string | Version and iteration |

apps/sim/app/api/tools/windchill/route.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import { createMockRequest, hybridAuthMockFns } from '@sim/testing'
55
import { NextResponse } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
8+
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
79

810
const {
911
MockWindchillProviderError,
@@ -425,6 +427,101 @@ describe('POST /api/tools/windchill', () => {
425427
expect(mockUploadWindchillContent).toHaveBeenCalledWith(
426428
expect.objectContaining({ primaryContent: false })
427429
)
430+
expect(mockDownloadServableFileFromStorage.mock.calls[0][3]).toEqual({
431+
maxBytes: MAX_FILE_SIZE,
432+
})
433+
expect(mockDownloadServableFileFromStorage.mock.calls[1][3]).toEqual({
434+
maxBytes: MAX_FILE_SIZE - 3,
435+
})
436+
})
437+
438+
it('rejects attachment counts above the contract limit before reading storage', async () => {
439+
const response = await POST(
440+
createMockRequest('POST', {
441+
...BASE_BODY,
442+
operation: 'windchill_upload_attachments',
443+
documentOid: DOCUMENT_OID,
444+
attachmentFiles: Array.from({ length: 11 }, (_, index) => ({
445+
key: `workspace/workspace-1/${index}.txt`,
446+
name: `${index}.txt`,
447+
size: 1,
448+
})),
449+
})
450+
)
451+
452+
expect(response.status).toBe(400)
453+
expect(mockAssertToolFileAccess).not.toHaveBeenCalled()
454+
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
455+
})
456+
457+
it('rejects declared aggregate upload size before reading storage', async () => {
458+
mockProcessFilesToUserFiles.mockReturnValueOnce([
459+
{
460+
key: 'workspace/workspace-1/oversized.bin',
461+
name: 'oversized.bin',
462+
size: MAX_FILE_SIZE + 1,
463+
type: 'application/octet-stream',
464+
},
465+
])
466+
467+
const response = await POST(
468+
createMockRequest('POST', {
469+
...BASE_BODY,
470+
operation: 'windchill_upload_primary_content',
471+
documentOid: DOCUMENT_OID,
472+
primaryFile: {
473+
key: 'workspace/workspace-1/oversized.bin',
474+
name: 'oversized.bin',
475+
size: MAX_FILE_SIZE + 1,
476+
},
477+
})
478+
)
479+
480+
expect(response.status).toBe(413)
481+
expect(mockAssertToolFileAccess).not.toHaveBeenCalled()
482+
expect(mockDownloadServableFileFromStorage).not.toHaveBeenCalled()
483+
})
484+
485+
it('stops an under-reported upload at the remaining aggregate byte budget', async () => {
486+
mockProcessFilesToUserFiles.mockReturnValueOnce([
487+
{ key: 'workspace/workspace-1/one.txt', name: 'one.txt', size: 1, type: 'text/plain' },
488+
{ key: 'workspace/workspace-1/two.txt', name: 'two.txt', size: 1, type: 'text/plain' },
489+
{
490+
key: 'workspace/workspace-1/three.txt',
491+
name: 'three.txt',
492+
size: 1,
493+
type: 'text/plain',
494+
},
495+
])
496+
mockDownloadServableFileFromStorage
497+
.mockResolvedValueOnce({ buffer: Buffer.from('one'), contentType: 'text/plain' })
498+
.mockRejectedValueOnce(
499+
new PayloadSizeLimitError({
500+
label: 'Uploaded file',
501+
maxBytes: MAX_FILE_SIZE - 3,
502+
observedBytes: MAX_FILE_SIZE - 2,
503+
})
504+
)
505+
506+
const response = await POST(
507+
createMockRequest('POST', {
508+
...BASE_BODY,
509+
operation: 'windchill_upload_attachments',
510+
documentOid: DOCUMENT_OID,
511+
attachmentFiles: [
512+
{ key: 'workspace/workspace-1/one.txt', name: 'one.txt', size: 1 },
513+
{ key: 'workspace/workspace-1/two.txt', name: 'two.txt', size: 1 },
514+
{ key: 'workspace/workspace-1/three.txt', name: 'three.txt', size: 1 },
515+
],
516+
})
517+
)
518+
519+
expect(response.status).toBe(413)
520+
expect(mockDownloadServableFileFromStorage).toHaveBeenCalledTimes(2)
521+
expect(mockDownloadServableFileFromStorage.mock.calls[1][3]).toEqual({
522+
maxBytes: MAX_FILE_SIZE - 3,
523+
})
524+
expect(mockUploadWindchillContent).not.toHaveBeenCalled()
428525
})
429526

430527
it('stops before storage or Windchill when file ownership is denied', async () => {

apps/sim/app/api/tools/windchill/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ async function loadUploadFiles(
386386
if (denied) return denied
387387
try {
388388
const servable = await downloadServableFileFromStorage(userFile, requestId, logger, {
389-
maxBytes: MAX_FILE_SIZE,
389+
maxBytes: MAX_FILE_SIZE - actualTotal,
390390
})
391391
actualTotal += servable.buffer.length
392392
if (actualTotal > MAX_FILE_SIZE) {
@@ -401,7 +401,10 @@ async function loadUploadFiles(
401401
} catch (error) {
402402
const notReady = docNotReadyResponse(error)
403403
if (notReady) return notReady
404-
return failureResponse(getErrorMessage(error, 'Failed to read uploaded file'), 400)
404+
return failureResponse(
405+
getErrorMessage(error, 'Failed to read uploaded file'),
406+
isPayloadSizeLimitError(error) ? 413 : 400
407+
)
405408
}
406409
}
407410
return files

0 commit comments

Comments
 (0)