Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/document-api/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ describe('document-api contract catalog', () => {
'diff',
'protection',
'permissionRanges',
'customXml',
];
for (const id of OPERATION_IDS) {
expect(validGroups, `${id} has invalid referenceGroup`).toContain(OPERATION_DEFINITIONS[id].referenceGroup);
Expand Down
83 changes: 82 additions & 1 deletion packages/document-api/src/contract/operation-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export type ReferenceGroupKey =
| 'selection'
| 'diff'
| 'protection'
| 'permissionRanges';
| 'permissionRanges'
| 'customXml';

// ---------------------------------------------------------------------------
// Entry shape
Expand Down Expand Up @@ -6259,6 +6260,86 @@ export const OPERATION_DEFINITIONS = {
referenceGroup: 'permissionRanges',
skipAsATool: true,
},

// -------------------------------------------------------------------------
// Custom XML Parts (ECMA-376 Part 1 §15.2.5, §15.2.6, §22.5)
// -------------------------------------------------------------------------

'customXml.parts.list': {
memberPath: 'customXml.parts.list',
description: 'List Custom XML Data Storage Parts in the document, optionally filtered by root namespace or schema reference.',
expectedResult: 'Returns a CustomXmlPartsListResult with summary entries (no content); fetch content via get.',
requiresDocumentContext: true,
metadata: readOperation({
idempotency: 'idempotent',
throws: T_REF_READ_LIST,
}),
referenceDocPath: 'custom-xml/parts/list.mdx',
referenceGroup: 'customXml',
},
'customXml.parts.get': {
memberPath: 'customXml.parts.get',
description:
'Get a single Custom XML Data Storage Part by itemID or package part name, including its full content. ' +
'v1 partName targeting is limited to Word-style customXml/itemN.xml paths.',
expectedResult: 'Returns a CustomXmlPartInfo with id, partName, namespaces, schemaRefs, and content; or null if not found.',
requiresDocumentContext: true,
metadata: readOperation({
throws: T_NOT_FOUND_CAPABLE,
}),
referenceDocPath: 'custom-xml/parts/get.mdx',
referenceGroup: 'customXml',
},
'customXml.parts.create': {
memberPath: 'customXml.parts.create',
description: 'Add a new Custom XML Data Storage Part to the document. Generates a fresh itemID GUID and emits the Properties Part.',
expectedResult: 'Returns a CustomXmlPartsCreateResult with the generated id and package part names on success.',
requiresDocumentContext: true,
metadata: mutationOperation({
idempotency: 'non-idempotent',
supportsDryRun: true,
supportsTrackedMode: false,
possibleFailureCodes: ['INVALID_INPUT'],
throws: T_REF_INSERT,
}),
referenceDocPath: 'custom-xml/parts/create.mdx',
referenceGroup: 'customXml',
},
'customXml.parts.patch': {
memberPath: 'customXml.parts.patch',
description:
'Replace the content and/or schemaRefs of an existing Custom XML Data Storage Part. ' +
'At least one of content or schemaRefs is required. ' +
'v1 partName targeting is limited to Word-style customXml/itemN.xml paths.',
expectedResult: 'Returns a CustomXmlPartsMutationResult indicating success with the resolved target or a failure.',
requiresDocumentContext: true,
metadata: mutationOperation({
idempotency: 'idempotent',
supportsDryRun: true,
supportsTrackedMode: false,
possibleFailureCodes: ['TARGET_NOT_FOUND', 'INVALID_INPUT'],
throws: T_REF_MUTATION,
}),
referenceDocPath: 'custom-xml/parts/patch.mdx',
referenceGroup: 'customXml',
},
'customXml.parts.remove': {
memberPath: 'customXml.parts.remove',
description:
'Remove a Custom XML Data Storage Part and clean up all linked package files (item, props, rels, content-types entry). ' +
'v1 partName targeting is limited to Word-style customXml/itemN.xml paths.',
expectedResult: 'Returns a CustomXmlPartsMutationResult indicating success or a failure.',
requiresDocumentContext: true,
metadata: mutationOperation({
idempotency: 'non-idempotent',
supportsDryRun: true,
supportsTrackedMode: false,
possibleFailureCodes: ['TARGET_NOT_FOUND'],
throws: T_REF_MUTATION_REMOVE,
}),
referenceDocPath: 'custom-xml/parts/remove.mdx',
referenceGroup: 'customXml',
},
} as const satisfies Record<string, OperationDefinitionEntry>;

// ---------------------------------------------------------------------------
Expand Down
39 changes: 39 additions & 0 deletions packages/document-api/src/contract/operation-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ import type {
BookmarkMutationResult,
} from '../bookmarks/bookmarks.types.js';

import type {
CustomXmlPartsListInput,
CustomXmlPartsListResult,
CustomXmlPartsGetInput,
CustomXmlPartInfo,
CustomXmlPartsCreateInput,
CustomXmlPartsCreateResult,
CustomXmlPartsPatchInput,
CustomXmlPartsRemoveInput,
CustomXmlPartsMutationResult,
} from '../customXml/customXml.types.js';

import type {
FootnoteListInput,
FootnotesListResult,
Expand Down Expand Up @@ -1544,6 +1556,33 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry {
options: MutationOptions;
output: PermissionRangeMutationResult;
};

// --- customXml.parts.* ---
'customXml.parts.list': {
input: CustomXmlPartsListInput | undefined;
options: never;
output: CustomXmlPartsListResult;
};
'customXml.parts.get': {
input: CustomXmlPartsGetInput;
options: never;
output: CustomXmlPartInfo | null;
};
'customXml.parts.create': {
input: CustomXmlPartsCreateInput;
options: MutationOptions;
output: CustomXmlPartsCreateResult;
};
'customXml.parts.patch': {
input: CustomXmlPartsPatchInput;
options: MutationOptions;
output: CustomXmlPartsMutationResult;
};
'customXml.parts.remove': {
input: CustomXmlPartsRemoveInput;
options: MutationOptions;
output: CustomXmlPartsMutationResult;
};
}

// --- Bidirectional completeness checks ---
Expand Down
6 changes: 6 additions & 0 deletions packages/document-api/src/contract/reference-doc-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ const GROUP_METADATA: Record<ReferenceGroupKey, { title: string; description: st
description: 'Permission range exception operations for protected documents.',
pagePath: 'permission-ranges/index.mdx',
},
customXml: {
title: 'Custom XML',
description:
'Custom XML Data Storage Part operations (ECMA-376 §15.2.5, §15.2.6). Raw read and write of custom XML parts in the OOXML package.',
pagePath: 'custom-xml/index.mdx',
},
};

export const REFERENCE_OPERATION_GROUPS: readonly ReferenceOperationGroupDefinition[] = (
Expand Down
74 changes: 74 additions & 0 deletions packages/document-api/src/contract/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@
const trackedChangeAddressSchema = ref('TrackedChangeAddress');
const entityAddressSchema = ref('EntityAddress');
const selectionTargetSchema = ref('SelectionTarget');
const targetLocatorSchema = ref('TargetLocator');

Check warning on line 616 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / build

'targetLocatorSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
const deleteBehaviorSchema = ref('DeleteBehavior');
const resolvedHandleSchema = ref('ResolvedHandle');
const pageInfoSchema = ref('PageInfo');
Expand Down Expand Up @@ -888,7 +888,7 @@
text: { type: 'string' },
});

const nodeInfoSchema: JsonSchema = {

Check warning on line 891 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / build

'nodeInfoSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
type: 'object',
required: ['nodeType', 'kind'],
properties: {
Expand All @@ -904,7 +904,7 @@
additionalProperties: false,
};

const matchContextSchema = objectSchema(

Check warning on line 907 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / build

'matchContextSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
address: nodeAddressSchema,
snippet: { type: 'string' },
Expand All @@ -915,7 +915,7 @@
['address', 'snippet', 'highlightRange'],
);

const unknownNodeDiagnosticSchema = objectSchema(

Check warning on line 918 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / build

'unknownNodeDiagnosticSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
message: { type: 'string' },
address: nodeAddressSchema,
Expand Down Expand Up @@ -2583,6 +2583,36 @@

const bookmarkMutation = refMutationSchemas({ bookmark: bookmarkAddressSchema }, ['bookmark']);

// --- Custom XML part schemas ---
const customXmlPartTargetSchema: JsonSchema = {
oneOf: [
// Empty strings are runtime-rejected (target validator requires
// non-zero length); reflect that in the contract so generated SDKs
// and tool callers see the same constraint.
objectSchema({ id: { type: 'string', minLength: 1 } }, ['id']),
objectSchema({ partName: { type: 'string', minLength: 1 } }, ['partName']),
],
};

const customXmlPartMutation = refMutationSchemas(
{
target: customXmlPartTargetSchema,
// Optional: surfaced when patch resolves or mints an itemID. See
// CustomXmlPartsMutationSuccess JSDoc for the patch-foreign-part case.
id: { type: 'string', minLength: 1 },
},
['target'],
);

const customXmlPartCreateMutation = refMutationSchemas(
{
id: { type: 'string' },
partName: { type: 'string' },
propsPartName: { type: 'string' },
},
['id', 'partName', 'propsPartName'],
);

// --- Footnote schemas ---
const footnoteAddressSchema: JsonSchema = objectSchema(
{ kind: { const: 'entity' }, entityType: { const: 'footnote' }, noteId: { type: 'string' } },
Expand Down Expand Up @@ -7628,6 +7658,50 @@
success: { type: 'object' },
failure: { type: 'object' },
},

// --- customXml.parts.* ---
'customXml.parts.list': {
input: objectSchema({
...refListQueryProperties,
rootNamespace: { type: 'string' },
schemaRef: { type: 'string' },
}),
output: discoveryOutputSchema,
},
'customXml.parts.get': {
input: objectSchema({ target: customXmlPartTargetSchema }, ['target']),
output: { oneOf: [{ type: 'object' }, { type: 'null' }] },
},
'customXml.parts.create': {
input: objectSchema(
{
content: { type: 'string', minLength: 1 },
schemaRefs: { type: 'array', items: { type: 'string', minLength: 1 } },
},
['content'],
),
...customXmlPartCreateMutation,
},
'customXml.parts.patch': {
// `target` is required; `content` and `schemaRefs` are both optional
// but at least one MUST be present. Encoded via JSON Schema's `anyOf`.
input: {
type: 'object',
properties: {
target: customXmlPartTargetSchema,
content: { type: 'string', minLength: 1 },
schemaRefs: { type: 'array', items: { type: 'string', minLength: 1 } },
},
required: ['target'],
anyOf: [{ required: ['content'] }, { required: ['schemaRefs'] }],
additionalProperties: false,
},
...customXmlPartMutation,
},
'customXml.parts.remove': {
input: objectSchema({ target: customXmlPartTargetSchema }, ['target']),
...customXmlPartMutation,
},
};

/**
Expand Down
Loading
Loading