@getodk/xforms-engine 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/InputNode.d.ts +4 -2
- package/dist/client/NoteNode.d.ts +4 -2
- package/dist/client/RootNode.d.ts +4 -4
- package/dist/client/UploadNode.d.ts +53 -0
- package/dist/client/attachments/InstanceAttachmentMeta.d.ts +8 -0
- package/dist/client/attachments/InstanceAttachmentsConfig.d.ts +8 -0
- package/dist/client/form/FormInstanceConfig.d.ts +2 -0
- package/dist/client/hierarchy.d.ts +4 -5
- package/dist/client/index.d.ts +5 -2
- package/dist/client/node-types.d.ts +2 -3
- package/dist/client/serialization/InstanceData.d.ts +7 -5
- package/dist/client/serialization/InstancePayloadOptions.d.ts +3 -2
- package/dist/error/UploadValueTypeError.d.ts +8 -0
- package/dist/index.js +16938 -29914
- package/dist/index.js.map +1 -1
- package/dist/instance/PrimaryInstance.d.ts +2 -3
- package/dist/instance/UploadControl.d.ts +58 -0
- package/dist/instance/attachments/InstanceAttachment.d.ts +42 -0
- package/dist/instance/attachments/InstanceAttachmentsState.d.ts +9 -0
- package/dist/instance/hierarchy.d.ts +6 -7
- package/dist/instance/internal-api/InstanceAttachmentContext.d.ts +19 -0
- package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableInstance.d.ts +2 -0
- package/dist/integration/xpath/adapter/traversal.d.ts +2 -2
- package/dist/lib/codecs/DateValueCodec.d.ts +7 -0
- package/dist/lib/codecs/getSharedValueCodec.d.ts +3 -2
- package/dist/lib/reactivity/createInstanceAttachment.d.ts +8 -0
- package/dist/lib/resource-helpers.d.ts +2 -0
- package/dist/parse/body/BodyDefinition.d.ts +2 -1
- package/dist/parse/body/control/UploadControlDefinition.d.ts +2 -0
- package/dist/parse/model/BindTypeDefinition.d.ts +2 -7
- package/dist/parse/text/abstract/TextElementDefinition.d.ts +1 -1
- package/dist/solid.js +16935 -29912
- package/dist/solid.js.map +1 -1
- package/package.json +16 -14
- package/src/client/InputNode.ts +4 -0
- package/src/client/NoteNode.ts +4 -0
- package/src/client/RootNode.ts +8 -3
- package/src/client/UploadNode.ts +78 -0
- package/src/client/attachments/InstanceAttachmentMeta.ts +10 -0
- package/src/client/attachments/InstanceAttachmentsConfig.ts +13 -0
- package/src/client/form/FormInstanceConfig.ts +3 -0
- package/src/client/hierarchy.ts +5 -8
- package/src/client/index.ts +4 -2
- package/src/client/node-types.ts +2 -5
- package/src/client/serialization/InstanceData.ts +14 -6
- package/src/client/serialization/InstancePayloadOptions.ts +3 -2
- package/src/entrypoints/FormInstance.ts +3 -2
- package/src/error/UploadValueTypeError.ts +13 -0
- package/src/instance/PrimaryInstance.ts +4 -5
- package/src/instance/UploadControl.ts +184 -0
- package/src/instance/attachments/InstanceAttachment.ts +69 -0
- package/src/instance/attachments/InstanceAttachmentsState.ts +18 -0
- package/src/instance/children/buildChildren.ts +5 -8
- package/src/instance/hierarchy.ts +6 -9
- package/src/instance/input/InstanceAttachmentMap.ts +33 -21
- package/src/instance/internal-api/InstanceAttachmentContext.ts +20 -0
- package/src/instance/internal-api/InstanceConfig.ts +3 -0
- package/src/instance/internal-api/serialization/ClientReactiveSerializableInstance.ts +2 -0
- package/src/integration/xpath/adapter/traversal.ts +4 -2
- package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +72 -8
- package/src/lib/codecs/DateValueCodec.ts +94 -0
- package/src/lib/codecs/getSharedValueCodec.ts +5 -3
- package/src/lib/reactivity/createInstanceAttachment.ts +212 -0
- package/src/lib/resource-helpers.ts +33 -0
- package/src/parse/XFormDOM.ts +1 -3
- package/src/parse/body/BodyDefinition.ts +4 -0
- package/src/parse/body/control/UploadControlDefinition.ts +42 -0
- package/src/parse/model/BindDefinition.ts +1 -1
- package/src/parse/model/BindTypeDefinition.ts +68 -26
- package/src/parse/model/ModelBindMap.ts +0 -5
- package/src/parse/model/SecondaryInstance/sources/ExternalSecondaryInstanceResource.ts +1 -30
- package/src/parse/text/abstract/TextElementDefinition.ts +2 -2
- package/dist/client/unsupported/UnsupportedControlNode.d.ts +0 -30
- package/dist/client/unsupported/UploadNode.d.ts +0 -9
- package/dist/instance/unsupported/UploadControl.d.ts +0 -34
- package/dist/lib/codecs/TempUnsupportedControlCodec.d.ts +0 -7
- package/src/client/unsupported/UnsupportedControlNode.ts +0 -36
- package/src/client/unsupported/UploadNode.ts +0 -14
- package/src/instance/unsupported/UploadControl.ts +0 -120
- package/src/lib/codecs/TempUnsupportedControlCodec.ts +0 -32
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Accessor } from 'solid-js';
|
|
2
|
+
import type { InstancePayload } from '../../client/index.ts';
|
|
3
|
+
import type { SimpleAtomicState, SimpleAtomicStateSetter } from '../../lib/reactivity/types.ts';
|
|
4
|
+
import type { InstanceAttachmentContext } from '../internal-api/InstanceAttachmentContext.ts';
|
|
5
|
+
import type { DecodeInstanceValue } from '../internal-api/InstanceValueContext.ts';
|
|
6
|
+
import type { InstanceAttachmentsState } from './InstanceAttachmentsState.ts';
|
|
7
|
+
|
|
8
|
+
export type InstanceAttachmentFileName = string;
|
|
9
|
+
export type InstanceAttachmentRuntimeValue = File | null;
|
|
10
|
+
|
|
11
|
+
export interface InstanceAttachmentOptions {
|
|
12
|
+
readonly getFileName: Accessor<InstanceAttachmentFileName | null>;
|
|
13
|
+
readonly getInstanceValue: Accessor<InstanceAttachmentFileName>;
|
|
14
|
+
readonly decodeInstanceValue: DecodeInstanceValue;
|
|
15
|
+
|
|
16
|
+
readonly getValue: Accessor<InstanceAttachmentRuntimeValue>;
|
|
17
|
+
readonly setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue>;
|
|
18
|
+
readonly valueState: SimpleAtomicState<InstanceAttachmentRuntimeValue>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InstanceAttachment {
|
|
22
|
+
/**
|
|
23
|
+
* 1. Creates {@link InstanceAttachment | attachment state} from
|
|
24
|
+
* {@link InstanceAttachmentOptions}
|
|
25
|
+
* 2. Registers that attachment state in an instance-global
|
|
26
|
+
* {@link InstanceAttachmentsState} entry
|
|
27
|
+
*
|
|
28
|
+
* This allows an instance to:
|
|
29
|
+
*
|
|
30
|
+
* - Produce distinct file names for each attachment
|
|
31
|
+
* - Track all attachments so they can be serialized in an
|
|
32
|
+
* {@link InstancePayload}
|
|
33
|
+
*/
|
|
34
|
+
static init(
|
|
35
|
+
context: InstanceAttachmentContext,
|
|
36
|
+
options: InstanceAttachmentOptions
|
|
37
|
+
): InstanceAttachment {
|
|
38
|
+
const attachment = new this(options);
|
|
39
|
+
const { attachments } = context.rootDocument;
|
|
40
|
+
|
|
41
|
+
attachments.set(context, attachment);
|
|
42
|
+
|
|
43
|
+
return attachment;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This property isn't used at runtime. It causes TypeScript to treat
|
|
48
|
+
* {@link InstanceAttachment} as a nominal type, ensuring
|
|
49
|
+
* {@link InstanceAttachment.init} is called to instantiate it.
|
|
50
|
+
*/
|
|
51
|
+
protected readonly _ = null;
|
|
52
|
+
|
|
53
|
+
readonly getFileName: Accessor<InstanceAttachmentFileName | null>;
|
|
54
|
+
readonly getInstanceValue: Accessor<InstanceAttachmentFileName>;
|
|
55
|
+
readonly decodeInstanceValue: DecodeInstanceValue;
|
|
56
|
+
|
|
57
|
+
readonly getValue: Accessor<InstanceAttachmentRuntimeValue>;
|
|
58
|
+
readonly setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue>;
|
|
59
|
+
readonly valueState: SimpleAtomicState<InstanceAttachmentRuntimeValue>;
|
|
60
|
+
|
|
61
|
+
private constructor(options: InstanceAttachmentOptions) {
|
|
62
|
+
this.getFileName = options.getFileName;
|
|
63
|
+
this.getInstanceValue = options.getInstanceValue;
|
|
64
|
+
this.decodeInstanceValue = options.decodeInstanceValue;
|
|
65
|
+
this.getValue = options.getValue;
|
|
66
|
+
this.setValue = options.setValue;
|
|
67
|
+
this.valueState = options.valueState;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StaticLeafElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
2
|
+
import type { InstanceAttachmentMap } from '../input/InstanceAttachmentMap.ts';
|
|
3
|
+
import type { InstanceAttachmentContext } from '../internal-api/InstanceAttachmentContext.ts';
|
|
4
|
+
import type { InstanceAttachment, InstanceAttachmentRuntimeValue } from './InstanceAttachment.ts';
|
|
5
|
+
|
|
6
|
+
export class InstanceAttachmentsState extends Map<InstanceAttachmentContext, InstanceAttachment> {
|
|
7
|
+
constructor(private readonly sourceAttachments: InstanceAttachmentMap | null = null) {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getInitialFileValue(instanceNode: StaticLeafElement | null): InstanceAttachmentRuntimeValue {
|
|
12
|
+
if (instanceNode == null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return this.sourceAttachments?.get(instanceNode.value) ?? null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { RankDefinition } from '../../client/RankNode.ts';
|
|
|
6
6
|
import type { SelectDefinition } from '../../client/SelectNode.ts';
|
|
7
7
|
import type { SubtreeDefinition } from '../../client/SubtreeNode.ts';
|
|
8
8
|
import type { TriggerNodeDefinition } from '../../client/TriggerNode.ts';
|
|
9
|
-
import type {
|
|
9
|
+
import type { UploadDefinition } from '../../client/UploadNode.ts';
|
|
10
10
|
import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
|
|
11
11
|
import type { LeafNodeDefinition } from '../../parse/model/LeafNodeDefinition.ts';
|
|
12
12
|
import { NoteNodeDefinition } from '../../parse/model/NoteNodeDefinition.ts';
|
|
@@ -28,7 +28,7 @@ import { RepeatRangeUncontrolled } from '../repeat/RepeatRangeUncontrolled.ts';
|
|
|
28
28
|
import { SelectControl } from '../SelectControl.ts';
|
|
29
29
|
import { Subtree } from '../Subtree.ts';
|
|
30
30
|
import { TriggerControl } from '../TriggerControl.ts';
|
|
31
|
-
import { UploadControl } from '../
|
|
31
|
+
import { UploadControl } from '../UploadControl.ts';
|
|
32
32
|
import { childrenInitOptions } from './childrenInitOptions.ts';
|
|
33
33
|
|
|
34
34
|
const isSubtreeDefinition = (
|
|
@@ -37,9 +37,6 @@ const isSubtreeDefinition = (
|
|
|
37
37
|
return definition.bodyElement == null;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
// prettier-ignore
|
|
41
|
-
type AnyUnsupportedControlDefinition = UploadNodeDefinition;
|
|
42
|
-
|
|
43
40
|
// prettier-ignore
|
|
44
41
|
type ControlNodeDefinition =
|
|
45
42
|
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
|
|
@@ -48,7 +45,7 @@ type ControlNodeDefinition =
|
|
|
48
45
|
| SelectDefinition
|
|
49
46
|
| RankDefinition
|
|
50
47
|
| TriggerNodeDefinition
|
|
51
|
-
|
|
|
48
|
+
| UploadDefinition;
|
|
52
49
|
|
|
53
50
|
type AnyLeafNodeDefinition = ControlNodeDefinition | ModelValueDefinition;
|
|
54
51
|
|
|
@@ -123,7 +120,7 @@ const isTriggerNodeDefinition = (
|
|
|
123
120
|
|
|
124
121
|
const isUploadNodeDefinition = (
|
|
125
122
|
definition: ControlNodeDefinition
|
|
126
|
-
): definition is
|
|
123
|
+
): definition is UploadDefinition => {
|
|
127
124
|
return definition.bodyElement.type === 'upload';
|
|
128
125
|
};
|
|
129
126
|
|
|
@@ -192,7 +189,7 @@ export const buildChildren = (parent: GeneralParentNode): GeneralChildNode[] =>
|
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
if (isUploadNodeDefinition(leafChild)) {
|
|
195
|
-
return
|
|
192
|
+
return UploadControl.from(parent, instanceNode, leafChild);
|
|
196
193
|
}
|
|
197
194
|
|
|
198
195
|
throw new UnreachableError(leafChild);
|
|
@@ -4,6 +4,7 @@ import type { AnyModelValue } from './ModelValue.ts';
|
|
|
4
4
|
import type { AnyNote } from './Note.ts';
|
|
5
5
|
import type { PrimaryInstance } from './PrimaryInstance.ts';
|
|
6
6
|
import type { AnyRangeControl } from './RangeControl.ts';
|
|
7
|
+
import type { RankControl } from './RankControl.ts';
|
|
7
8
|
import type { RepeatInstance } from './repeat/RepeatInstance.ts';
|
|
8
9
|
import type { RepeatRangeControlled } from './repeat/RepeatRangeControlled.ts';
|
|
9
10
|
import type { RepeatRangeUncontrolled } from './repeat/RepeatRangeUncontrolled.ts';
|
|
@@ -11,14 +12,10 @@ import type { Root } from './Root.ts';
|
|
|
11
12
|
import type { SelectControl } from './SelectControl.ts';
|
|
12
13
|
import type { Subtree } from './Subtree.ts';
|
|
13
14
|
import type { TriggerControl } from './TriggerControl.ts';
|
|
14
|
-
import type {
|
|
15
|
-
import type { UploadControl } from './unsupported/UploadControl.ts';
|
|
15
|
+
import type { UploadControl } from './UploadControl.ts';
|
|
16
16
|
|
|
17
17
|
export type RepeatRange = RepeatRangeControlled | RepeatRangeUncontrolled;
|
|
18
18
|
|
|
19
|
-
// prettier-ignore
|
|
20
|
-
export type AnyUnsupportedControl = UploadControl;
|
|
21
|
-
|
|
22
19
|
// prettier-ignore
|
|
23
20
|
export type AnyNode =
|
|
24
21
|
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
|
|
@@ -35,7 +32,7 @@ export type AnyNode =
|
|
|
35
32
|
| RankControl
|
|
36
33
|
| SelectControl
|
|
37
34
|
| TriggerControl
|
|
38
|
-
|
|
|
35
|
+
| UploadControl;
|
|
39
36
|
|
|
40
37
|
// prettier-ignore
|
|
41
38
|
export type AnyParentNode =
|
|
@@ -70,7 +67,7 @@ export type AnyChildNode =
|
|
|
70
67
|
| RankControl
|
|
71
68
|
| SelectControl
|
|
72
69
|
| TriggerControl
|
|
73
|
-
|
|
|
70
|
+
| UploadControl;
|
|
74
71
|
|
|
75
72
|
// prettier-ignore
|
|
76
73
|
export type GeneralChildNode =
|
|
@@ -85,7 +82,7 @@ export type GeneralChildNode =
|
|
|
85
82
|
| RankControl
|
|
86
83
|
| SelectControl
|
|
87
84
|
| TriggerControl
|
|
88
|
-
|
|
|
85
|
+
| UploadControl;
|
|
89
86
|
|
|
90
87
|
// prettier-ignore
|
|
91
88
|
export type AnyValueNode =
|
|
@@ -97,4 +94,4 @@ export type AnyValueNode =
|
|
|
97
94
|
| RankControl
|
|
98
95
|
| SelectControl
|
|
99
96
|
| TriggerControl
|
|
100
|
-
|
|
|
97
|
+
| UploadControl;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { getBlobData } from '@getodk/common/lib/web-compat/blob.ts';
|
|
1
2
|
import { INSTANCE_FILE_NAME } from '../../client/constants.ts';
|
|
2
3
|
import type { ResolvableInstanceAttachmentsMap } from '../../client/form/EditFormInstance.ts';
|
|
3
4
|
import { MalformedInstanceDataError } from '../../error/MalformedInstanceDataError.ts';
|
|
5
|
+
import { getResponseContentType } from '../../lib/resource-helpers.ts';
|
|
6
|
+
import type { FetchResourceResponse } from '../resource.ts';
|
|
4
7
|
|
|
5
8
|
type InstanceAttachmentMapSourceEntry = readonly [key: string, value: FormDataEntryValue];
|
|
6
9
|
|
|
@@ -13,14 +16,35 @@ type InstanceAttachmentMapSources = readonly [
|
|
|
13
16
|
...InstanceAttachmentMapSource[],
|
|
14
17
|
];
|
|
15
18
|
|
|
19
|
+
const DEFAULT_ATTACHMENT_TYPE = 'application/octet-stream';
|
|
20
|
+
|
|
21
|
+
const resolveContentType = (response: FetchResourceResponse, blob: Blob): string => {
|
|
22
|
+
let result = blob.type;
|
|
23
|
+
|
|
24
|
+
if (result === '') {
|
|
25
|
+
result = getResponseContentType(response) ?? result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result === '') {
|
|
29
|
+
return DEFAULT_ATTACHMENT_TYPE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const resolveInstanceAttachmentFile = async (
|
|
36
|
+
response: FetchResourceResponse,
|
|
37
|
+
fileName: string
|
|
38
|
+
): Promise<File> => {
|
|
39
|
+
const blob = await response.blob();
|
|
40
|
+
const blobData = await getBlobData(blob);
|
|
41
|
+
|
|
42
|
+
return new File([blobData], fileName, {
|
|
43
|
+
type: resolveContentType(response, blob),
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
16
47
|
/**
|
|
17
|
-
* @todo This currently short-circuits if there are actually any instance
|
|
18
|
-
* attachments to resolve. As described below, much of the approach is pretty
|
|
19
|
-
* naive now anyway, and none of it is really "ready" until we have something to
|
|
20
|
-
* actually _use the instance attachments_ once they're resolved! When we are
|
|
21
|
-
* ready, the functionality can be unblocked as in
|
|
22
|
-
* {@link https://github.com/getodk/web-forms/commit/88ee1b91c1f68d53ce9ba551bab334852e1e60cd | this commit}.
|
|
23
|
-
*
|
|
24
48
|
* @todo Everything about this is incredibly naive! We should almost certainly
|
|
25
49
|
* do _at least_ the following:
|
|
26
50
|
*
|
|
@@ -37,22 +61,10 @@ type InstanceAttachmentMapSources = readonly [
|
|
|
37
61
|
const resolveInstanceAttachmentMapSource = async (
|
|
38
62
|
input: ResolvableInstanceAttachmentsMap
|
|
39
63
|
): Promise<InstanceAttachmentMapSource> => {
|
|
40
|
-
const inputEntries = Array.from(input.entries());
|
|
41
|
-
|
|
42
|
-
if (inputEntries.length > 0) {
|
|
43
|
-
const fileNames = Array.from(input.keys());
|
|
44
|
-
const errors = fileNames.map((fileName) => {
|
|
45
|
-
return new Error(`Failed to resolve instance attachment with file name "${fileName}"`);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
throw new AggregateError(errors, 'Not implemented: instance attachment resource resolution');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
64
|
const entries = await Promise.all<InstanceAttachmentMapSourceEntry>(
|
|
52
|
-
|
|
65
|
+
Array.from(input.entries()).map(async ([fileName, resolveAttachment]) => {
|
|
53
66
|
const response = await resolveAttachment();
|
|
54
|
-
const
|
|
55
|
-
const value = new File([blob], fileName);
|
|
67
|
+
const value = await resolveInstanceAttachmentFile(response, fileName);
|
|
56
68
|
|
|
57
69
|
return [fileName, value] as const;
|
|
58
70
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Accessor } from 'solid-js';
|
|
2
|
+
import type { FormNodeID } from '../../client/identity.ts';
|
|
3
|
+
import type { StaticLeafElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
4
|
+
import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
|
|
5
|
+
import type { InstanceAttachmentsState } from '../attachments/InstanceAttachmentsState.ts';
|
|
6
|
+
import type { InstanceConfig } from './InstanceConfig.ts';
|
|
7
|
+
|
|
8
|
+
interface InstanceAttachmentRootDocument {
|
|
9
|
+
readonly attachments: InstanceAttachmentsState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface InstanceAttachmentContext {
|
|
13
|
+
readonly instanceConfig: InstanceConfig;
|
|
14
|
+
readonly scope: ReactiveScope;
|
|
15
|
+
readonly rootDocument: InstanceAttachmentRootDocument;
|
|
16
|
+
readonly nodeId: FormNodeID;
|
|
17
|
+
readonly instanceNode: StaticLeafElement | null;
|
|
18
|
+
readonly isRelevant: Accessor<boolean>;
|
|
19
|
+
readonly isAttached: Accessor<boolean>;
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InstanceAttachmentFileNameFactory } from '../../client/attachments/InstanceAttachmentsConfig.ts';
|
|
1
2
|
import type { FormInstanceConfig } from '../../client/form/FormInstanceConfig.ts';
|
|
2
3
|
import type { OpaqueReactiveObjectFactory } from '../../client/OpaqueReactiveObjectFactory.ts';
|
|
3
4
|
|
|
@@ -6,4 +7,6 @@ export interface InstanceConfig {
|
|
|
6
7
|
* @see {@link FormInstanceConfig.stateFactory}
|
|
7
8
|
*/
|
|
8
9
|
readonly clientStateFactory: OpaqueReactiveObjectFactory;
|
|
10
|
+
|
|
11
|
+
readonly computeAttachmentName: InstanceAttachmentFileNameFactory;
|
|
9
12
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SubmissionMeta } from '../../../client/submission/SubmissionMeta.ts';
|
|
2
2
|
import type { AncestorNodeValidationState } from '../../../client/validation.ts';
|
|
3
|
+
import type { InstanceAttachmentsState } from '../../attachments/InstanceAttachmentsState.ts';
|
|
3
4
|
import type { Root } from '../../Root.ts';
|
|
4
5
|
import type {
|
|
5
6
|
ClientReactiveSerializableParentNode,
|
|
@@ -16,5 +17,6 @@ export interface ClientReactiveSerializableInstance
|
|
|
16
17
|
readonly definition: ClientReactiveSerializableInstanceDefinition;
|
|
17
18
|
readonly root: Root;
|
|
18
19
|
readonly parent: null;
|
|
20
|
+
readonly attachments: InstanceAttachmentsState;
|
|
19
21
|
readonly validationState: AncestorNodeValidationState;
|
|
20
22
|
}
|
|
@@ -18,7 +18,9 @@ export const getContainingEngineXPathDocument = (node: EngineXPathNode): EngineX
|
|
|
18
18
|
return node.rootDocument;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export const getEngineXPathAttributes = (
|
|
21
|
+
export const getEngineXPathAttributes = (
|
|
22
|
+
node: EngineXPathNode
|
|
23
|
+
): readonly EngineXPathAttribute[] => {
|
|
22
24
|
if (node.nodeType === 'static-element') {
|
|
23
25
|
return node.attributes;
|
|
24
26
|
}
|
|
@@ -39,7 +41,7 @@ export const getEngineXPathAttributes = (node: EngineXPathNode): Iterable<Engine
|
|
|
39
41
|
* it throw? It might be nice to be alerted if the assumptions in point 2 above
|
|
40
42
|
* are somehow wrong (or become wrong).
|
|
41
43
|
*/
|
|
42
|
-
export const getNamespaceDeclarations = ():
|
|
44
|
+
export const getNamespaceDeclarations = (): readonly [] => [];
|
|
43
45
|
|
|
44
46
|
export const getParentNode = (node: EngineXPathNode): EngineXPathParentNode | null => {
|
|
45
47
|
if (node.nodeType === 'repeat-instance') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
|
|
2
|
+
import { bestFitDecreasing } from 'bin-packer';
|
|
2
3
|
import { INSTANCE_FILE_NAME, INSTANCE_FILE_TYPE } from '../../../client/constants.ts';
|
|
3
4
|
import type { InstanceData as ClientInstanceData } from '../../../client/serialization/InstanceData.ts';
|
|
4
5
|
import type { InstanceFile as ClientInstanceFile } from '../../../client/serialization/InstanceFile.ts';
|
|
@@ -10,8 +11,22 @@ import type {
|
|
|
10
11
|
import type { InstancePayloadType } from '../../../client/serialization/InstancePayloadOptions.ts';
|
|
11
12
|
import type { SubmissionMeta } from '../../../client/submission/SubmissionMeta.ts';
|
|
12
13
|
import type { DescendantNodeViolationReference } from '../../../client/validation.ts';
|
|
14
|
+
import { ErrorProductionDesignPendingError } from '../../../error/ErrorProductionDesignPendingError.ts';
|
|
15
|
+
import type { InstanceAttachmentsState } from '../../../instance/attachments/InstanceAttachmentsState.ts';
|
|
13
16
|
import type { ClientReactiveSerializableInstance } from '../../../instance/internal-api/serialization/ClientReactiveSerializableInstance.ts';
|
|
14
17
|
|
|
18
|
+
const collectInstanceAttachmentFiles = (attachments: InstanceAttachmentsState): readonly File[] => {
|
|
19
|
+
const files = Array.from(attachments.entries()).map(([context, attachment]) => {
|
|
20
|
+
if (!context.isAttached() || !context.isRelevant()) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return attachment.getValue();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return files.filter((file) => file != null);
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
class InstanceFile extends File implements ClientInstanceFile {
|
|
16
31
|
override readonly name = INSTANCE_FILE_NAME;
|
|
17
32
|
override readonly type = INSTANCE_FILE_TYPE;
|
|
@@ -25,10 +40,26 @@ class InstanceFile extends File implements ClientInstanceFile {
|
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
type AssertFile = (value: FormDataEntryValue) => asserts value is File;
|
|
44
|
+
|
|
45
|
+
const assertFile: AssertFile = (value) => {
|
|
46
|
+
if (!(value instanceof File)) {
|
|
47
|
+
throw new ErrorProductionDesignPendingError('Expected an instance of File');
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
28
51
|
type AssertInstanceData = (data: FormData) => asserts data is ClientInstanceData;
|
|
29
52
|
|
|
30
53
|
const assertInstanceData: AssertInstanceData = (data) => {
|
|
31
|
-
|
|
54
|
+
let instanceFile: File | null = null;
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of data.entries()) {
|
|
57
|
+
assertFile(value);
|
|
58
|
+
|
|
59
|
+
if (key === INSTANCE_FILE_NAME) {
|
|
60
|
+
instanceFile = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
|
|
33
64
|
if (!(instanceFile instanceof InstanceFile)) {
|
|
34
65
|
throw new Error(`Invalid InstanceData`);
|
|
@@ -114,6 +145,43 @@ interface ChunkedInstancePayloadOptions {
|
|
|
114
145
|
readonly maxSize: number;
|
|
115
146
|
}
|
|
116
147
|
|
|
148
|
+
type PartitionedInstanceData = readonly [ClientInstanceData, ...ClientInstanceData[]];
|
|
149
|
+
|
|
150
|
+
const partitionInstanceData = (
|
|
151
|
+
instanceFile: InstanceFile,
|
|
152
|
+
attachments: readonly File[],
|
|
153
|
+
options: ChunkedInstancePayloadOptions
|
|
154
|
+
): PartitionedInstanceData => {
|
|
155
|
+
const { maxSize } = options;
|
|
156
|
+
const maxAttachmentSize = maxSize - instanceFile.size;
|
|
157
|
+
const { bins, oversized } = bestFitDecreasing(
|
|
158
|
+
attachments,
|
|
159
|
+
(attachment) => {
|
|
160
|
+
return attachment.size;
|
|
161
|
+
},
|
|
162
|
+
maxAttachmentSize
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const errors = oversized.map((attachment) => {
|
|
166
|
+
return new Error(
|
|
167
|
+
`Combined size of instance XML (${instanceFile.size}) and attachment (${attachment.size}) exceeds maxSize (${maxSize}).`
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
throw new AggregateError(errors, 'Failed to produce chunked instance payload');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const [
|
|
176
|
+
// Ensure at least one `InstanceData` is produced, in case there are no
|
|
177
|
+
// attachments present at all
|
|
178
|
+
head = InstanceData.from(instanceFile, []),
|
|
179
|
+
...tail
|
|
180
|
+
] = bins.map((bin) => InstanceData.from(instanceFile, bin));
|
|
181
|
+
|
|
182
|
+
return [head, ...tail];
|
|
183
|
+
};
|
|
184
|
+
|
|
117
185
|
const chunkedInstancePayload = (
|
|
118
186
|
validation: InstanceStateValidation,
|
|
119
187
|
submissionMeta: SubmissionMeta,
|
|
@@ -121,17 +189,13 @@ const chunkedInstancePayload = (
|
|
|
121
189
|
attachments: readonly File[],
|
|
122
190
|
options: ChunkedInstancePayloadOptions
|
|
123
191
|
): ChunkedInstancePayload => {
|
|
124
|
-
|
|
125
|
-
throw new Error('InstancePayload chunking pending implementation');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const data = InstanceData.from(instanceFile, attachments);
|
|
192
|
+
const data = partitionInstanceData(instanceFile, attachments, options);
|
|
129
193
|
|
|
130
194
|
return {
|
|
131
195
|
payloadType: 'chunked',
|
|
132
196
|
...validation,
|
|
133
197
|
submissionMeta,
|
|
134
|
-
data
|
|
198
|
+
data,
|
|
135
199
|
};
|
|
136
200
|
};
|
|
137
201
|
|
|
@@ -147,7 +211,7 @@ export const prepareInstancePayload = <PayloadType extends InstancePayloadType>(
|
|
|
147
211
|
const validation = validateInstance(instanceRoot);
|
|
148
212
|
const submissionMeta = instanceRoot.definition.submission;
|
|
149
213
|
const instanceFile = new InstanceFile(instanceRoot);
|
|
150
|
-
const attachments
|
|
214
|
+
const attachments = collectInstanceAttachmentFiles(instanceRoot.attachments);
|
|
151
215
|
|
|
152
216
|
switch (options.payloadType) {
|
|
153
217
|
case 'chunked':
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ISO_DATE_OR_DATE_TIME_NO_OFFSET_PATTERN } from '@getodk/common/constants/datetime.ts';
|
|
2
|
+
import { Temporal } from 'temporal-polyfill';
|
|
3
|
+
import { type CodecDecoder, type CodecEncoder, ValueCodec } from './ValueCodec.ts';
|
|
4
|
+
|
|
5
|
+
export type DatetimeRuntimeValue = Temporal.PlainDate | null;
|
|
6
|
+
|
|
7
|
+
export type DatetimeInputValue =
|
|
8
|
+
| Date
|
|
9
|
+
| Temporal.PlainDate
|
|
10
|
+
| Temporal.PlainDateTime
|
|
11
|
+
| Temporal.ZonedDateTime
|
|
12
|
+
| string
|
|
13
|
+
| null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses a string in the format 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS' (no offset)
|
|
17
|
+
* into a Temporal.PlainDate.
|
|
18
|
+
* TODO: Datetimes with a valid timezone offset are treated as errors.
|
|
19
|
+
* User research is needed to determine whether the date should honor
|
|
20
|
+
* the timezone or be truncated to the yyyy-mm-dd format only.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The string to parse.
|
|
23
|
+
* @returns A {@link DatetimeRuntimeValue}
|
|
24
|
+
*/
|
|
25
|
+
const parseString = (value: string): DatetimeRuntimeValue => {
|
|
26
|
+
if (
|
|
27
|
+
value == null ||
|
|
28
|
+
typeof value !== 'string' ||
|
|
29
|
+
!ISO_DATE_OR_DATE_TIME_NO_OFFSET_PATTERN.test(value)
|
|
30
|
+
) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const dateOnly = value.split('T')[0]!;
|
|
36
|
+
return Temporal.PlainDate.from(dateOnly);
|
|
37
|
+
} catch {
|
|
38
|
+
// TODO: should we throw when codec cannot interpret the value?
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Converts a date-like value ({@link DatetimeInputValue}) to a 'YYYY-MM-DD' string.
|
|
45
|
+
* TODO: Datetimes with a valid timezone offset are treated as errors.
|
|
46
|
+
* User research is needed to determine whether the date should honor
|
|
47
|
+
* the timezone or be truncated to the yyyy-mm-dd format only.
|
|
48
|
+
*
|
|
49
|
+
* @param value - The value to convert.
|
|
50
|
+
* @returns A date string or empty string if invalid.
|
|
51
|
+
*/
|
|
52
|
+
const toDateString = (value: DatetimeInputValue): string => {
|
|
53
|
+
if (value == null || value instanceof Temporal.ZonedDateTime) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (value instanceof Temporal.PlainDate) {
|
|
59
|
+
return value.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (value instanceof Temporal.PlainDateTime) {
|
|
63
|
+
return value.toPlainDate().toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value instanceof Date) {
|
|
67
|
+
return Temporal.PlainDate.from({
|
|
68
|
+
year: value.getFullYear(),
|
|
69
|
+
month: value.getMonth() + 1,
|
|
70
|
+
day: value.getDate(),
|
|
71
|
+
}).toString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parsed = parseString(String(value));
|
|
75
|
+
return parsed?.toString() ?? '';
|
|
76
|
+
} catch {
|
|
77
|
+
// TODO: should we throw when codec cannot interpret the value?
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export class DateValueCodec extends ValueCodec<'date', DatetimeRuntimeValue, DatetimeInputValue> {
|
|
83
|
+
constructor() {
|
|
84
|
+
const encodeValue: CodecEncoder<DatetimeInputValue> = (value) => {
|
|
85
|
+
return toDateString(value);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const decodeValue: CodecDecoder<DatetimeRuntimeValue> = (value: string) => {
|
|
89
|
+
return parseString(value);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
super('date', encodeValue, decodeValue);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ValueType } from '../../client/ValueType.ts';
|
|
2
|
+
import type { DatetimeInputValue, DatetimeRuntimeValue } from './DateValueCodec.ts';
|
|
3
|
+
import { DateValueCodec } from './DateValueCodec.ts';
|
|
2
4
|
import {
|
|
3
5
|
DecimalValueCodec,
|
|
4
6
|
type DecimalInputValue,
|
|
@@ -16,7 +18,7 @@ interface RuntimeValuesByType {
|
|
|
16
18
|
readonly int: IntRuntimeValue;
|
|
17
19
|
readonly decimal: DecimalRuntimeValue;
|
|
18
20
|
readonly boolean: string;
|
|
19
|
-
readonly date:
|
|
21
|
+
readonly date: DatetimeRuntimeValue;
|
|
20
22
|
readonly time: string;
|
|
21
23
|
readonly dateTime: string;
|
|
22
24
|
readonly geopoint: GeopointRuntimeValue;
|
|
@@ -34,7 +36,7 @@ interface RuntimeInputValuesByType {
|
|
|
34
36
|
readonly int: IntInputValue;
|
|
35
37
|
readonly decimal: DecimalInputValue;
|
|
36
38
|
readonly boolean: string;
|
|
37
|
-
readonly date:
|
|
39
|
+
readonly date: DatetimeInputValue;
|
|
38
40
|
readonly time: string;
|
|
39
41
|
readonly dateTime: string;
|
|
40
42
|
readonly geopoint: GeopointInputValue;
|
|
@@ -63,7 +65,7 @@ export const sharedValueCodecs: SharedValueCodecs = {
|
|
|
63
65
|
int: new IntValueCodec(),
|
|
64
66
|
decimal: new DecimalValueCodec(),
|
|
65
67
|
boolean: new ValueTypePlaceholderCodec('boolean'),
|
|
66
|
-
date: new
|
|
68
|
+
date: new DateValueCodec(),
|
|
67
69
|
time: new ValueTypePlaceholderCodec('time'),
|
|
68
70
|
dateTime: new ValueTypePlaceholderCodec('dateTime'),
|
|
69
71
|
geopoint: new GeopointValueCodec(),
|