diff --git a/packages/xforms-engine/src/instance/Root.ts b/packages/xforms-engine/src/instance/Root.ts index bacb66db4..ed868fc69 100644 --- a/packages/xforms-engine/src/instance/Root.ts +++ b/packages/xforms-engine/src/instance/Root.ts @@ -4,6 +4,7 @@ import { createSignal } from 'solid-js'; import type { ActiveLanguage, FormLanguage, FormLanguages } from '../client/FormLanguage.ts'; import type { FormNodeID } from '../client/identity.ts'; import type { RootNode } from '../client/RootNode.ts'; +import type { SubmissionDefinition } from '../client/submission/SubmissionDefinition.ts'; import type { SubmissionChunkedType, SubmissionOptions, @@ -12,6 +13,7 @@ import type { SubmissionResult } from '../client/submission/SubmissionResult.ts' import type { SubmissionState } from '../client/submission/SubmissionState.ts'; import type { AncestorNodeValidationState } from '../client/validation.ts'; import { createParentNodeSubmissionState } from '../lib/client-reactivity/submission/createParentNodeSubmissionState.ts'; +import { prepareSubmission } from '../lib/client-reactivity/submission/prepareSubmission.ts'; import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts'; import { createChildrenState } from '../lib/reactivity/createChildrenState.ts'; import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts'; @@ -29,7 +31,7 @@ import { buildChildren } from './children.ts'; import type { GeneralChildNode } from './hierarchy.ts'; import type { EvaluationContext, EvaluationContextRoot } from './internal-api/EvaluationContext.ts'; import type { InstanceConfig } from './internal-api/InstanceConfig.ts'; -import type { ClientReactiveSubmittableParentNode } from './internal-api/submission/ClientReactiveSubmittableParentNode.ts'; +import type { ClientReactiveSubmittableInstance } from './internal-api/submission/ClientReactiveSubmittableInstance.ts'; import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts'; import type { TranslationContext } from './internal-api/TranslationContext.ts'; @@ -108,7 +110,7 @@ export class Root EvaluationContextRoot, SubscribableDependency, TranslationContext, - ClientReactiveSubmittableParentNode + ClientReactiveSubmittableInstance { private readonly childrenState: ChildrenState; @@ -128,6 +130,11 @@ export class Root readonly validationState: AncestorNodeValidationState; readonly submissionState: SubmissionState; + // ClientReactiveSubmittableInstance + get submissionDefinition(): SubmissionDefinition { + return this.definition.submission; + } + protected readonly instanceDOM: XFormDOM; // BaseNode @@ -228,10 +235,15 @@ export class Root return this; } - prepareSubmission( - _options?: SubmissionOptions + prepareSubmission( + options?: SubmissionOptions ): Promise> { - throw new Error('Not implemented'); + const result = prepareSubmission(this, { + chunked: (options?.chunked ?? 'monolithic') as ChunkedType, + maxSize: options?.maxSize ?? Infinity, + }); + + return Promise.resolve(result); } // SubscribableDependency diff --git a/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableInstance.ts b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableInstance.ts new file mode 100644 index 000000000..49f5ca63f --- /dev/null +++ b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableInstance.ts @@ -0,0 +1,19 @@ +import type { SubmissionDefinition } from '../../../client/submission/SubmissionDefinition.ts'; +import type { AncestorNodeValidationState } from '../../../client/validation.ts'; +import type { GeneralChildNode } from '../../hierarchy.ts'; +import type { + ClientReactiveSubmittableParentNode, + ClientReactiveSubmittableParentNodeDefinition, +} from './ClientReactiveSubmittableParentNode.ts'; + +interface ClientReactiveSubmittableInstanceDefinition + extends ClientReactiveSubmittableParentNodeDefinition { + readonly submission: SubmissionDefinition; +} + +export interface ClientReactiveSubmittableInstance + extends ClientReactiveSubmittableParentNode { + readonly definition: ClientReactiveSubmittableInstanceDefinition; + readonly parent: null; + readonly validationState: AncestorNodeValidationState; +} diff --git a/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableLeafNode.ts b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableLeafNode.ts index eda668999..d224eb2b9 100644 --- a/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableLeafNode.ts +++ b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableLeafNode.ts @@ -1,5 +1,9 @@ import type { SubmissionState } from '../../../client/submission/SubmissionState.ts'; import type { EscapedXMLText } from '../../../lib/xml-serialization.ts'; +import type { + ClientReactiveSubmittableChildNode, + ClientReactiveSubmittableParentNode, +} from './ClientReactiveSubmittableParentNode.ts'; interface ClientReactiveSubmittableLeafNodeCurrentState { get relevant(): boolean; @@ -14,6 +18,7 @@ interface ClientReactiveSubmittableLeafNodeDefinition { export interface ClientReactiveSubmittableLeafNode { readonly definition: ClientReactiveSubmittableLeafNodeDefinition; + readonly parent: ClientReactiveSubmittableParentNode; readonly currentState: ClientReactiveSubmittableLeafNodeCurrentState; /** diff --git a/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableParentNode.ts b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableParentNode.ts index 3c2656120..aa64e5a74 100644 --- a/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableParentNode.ts +++ b/packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableParentNode.ts @@ -11,7 +11,7 @@ interface ClientReactiveSubmittableParentNodeCurrentState< get children(): readonly Child[]; } -interface ClientReactiveSubmittableParentNodeDefinition { +export interface ClientReactiveSubmittableParentNodeDefinition { readonly nodeName: string; } @@ -19,6 +19,7 @@ export interface ClientReactiveSubmittableParentNode< Child extends ClientReactiveSubmittableChildNode, > { readonly definition: ClientReactiveSubmittableParentNodeDefinition; + readonly parent: ClientReactiveSubmittableParentNode | null; readonly currentState: ClientReactiveSubmittableParentNodeCurrentState; readonly submissionState: SubmissionState; } diff --git a/packages/xforms-engine/src/lib/client-reactivity/submission/prepareSubmission.ts b/packages/xforms-engine/src/lib/client-reactivity/submission/prepareSubmission.ts new file mode 100644 index 000000000..6a72075f6 --- /dev/null +++ b/packages/xforms-engine/src/lib/client-reactivity/submission/prepareSubmission.ts @@ -0,0 +1,172 @@ +import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts'; +import { + SUBMISSION_INSTANCE_FILE_NAME, + SUBMISSION_INSTANCE_FILE_TYPE, +} from '../../../client/constants.ts'; +import type { SubmissionData } from '../../../client/submission/SubmissionData.ts'; +import type { SubmissionDefinition } from '../../../client/submission/SubmissionDefinition.ts'; +import type { SubmissionInstanceFile } from '../../../client/submission/SubmissionInstanceFile.ts'; +import type { SubmissionChunkedType } from '../../../client/submission/SubmissionOptions.ts'; +import type { SubmissionResult } from '../../../client/submission/SubmissionResult.ts'; +import type { DescendantNodeViolationReference } from '../../../client/validation.ts'; +import type { ClientReactiveSubmittableInstance } from '../../../instance/internal-api/submission/ClientReactiveSubmittableInstance.ts'; + +class InstanceFile extends File implements SubmissionInstanceFile { + override readonly name = SUBMISSION_INSTANCE_FILE_NAME; + override readonly type = SUBMISSION_INSTANCE_FILE_TYPE; + + constructor(instanceRoot: ClientReactiveSubmittableInstance) { + const { submissionXML } = instanceRoot.submissionState; + + super([submissionXML], SUBMISSION_INSTANCE_FILE_NAME, { + type: SUBMISSION_INSTANCE_FILE_TYPE, + }); + } +} + +type AssertSubmissionData = (data: FormData) => asserts data is SubmissionData; + +const assertSubmissionData: AssertSubmissionData = (data) => { + const instanceFile = data.get(SUBMISSION_INSTANCE_FILE_NAME); + + if (!(instanceFile instanceof InstanceFile)) { + throw new Error(`Invalid SubmissionData`); + } +}; + +class InstanceSubmissionData extends FormData { + static from(instanceFile: InstanceFile, attachments: readonly File[]): SubmissionData { + const data = new this(instanceFile, attachments); + + assertSubmissionData(data); + + return data; + } + + private constructor( + readonly instanceFile: InstanceFile, + readonly attachments: readonly File[] + ) { + super(); + + this.set(SUBMISSION_INSTANCE_FILE_NAME, instanceFile); + + attachments.forEach((attachment) => { + const { name } = attachment; + + if (name === SUBMISSION_INSTANCE_FILE_NAME && attachment !== instanceFile) { + throw new Error( + `Failed to add conflicting attachment with name ${SUBMISSION_INSTANCE_FILE_NAME}` + ); + } + + this.set(name, attachment); + }); + } +} + +interface PendingValidation { + readonly status: 'pending'; + readonly violations: readonly DescendantNodeViolationReference[]; +} + +interface ReadyValidation { + readonly status: 'ready'; + readonly violations: null; +} + +type SubmissionInstanceStateValidation = PendingValidation | ReadyValidation; + +const validateSubmission = ( + instanceRoot: ClientReactiveSubmittableInstance +): SubmissionInstanceStateValidation => { + const { violations } = instanceRoot.validationState; + + if (violations.length === 0) { + return { + status: 'ready', + violations: null, + }; + } + + return { + status: 'pending', + violations, + }; +}; + +const monolithicSubmissionResult = ( + validation: SubmissionInstanceStateValidation, + definition: SubmissionDefinition, + instanceFile: InstanceFile, + attachments: readonly File[] +): SubmissionResult<'monolithic'> => { + const data = InstanceSubmissionData.from(instanceFile, attachments); + + return { + ...validation, + definition, + data, + }; +}; + +interface ChunkedSubmissionResultOptions { + readonly maxSize: number; +} + +const chunkedSubmissionResult = ( + validation: SubmissionInstanceStateValidation, + definition: SubmissionDefinition, + instanceFile: InstanceFile, + attachments: readonly File[], + options: ChunkedSubmissionResultOptions +): SubmissionResult<'chunked'> => { + if (attachments.length > 0 || options.maxSize !== Infinity) { + throw new Error('Submission chunking pending implementation'); + } + + const data = InstanceSubmissionData.from(instanceFile, attachments); + + return { + ...validation, + definition, + data: [data], + }; +}; + +export interface PrepeareSubmissionOptions { + readonly chunked: ChunkedType; + readonly maxSize: number; +} + +export const prepareSubmission = ( + instanceRoot: ClientReactiveSubmittableInstance, + options: PrepeareSubmissionOptions +): SubmissionResult => { + const validation = validateSubmission(instanceRoot); + const definition = instanceRoot.definition.submission; + const instanceFile = new InstanceFile(instanceRoot); + const attachments: readonly File[] = []; + + switch (options.chunked) { + case 'chunked': + return chunkedSubmissionResult( + validation, + definition, + instanceFile, + attachments, + options + ) satisfies SubmissionResult<'chunked'> as SubmissionResult; + + case 'monolithic': + return monolithicSubmissionResult( + validation, + definition, + instanceFile, + attachments + ) satisfies SubmissionResult<'monolithic'> as SubmissionResult; + + default: + throw new UnreachableError(options.chunked); + } +};