Skip to content

Commit

Permalink
engine: implementation of applicable aspects of prepareSubmission
Browse files Browse the repository at this point in the history
- 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?
  • Loading branch information
eyelidlessness committed Sep 26, 2024
1 parent e157c37 commit ddf6687
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 6 deletions.
22 changes: 17 additions & 5 deletions packages/xforms-engine/src/instance/Root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -108,7 +110,7 @@ export class Root
EvaluationContextRoot,
SubscribableDependency,
TranslationContext,
ClientReactiveSubmittableParentNode<GeneralChildNode>
ClientReactiveSubmittableInstance
{
private readonly childrenState: ChildrenState<GeneralChildNode>;

Expand All @@ -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
Expand Down Expand Up @@ -228,10 +235,15 @@ export class Root
return this;
}

prepareSubmission<ChunkedType extends SubmissionChunkedType>(
_options?: SubmissionOptions<ChunkedType>
prepareSubmission<ChunkedType extends SubmissionChunkedType = 'monolithic'>(
options?: SubmissionOptions<ChunkedType>
): Promise<SubmissionResult<ChunkedType>> {
throw new Error('Not implemented');
const result = prepareSubmission(this, {
chunked: (options?.chunked ?? 'monolithic') as ChunkedType,
maxSize: options?.maxSize ?? Infinity,
});

return Promise.resolve(result);
}

// SubscribableDependency
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GeneralChildNode> {
readonly definition: ClientReactiveSubmittableInstanceDefinition;
readonly parent: null;
readonly validationState: AncestorNodeValidationState;
}
Original file line number Diff line number Diff line change
@@ -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<RuntimeValue> {
get relevant(): boolean;
Expand All @@ -14,6 +18,7 @@ interface ClientReactiveSubmittableLeafNodeDefinition {

export interface ClientReactiveSubmittableLeafNode<RuntimeValue> {
readonly definition: ClientReactiveSubmittableLeafNodeDefinition;
readonly parent: ClientReactiveSubmittableParentNode<ClientReactiveSubmittableChildNode>;
readonly currentState: ClientReactiveSubmittableLeafNodeCurrentState<RuntimeValue>;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ interface ClientReactiveSubmittableParentNodeCurrentState<
get children(): readonly Child[];
}

interface ClientReactiveSubmittableParentNodeDefinition {
export interface ClientReactiveSubmittableParentNodeDefinition {
readonly nodeName: string;
}

export interface ClientReactiveSubmittableParentNode<
Child extends ClientReactiveSubmittableChildNode,
> {
readonly definition: ClientReactiveSubmittableParentNodeDefinition;
readonly parent: ClientReactiveSubmittableParentNode<ClientReactiveSubmittableChildNode> | null;
readonly currentState: ClientReactiveSubmittableParentNodeCurrentState<Child>;
readonly submissionState: SubmissionState;
}
Original file line number Diff line number Diff line change
@@ -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<ChunkedType extends SubmissionChunkedType> {
readonly chunked: ChunkedType;
readonly maxSize: number;
}

export const prepareSubmission = <ChunkedType extends SubmissionChunkedType>(
instanceRoot: ClientReactiveSubmittableInstance,
options: PrepeareSubmissionOptions<ChunkedType>
): SubmissionResult<ChunkedType> => {
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<ChunkedType>;

case 'monolithic':
return monolithicSubmissionResult(
validation,
definition,
instanceFile,
attachments
) satisfies SubmissionResult<'monolithic'> as SubmissionResult<ChunkedType>;

default:
throw new UnreachableError(options.chunked);
}
};

0 comments on commit ddf6687

Please sign in to comment.