From ddf6687f8ca8d10246943bc57eb5e5130bd82b87 Mon Sep 17 00:00:00 2001 From: eyelidlessness Date: Thu, 26 Sep 2024 16:04:19 -0700 Subject: [PATCH] engine: implementation of applicable aspects of `prepareSubmission` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full support for `SubmissionResult<‘monolithic’>` - Minimal support for `SubmissionResult<‘chunked’>` Open questions: 1. Should we totally stub off the chunked option for now? I would normally say yes, as it’ll give us more feedback as we implement missing functionality. Hesitation is that the minimal support is already somewhat useful/usable until we support submission attachments. 2. Regardless of how extensively we support the chunked option now, do we have enough of the prerequisites in place to add stub test failures which will alert us when those tests need to be updated for more complete chunked submission support? --- packages/xforms-engine/src/instance/Root.ts | 22 ++- .../ClientReactiveSubmittableInstance.ts | 19 ++ .../ClientReactiveSubmittableLeafNode.ts | 5 + .../ClientReactiveSubmittableParentNode.ts | 3 +- .../submission/prepareSubmission.ts | 172 ++++++++++++++++++ 5 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 packages/xforms-engine/src/instance/internal-api/submission/ClientReactiveSubmittableInstance.ts create mode 100644 packages/xforms-engine/src/lib/client-reactivity/submission/prepareSubmission.ts 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); + } +};