@open-mercato/documents 0.6.8-develop.7016.1.4ed7b1e49d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/build.mjs +23 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/documents/acl.js +15 -0
- package/dist/modules/documents/acl.js.map +7 -0
- package/dist/modules/documents/api/[id]/archive/route.js +116 -0
- package/dist/modules/documents/api/[id]/archive/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js +155 -0
- package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/attachments/route.js +153 -0
- package/dist/modules/documents/api/[id]/attachments/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/collab-token/route.js +180 -0
- package/dist/modules/documents/api/[id]/collab-token/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/comments/access-check/route.js +86 -0
- package/dist/modules/documents/api/[id]/comments/access-check/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/comments/route.js +387 -0
- package/dist/modules/documents/api/[id]/comments/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/content/route.js +143 -0
- package/dist/modules/documents/api/[id]/content/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/duplicate/route.js +200 -0
- package/dist/modules/documents/api/[id]/duplicate/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/export/route.js +421 -0
- package/dist/modules/documents/api/[id]/export/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/favorite/route.js +96 -0
- package/dist/modules/documents/api/[id]/favorite/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/links/[linkId]/route.js +85 -0
- package/dist/modules/documents/api/[id]/links/[linkId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/links/route.js +257 -0
- package/dist/modules/documents/api/[id]/links/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/principals/route.js +299 -0
- package/dist/modules/documents/api/[id]/principals/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/route.js +246 -0
- package/dist/modules/documents/api/[id]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/shares/route.js +335 -0
- package/dist/modules/documents/api/[id]/shares/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/unarchive/route.js +39 -0
- package/dist/modules/documents/api/[id]/unarchive/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js +133 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/route.js +93 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/route.js +193 -0
- package/dist/modules/documents/api/[id]/versions/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/watch/route.js +97 -0
- package/dist/modules/documents/api/[id]/watch/route.js.map +7 -0
- package/dist/modules/documents/api/_commands.js +34 -0
- package/dist/modules/documents/api/_commands.js.map +7 -0
- package/dist/modules/documents/api/_shared.js +490 -0
- package/dist/modules/documents/api/_shared.js.map +7 -0
- package/dist/modules/documents/api/folders/route.js +256 -0
- package/dist/modules/documents/api/folders/route.js.map +7 -0
- package/dist/modules/documents/api/instantiate/route.js +108 -0
- package/dist/modules/documents/api/instantiate/route.js.map +7 -0
- package/dist/modules/documents/api/route.js +322 -0
- package/dist/modules/documents/api/route.js.map +7 -0
- package/dist/modules/documents/api/templates/[templateId]/preview/route.js +94 -0
- package/dist/modules/documents/api/templates/[templateId]/preview/route.js.map +7 -0
- package/dist/modules/documents/api/templates/[templateId]/route.js +82 -0
- package/dist/modules/documents/api/templates/[templateId]/route.js.map +7 -0
- package/dist/modules/documents/api/templates/route.js +344 -0
- package/dist/modules/documents/api/templates/route.js.map +7 -0
- package/dist/modules/documents/backend/documents/DocumentsPageClient.js +160 -0
- package/dist/modules/documents/backend/documents/DocumentsPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/DocumentsTable.js +143 -0
- package/dist/modules/documents/backend/documents/DocumentsTable.js.map +7 -0
- package/dist/modules/documents/backend/documents/FolderDialog.js +55 -0
- package/dist/modules/documents/backend/documents/FolderDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/FolderTree.js +40 -0
- package/dist/modules/documents/backend/documents/FolderTree.js.map +7 -0
- package/dist/modules/documents/backend/documents/MoveDocumentDialog.js +64 -0
- package/dist/modules/documents/backend/documents/MoveDocumentDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js +77 -0
- package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentComposer.js +126 -0
- package/dist/modules/documents/backend/documents/[id]/CommentComposer.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js +88 -0
- package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentsRail.js +123 -0
- package/dist/modules/documents/backend/documents/[id]/CommentsRail.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js +91 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js +27 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js +50 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js +168 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js +129 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js +429 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js +68 -0
- package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js +69 -0
- package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js +151 -0
- package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/ExportMenu.js +121 -0
- package/dist/modules/documents/backend/documents/[id]/ExportMenu.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/MentionPicker.js +114 -0
- package/dist/modules/documents/backend/documents/[id]/MentionPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/OutlinePane.js +70 -0
- package/dist/modules/documents/backend/documents/[id]/OutlinePane.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js +157 -0
- package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js +266 -0
- package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js +227 -0
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js +157 -0
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/commentTypes.js +150 -0
- package/dist/modules/documents/backend/documents/[id]/commentTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js +13 -0
- package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/documentPagination.js +295 -0
- package/dist/modules/documents/backend/documents/[id]/documentPagination.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js +81 -0
- package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/editorTypes.js +52 -0
- package/dist/modules/documents/backend/documents/[id]/editorTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/page.js +10 -0
- package/dist/modules/documents/backend/documents/[id]/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/page.meta.js +16 -0
- package/dist/modules/documents/backend/documents/[id]/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js +38 -0
- package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/restoreVersion.js +33 -0
- package/dist/modules/documents/backend/documents/[id]/restoreVersion.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js +392 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js +253 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js +149 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js +82 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js +144 -0
- package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js +237 -0
- package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js +216 -0
- package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js +25 -0
- package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/EntityPicker.js +156 -0
- package/dist/modules/documents/backend/documents/components/EntityPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/EntityPickerResults.js +46 -0
- package/dist/modules/documents/backend/documents/components/EntityPickerResults.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js +127 -0
- package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/PrincipalPicker.js +84 -0
- package/dist/modules/documents/backend/documents/components/PrincipalPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialog.js +77 -0
- package/dist/modules/documents/backend/documents/components/ShareDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js +93 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogList.js +82 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogList.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js +96 -0
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js +169 -0
- package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js +38 -0
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js +69 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js +71 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/principalPickerModel.js +63 -0
- package/dist/modules/documents/backend/documents/components/principalPickerModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/shareDialogModel.js +68 -0
- package/dist/modules/documents/backend/documents/components/shareDialogModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/templateUi.js +89 -0
- package/dist/modules/documents/backend/documents/components/templateUi.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js +30 -0
- package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useEntitySearch.js +191 -0
- package/dist/modules/documents/backend/documents/components/useEntitySearch.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js +221 -0
- package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useShareDialog.js +236 -0
- package/dist/modules/documents/backend/documents/components/useShareDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useTemplateDetail.js +49 -0
- package/dist/modules/documents/backend/documents/components/useTemplateDetail.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js +234 -0
- package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js.map +7 -0
- package/dist/modules/documents/backend/documents/documentUi.js +106 -0
- package/dist/modules/documents/backend/documents/documentUi.js.map +7 -0
- package/dist/modules/documents/backend/documents/documentsListTypes.js +132 -0
- package/dist/modules/documents/backend/documents/documentsListTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/page.js +9 -0
- package/dist/modules/documents/backend/documents/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/page.meta.js +26 -0
- package/dist/modules/documents/backend/documents/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js +91 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesTable.js +58 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesTable.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/page.js +9 -0
- package/dist/modules/documents/backend/documents/templates/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/page.meta.js +34 -0
- package/dist/modules/documents/backend/documents/templates/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js +139 -0
- package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js.map +7 -0
- package/dist/modules/documents/backend/documents/useDocumentsList.js +314 -0
- package/dist/modules/documents/backend/documents/useDocumentsList.js.map +7 -0
- package/dist/modules/documents/ce.js +8 -0
- package/dist/modules/documents/ce.js.map +7 -0
- package/dist/modules/documents/commands/aggregate.js +99 -0
- package/dist/modules/documents/commands/aggregate.js.map +7 -0
- package/dist/modules/documents/commands/attachments.js +252 -0
- package/dist/modules/documents/commands/attachments.js.map +7 -0
- package/dist/modules/documents/commands/comments.js +782 -0
- package/dist/modules/documents/commands/comments.js.map +7 -0
- package/dist/modules/documents/commands/content.js +238 -0
- package/dist/modules/documents/commands/content.js.map +7 -0
- package/dist/modules/documents/commands/document-crud.js +505 -0
- package/dist/modules/documents/commands/document-crud.js.map +7 -0
- package/dist/modules/documents/commands/documents.js +473 -0
- package/dist/modules/documents/commands/documents.js.map +7 -0
- package/dist/modules/documents/commands/duplicate.js +640 -0
- package/dist/modules/documents/commands/duplicate.js.map +7 -0
- package/dist/modules/documents/commands/favorites.js +150 -0
- package/dist/modules/documents/commands/favorites.js.map +7 -0
- package/dist/modules/documents/commands/folders.js +420 -0
- package/dist/modules/documents/commands/folders.js.map +7 -0
- package/dist/modules/documents/commands/index.js +64 -0
- package/dist/modules/documents/commands/index.js.map +7 -0
- package/dist/modules/documents/commands/interceptors.js +300 -0
- package/dist/modules/documents/commands/interceptors.js.map +7 -0
- package/dist/modules/documents/commands/lifecycle.js +249 -0
- package/dist/modules/documents/commands/lifecycle.js.map +7 -0
- package/dist/modules/documents/commands/links.js +410 -0
- package/dist/modules/documents/commands/links.js.map +7 -0
- package/dist/modules/documents/commands/mutation-helpers.js +42 -0
- package/dist/modules/documents/commands/mutation-helpers.js.map +7 -0
- package/dist/modules/documents/commands/projection-types.js +1 -0
- package/dist/modules/documents/commands/projection-types.js.map +7 -0
- package/dist/modules/documents/commands/shared.js +143 -0
- package/dist/modules/documents/commands/shared.js.map +7 -0
- package/dist/modules/documents/commands/shares.js +527 -0
- package/dist/modules/documents/commands/shares.js.map +7 -0
- package/dist/modules/documents/commands/side-effects.js +103 -0
- package/dist/modules/documents/commands/side-effects.js.map +7 -0
- package/dist/modules/documents/commands/templates.js +461 -0
- package/dist/modules/documents/commands/templates.js.map +7 -0
- package/dist/modules/documents/commands/versions.js +432 -0
- package/dist/modules/documents/commands/versions.js.map +7 -0
- package/dist/modules/documents/commands/watchers.js +164 -0
- package/dist/modules/documents/commands/watchers.js.map +7 -0
- package/dist/modules/documents/data/entities.js +653 -0
- package/dist/modules/documents/data/entities.js.map +7 -0
- package/dist/modules/documents/data/extensions.js +44 -0
- package/dist/modules/documents/data/extensions.js.map +7 -0
- package/dist/modules/documents/data/validators.js +219 -0
- package/dist/modules/documents/data/validators.js.map +7 -0
- package/dist/modules/documents/di.js +35 -0
- package/dist/modules/documents/di.js.map +7 -0
- package/dist/modules/documents/encryption.js +12 -0
- package/dist/modules/documents/encryption.js.map +7 -0
- package/dist/modules/documents/events.js +27 -0
- package/dist/modules/documents/events.js.map +7 -0
- package/dist/modules/documents/index.js +19 -0
- package/dist/modules/documents/index.js.map +7 -0
- package/dist/modules/documents/lib/attachmentServicePort.js +30 -0
- package/dist/modules/documents/lib/attachmentServicePort.js.map +7 -0
- package/dist/modules/documents/lib/capabilities.js +32 -0
- package/dist/modules/documents/lib/capabilities.js.map +7 -0
- package/dist/modules/documents/lib/collabCloseEvents.js +17 -0
- package/dist/modules/documents/lib/collabCloseEvents.js.map +7 -0
- package/dist/modules/documents/lib/collabEndpoint.js +22 -0
- package/dist/modules/documents/lib/collabEndpoint.js.map +7 -0
- package/dist/modules/documents/lib/collabMaterializer.js +107 -0
- package/dist/modules/documents/lib/collabMaterializer.js.map +7 -0
- package/dist/modules/documents/lib/collabToken.js +120 -0
- package/dist/modules/documents/lib/collabToken.js.map +7 -0
- package/dist/modules/documents/lib/collaborationAwareness.js +68 -0
- package/dist/modules/documents/lib/collaborationAwareness.js.map +7 -0
- package/dist/modules/documents/lib/commentAnchors.js +15 -0
- package/dist/modules/documents/lib/commentAnchors.js.map +7 -0
- package/dist/modules/documents/lib/constants.js +25 -0
- package/dist/modules/documents/lib/constants.js.map +7 -0
- package/dist/modules/documents/lib/contentService.js +248 -0
- package/dist/modules/documents/lib/contentService.js.map +7 -0
- package/dist/modules/documents/lib/displayLabels.js +23 -0
- package/dist/modules/documents/lib/displayLabels.js.map +7 -0
- package/dist/modules/documents/lib/docxRenderer.js +601 -0
- package/dist/modules/documents/lib/docxRenderer.js.map +7 -0
- package/dist/modules/documents/lib/duplicateContent.js +22 -0
- package/dist/modules/documents/lib/duplicateContent.js.map +7 -0
- package/dist/modules/documents/lib/editorConfig.js +287 -0
- package/dist/modules/documents/lib/editorConfig.js.map +7 -0
- package/dist/modules/documents/lib/entityLinks.js +148 -0
- package/dist/modules/documents/lib/entityLinks.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistry.js +258 -0
- package/dist/modules/documents/lib/entityRegistry.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistry.server.js +189 -0
- package/dist/modules/documents/lib/entityRegistry.server.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistryAvailability.server.js +9 -0
- package/dist/modules/documents/lib/entityRegistryAvailability.server.js.map +7 -0
- package/dist/modules/documents/lib/entitySuggestion.js +50 -0
- package/dist/modules/documents/lib/entitySuggestion.js.map +7 -0
- package/dist/modules/documents/lib/favorites.js +22 -0
- package/dist/modules/documents/lib/favorites.js.map +7 -0
- package/dist/modules/documents/lib/folderHierarchySerialization.js +19 -0
- package/dist/modules/documents/lib/folderHierarchySerialization.js.map +7 -0
- package/dist/modules/documents/lib/historyLimits.js +100 -0
- package/dist/modules/documents/lib/historyLimits.js.map +7 -0
- package/dist/modules/documents/lib/historyPolicy.js +14 -0
- package/dist/modules/documents/lib/historyPolicy.js.map +7 -0
- package/dist/modules/documents/lib/organizationAccess.js +17 -0
- package/dist/modules/documents/lib/organizationAccess.js.map +7 -0
- package/dist/modules/documents/lib/pdfHtml.js +62 -0
- package/dist/modules/documents/lib/pdfHtml.js.map +7 -0
- package/dist/modules/documents/lib/pdfRenderer.js +284 -0
- package/dist/modules/documents/lib/pdfRenderer.js.map +7 -0
- package/dist/modules/documents/lib/permissions.js +222 -0
- package/dist/modules/documents/lib/permissions.js.map +7 -0
- package/dist/modules/documents/lib/platformServices.js +35 -0
- package/dist/modules/documents/lib/platformServices.js.map +7 -0
- package/dist/modules/documents/lib/recordFieldInsertion.js +58 -0
- package/dist/modules/documents/lib/recordFieldInsertion.js.map +7 -0
- package/dist/modules/documents/lib/requestBody.js +80 -0
- package/dist/modules/documents/lib/requestBody.js.map +7 -0
- package/dist/modules/documents/lib/resourceLimits.js +62 -0
- package/dist/modules/documents/lib/resourceLimits.js.map +7 -0
- package/dist/modules/documents/lib/shareCounts.js +25 -0
- package/dist/modules/documents/lib/shareCounts.js.map +7 -0
- package/dist/modules/documents/lib/templateFill.js +84 -0
- package/dist/modules/documents/lib/templateFill.js.map +7 -0
- package/dist/modules/documents/lib/templateInstantiation.js +147 -0
- package/dist/modules/documents/lib/templateInstantiation.js.map +7 -0
- package/dist/modules/documents/lib/templatePreviewDigest.js +82 -0
- package/dist/modules/documents/lib/templatePreviewDigest.js.map +7 -0
- package/dist/modules/documents/lib/templateSeeds.js +201 -0
- package/dist/modules/documents/lib/templateSeeds.js.map +7 -0
- package/dist/modules/documents/lib/userLabels.js +37 -0
- package/dist/modules/documents/lib/userLabels.js.map +7 -0
- package/dist/modules/documents/lib/versionContent.js +131 -0
- package/dist/modules/documents/lib/versionContent.js.map +7 -0
- package/dist/modules/documents/lib/versionLabels.js +12 -0
- package/dist/modules/documents/lib/versionLabels.js.map +7 -0
- package/dist/modules/documents/lib/versioning.js +14 -0
- package/dist/modules/documents/lib/versioning.js.map +7 -0
- package/dist/modules/documents/lib/visibility.js +422 -0
- package/dist/modules/documents/lib/visibility.js.map +7 -0
- package/dist/modules/documents/lib/watchers.js +106 -0
- package/dist/modules/documents/lib/watchers.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260708163836_documents.js +44 -0
- package/dist/modules/documents/migrations/Migration20260708163836_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260709164720_documents.js +17 -0
- package/dist/modules/documents/migrations/Migration20260709164720_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260710002318_documents.js +32 -0
- package/dist/modules/documents/migrations/Migration20260710002318_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260710071003_documents.js +13 -0
- package/dist/modules/documents/migrations/Migration20260710071003_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260712120000_documents.js +13 -0
- package/dist/modules/documents/migrations/Migration20260712120000_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260712183000_documents.js +14 -0
- package/dist/modules/documents/migrations/Migration20260712183000_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260713092156_documents.js +29 -0
- package/dist/modules/documents/migrations/Migration20260713092156_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260717000000_documents.js +37 -0
- package/dist/modules/documents/migrations/Migration20260717000000_documents.js.map +7 -0
- package/dist/modules/documents/notifications.js +47 -0
- package/dist/modules/documents/notifications.js.map +7 -0
- package/dist/modules/documents/search.js +101 -0
- package/dist/modules/documents/search.js.map +7 -0
- package/dist/modules/documents/setup.js +19 -0
- package/dist/modules/documents/setup.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js +214 -0
- package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/context.js +84 -0
- package/dist/modules/documents/widgets/injection/related-documents/context.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js +98 -0
- package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.client.js +83 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.client.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.js +19 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.js.map +7 -0
- package/dist/modules/documents/widgets/injection-table.js +21 -0
- package/dist/modules/documents/widgets/injection-table.js.map +7 -0
- package/dist/server/documents-collab-server.js +2260 -0
- package/dist/server/documents-collab-server.js.map +7 -0
- package/generated/entities/document/index.ts +12 -0
- package/generated/entities/document_attachment/index.ts +9 -0
- package/generated/entities/document_comment/index.ts +14 -0
- package/generated/entities/document_content/index.ts +11 -0
- package/generated/entities/document_entity_link/index.ts +19 -0
- package/generated/entities/document_favorite/index.ts +7 -0
- package/generated/entities/document_folder/index.ts +9 -0
- package/generated/entities/document_share/index.ts +11 -0
- package/generated/entities/document_template/index.ts +13 -0
- package/generated/entities/document_version/index.ts +9 -0
- package/generated/entities/document_watcher/index.ts +7 -0
- package/generated/entities.ids.generated.ts +21 -0
- package/generated/entity-fields-registry.ts +151 -0
- package/jest.config.cjs +34 -0
- package/package.json +149 -0
- package/server/documents-collab-server.ts +3416 -0
- package/src/index.ts +1 -0
- package/src/modules/documents/acl.ts +11 -0
- package/src/modules/documents/api/[id]/archive/route.ts +132 -0
- package/src/modules/documents/api/[id]/attachments/[attachmentId]/route.ts +166 -0
- package/src/modules/documents/api/[id]/attachments/route.ts +173 -0
- package/src/modules/documents/api/[id]/collab-token/route.ts +203 -0
- package/src/modules/documents/api/[id]/comments/access-check/route.ts +96 -0
- package/src/modules/documents/api/[id]/comments/route.ts +442 -0
- package/src/modules/documents/api/[id]/content/route.ts +156 -0
- package/src/modules/documents/api/[id]/duplicate/route.ts +231 -0
- package/src/modules/documents/api/[id]/export/route.ts +535 -0
- package/src/modules/documents/api/[id]/favorite/route.ts +110 -0
- package/src/modules/documents/api/[id]/links/[linkId]/route.ts +92 -0
- package/src/modules/documents/api/[id]/links/route.ts +301 -0
- package/src/modules/documents/api/[id]/principals/route.ts +375 -0
- package/src/modules/documents/api/[id]/route.ts +267 -0
- package/src/modules/documents/api/[id]/shares/route.ts +372 -0
- package/src/modules/documents/api/[id]/unarchive/route.ts +42 -0
- package/src/modules/documents/api/[id]/versions/[versionId]/restore/route.ts +152 -0
- package/src/modules/documents/api/[id]/versions/[versionId]/route.ts +100 -0
- package/src/modules/documents/api/[id]/versions/route.ts +207 -0
- package/src/modules/documents/api/[id]/watch/route.ts +111 -0
- package/src/modules/documents/api/_commands.ts +53 -0
- package/src/modules/documents/api/_shared.ts +676 -0
- package/src/modules/documents/api/folders/route.ts +287 -0
- package/src/modules/documents/api/instantiate/route.ts +114 -0
- package/src/modules/documents/api/route.ts +362 -0
- package/src/modules/documents/api/templates/[templateId]/preview/route.ts +97 -0
- package/src/modules/documents/api/templates/[templateId]/route.ts +92 -0
- package/src/modules/documents/api/templates/route.ts +370 -0
- package/src/modules/documents/backend/documents/DocumentsPageClient.tsx +150 -0
- package/src/modules/documents/backend/documents/DocumentsTable.tsx +168 -0
- package/src/modules/documents/backend/documents/FolderDialog.tsx +62 -0
- package/src/modules/documents/backend/documents/FolderTree.tsx +72 -0
- package/src/modules/documents/backend/documents/MoveDocumentDialog.tsx +70 -0
- package/src/modules/documents/backend/documents/[id]/CommentAnchorNavigation.ts +88 -0
- package/src/modules/documents/backend/documents/[id]/CommentComposer.tsx +136 -0
- package/src/modules/documents/backend/documents/[id]/CommentThreadList.tsx +131 -0
- package/src/modules/documents/backend/documents/[id]/CommentsRail.tsx +138 -0
- package/src/modules/documents/backend/documents/[id]/DocumentCanvas.tsx +111 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.tsx +39 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorIsland.tsx +50 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +162 -0
- package/src/modules/documents/backend/documents/[id]/DocumentNavigator.tsx +129 -0
- package/src/modules/documents/backend/documents/[id]/DocumentPageClient.tsx +463 -0
- package/src/modules/documents/backend/documents/[id]/EditorColorTools.tsx +71 -0
- package/src/modules/documents/backend/documents/[id]/EditorStatusPresence.tsx +78 -0
- package/src/modules/documents/backend/documents/[id]/EditorToolbar.tsx +108 -0
- package/src/modules/documents/backend/documents/[id]/ExportMenu.tsx +144 -0
- package/src/modules/documents/backend/documents/[id]/MentionPicker.tsx +137 -0
- package/src/modules/documents/backend/documents/[id]/OutlinePane.tsx +98 -0
- package/src/modules/documents/backend/documents/[id]/RecordFieldsDialog.tsx +185 -0
- package/src/modules/documents/backend/documents/[id]/RelatedRecordsPanel.tsx +320 -0
- package/src/modules/documents/backend/documents/[id]/VersionHistoryPanel.tsx +244 -0
- package/src/modules/documents/backend/documents/[id]/VersionPreviewDialog.tsx +197 -0
- package/src/modules/documents/backend/documents/[id]/commentTypes.ts +192 -0
- package/src/modules/documents/backend/documents/[id]/componentCapabilities.ts +17 -0
- package/src/modules/documents/backend/documents/[id]/documentPagination.ts +352 -0
- package/src/modules/documents/backend/documents/[id]/editorPresenceStyles.ts +77 -0
- package/src/modules/documents/backend/documents/[id]/editorTypes.ts +84 -0
- package/src/modules/documents/backend/documents/[id]/page.meta.ts +12 -0
- package/src/modules/documents/backend/documents/[id]/page.tsx +6 -0
- package/src/modules/documents/backend/documents/[id]/relatedRecordModel.ts +49 -0
- package/src/modules/documents/backend/documents/[id]/restoreVersion.ts +49 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentCollaboration.ts +458 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentComments.ts +286 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentEditor.ts +157 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentTitle.ts +77 -0
- package/src/modules/documents/backend/documents/[id]/useEditorInsertions.ts +147 -0
- package/src/modules/documents/backend/documents/[id]/useFallbackContentPersistence.ts +277 -0
- package/src/modules/documents/backend/documents/[id]/useMentionPicker.ts +259 -0
- package/src/modules/documents/backend/documents/[id]/versionHistoryModel.ts +29 -0
- package/src/modules/documents/backend/documents/components/EntityPicker.tsx +171 -0
- package/src/modules/documents/backend/documents/components/EntityPickerResults.tsx +64 -0
- package/src/modules/documents/backend/documents/components/NewFromTemplateDialog.tsx +136 -0
- package/src/modules/documents/backend/documents/components/PrincipalPicker.tsx +90 -0
- package/src/modules/documents/backend/documents/components/ShareDialog.tsx +88 -0
- package/src/modules/documents/backend/documents/components/ShareDialogAddForm.tsx +113 -0
- package/src/modules/documents/backend/documents/components/ShareDialogList.tsx +117 -0
- package/src/modules/documents/backend/documents/components/TemplateBodyEditor.tsx +96 -0
- package/src/modules/documents/backend/documents/components/TemplateEditorDialog.tsx +171 -0
- package/src/modules/documents/backend/documents/components/TemplatePreview.tsx +43 -0
- package/src/modules/documents/backend/documents/components/TemplateSlotFields.tsx +72 -0
- package/src/modules/documents/backend/documents/components/TemplateSlotsEditor.tsx +72 -0
- package/src/modules/documents/backend/documents/components/principalPickerModel.ts +86 -0
- package/src/modules/documents/backend/documents/components/shareDialogModel.ts +94 -0
- package/src/modules/documents/backend/documents/components/templateUi.ts +128 -0
- package/src/modules/documents/backend/documents/components/useAvailableEntityRegistry.ts +43 -0
- package/src/modules/documents/backend/documents/components/useEntitySearch.ts +197 -0
- package/src/modules/documents/backend/documents/components/usePrincipalPicker.ts +165 -0
- package/src/modules/documents/backend/documents/components/useShareDialog.ts +267 -0
- package/src/modules/documents/backend/documents/components/useTemplateDetail.ts +54 -0
- package/src/modules/documents/backend/documents/components/useTemplateInstantiation.ts +252 -0
- package/src/modules/documents/backend/documents/documentUi.ts +134 -0
- package/src/modules/documents/backend/documents/documentsListTypes.ts +163 -0
- package/src/modules/documents/backend/documents/page.meta.ts +24 -0
- package/src/modules/documents/backend/documents/page.tsx +5 -0
- package/src/modules/documents/backend/documents/templates/TemplatesPageClient.tsx +93 -0
- package/src/modules/documents/backend/documents/templates/TemplatesTable.tsx +71 -0
- package/src/modules/documents/backend/documents/templates/page.meta.ts +32 -0
- package/src/modules/documents/backend/documents/templates/page.tsx +5 -0
- package/src/modules/documents/backend/documents/templates/useTemplatesPage.tsx +148 -0
- package/src/modules/documents/backend/documents/useDocumentsList.tsx +305 -0
- package/src/modules/documents/ce.ts +6 -0
- package/src/modules/documents/commands/aggregate.ts +128 -0
- package/src/modules/documents/commands/attachments.ts +309 -0
- package/src/modules/documents/commands/comments.ts +966 -0
- package/src/modules/documents/commands/content.ts +294 -0
- package/src/modules/documents/commands/document-crud.ts +609 -0
- package/src/modules/documents/commands/documents.ts +581 -0
- package/src/modules/documents/commands/duplicate.ts +772 -0
- package/src/modules/documents/commands/favorites.ts +171 -0
- package/src/modules/documents/commands/folders.ts +506 -0
- package/src/modules/documents/commands/index.ts +33 -0
- package/src/modules/documents/commands/interceptors.ts +402 -0
- package/src/modules/documents/commands/lifecycle.ts +333 -0
- package/src/modules/documents/commands/links.ts +486 -0
- package/src/modules/documents/commands/mutation-helpers.ts +48 -0
- package/src/modules/documents/commands/projection-types.ts +77 -0
- package/src/modules/documents/commands/shared.ts +219 -0
- package/src/modules/documents/commands/shares.ts +612 -0
- package/src/modules/documents/commands/side-effects.ts +130 -0
- package/src/modules/documents/commands/templates.ts +522 -0
- package/src/modules/documents/commands/versions.ts +537 -0
- package/src/modules/documents/commands/watchers.ts +181 -0
- package/src/modules/documents/data/entities.ts +631 -0
- package/src/modules/documents/data/extensions.ts +42 -0
- package/src/modules/documents/data/validators.ts +248 -0
- package/src/modules/documents/di.ts +33 -0
- package/src/modules/documents/encryption.ts +14 -0
- package/src/modules/documents/events.ts +23 -0
- package/src/modules/documents/i18n/.hardcoded-allowlist.json +9 -0
- package/src/modules/documents/i18n/de.json +522 -0
- package/src/modules/documents/i18n/en.json +522 -0
- package/src/modules/documents/i18n/es.json +522 -0
- package/src/modules/documents/i18n/ko.json +522 -0
- package/src/modules/documents/i18n/pl.json +522 -0
- package/src/modules/documents/index.ts +17 -0
- package/src/modules/documents/lib/attachmentServicePort.ts +94 -0
- package/src/modules/documents/lib/capabilities.ts +57 -0
- package/src/modules/documents/lib/collabCloseEvents.ts +26 -0
- package/src/modules/documents/lib/collabEndpoint.ts +43 -0
- package/src/modules/documents/lib/collabMaterializer.ts +140 -0
- package/src/modules/documents/lib/collabToken.ts +170 -0
- package/src/modules/documents/lib/collaborationAwareness.ts +102 -0
- package/src/modules/documents/lib/commentAnchors.ts +19 -0
- package/src/modules/documents/lib/constants.ts +39 -0
- package/src/modules/documents/lib/contentService.ts +340 -0
- package/src/modules/documents/lib/displayLabels.ts +31 -0
- package/src/modules/documents/lib/docxRenderer.ts +685 -0
- package/src/modules/documents/lib/duplicateContent.ts +32 -0
- package/src/modules/documents/lib/editorConfig.ts +371 -0
- package/src/modules/documents/lib/entityLinks.ts +250 -0
- package/src/modules/documents/lib/entityRegistry.server.ts +267 -0
- package/src/modules/documents/lib/entityRegistry.ts +345 -0
- package/src/modules/documents/lib/entityRegistryAvailability.server.ts +19 -0
- package/src/modules/documents/lib/entitySuggestion.ts +61 -0
- package/src/modules/documents/lib/favorites.ts +35 -0
- package/src/modules/documents/lib/folderHierarchySerialization.ts +34 -0
- package/src/modules/documents/lib/historyLimits.ts +154 -0
- package/src/modules/documents/lib/historyPolicy.ts +7 -0
- package/src/modules/documents/lib/organizationAccess.ts +44 -0
- package/src/modules/documents/lib/pdfHtml.ts +65 -0
- package/src/modules/documents/lib/pdfRenderer.ts +367 -0
- package/src/modules/documents/lib/permissions.ts +299 -0
- package/src/modules/documents/lib/platformServices.ts +70 -0
- package/src/modules/documents/lib/recordFieldInsertion.ts +72 -0
- package/src/modules/documents/lib/requestBody.ts +97 -0
- package/src/modules/documents/lib/resourceLimits.ts +94 -0
- package/src/modules/documents/lib/shareCounts.ts +44 -0
- package/src/modules/documents/lib/templateFill.ts +138 -0
- package/src/modules/documents/lib/templateInstantiation.ts +206 -0
- package/src/modules/documents/lib/templatePreviewDigest.ts +122 -0
- package/src/modules/documents/lib/templateSeeds.ts +248 -0
- package/src/modules/documents/lib/userLabels.ts +58 -0
- package/src/modules/documents/lib/versionContent.ts +163 -0
- package/src/modules/documents/lib/versionLabels.ts +9 -0
- package/src/modules/documents/lib/versioning.ts +29 -0
- package/src/modules/documents/lib/visibility.ts +610 -0
- package/src/modules/documents/lib/watchers.ts +129 -0
- package/src/modules/documents/migrations/.snapshot-open-mercato.json +2892 -0
- package/src/modules/documents/migrations/Migration20260708163836_documents.ts +50 -0
- package/src/modules/documents/migrations/Migration20260709164720_documents.ts +19 -0
- package/src/modules/documents/migrations/Migration20260710002318_documents.ts +35 -0
- package/src/modules/documents/migrations/Migration20260710071003_documents.ts +13 -0
- package/src/modules/documents/migrations/Migration20260712120000_documents.ts +13 -0
- package/src/modules/documents/migrations/Migration20260712183000_documents.ts +12 -0
- package/src/modules/documents/migrations/Migration20260713092156_documents.ts +43 -0
- package/src/modules/documents/migrations/Migration20260717000000_documents.ts +40 -0
- package/src/modules/documents/notifications.ts +45 -0
- package/src/modules/documents/search.ts +118 -0
- package/src/modules/documents/setup.ts +19 -0
- package/src/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.tsx +224 -0
- package/src/modules/documents/widgets/injection/related-documents/context.ts +101 -0
- package/src/modules/documents/widgets/injection/related-documents/useRelatedDocuments.ts +112 -0
- package/src/modules/documents/widgets/injection/related-documents/widget.client.tsx +76 -0
- package/src/modules/documents/widgets/injection/related-documents/widget.ts +19 -0
- package/src/modules/documents/widgets/injection-table.ts +20 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @open-mercato/documents
|
|
2
|
+
|
|
3
|
+
Collaborative internal documents for Open Mercato: a tenant/organization-scoped backoffice module where staff co-author rich-text documents in real time (TipTap + Yjs), organized in folders, shared per-document (owner / editor / commenter / viewer), annotated with inline comments + @mentions, versioned, and exported to `.docx`/PDF.
|
|
4
|
+
|
|
5
|
+
Spec: [Documents collaborative editor](../../.ai/specs/2026-07-08-documents-collaborative-editor.md).
|
|
6
|
+
|
|
7
|
+
## Realtime collaboration sidecar (M2)
|
|
8
|
+
|
|
9
|
+
Real-time editing is served by a **Hocuspocus WebSocket sidecar** — a separate long-lived Node process, **not** a Next.js route (App Router route handlers can't hold long-lived sockets). Entry: [`server/documents-collab-server.ts`](server/documents-collab-server.ts).
|
|
10
|
+
|
|
11
|
+
### Run it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Dev (from the repo root, alongside `yarn dev`):
|
|
15
|
+
yarn workspace @open-mercato/documents collab # tsx server/documents-collab-server.ts
|
|
16
|
+
|
|
17
|
+
# Production package entry:
|
|
18
|
+
yarn workspace @open-mercato/documents build
|
|
19
|
+
yarn workspace @open-mercato/documents collab:prod
|
|
20
|
+
|
|
21
|
+
# Scaffolded standalone app (run as a second workload from the same image):
|
|
22
|
+
yarn documents:collab
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The sidecar bootstraps the app's module registry + ORM via `bootstrapFromAppRoot()` (the same path the `mercato queue worker` fleet uses), then opens a fresh request-scoped container per document load/store so every query is tenant/org-scoped.
|
|
26
|
+
|
|
27
|
+
The create-app Docker Compose templates include a `documents-collab` service on port 4101. In other schedulers, run `yarn documents:collab` as a separate service and set `NEXT_PUBLIC_DOCUMENTS_COLLAB_URL` before building the app to its browser-reachable `ws://` or `wss://` endpoint.
|
|
28
|
+
|
|
29
|
+
### Environment
|
|
30
|
+
|
|
31
|
+
| Var | Where | Default | Purpose |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `DOCUMENTS_COLLAB_PORT` | sidecar | `4101` | WebSocket listen port |
|
|
34
|
+
| `DOCUMENTS_COLLAB_REDIS_URL` | sidecar | unset (or `REDIS_URL`) | Redis used to synchronize Yjs updates and awareness across sidecar replicas. When neither is set the sidecar runs single-node mode and logs a startup warning; multi-instance deployments require Redis |
|
|
35
|
+
| `DOCUMENTS_COLLAB_REDIS_PREFIX` | sidecar | development-only local namespace | Deployment-scoped Redis key prefix. Required whenever Redis is configured in production. All replicas in one deployment must share it; deployments using the same Redis database must use different values |
|
|
36
|
+
| `NEXT_PUBLIC_DOCUMENTS_COLLAB_URL` | app (client) | — | `ws(s)://…` the browser connects to; when unset, users with edit capability get optimistic-locked single-user autosave while read-only users keep the last-saved view |
|
|
37
|
+
| `DOCUMENTS_COLLAB_ALLOWED_ORIGINS` | sidecar | local/test: all | comma-separated exact browser origins allowed during the WebSocket handshake. In production the sidecar requires an allowed origin, sourced from this var **or** `APP_URL`/`NEXT_PUBLIC_APP_URL` |
|
|
38
|
+
| `DOCUMENTS_COLLAB_JWT_SECRET_V2` | app + sidecar | unset (fails closed) | shared secret for v2 capability tokens; must contain at least 32 UTF-8 bytes. When unset the app mints no token and clients fall back to non-collaborative editing |
|
|
39
|
+
| `DOCUMENTS_COLLAB_JWT_SECRET` | app + sidecar | — | optional v1 rollout secret; the sidecar accepts legacy v1 tokens **only** while this is set to a ≥32-byte value that differs from the v2 secret |
|
|
40
|
+
| `DOCUMENTS_COLLAB_APP_ROOT` | sidecar | auto-resolved | app root for `bootstrapFromAppRoot` (defaults to the resolved mercato app) |
|
|
41
|
+
| `DOCUMENTS_COLLAB_START` | sidecar | on | set `off` to import the module without auto-starting the server (used by tests) |
|
|
42
|
+
| `DATABASE_URL` | sidecar | — | required for the cross-process event bridge (force-close on unshare/delete/restore) |
|
|
43
|
+
| `DOCUMENTS_PDF_CHROMIUM_PATH` | app (M4) | auto-probe | Chromium/Chrome executable for PDF export (falls back to `PUPPETEER_EXECUTABLE_PATH` then common system paths) |
|
|
44
|
+
|
|
45
|
+
### Security model (the sidecar is the chokepoint)
|
|
46
|
+
|
|
47
|
+
1. The browser never holds the raw httpOnly session token. It calls `GET /api/documents/[id]/collab-token`, which verifies the session server-side, computes the caller's effective per-doc capabilities, and returns a **~60s** v2 token scoped to the actor, tenant, organization, document, tier, and read-only state on a dedicated collaboration audience.
|
|
48
|
+
2. `onAuthenticate` verifies the v2 secret, expiry, audience, room binding, tenant/organization scope, and the browser `Origin`; viewer/commenter connections are read-only and failures deny by default.
|
|
49
|
+
3. Write enforcement is Hocuspocus's native `connection.readOnly` (drops a read-only connection's `syncStep2`/`update` messages while still serving reads/awareness), a fresh authorization check before every writable frame is applied, and an `onStoreDocument` read-only-tier early-return.
|
|
50
|
+
4. `onLoadDocument`/`onStoreDocument` open a **fresh request-scoped container per operation** and scope every `DocumentContentService` query by the token's `{ tenantId, organizationId }`. Persist writes `yjs_state` + materializes `content_html`/`content_text` + reindexes.
|
|
51
|
+
5. `documents.document.deleted` / `.unshared` / `.version.restored` (all `crossProcessBroadcast: true`, not browser `clientBroadcast` events) reach the sidecar over the cross-process pg bridge and force-close the affected room; a "closing" flag suppresses the room's final store so a restore can't be clobbered.
|
|
52
|
+
|
|
53
|
+
### Degrade
|
|
54
|
+
|
|
55
|
+
If the sidecar is unreachable (or `NEXT_PUBLIC_DOCUMENTS_COLLAB_URL` is unset), a user who still has edit capability gets an **editable single-user fallback**. It persists through the bounded content `PUT` path with the content row's optimistic-lock token, exposes explicit save status/control, and guards navigation while a save is pending. A definitive collaboration authorization rejection, revoked capability, viewer, or commenter remains read-only and fail-closed. PostgreSQL remains authoritative in both modes.
|
|
56
|
+
|
|
57
|
+
Redis replication is transport-only: the authenticated source replica owns
|
|
58
|
+
persistence and publishes its exact Yjs state only after that store succeeds.
|
|
59
|
+
A dedicated command-bounded publisher releases the store lock before fanout;
|
|
60
|
+
if Redis never acknowledges the release, the sidecar resumes after the lock's
|
|
61
|
+
expiry instead of retaining Hocuspocus' save mutex indefinitely. Redis
|
|
62
|
+
delivery failures retain and retry only the latest durable state for each room,
|
|
63
|
+
so post-store Redis recovery never blocks later PostgreSQL persistence. Each
|
|
64
|
+
durable frame carries the content collaboration generation, and replicas reject
|
|
65
|
+
pre-restore frames before they can merge stale Yjs structs into a replacement room.
|
|
66
|
+
The `OMDF1` generation envelope predates the module's first release. Any future
|
|
67
|
+
wire-version change must use a coordinated, non-overlapping sidecar rollout
|
|
68
|
+
(drain and restart every replica); mixed wire versions are intentionally rejected.
|
|
69
|
+
A receiving replica validates the complete merged state before applying or
|
|
70
|
+
broadcasting it and never competes for the durable store lock with an empty
|
|
71
|
+
authorization context. Receivers subscribe before reading their scoped
|
|
72
|
+
PostgreSQL snapshot and retain the loading Y.Doc until registration, so a
|
|
73
|
+
durable publish racing that read cannot be dropped. Set
|
|
74
|
+
`DOCUMENTS_COLLAB_REDIS_PREFIX` to the same deployment-specific value on every
|
|
75
|
+
replica so collaboration traffic cannot cross application environments that
|
|
76
|
+
share a Redis database.
|
|
77
|
+
|
|
78
|
+
The real multi-instance regression starts two Hocuspocus servers plus isolated
|
|
79
|
+
Redis and PostgreSQL containers, edits through both replicas, and verifies the
|
|
80
|
+
merged Yjs state in PostgreSQL:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
yarn workspace @open-mercato/documents test:multi-instance
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The main CI workflow runs this command as the required Docker-capable
|
|
87
|
+
`documents-multi-instance` job, independently from the standard Jest suite
|
|
88
|
+
where the test remains skipped by default.
|
package/build.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { buildPackage } from '../../scripts/build-package.mjs'
|
|
4
|
+
|
|
5
|
+
const packageDir = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const watch = process.argv.includes('--watch')
|
|
7
|
+
|
|
8
|
+
await buildPackage(packageDir, {
|
|
9
|
+
name: 'documents',
|
|
10
|
+
clearDist: true,
|
|
11
|
+
watch,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// The collaboration server is a separate production workload rather than a
|
|
15
|
+
// Next.js route. Compile its public entry so scaffolded apps can run the same
|
|
16
|
+
// package image after development dependencies (including tsx) are pruned.
|
|
17
|
+
await buildPackage(packageDir, {
|
|
18
|
+
name: 'documents-collab',
|
|
19
|
+
entryPoints: [join(packageDir, 'server', 'documents-collab-server.ts')],
|
|
20
|
+
outdir: 'dist',
|
|
21
|
+
outbase: '.',
|
|
22
|
+
watch,
|
|
23
|
+
})
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const features = [
|
|
2
|
+
{ id: "documents.view", title: "View documents", module: "documents" },
|
|
3
|
+
{ id: "documents.create", title: "Create documents", module: "documents", dependsOn: ["documents.view"] },
|
|
4
|
+
{ id: "documents.edit", title: "Edit documents", module: "documents", dependsOn: ["documents.view"] },
|
|
5
|
+
{ id: "documents.delete", title: "Delete documents", module: "documents", dependsOn: ["documents.view"] },
|
|
6
|
+
{ id: "documents.share", title: "Share documents", module: "documents", dependsOn: ["documents.view"] },
|
|
7
|
+
{ id: "documents.templates.manage", title: "Manage document templates", module: "documents", dependsOn: ["documents.view"] },
|
|
8
|
+
{ id: "documents.manage", title: "Manage all documents", module: "documents", dependsOn: ["documents.view"] }
|
|
9
|
+
];
|
|
10
|
+
var acl_default = features;
|
|
11
|
+
export {
|
|
12
|
+
acl_default as default,
|
|
13
|
+
features
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/documents/acl.ts"],
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'documents.view', title: 'View documents', module: 'documents' },\n { id: 'documents.create', title: 'Create documents', module: 'documents', dependsOn: ['documents.view'] },\n { id: 'documents.edit', title: 'Edit documents', module: 'documents', dependsOn: ['documents.view'] },\n { id: 'documents.delete', title: 'Delete documents', module: 'documents', dependsOn: ['documents.view'] },\n { id: 'documents.share', title: 'Share documents', module: 'documents', dependsOn: ['documents.view'] },\n { id: 'documents.templates.manage', title: 'Manage document templates', module: 'documents', dependsOn: ['documents.view'] },\n { id: 'documents.manage', title: 'Manage all documents', module: 'documents', dependsOn: ['documents.view'] },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE,EAAE,IAAI,oBAAoB,OAAO,oBAAoB,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAAA,EACxG,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAAA,EACpG,EAAE,IAAI,oBAAoB,OAAO,oBAAoB,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAAA,EACxG,EAAE,IAAI,mBAAmB,OAAO,mBAAmB,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAAA,EACtG,EAAE,IAAI,8BAA8B,OAAO,6BAA6B,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAAA,EAC3H,EAAE,IAAI,oBAAoB,OAAO,wBAAwB,QAAQ,aAAa,WAAW,CAAC,gBAAgB,EAAE;AAC9G;AAEA,IAAO,cAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { Document } from "../../../data/entities.js";
|
|
7
|
+
import { DOCUMENTS_ENTITY_IDS } from "../../../lib/constants.js";
|
|
8
|
+
import {
|
|
9
|
+
attachDocumentsOperationMetadata,
|
|
10
|
+
buildDocumentsCommandRuntimeContext,
|
|
11
|
+
resolveDocumentsCommandBus
|
|
12
|
+
} from "../../_commands.js";
|
|
13
|
+
import {
|
|
14
|
+
handleDocumentsRouteError,
|
|
15
|
+
resolveActorUserId,
|
|
16
|
+
resolveDocumentCapabilityProjection,
|
|
17
|
+
resolveDocumentsContext,
|
|
18
|
+
routeErrorSchema,
|
|
19
|
+
runMutationGuardAfterSuccess,
|
|
20
|
+
validateMutationGuard,
|
|
21
|
+
withDocumentsContextErrors
|
|
22
|
+
} from "../../_shared.js";
|
|
23
|
+
const lifecycleResponseSchema = z.object({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
archivedAt: z.string().nullable(),
|
|
26
|
+
updatedAt: z.string()
|
|
27
|
+
});
|
|
28
|
+
const metadata = {
|
|
29
|
+
POST: { requireAuth: true, requireFeatures: ["documents.edit"] }
|
|
30
|
+
};
|
|
31
|
+
async function runDocumentLifecycleRoute(request, context, commandId, errorScope) {
|
|
32
|
+
try {
|
|
33
|
+
const { id: documentId } = await context.params;
|
|
34
|
+
const ctx = await resolveDocumentsContext(request, ["documents.edit"]);
|
|
35
|
+
const projection = await resolveDocumentCapabilityProjection(ctx, documentId);
|
|
36
|
+
if (!projection.capabilities.canArchive) throw new CrudHttpError(403, { error: "Forbidden" });
|
|
37
|
+
const document = await findOneWithDecryption(
|
|
38
|
+
ctx.em,
|
|
39
|
+
Document,
|
|
40
|
+
{
|
|
41
|
+
id: documentId,
|
|
42
|
+
tenantId: ctx.tenantId,
|
|
43
|
+
organizationId: ctx.organizationId,
|
|
44
|
+
deletedAt: null
|
|
45
|
+
},
|
|
46
|
+
{ fields: ["id", "archivedAt", "updatedAt"] },
|
|
47
|
+
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
48
|
+
);
|
|
49
|
+
if (!document) throw new CrudHttpError(404, { error: "documents.documents.notFound" });
|
|
50
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
51
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.document,
|
|
52
|
+
resourceId: documentId,
|
|
53
|
+
current: document.updatedAt,
|
|
54
|
+
request
|
|
55
|
+
});
|
|
56
|
+
const guardResult = await validateMutationGuard(ctx, {
|
|
57
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.document,
|
|
58
|
+
resourceId: documentId,
|
|
59
|
+
operation: "update",
|
|
60
|
+
mutationPayload: { action: commandId === "documents.document.archive" ? "archive" : "unarchive" }
|
|
61
|
+
});
|
|
62
|
+
const input = {
|
|
63
|
+
tenantId: ctx.tenantId,
|
|
64
|
+
organizationId: ctx.organizationId,
|
|
65
|
+
documentId,
|
|
66
|
+
actorUserId: resolveActorUserId(ctx.auth),
|
|
67
|
+
expectedUpdatedAt: document.updatedAt.toISOString()
|
|
68
|
+
};
|
|
69
|
+
const execution = await resolveDocumentsCommandBus(ctx).execute(commandId, { input, ctx: buildDocumentsCommandRuntimeContext(ctx) });
|
|
70
|
+
await runMutationGuardAfterSuccess(ctx, guardResult, {
|
|
71
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.document,
|
|
72
|
+
resourceId: documentId,
|
|
73
|
+
operation: "update"
|
|
74
|
+
});
|
|
75
|
+
return attachDocumentsOperationMetadata(
|
|
76
|
+
NextResponse.json({
|
|
77
|
+
id: execution.result.id,
|
|
78
|
+
archivedAt: execution.result.archivedAt,
|
|
79
|
+
updatedAt: execution.result.updatedAt
|
|
80
|
+
}),
|
|
81
|
+
execution.logEntry,
|
|
82
|
+
{ resourceKind: DOCUMENTS_ENTITY_IDS.document, resourceId: documentId }
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return handleDocumentsRouteError(error, errorScope);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function POST(request, context) {
|
|
89
|
+
return runDocumentLifecycleRoute(request, context, "documents.document.archive", "documents.archive");
|
|
90
|
+
}
|
|
91
|
+
const openApi = withDocumentsContextErrors({
|
|
92
|
+
tag: "Documents",
|
|
93
|
+
summary: "Archive document",
|
|
94
|
+
pathParams: z.object({ id: z.string().uuid() }),
|
|
95
|
+
methods: {
|
|
96
|
+
POST: {
|
|
97
|
+
summary: "Archive a document, making it read-only and hidden from default listings",
|
|
98
|
+
responses: [{ status: 200, description: "Archived document state", schema: lifecycleResponseSchema }],
|
|
99
|
+
errors: [
|
|
100
|
+
{ status: 401, description: "Unauthorized", schema: routeErrorSchema },
|
|
101
|
+
{ status: 403, description: "Forbidden", schema: routeErrorSchema },
|
|
102
|
+
{ status: 404, description: "Document not found", schema: routeErrorSchema },
|
|
103
|
+
{ status: 409, description: "Document changed concurrently", schema: routeErrorSchema }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
var route_default = { POST };
|
|
109
|
+
export {
|
|
110
|
+
POST,
|
|
111
|
+
route_default as default,
|
|
112
|
+
metadata,
|
|
113
|
+
openApi,
|
|
114
|
+
runDocumentLifecycleRoute
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/documents/api/%5Bid%5D/archive/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Document } from '../../../data/entities'\nimport { DOCUMENTS_ENTITY_IDS } from '../../../lib/constants'\nimport type { DocumentLifecycleCommandInput, DocumentLifecycleCommandResult } from '../../../commands/lifecycle'\nimport {\n attachDocumentsOperationMetadata,\n buildDocumentsCommandRuntimeContext,\n resolveDocumentsCommandBus,\n} from '../../_commands'\nimport {\n handleDocumentsRouteError,\n resolveActorUserId,\n resolveDocumentCapabilityProjection,\n resolveDocumentsContext,\n routeErrorSchema,\n runMutationGuardAfterSuccess,\n validateMutationGuard,\n withDocumentsContextErrors,\n} from '../../_shared'\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst lifecycleResponseSchema = z.object({\n id: z.string().uuid(),\n archivedAt: z.string().nullable(),\n updatedAt: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['documents.edit'] },\n}\n\nexport async function runDocumentLifecycleRoute(\n request: Request,\n context: RouteContext,\n commandId: 'documents.document.archive' | 'documents.document.unarchive',\n errorScope: string,\n): Promise<Response> {\n try {\n const { id: documentId } = await context.params\n const ctx = await resolveDocumentsContext(request, ['documents.edit'])\n const projection = await resolveDocumentCapabilityProjection(ctx, documentId)\n if (!projection.capabilities.canArchive) throw new CrudHttpError(403, { error: 'Forbidden' })\n\n const document = await findOneWithDecryption(\n ctx.em,\n Document,\n {\n id: documentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n { fields: ['id', 'archivedAt', 'updatedAt'] },\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!document) throw new CrudHttpError(404, { error: 'documents.documents.notFound' })\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: documentId,\n current: document.updatedAt,\n request,\n })\n\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: documentId,\n operation: 'update',\n mutationPayload: { action: commandId === 'documents.document.archive' ? 'archive' : 'unarchive' },\n })\n\n const input: DocumentLifecycleCommandInput = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n documentId,\n actorUserId: resolveActorUserId(ctx.auth),\n expectedUpdatedAt: document.updatedAt.toISOString(),\n }\n const execution = await resolveDocumentsCommandBus(ctx).execute<\n DocumentLifecycleCommandInput,\n DocumentLifecycleCommandResult\n >(commandId, { input, ctx: buildDocumentsCommandRuntimeContext(ctx) })\n\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.document,\n resourceId: documentId,\n operation: 'update',\n })\n return attachDocumentsOperationMetadata(\n NextResponse.json({\n id: execution.result.id,\n archivedAt: execution.result.archivedAt,\n updatedAt: execution.result.updatedAt,\n }),\n execution.logEntry,\n { resourceKind: DOCUMENTS_ENTITY_IDS.document, resourceId: documentId },\n )\n } catch (error) {\n return handleDocumentsRouteError(error, errorScope)\n }\n}\n\nexport async function POST(request: Request, context: RouteContext): Promise<Response> {\n return runDocumentLifecycleRoute(request, context, 'documents.document.archive', 'documents.archive')\n}\n\nexport const openApi: OpenApiRouteDoc = withDocumentsContextErrors({\n tag: 'Documents',\n summary: 'Archive document',\n pathParams: z.object({ id: z.string().uuid() }),\n methods: {\n POST: {\n summary: 'Archive a document, making it read-only and hidden from default listings',\n responses: [{ status: 200, description: 'Archived document state', schema: lifecycleResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Document not found', schema: routeErrorSchema },\n { status: 409, description: 'Document changed concurrently', schema: routeErrorSchema },\n ],\n },\n },\n})\n\nexport default { POST }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
import { DocumentAttachment } from "../../../../data/entities.js";
|
|
6
|
+
import { DOCUMENTS_ENTITY_IDS } from "../../../../lib/constants.js";
|
|
7
|
+
import { resolveAttachmentServicePort } from "../../../../lib/attachmentServicePort.js";
|
|
8
|
+
import { assertTier } from "../../../../lib/permissions.js";
|
|
9
|
+
import {
|
|
10
|
+
assertDocumentNotArchived,
|
|
11
|
+
handleDocumentsRouteError,
|
|
12
|
+
resolveDocumentsContext,
|
|
13
|
+
routeErrorSchema,
|
|
14
|
+
runMutationGuardAfterSuccess,
|
|
15
|
+
validateMutationGuard,
|
|
16
|
+
withDocumentsContextErrors
|
|
17
|
+
} from "../../../_shared.js";
|
|
18
|
+
import {
|
|
19
|
+
buildDocumentsCommandRuntimeContext,
|
|
20
|
+
resolveDocumentsCommandBus
|
|
21
|
+
} from "../../../_commands.js";
|
|
22
|
+
const attachmentBinaryResponseSchema = z.unknown().describe("Binary file content");
|
|
23
|
+
const attachmentDeleteResponseSchema = z.object({ ok: z.boolean() });
|
|
24
|
+
const DOCUMENT_ATTACHMENT_PARTITION_CODE = "privateAttachments";
|
|
25
|
+
const metadata = {
|
|
26
|
+
GET: { requireAuth: true, requireFeatures: ["documents.view"] },
|
|
27
|
+
DELETE: { requireAuth: true, requireFeatures: ["documents.edit"] }
|
|
28
|
+
};
|
|
29
|
+
async function resolveParams(context) {
|
|
30
|
+
const params = await context.params;
|
|
31
|
+
return { documentId: params.id, attachmentId: params.attachmentId };
|
|
32
|
+
}
|
|
33
|
+
async function loadDocumentAttachment(ctx, documentId, attachmentId) {
|
|
34
|
+
const documentAttachment = await findOneWithDecryption(
|
|
35
|
+
ctx.em,
|
|
36
|
+
DocumentAttachment,
|
|
37
|
+
{
|
|
38
|
+
documentId,
|
|
39
|
+
attachmentId,
|
|
40
|
+
tenantId: ctx.tenantId,
|
|
41
|
+
organizationId: ctx.organizationId,
|
|
42
|
+
deletedAt: null
|
|
43
|
+
},
|
|
44
|
+
void 0,
|
|
45
|
+
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
46
|
+
);
|
|
47
|
+
if (!documentAttachment) throw new CrudHttpError(404, { error: "Attachment not found" });
|
|
48
|
+
return documentAttachment;
|
|
49
|
+
}
|
|
50
|
+
async function GET(request, context) {
|
|
51
|
+
try {
|
|
52
|
+
const { documentId, attachmentId } = await resolveParams(context);
|
|
53
|
+
const ctx = await resolveDocumentsContext(request, ["documents.view"]);
|
|
54
|
+
await assertTier(ctx.em, documentId, ctx.auth, "viewer");
|
|
55
|
+
await loadDocumentAttachment(ctx, documentId, attachmentId);
|
|
56
|
+
const forceDownload = new URL(request.url).searchParams.get("download") === "1";
|
|
57
|
+
const attachmentService = resolveAttachmentServicePort(ctx.container);
|
|
58
|
+
const result = await attachmentService.readScoped({
|
|
59
|
+
attachmentId,
|
|
60
|
+
auth: ctx.auth,
|
|
61
|
+
expectedOwner: { entityId: DOCUMENTS_ENTITY_IDS.document, recordId: documentId },
|
|
62
|
+
expectedAssignment: { type: DOCUMENTS_ENTITY_IDS.document, id: documentId },
|
|
63
|
+
expectedPartitionCode: DOCUMENT_ATTACHMENT_PARTITION_CODE,
|
|
64
|
+
requirePrivatePartition: true,
|
|
65
|
+
forceDownload
|
|
66
|
+
});
|
|
67
|
+
const headers = {
|
|
68
|
+
"Cache-Control": "private, no-store, max-age=0, must-revalidate",
|
|
69
|
+
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
70
|
+
"Content-Type": result.contentType,
|
|
71
|
+
"Content-Disposition": result.contentDisposition,
|
|
72
|
+
"Content-Length": String(result.buffer.length),
|
|
73
|
+
"Expires": "0",
|
|
74
|
+
"Pragma": "no-cache",
|
|
75
|
+
"X-Content-Type-Options": "nosniff"
|
|
76
|
+
};
|
|
77
|
+
return new NextResponse(new Uint8Array(result.buffer), { status: 200, headers });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return handleDocumentsRouteError(error, "documents.attachments.read");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function DELETE(request, context) {
|
|
83
|
+
try {
|
|
84
|
+
const { documentId, attachmentId } = await resolveParams(context);
|
|
85
|
+
const ctx = await resolveDocumentsContext(request, ["documents.edit"]);
|
|
86
|
+
await assertTier(ctx.em, documentId, ctx.auth, "editor");
|
|
87
|
+
await assertDocumentNotArchived(ctx, documentId);
|
|
88
|
+
const documentAttachment = await loadDocumentAttachment(ctx, documentId, attachmentId);
|
|
89
|
+
const guardResult = await validateMutationGuard(ctx, {
|
|
90
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,
|
|
91
|
+
resourceId: documentAttachment.id,
|
|
92
|
+
operation: "delete",
|
|
93
|
+
mutationPayload: { documentId, attachmentId }
|
|
94
|
+
});
|
|
95
|
+
const input = {
|
|
96
|
+
documentId,
|
|
97
|
+
attachmentId,
|
|
98
|
+
tenantId: ctx.tenantId,
|
|
99
|
+
organizationId: ctx.organizationId
|
|
100
|
+
};
|
|
101
|
+
await resolveDocumentsCommandBus(ctx).execute("documents.attachment.delete", {
|
|
102
|
+
input,
|
|
103
|
+
ctx: buildDocumentsCommandRuntimeContext(ctx)
|
|
104
|
+
});
|
|
105
|
+
await runMutationGuardAfterSuccess(ctx, guardResult, {
|
|
106
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,
|
|
107
|
+
resourceId: documentAttachment.id,
|
|
108
|
+
operation: "delete"
|
|
109
|
+
});
|
|
110
|
+
return NextResponse.json({ ok: true });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return handleDocumentsRouteError(error, "documents.attachments.delete");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const openApi = withDocumentsContextErrors({
|
|
116
|
+
tag: "Documents",
|
|
117
|
+
summary: "Read document attachment",
|
|
118
|
+
pathParams: z.object({
|
|
119
|
+
id: z.string().uuid(),
|
|
120
|
+
attachmentId: z.string().uuid()
|
|
121
|
+
}),
|
|
122
|
+
methods: {
|
|
123
|
+
GET: {
|
|
124
|
+
summary: "Read a document-scoped attachment",
|
|
125
|
+
responses: [{ status: 200, description: "Attachment bytes", schema: attachmentBinaryResponseSchema }],
|
|
126
|
+
errors: [
|
|
127
|
+
{ status: 401, description: "Unauthorized", schema: routeErrorSchema },
|
|
128
|
+
{ status: 403, description: "Forbidden", schema: routeErrorSchema },
|
|
129
|
+
{ status: 404, description: "Attachment not found", schema: routeErrorSchema },
|
|
130
|
+
{ status: 500, description: "Partition misconfigured", schema: routeErrorSchema },
|
|
131
|
+
{ status: 503, description: "Attachment service unavailable", schema: routeErrorSchema }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
DELETE: {
|
|
135
|
+
summary: "Detach a document-scoped attachment",
|
|
136
|
+
description: "Transactionally removes the document attachment row and quota usage, then runs reference-checked provider cleanup after commit. This operation is audited and intentionally not undoable.",
|
|
137
|
+
responses: [{ status: 200, description: "Attachment deletion committed", schema: attachmentDeleteResponseSchema }],
|
|
138
|
+
errors: [
|
|
139
|
+
{ status: 401, description: "Unauthorized", schema: routeErrorSchema },
|
|
140
|
+
{ status: 403, description: "Forbidden", schema: routeErrorSchema },
|
|
141
|
+
{ status: 404, description: "Attachment not found", schema: routeErrorSchema },
|
|
142
|
+
{ status: 409, description: "Optimistic lock conflict or attachment still referenced", schema: routeErrorSchema }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
var route_default = { GET, DELETE };
|
|
148
|
+
export {
|
|
149
|
+
DELETE,
|
|
150
|
+
GET,
|
|
151
|
+
route_default as default,
|
|
152
|
+
metadata,
|
|
153
|
+
openApi
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/documents/api/%5Bid%5D/attachments/%5BattachmentId%5D/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { DocumentAttachment } from '../../../../data/entities'\nimport { DOCUMENTS_ENTITY_IDS } from '../../../../lib/constants'\nimport { resolveAttachmentServicePort } from '../../../../lib/attachmentServicePort'\nimport { assertTier } from '../../../../lib/permissions'\nimport {\n assertDocumentNotArchived,\n handleDocumentsRouteError,\n resolveDocumentsContext,\n routeErrorSchema,\n runMutationGuardAfterSuccess,\n validateMutationGuard,\n withDocumentsContextErrors,\n} from '../../../_shared'\nimport {\n buildDocumentsCommandRuntimeContext,\n resolveDocumentsCommandBus,\n} from '../../../_commands'\nimport type { DocumentAttachmentDeleteCommandInput } from '../../../../commands/attachments'\n\ntype RouteContext = {\n params: Promise<{ id: string; attachmentId: string }> | { id: string; attachmentId: string }\n}\n\nconst attachmentBinaryResponseSchema = z.unknown().describe('Binary file content')\nconst attachmentDeleteResponseSchema = z.object({ ok: z.boolean() })\nconst DOCUMENT_ATTACHMENT_PARTITION_CODE = 'privateAttachments'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['documents.view'] },\n DELETE: { requireAuth: true, requireFeatures: ['documents.edit'] },\n}\n\nasync function resolveParams(context: RouteContext): Promise<{ documentId: string; attachmentId: string }> {\n const params = await context.params\n return { documentId: params.id, attachmentId: params.attachmentId }\n}\n\nasync function loadDocumentAttachment(\n ctx: Awaited<ReturnType<typeof resolveDocumentsContext>>,\n documentId: string,\n attachmentId: string,\n): Promise<DocumentAttachment> {\n const documentAttachment = await findOneWithDecryption(\n ctx.em,\n DocumentAttachment,\n {\n documentId,\n attachmentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n if (!documentAttachment) throw new CrudHttpError(404, { error: 'Attachment not found' })\n return documentAttachment\n}\n\nexport async function GET(request: Request, context: RouteContext): Promise<Response> {\n try {\n const { documentId, attachmentId } = await resolveParams(context)\n const ctx = await resolveDocumentsContext(request, ['documents.view'])\n await assertTier(ctx.em, documentId, ctx.auth, 'viewer')\n await loadDocumentAttachment(ctx, documentId, attachmentId)\n const forceDownload = new URL(request.url).searchParams.get('download') === '1'\n const attachmentService = resolveAttachmentServicePort(ctx.container)\n const result = await attachmentService.readScoped({\n attachmentId,\n auth: ctx.auth,\n expectedOwner: { entityId: DOCUMENTS_ENTITY_IDS.document, recordId: documentId },\n expectedAssignment: { type: DOCUMENTS_ENTITY_IDS.document, id: documentId },\n expectedPartitionCode: DOCUMENT_ATTACHMENT_PARTITION_CODE,\n requirePrivatePartition: true,\n forceDownload,\n })\n const headers: Record<string, string> = {\n 'Cache-Control': 'private, no-store, max-age=0, must-revalidate',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n 'Content-Type': result.contentType,\n 'Content-Disposition': result.contentDisposition,\n 'Content-Length': String(result.buffer.length),\n 'Expires': '0',\n 'Pragma': 'no-cache',\n 'X-Content-Type-Options': 'nosniff',\n }\n\n return new NextResponse(new Uint8Array(result.buffer), { status: 200, headers })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.attachments.read')\n }\n}\n\nexport async function DELETE(request: Request, context: RouteContext): Promise<Response> {\n try {\n const { documentId, attachmentId } = await resolveParams(context)\n const ctx = await resolveDocumentsContext(request, ['documents.edit'])\n await assertTier(ctx.em, documentId, ctx.auth, 'editor')\n await assertDocumentNotArchived(ctx, documentId)\n const documentAttachment = await loadDocumentAttachment(ctx, documentId, attachmentId)\n const guardResult = await validateMutationGuard(ctx, {\n resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,\n resourceId: documentAttachment.id,\n operation: 'delete',\n mutationPayload: { documentId, attachmentId },\n })\n const input: DocumentAttachmentDeleteCommandInput = {\n documentId,\n attachmentId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n }\n await resolveDocumentsCommandBus(ctx).execute('documents.attachment.delete', {\n input,\n ctx: buildDocumentsCommandRuntimeContext(ctx),\n })\n await runMutationGuardAfterSuccess(ctx, guardResult, {\n resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,\n resourceId: documentAttachment.id,\n operation: 'delete',\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n return handleDocumentsRouteError(error, 'documents.attachments.delete')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = withDocumentsContextErrors({\n tag: 'Documents',\n summary: 'Read document attachment',\n pathParams: z.object({\n id: z.string().uuid(),\n attachmentId: z.string().uuid(),\n }),\n methods: {\n GET: {\n summary: 'Read a document-scoped attachment',\n responses: [{ status: 200, description: 'Attachment bytes', schema: attachmentBinaryResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Attachment not found', schema: routeErrorSchema },\n { status: 500, description: 'Partition misconfigured', schema: routeErrorSchema },\n { status: 503, description: 'Attachment service unavailable', schema: routeErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Detach a document-scoped attachment',\n description: 'Transactionally removes the document attachment row and quota usage, then runs reference-checked provider cleanup after commit. This operation is audited and intentionally not undoable.',\n responses: [{ status: 200, description: 'Attachment deletion committed', schema: attachmentDeleteResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized', schema: routeErrorSchema },\n { status: 403, description: 'Forbidden', schema: routeErrorSchema },\n { status: 404, description: 'Attachment not found', schema: routeErrorSchema },\n { status: 409, description: 'Optimistic lock conflict or attachment still referenced', schema: routeErrorSchema },\n ],\n },\n },\n})\n\nexport default { GET, DELETE }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import {
|
|
5
|
+
DOCUMENT_ATTACHMENT_UPLOAD_CONTEXT
|
|
6
|
+
} from "../../../commands/attachments.js";
|
|
7
|
+
import { DOCUMENTS_ENTITY_IDS } from "../../../lib/constants.js";
|
|
8
|
+
import { readAttachmentUploadForm, resolveAttachmentServicePort } from "../../../lib/attachmentServicePort.js";
|
|
9
|
+
import { assertTier } from "../../../lib/permissions.js";
|
|
10
|
+
import {
|
|
11
|
+
assertDocumentNotArchived,
|
|
12
|
+
handleDocumentsRouteError,
|
|
13
|
+
loadScopedDocument,
|
|
14
|
+
resolveDocumentsContext,
|
|
15
|
+
routeErrorSchema,
|
|
16
|
+
runMutationGuardAfterSuccess,
|
|
17
|
+
validateMutationGuard,
|
|
18
|
+
withDocumentsContextErrors
|
|
19
|
+
} from "../../_shared.js";
|
|
20
|
+
import {
|
|
21
|
+
attachDocumentsOperationMetadata,
|
|
22
|
+
buildDocumentsCommandRuntimeContext,
|
|
23
|
+
resolveDocumentsCommandBus
|
|
24
|
+
} from "../../_commands.js";
|
|
25
|
+
const attachmentUploadBodySchema = z.object({
|
|
26
|
+
file: z.string().min(1).describe("Binary file payload supplied as multipart form-data")
|
|
27
|
+
});
|
|
28
|
+
const attachmentGuardPayloadSchema = z.object({
|
|
29
|
+
fileName: z.string().trim().min(1),
|
|
30
|
+
fileType: z.string(),
|
|
31
|
+
fileSize: z.number().int().nonnegative()
|
|
32
|
+
});
|
|
33
|
+
const attachmentUploadResponseSchema = z.object({
|
|
34
|
+
id: z.string().uuid(),
|
|
35
|
+
attachmentId: z.string().uuid(),
|
|
36
|
+
updatedAt: z.string().datetime(),
|
|
37
|
+
url: z.string()
|
|
38
|
+
});
|
|
39
|
+
const metadata = {
|
|
40
|
+
POST: { requireAuth: true, requireFeatures: ["documents.edit"] }
|
|
41
|
+
};
|
|
42
|
+
async function resolveId(context) {
|
|
43
|
+
const params = await context.params;
|
|
44
|
+
return params.id;
|
|
45
|
+
}
|
|
46
|
+
function assertMultipartUpload(request) {
|
|
47
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
48
|
+
if (!contentType.toLowerCase().includes("multipart/form-data")) {
|
|
49
|
+
throw new CrudHttpError(400, { error: "Expected multipart/form-data" });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function readUploadFile(form) {
|
|
53
|
+
const file = form.get("file");
|
|
54
|
+
if (!(file instanceof File)) {
|
|
55
|
+
throw new CrudHttpError(400, { error: "File is required" });
|
|
56
|
+
}
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
async function POST(request, context) {
|
|
60
|
+
try {
|
|
61
|
+
const documentId = await resolveId(context);
|
|
62
|
+
const ctx = await resolveDocumentsContext(request, ["documents.edit"]);
|
|
63
|
+
await assertTier(ctx.em, documentId, ctx.auth, "editor");
|
|
64
|
+
await assertDocumentNotArchived(ctx, documentId);
|
|
65
|
+
await loadScopedDocument(ctx, documentId);
|
|
66
|
+
const attachmentService = resolveAttachmentServicePort(ctx.container);
|
|
67
|
+
assertMultipartUpload(request);
|
|
68
|
+
const form = await readAttachmentUploadForm(attachmentService, request);
|
|
69
|
+
const file = readUploadFile(form);
|
|
70
|
+
attachmentService.validateUpload({ fileName: file.name, fileSize: file.size });
|
|
71
|
+
const guardPayload = attachmentGuardPayloadSchema.parse({
|
|
72
|
+
fileName: file.name,
|
|
73
|
+
fileType: file.type,
|
|
74
|
+
fileSize: file.size
|
|
75
|
+
});
|
|
76
|
+
const guardResult = await validateMutationGuard(ctx, {
|
|
77
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,
|
|
78
|
+
resourceId: documentId,
|
|
79
|
+
operation: "create",
|
|
80
|
+
mutationPayload: guardPayload,
|
|
81
|
+
mutationPayloadSchema: attachmentGuardPayloadSchema.refine(
|
|
82
|
+
(payload) => payload.fileSize === file.size,
|
|
83
|
+
{ path: ["fileSize"], message: "[internal] mutation guard changed the file size" }
|
|
84
|
+
)
|
|
85
|
+
});
|
|
86
|
+
attachmentService.validateUpload({
|
|
87
|
+
fileName: guardPayload.fileName,
|
|
88
|
+
fileSize: guardPayload.fileSize
|
|
89
|
+
});
|
|
90
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
91
|
+
const commandInput = {
|
|
92
|
+
documentId,
|
|
93
|
+
tenantId: ctx.tenantId,
|
|
94
|
+
organizationId: ctx.organizationId,
|
|
95
|
+
fileName: guardPayload.fileName,
|
|
96
|
+
fileType: guardPayload.fileType || null,
|
|
97
|
+
fileSize: guardPayload.fileSize
|
|
98
|
+
};
|
|
99
|
+
const commandContext = {
|
|
100
|
+
...buildDocumentsCommandRuntimeContext(ctx),
|
|
101
|
+
[DOCUMENT_ATTACHMENT_UPLOAD_CONTEXT]: { buffer }
|
|
102
|
+
};
|
|
103
|
+
const { result, logEntry } = await resolveDocumentsCommandBus(ctx).execute("documents.attachment.create", {
|
|
104
|
+
input: commandInput,
|
|
105
|
+
ctx: commandContext
|
|
106
|
+
});
|
|
107
|
+
await runMutationGuardAfterSuccess(ctx, guardResult, {
|
|
108
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,
|
|
109
|
+
resourceId: result.linkId,
|
|
110
|
+
operation: "create"
|
|
111
|
+
});
|
|
112
|
+
const url = `/api/documents/${encodeURIComponent(documentId)}/attachments/${encodeURIComponent(result.attachmentId)}`;
|
|
113
|
+
const response = NextResponse.json({
|
|
114
|
+
id: result.id,
|
|
115
|
+
attachmentId: result.attachmentId,
|
|
116
|
+
updatedAt: result.updatedAt,
|
|
117
|
+
url
|
|
118
|
+
}, { status: 201 });
|
|
119
|
+
return attachDocumentsOperationMetadata(response, logEntry, {
|
|
120
|
+
resourceKind: DOCUMENTS_ENTITY_IDS.documentAttachment,
|
|
121
|
+
resourceId: result.linkId
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return handleDocumentsRouteError(error, "documents.attachments.create");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const openApi = withDocumentsContextErrors({
|
|
128
|
+
tag: "Documents",
|
|
129
|
+
summary: "Document attachments",
|
|
130
|
+
pathParams: z.object({ id: z.string().uuid() }),
|
|
131
|
+
methods: {
|
|
132
|
+
POST: {
|
|
133
|
+
summary: "Upload document attachment",
|
|
134
|
+
requestBody: { contentType: "multipart/form-data", schema: attachmentUploadBodySchema },
|
|
135
|
+
responses: [{ status: 201, description: "Document-scoped attachment uploaded", schema: attachmentUploadResponseSchema }],
|
|
136
|
+
errors: [
|
|
137
|
+
{ status: 400, description: "Payload validation error", schema: routeErrorSchema },
|
|
138
|
+
{ status: 401, description: "Unauthorized", schema: routeErrorSchema },
|
|
139
|
+
{ status: 403, description: "Forbidden", schema: routeErrorSchema },
|
|
140
|
+
{ status: 413, description: "Attachment too large", schema: routeErrorSchema },
|
|
141
|
+
{ status: 503, description: "Attachment service unavailable", schema: routeErrorSchema }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
var route_default = { POST };
|
|
147
|
+
export {
|
|
148
|
+
POST,
|
|
149
|
+
route_default as default,
|
|
150
|
+
metadata,
|
|
151
|
+
openApi
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=route.js.map
|