diff --git a/plugins/export-assets/lang/cs.json b/plugins/export-assets/lang/cs.json
index 64de0ca22c..f464a9fc35 100644
--- a/plugins/export-assets/lang/cs.json
+++ b/plugins/export-assets/lang/cs.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Pouze platné dokumenty",
"ExportFilterSkipArchivedObsolete": "Vše kromě archivovaných a zastaralých",
"ExportFilterAll": "Všechny dokumenty",
+ "ExportChildDocuments": "Exportovat podřízené dokumenty",
+ "ExportChildDocumentsDescription": "Také exportovat dokumenty připojené k vybraným",
"ExportResultRecordTitle": "Export dokumentů z pracovního prostoru {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/de.json b/plugins/export-assets/lang/de.json
index 1fe8f24d0c..721ab90b39 100644
--- a/plugins/export-assets/lang/de.json
+++ b/plugins/export-assets/lang/de.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Nur wirksame Dokumente exportieren",
"ExportFilterSkipArchivedObsolete": "Alle Dokumente außer archivierten und veralteten",
"ExportFilterAll": "Alle Dokumente",
+ "ExportChildDocuments": "Untergeordnete Dokumente exportieren",
+ "ExportChildDocumentsDescription": "Auch Dokumente exportieren, die an die ausgewählten angehängt sind",
"ExportResultRecordTitle": "Export von Dokumenten aus dem Arbeitsbereich {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/en.json b/plugins/export-assets/lang/en.json
index 4d54dc5009..c7dae4bbd5 100644
--- a/plugins/export-assets/lang/en.json
+++ b/plugins/export-assets/lang/en.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Export only effective documents",
"ExportFilterSkipArchivedObsolete": "All documents except archived and obsolete",
"ExportFilterAll": "All documents",
+ "ExportChildDocuments": "Export child documents",
+ "ExportChildDocumentsDescription": "Also export documents attached to the selected ones",
"ExportResultRecordTitle": "Export of documents from {workspace} workspace ({date})"
}
}
diff --git a/plugins/export-assets/lang/es.json b/plugins/export-assets/lang/es.json
index db2c203cea..dbf975aa75 100644
--- a/plugins/export-assets/lang/es.json
+++ b/plugins/export-assets/lang/es.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Solo documentos efectivos",
"ExportFilterSkipArchivedObsolete": "Todos excepto archivados y obsoletos",
"ExportFilterAll": "Todos los documentos",
+ "ExportChildDocuments": "Exportar documentos secundarios",
+ "ExportChildDocumentsDescription": "Exportar también los documentos adjuntos a los seleccionados",
"ExportResultRecordTitle": "Exportación de documentos del espacio de trabajo {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/fr.json b/plugins/export-assets/lang/fr.json
index dfeb48f579..2587e73342 100644
--- a/plugins/export-assets/lang/fr.json
+++ b/plugins/export-assets/lang/fr.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Seulement les documents effectifs",
"ExportFilterSkipArchivedObsolete": "Tous sauf archivés et obsolètes",
"ExportFilterAll": "Tous les documents",
+ "ExportChildDocuments": "Exporter les documents enfants",
+ "ExportChildDocumentsDescription": "Exporter aussi les documents rattachés aux éléments sélectionnés",
"ExportResultRecordTitle": "Export de documents depuis l'espace de travail {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/it.json b/plugins/export-assets/lang/it.json
index 1098bac228..7972c44049 100644
--- a/plugins/export-assets/lang/it.json
+++ b/plugins/export-assets/lang/it.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Solo documenti effettivi",
"ExportFilterSkipArchivedObsolete": "Tutti tranne archiviati e obsoleti",
"ExportFilterAll": "Tutti i documenti",
+ "ExportChildDocuments": "Esporta documenti figlio",
+ "ExportChildDocumentsDescription": "Esporta anche i documenti collegati a quelli selezionati",
"ExportResultRecordTitle": "Esportazione di documenti dallo spazio di lavoro {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/ja.json b/plugins/export-assets/lang/ja.json
index 015535c90f..5bc3f4de6e 100644
--- a/plugins/export-assets/lang/ja.json
+++ b/plugins/export-assets/lang/ja.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "有効なドキュメントのみ",
"ExportFilterSkipArchivedObsolete": "アーカイブ・廃止を除くすべて",
"ExportFilterAll": "すべてのドキュメント",
+ "ExportChildDocuments": "子ドキュメントをエクスポート",
+ "ExportChildDocumentsDescription": "選択したドキュメントに添付されたドキュメントもエクスポートします",
"ExportResultRecordTitle": "ワークスペース {workspace} からのドキュメントのエクスポート ({date})"
}
}
diff --git a/plugins/export-assets/lang/ko.json b/plugins/export-assets/lang/ko.json
index 5147c679c3..0812b3cc03 100644
--- a/plugins/export-assets/lang/ko.json
+++ b/plugins/export-assets/lang/ko.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "유효한 문서만",
"ExportFilterSkipArchivedObsolete": "보관/폐기 제외 전체",
"ExportFilterAll": "모든 문서",
+ "ExportChildDocuments": "하위 문서 내보내기",
+ "ExportChildDocumentsDescription": "선택한 문서에 첨부된 문서도 함께 내보냅니다",
"ExportResultRecordTitle": "{workspace} 워크스페이스 문서 내보내기 ({date})"
}
}
diff --git a/plugins/export-assets/lang/pt-br.json b/plugins/export-assets/lang/pt-br.json
index 5db9373361..aa2b67a7cb 100644
--- a/plugins/export-assets/lang/pt-br.json
+++ b/plugins/export-assets/lang/pt-br.json
@@ -54,6 +54,8 @@
"ExportFilterEffectiveOnly": "Apenas documentos efetivos",
"ExportFilterSkipArchivedObsolete": "Todos exceto arquivados e obsoletos",
"ExportFilterAll": "Todos os documentos",
+ "ExportChildDocuments": "Exportar documentos filhos",
+ "ExportChildDocumentsDescription": "Exportar também os documentos anexados aos selecionados",
"ExportResultRecordTitle": "Exportação de documentos do espaço de trabalho {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/pt.json b/plugins/export-assets/lang/pt.json
index 883ecd1ddf..ab3e3ed599 100644
--- a/plugins/export-assets/lang/pt.json
+++ b/plugins/export-assets/lang/pt.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Apenas documentos efetivos",
"ExportFilterSkipArchivedObsolete": "Todos exceto arquivados e obsoletos",
"ExportFilterAll": "Todos os documentos",
+ "ExportChildDocuments": "Exportar documentos filhos",
+ "ExportChildDocumentsDescription": "Exportar também os documentos anexados aos selecionados",
"ExportResultRecordTitle": "Exportação de documentos do espaço de trabalho {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/ru.json b/plugins/export-assets/lang/ru.json
index ff3b62de91..590bfa996f 100644
--- a/plugins/export-assets/lang/ru.json
+++ b/plugins/export-assets/lang/ru.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Только действующие документы",
"ExportFilterSkipArchivedObsolete": "Все кроме архивных и устаревших",
"ExportFilterAll": "Все документы",
+ "ExportChildDocuments": "Экспортировать дочерние документы",
+ "ExportChildDocumentsDescription": "Также экспортировать документы, прикреплённые к выбранным",
"ExportResultRecordTitle": "Экспорт документов из пространства {workspace} ({date})"
}
}
diff --git a/plugins/export-assets/lang/tr.json b/plugins/export-assets/lang/tr.json
index 3f897ebf75..c4295c1ba7 100644
--- a/plugins/export-assets/lang/tr.json
+++ b/plugins/export-assets/lang/tr.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "Yalnızca geçerli belgeler",
"ExportFilterSkipArchivedObsolete": "Arşivlenmiş ve geçersiz hariç tümü",
"ExportFilterAll": "Tüm belgeler",
+ "ExportChildDocuments": "Alt belgeleri dışa aktar",
+ "ExportChildDocumentsDescription": "Seçili belgelere ekli belgeleri de dışa aktar",
"ExportResultRecordTitle": "{workspace} çalışma alanından belge dışa aktarma ({date})"
}
}
diff --git a/plugins/export-assets/lang/zh.json b/plugins/export-assets/lang/zh.json
index e8968fe0a0..52fed6ed51 100644
--- a/plugins/export-assets/lang/zh.json
+++ b/plugins/export-assets/lang/zh.json
@@ -53,6 +53,8 @@
"ExportFilterEffectiveOnly": "仅有效文档",
"ExportFilterSkipArchivedObsolete": "除已归档和已过时外的全部",
"ExportFilterAll": "全部文档",
+ "ExportChildDocuments": "导出子文档",
+ "ExportChildDocumentsDescription": "同时导出附加到所选文档上的文档",
"ExportResultRecordTitle": "从工作区 {workspace} 导出文档 ({date})"
}
}
diff --git a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte
index 19dfc4a85a..2d9d39b7d0 100644
--- a/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte
+++ b/plugins/export-resources/src/components/ExportToWorkspaceModal.svelte
@@ -25,7 +25,7 @@
type Class
} from '@hcengineering/core'
import { Card, getClient, getCurrentWorkspaceUuid } from '@hcengineering/presentation'
- import { DropdownLabels, DropdownLabelsIntl, Label } from '@hcengineering/ui'
+ import { DropdownLabels, DropdownLabelsIntl, Label, ToggleWithLabel } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import login from '@hcengineering/login'
import { shouldSkipDocument, isEffectiveDocument } from '@hcengineering/export'
@@ -52,6 +52,11 @@
type ExportFilterMode = 'effectiveOnly' | 'skipArchivedObsolete' | 'all'
let exportFilterMode: ExportFilterMode = 'effectiveOnly'
+ // Whether to recursively export child (collection) documents of the selected docs.
+ // Hidden and forced to true when exporting an entire space — the space already
+ // enumerates every document inside it.
+ let includeChildren: boolean = false
+
const exportFilterItems = [
{ id: 'effectiveOnly' as const, label: plugin.string.ExportFilterEffectiveOnly },
{ id: 'skipArchivedObsolete' as const, label: plugin.string.ExportFilterSkipArchivedObsolete },
@@ -142,6 +147,10 @@
const effectiveDocs = projectDocExport === true ? await getExportDocuments() : filteredSelectedDocs
+ // When exporting an entire space, child documents are picked up by the
+ // space-wide traversal — force the flag on regardless of UI state.
+ const effectiveIncludeChildren = spaceExport === true ? true : includeChildren
+
void exportToWorkspace(
_class,
exportQuery,
@@ -149,7 +158,8 @@
targetWorkspace,
undefined,
exportFilterMode === 'skipArchivedObsolete',
- exportFilterMode === 'effectiveOnly'
+ exportFilterMode === 'effectiveOnly',
+ effectiveIncludeChildren
)
loading = false
dispatch('close', true)
@@ -191,5 +201,14 @@
+ {#if spaceExport !== true}
+
+
+
+ {/if}
diff --git a/plugins/export-resources/src/export.ts b/plugins/export-resources/src/export.ts
index 69333a989b..12c227e431 100644
--- a/plugins/export-resources/src/export.ts
+++ b/plugins/export-resources/src/export.ts
@@ -26,7 +26,8 @@ export async function exportToWorkspace (
targetWorkspace: string | undefined,
relations?: RelationDefinition[] | undefined,
skipDeletedObsolete?: boolean,
- exportOnlyEffective?: boolean
+ exportOnlyEffective?: boolean,
+ includeChildren?: boolean
): Promise {
const lang = getCurrentLanguage()
@@ -73,7 +74,8 @@ export async function exportToWorkspace (
_class,
fieldMappers,
skipDeletedObsolete,
- exportOnlyEffective
+ exportOnlyEffective,
+ includeChildren
}
if (relations != null) {
body.relations = relations
diff --git a/plugins/export-resources/src/plugin.ts b/plugins/export-resources/src/plugin.ts
index 34f8804ce0..6d77384dfc 100644
--- a/plugins/export-resources/src/plugin.ts
+++ b/plugins/export-resources/src/plugin.ts
@@ -56,6 +56,8 @@ export default mergeIds(exportId, exportPlugin, {
ExportFilterMode: '' as IntlString,
ExportFilterEffectiveOnly: '' as IntlString,
ExportFilterSkipArchivedObsolete: '' as IntlString,
- ExportFilterAll: '' as IntlString
+ ExportFilterAll: '' as IntlString,
+ ExportChildDocuments: '' as IntlString,
+ ExportChildDocumentsDescription: '' as IntlString
}
})
diff --git a/services/export/pod-export/src/__tests__/relation-exporter.test.ts b/services/export/pod-export/src/__tests__/relation-exporter.test.ts
index 2dc20315be..fa2e8ed339 100644
--- a/services/export/pod-export/src/__tests__/relation-exporter.test.ts
+++ b/services/export/pod-export/src/__tests__/relation-exporter.test.ts
@@ -127,7 +127,8 @@ describe('RelationExporter sourceClass', () => {
hierarchy,
lowLevel,
expect.any(Map),
- relations
+ relations,
+ false
)
})
@@ -178,7 +179,8 @@ describe('RelationExporter sourceClass', () => {
hierarchy,
lowLevel,
expect.any(Map),
- relations
+ relations,
+ false
)
})
@@ -219,7 +221,8 @@ describe('RelationExporter sourceClass', () => {
hierarchy,
lowLevel,
expect.any(Map),
- relations
+ relations,
+ false
)
})
diff --git a/services/export/pod-export/src/__tests__/workspace-exporter.test.ts b/services/export/pod-export/src/__tests__/workspace-exporter.test.ts
index cfd07b2f83..7873797622 100644
--- a/services/export/pod-export/src/__tests__/workspace-exporter.test.ts
+++ b/services/export/pod-export/src/__tests__/workspace-exporter.test.ts
@@ -541,6 +541,149 @@ describe('CrossWorkspaceExporter', () => {
expect(attachedToArg).not.toBe(SOURCE_DOC_1)
}
})
+
+ it('should NOT export child collection documents when includeChildren is false', async () => {
+ const sourceSpace = createMockSpace(SOURCE_SPACE_ID, 'Test Space')
+ const parentDoc = createMockDoc(SOURCE_DOC_1, mockDocClass, SOURCE_SPACE_ID)
+ const attachedDoc = createMockAttachedDoc(
+ SOURCE_ATTACHED_1,
+ mockAttachedDocClass,
+ SOURCE_SPACE_ID,
+ SOURCE_DOC_1,
+ mockDocClass,
+ 'children',
+ { title: 'Attached Doc' }
+ )
+
+ const hierarchy = createMockHierarchy({
+ domains: new Map([
+ [mockDocClass, 'test_domain'],
+ [mockAttachedDocClass, 'test_domain'],
+ [core.class.Space, 'space_domain']
+ ]),
+ isDerived: new Map([[mockAttachedDocClass, true]]),
+ attributes: new Map([
+ [
+ mockDocClass,
+ new Map([['children', { type: { _class: core.class.Collection, of: mockAttachedDocClass } }]])
+ ],
+ [
+ mockAttachedDocClass,
+ new Map([
+ ['attachedTo', { type: { _class: core.class.RefTo } }],
+ ['attachedToClass', { type: { _class: 'core:class:TypeString' as Ref> } }],
+ ['collection', { type: { _class: 'core:class:TypeString' as Ref> } }]
+ ])
+ ]
+ ])
+ })
+
+ const lowLevelStorage = createMockLowLevelStorage(
+ new Map([
+ ['test_domain', [parentDoc, attachedDoc]],
+ ['space_domain', [sourceSpace]]
+ ])
+ )
+
+ const targetClient = createMockTxOperations([], [], hierarchy)
+ const pipelineFactory = createMockPipelineFactory(hierarchy, lowLevelStorage)
+
+ const exporter = new CrossWorkspaceExporter(
+ mockContext,
+ pipelineFactory,
+ targetClient,
+ mockStorage,
+ undefined,
+ createWorkspaceIds('source-ws'),
+ createWorkspaceIds('target-ws')
+ )
+
+ // Query the parent class only — and explicitly opt OUT of child export.
+ const result = await exporter.export({
+ sourceWorkspace: createWorkspaceIds('source-ws'),
+ targetWorkspace: createWorkspaceIds('target-ws'),
+ sourceQuery: { _id: SOURCE_DOC_1 } as any,
+ _class: mockDocClass,
+ includeChildren: false
+ })
+
+ expect(result.success).toBe(true)
+ // The parent doc must be created via createDoc, but the attached collection
+ // item must NOT go through addCollection because includeChildren is false.
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ expect((targetClient.addCollection as jest.Mock).mock.calls.length).toBe(0)
+ })
+
+ it('should export child collection documents when includeChildren is true', async () => {
+ const sourceSpace = createMockSpace(SOURCE_SPACE_ID, 'Test Space')
+ const parentDoc = createMockDoc(SOURCE_DOC_1, mockDocClass, SOURCE_SPACE_ID)
+ const attachedDoc = createMockAttachedDoc(
+ SOURCE_ATTACHED_1,
+ mockAttachedDocClass,
+ SOURCE_SPACE_ID,
+ SOURCE_DOC_1,
+ mockDocClass,
+ 'children',
+ { title: 'Attached Doc' }
+ )
+
+ const hierarchy = createMockHierarchy({
+ domains: new Map([
+ [mockDocClass, 'test_domain'],
+ [mockAttachedDocClass, 'test_domain'],
+ [core.class.Space, 'space_domain']
+ ]),
+ isDerived: new Map([[mockAttachedDocClass, true]]),
+ attributes: new Map([
+ [
+ mockDocClass,
+ new Map([['children', { type: { _class: core.class.Collection, of: mockAttachedDocClass } }]])
+ ],
+ [
+ mockAttachedDocClass,
+ new Map([
+ ['attachedTo', { type: { _class: core.class.RefTo } }],
+ ['attachedToClass', { type: { _class: 'core:class:TypeString' as Ref> } }],
+ ['collection', { type: { _class: 'core:class:TypeString' as Ref> } }]
+ ])
+ ]
+ ])
+ })
+
+ const lowLevelStorage = createMockLowLevelStorage(
+ new Map([
+ ['test_domain', [parentDoc, attachedDoc]],
+ ['space_domain', [sourceSpace]]
+ ])
+ )
+
+ const targetClient = createMockTxOperations([], [], hierarchy)
+ const pipelineFactory = createMockPipelineFactory(hierarchy, lowLevelStorage)
+
+ const exporter = new CrossWorkspaceExporter(
+ mockContext,
+ pipelineFactory,
+ targetClient,
+ mockStorage,
+ undefined,
+ createWorkspaceIds('source-ws'),
+ createWorkspaceIds('target-ws')
+ )
+
+ // Query the parent class only — and opt IN to recursive child export.
+ const result = await exporter.export({
+ sourceWorkspace: createWorkspaceIds('source-ws'),
+ targetWorkspace: createWorkspaceIds('target-ws'),
+ sourceQuery: { _id: SOURCE_DOC_1 } as any,
+ _class: mockDocClass,
+ includeChildren: true
+ })
+
+ expect(result.success).toBe(true)
+ // With the flag on, the attached collection item must be created via addCollection.
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ expect((targetClient.addCollection as jest.Mock).mock.calls.length).toBeGreaterThan(0)
+ })
})
describe('Forward Relations', () => {
diff --git a/services/export/pod-export/src/server.ts b/services/export/pod-export/src/server.ts
index 0016569bbf..82e449a45e 100644
--- a/services/export/pod-export/src/server.ts
+++ b/services/export/pod-export/src/server.ts
@@ -414,7 +414,8 @@ export function createServer (
relations: rawRelations,
fieldMappers,
skipDeletedObsolete,
- exportOnlyEffective
+ exportOnlyEffective,
+ includeChildren
}: {
targetWorkspace: WorkspaceUuid
_class: Ref>
@@ -427,6 +428,7 @@ export function createServer (
fieldMappers?: Record>
skipDeletedObsolete?: boolean
exportOnlyEffective?: boolean
+ includeChildren?: boolean
} = req.body
// Validate required parameters
@@ -543,6 +545,7 @@ export function createServer (
fieldMappers,
skipDeletedObsolete: skipDeletedObsolete ?? true,
exportOnlyEffective: exportOnlyEffective ?? false,
+ includeChildren: includeChildren ?? false,
customHandlers: [createProductVersionHandler()]
}
diff --git a/services/export/pod-export/src/workspace/document-exporter.ts b/services/export/pod-export/src/workspace/document-exporter.ts
index a73979a2f1..86344d1fe7 100644
--- a/services/export/pod-export/src/workspace/document-exporter.ts
+++ b/services/export/pod-export/src/workspace/document-exporter.ts
@@ -75,7 +75,8 @@ export class DocumentExporter {
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
existingDocsMap: Map[, Doc>,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean = false
): Promise {
if (this.state.processingDocs.has(doc._id)) {
return false
@@ -127,7 +128,8 @@ export class DocumentExporter {
conflictStrategy,
includeAttachments,
sourceHierarchy,
- sourceLowLevel
+ sourceLowLevel,
+ includeChildren
)
// Create the document
@@ -145,7 +147,8 @@ export class DocumentExporter {
conflictStrategy,
includeAttachments,
sourceHierarchy,
- sourceLowLevel
+ sourceLowLevel,
+ includeChildren
)
await this.exportSpaceRelations(
doc,
@@ -154,7 +157,8 @@ export class DocumentExporter {
includeAttachments,
sourceHierarchy,
sourceLowLevel,
- relations
+ relations,
+ includeChildren
)
// Handle attachments
@@ -165,8 +169,12 @@ export class DocumentExporter {
// Handle collaborative content blobs (e.g., document content)
await this.attachmentExporter.exportCollaborativeContent(doc, sourceHierarchy)
- // Handle collections (child documents)
- await this.exportCollections(doc, targetId, sourceHierarchy, sourceLowLevel, relations)
+ // Handle collections (child documents) — only when explicitly requested.
+ // When disabled, only the top-level documents matched by the export query
+ // are exported and their attached collection items are skipped.
+ if (includeChildren) {
+ await this.exportCollections(doc, targetId, sourceHierarchy, sourceLowLevel, relations, includeChildren)
+ }
return true
} catch (err: any) {
@@ -230,7 +238,8 @@ export class DocumentExporter {
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
): Promise {
try {
if (this.relationExporter === undefined) {
@@ -256,7 +265,8 @@ export class DocumentExporter {
conflictStrategy,
includeAttachments,
sourceHierarchy,
- sourceLowLevel
+ sourceLowLevel,
+ includeChildren
)
} catch (err: any) {
this.context.error(`Failed to export relations for space ${space}:`, {
@@ -310,7 +320,8 @@ export class DocumentExporter {
targetDocId: Ref,
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
): Promise {
const attributes = sourceHierarchy.getAllAttributes(sourceDoc._class)
@@ -349,7 +360,8 @@ export class DocumentExporter {
sourceHierarchy,
sourceLowLevel,
new Map(),
- relations
+ relations,
+ includeChildren
)
} catch (err: any) {
this.context.error(`Failed to export collection item ${collectionDoc._id}:`, {
diff --git a/services/export/pod-export/src/workspace/relation-exporter.ts b/services/export/pod-export/src/workspace/relation-exporter.ts
index 99de990cc4..cfebb471e7 100644
--- a/services/export/pod-export/src/workspace/relation-exporter.ts
+++ b/services/export/pod-export/src/workspace/relation-exporter.ts
@@ -34,7 +34,8 @@ export type ExportDocumentFn = (
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
existingDocsMap: Map][, Doc>,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
) => Promise
/**
@@ -56,7 +57,8 @@ export class RelationExporter {
conflictStrategy: 'skip' | 'duplicate',
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
- sourceLowLevel: LowLevelStorage
+ sourceLowLevel: LowLevelStorage,
+ includeChildren: boolean = false
): Promise {
for (const relation of relations) {
const direction = relation.direction ?? 'forward'
@@ -71,7 +73,8 @@ export class RelationExporter {
includeAttachments,
sourceHierarchy,
sourceLowLevel,
- relations
+ relations,
+ includeChildren
)
} catch (err: any) {
this.context.error(`Failed to export forward relation ${relation.field} for document ${doc._id}:`, {
@@ -92,7 +95,8 @@ export class RelationExporter {
conflictStrategy: 'skip' | 'duplicate',
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
- sourceLowLevel: LowLevelStorage
+ sourceLowLevel: LowLevelStorage,
+ includeChildren: boolean = false
): Promise {
for (const relation of relations) {
const direction = relation.direction ?? 'forward'
@@ -107,7 +111,8 @@ export class RelationExporter {
includeAttachments,
sourceHierarchy,
sourceLowLevel,
- relations
+ relations,
+ includeChildren
)
} catch (err: any) {
this.context.error(`Failed to export inverse relation ${relation.field} for document ${doc._id}:`, {
@@ -125,7 +130,8 @@ export class RelationExporter {
conflictStrategy: 'skip' | 'duplicate',
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
- sourceLowLevel: LowLevelStorage
+ sourceLowLevel: LowLevelStorage,
+ includeChildren: boolean = false
): Promise {
await this.exportForwardRelations(
doc,
@@ -133,7 +139,8 @@ export class RelationExporter {
conflictStrategy,
includeAttachments,
sourceHierarchy,
- sourceLowLevel
+ sourceLowLevel,
+ includeChildren
)
await this.exportInverseRelations(
doc,
@@ -141,7 +148,8 @@ export class RelationExporter {
conflictStrategy,
includeAttachments,
sourceHierarchy,
- sourceLowLevel
+ sourceLowLevel,
+ includeChildren
)
}
@@ -152,7 +160,8 @@ export class RelationExporter {
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
): Promise {
if (relation.sourceClass !== undefined && !sourceHierarchy.isDerived(doc._class, relation.sourceClass)) {
return
@@ -185,7 +194,8 @@ export class RelationExporter {
includeAttachments,
sourceHierarchy,
sourceLowLevel,
- relations
+ relations,
+ includeChildren
)
}
}
@@ -197,7 +207,8 @@ export class RelationExporter {
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
): Promise {
if (relation.sourceClass !== undefined && !sourceHierarchy.isDerived(doc._class, relation.sourceClass)) {
return
@@ -232,7 +243,8 @@ export class RelationExporter {
sourceHierarchy,
sourceLowLevel,
new Map(),
- relations
+ relations,
+ includeChildren
)
}
}
@@ -244,7 +256,8 @@ export class RelationExporter {
includeAttachments: boolean,
sourceHierarchy: Hierarchy,
sourceLowLevel: LowLevelStorage,
- relations: RelationDefinition[]
+ relations: RelationDefinition[],
+ includeChildren: boolean
): Promise {
if (this.state.idMapping.has(ref)) {
return
@@ -274,7 +287,8 @@ export class RelationExporter {
sourceHierarchy,
sourceLowLevel,
new Map(),
- relations
+ relations,
+ includeChildren
)
}
}
diff --git a/services/export/pod-export/src/workspace/types.ts b/services/export/pod-export/src/workspace/types.ts
index 369b6bde29..d36202d0f5 100644
--- a/services/export/pod-export/src/workspace/types.ts
+++ b/services/export/pod-export/src/workspace/types.ts
@@ -84,6 +84,10 @@ export interface ExportOptions {
skipDeletedObsolete?: boolean
// Whether to export only documents with effective status
exportOnlyEffective?: boolean
+ // Whether to recursively export child (collection) documents of each exported doc.
+ // When false, only the documents matched by the top-level query are exported and
+ // their attached collection items are skipped.
+ includeChildren?: boolean
// Class-specific export handlers, applied before the default flow. Useful
// for collapsing or deduplicating documents (e.g. ProductVersion).
customHandlers?: CustomExportHandler[]
diff --git a/services/export/pod-export/src/workspace/workspace-exporter.ts b/services/export/pod-export/src/workspace/workspace-exporter.ts
index 62eed3501c..ddf6d606fe 100644
--- a/services/export/pod-export/src/workspace/workspace-exporter.ts
+++ b/services/export/pod-export/src/workspace/workspace-exporter.ts
@@ -98,7 +98,8 @@ export class CrossWorkspaceExporter {
sourceHierarchy,
sourceLowLevel,
existingDocsMap,
- relations
+ relations,
+ includeChildren
) => {
return await this.documentExporter.exportDocument(
doc,
@@ -107,7 +108,8 @@ export class CrossWorkspaceExporter {
sourceHierarchy,
sourceLowLevel,
existingDocsMap,
- relations
+ relations,
+ includeChildren
)
}
)
@@ -128,6 +130,7 @@ export class CrossWorkspaceExporter {
fieldMappers = {},
skipDeletedObsolete = true,
exportOnlyEffective = false,
+ includeChildren = false,
customHandlers = []
} = options
@@ -258,7 +261,8 @@ export class CrossWorkspaceExporter {
hierarchy,
lowLevelStorage,
existingDocsMap,
- resolvedRelations
+ resolvedRelations,
+ includeChildren
)
if (exported) {
result.exportedCount++
]