@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,212 @@
|
|
|
1
|
+
import { createMemo, createSignal } from 'solid-js';
|
|
2
|
+
import type { FormNodeID } from '../../client/identity.ts';
|
|
3
|
+
import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
|
|
4
|
+
import type { InstanceAttachmentFileName } from '../../instance/attachments/InstanceAttachment.ts';
|
|
5
|
+
import { InstanceAttachment } from '../../instance/attachments/InstanceAttachment.ts';
|
|
6
|
+
import type { InstanceAttachmentContext } from '../../instance/internal-api/InstanceAttachmentContext.ts';
|
|
7
|
+
import type { DecodeInstanceValue } from '../../instance/internal-api/InstanceValueContext.ts';
|
|
8
|
+
import type { SimpleAtomicStateSetter } from './types.ts';
|
|
9
|
+
|
|
10
|
+
type FileNameExtension = `.${string}`;
|
|
11
|
+
|
|
12
|
+
type AssertFileNameExtension = (value: string) => asserts value is FileNameExtension;
|
|
13
|
+
|
|
14
|
+
const assertFileNameExtension: AssertFileNameExtension = (value) => {
|
|
15
|
+
if (!value.startsWith('.')) {
|
|
16
|
+
throw new ErrorProductionDesignPendingError('Expected file name extension to start with "."');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface SplitFileNameResult {
|
|
21
|
+
readonly basename: string;
|
|
22
|
+
readonly extension: FileNameExtension | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EXTENSION_PATTERN = /\.[^.]+?$/;
|
|
26
|
+
|
|
27
|
+
interface SearchPatternResult extends Array<string> {
|
|
28
|
+
readonly 0?: string;
|
|
29
|
+
readonly index?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const searchPattern = (pattern: RegExp, string: string): SearchPatternResult => {
|
|
33
|
+
return pattern.exec(string) ?? [];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const splitFileName = (fileName: string): SplitFileNameResult => {
|
|
37
|
+
const extensionMatches = searchPattern(EXTENSION_PATTERN, fileName);
|
|
38
|
+
const [extension = null] = extensionMatches;
|
|
39
|
+
const basename = fileName.slice(0, extensionMatches.index);
|
|
40
|
+
|
|
41
|
+
if (extension == null) {
|
|
42
|
+
return {
|
|
43
|
+
basename,
|
|
44
|
+
extension,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
assertFileNameExtension(extension);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
basename,
|
|
52
|
+
extension,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type InstanceAttachmentRuntimeValue = File | null;
|
|
57
|
+
|
|
58
|
+
export type InstanceAttachmentFormDataEntry = readonly [
|
|
59
|
+
key: InstanceAttachmentFileName,
|
|
60
|
+
value: NonNullable<InstanceAttachmentRuntimeValue>,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
interface InstanceAttachmentValueOptions {
|
|
64
|
+
readonly nodeId: FormNodeID;
|
|
65
|
+
readonly writtenAt: Date | null;
|
|
66
|
+
readonly file: InstanceAttachmentRuntimeValue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BaseInstanceAttachmentState {
|
|
70
|
+
readonly computedName: string | null;
|
|
71
|
+
readonly intrinsicName: string | null;
|
|
72
|
+
readonly file: InstanceAttachmentRuntimeValue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface BlankInstanceAttachmentState extends BaseInstanceAttachmentState {
|
|
76
|
+
readonly computedName: null;
|
|
77
|
+
readonly intrinsicName: null;
|
|
78
|
+
readonly file: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface NonBlankInstanceAttachmentState extends BaseInstanceAttachmentState {
|
|
82
|
+
readonly computedName: string | null;
|
|
83
|
+
readonly intrinsicName: string;
|
|
84
|
+
readonly file: NonNullable<InstanceAttachmentRuntimeValue>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// prettier-ignore
|
|
88
|
+
type InstanceAttachmentState =
|
|
89
|
+
| BlankInstanceAttachmentState
|
|
90
|
+
| NonBlankInstanceAttachmentState;
|
|
91
|
+
|
|
92
|
+
const instanceAttachmentState = (
|
|
93
|
+
context: InstanceAttachmentContext,
|
|
94
|
+
options: InstanceAttachmentValueOptions
|
|
95
|
+
): InstanceAttachmentState => {
|
|
96
|
+
const { nodeId, file, writtenAt } = options;
|
|
97
|
+
|
|
98
|
+
// No file -> no intrinsic name, no name to compute
|
|
99
|
+
if (file == null) {
|
|
100
|
+
return {
|
|
101
|
+
computedName: null,
|
|
102
|
+
intrinsicName: null,
|
|
103
|
+
file: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const intrinsicName = file.name;
|
|
108
|
+
|
|
109
|
+
// File exists, not written by client -> preserve instance input name
|
|
110
|
+
if (writtenAt == null) {
|
|
111
|
+
return {
|
|
112
|
+
computedName: null,
|
|
113
|
+
intrinsicName,
|
|
114
|
+
file,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// File was written by client, name is computed as configured by client
|
|
119
|
+
const { basename, extension } = splitFileName(intrinsicName);
|
|
120
|
+
const computedName = context.instanceConfig.computeAttachmentName({
|
|
121
|
+
nodeId,
|
|
122
|
+
writtenAt,
|
|
123
|
+
basename,
|
|
124
|
+
extension,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
computedName,
|
|
129
|
+
intrinsicName,
|
|
130
|
+
file,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const createInstanceAttachment = (
|
|
135
|
+
context: InstanceAttachmentContext
|
|
136
|
+
): InstanceAttachment => {
|
|
137
|
+
return context.scope.runTask(() => {
|
|
138
|
+
const { rootDocument, nodeId } = context;
|
|
139
|
+
const { attachments } = rootDocument;
|
|
140
|
+
const file = attachments.getInitialFileValue(context.instanceNode);
|
|
141
|
+
const initialState = instanceAttachmentState(context, {
|
|
142
|
+
nodeId,
|
|
143
|
+
file,
|
|
144
|
+
writtenAt: null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const [getState, setState] = createSignal<InstanceAttachmentState>(initialState);
|
|
148
|
+
|
|
149
|
+
const decodeInstanceValue: DecodeInstanceValue = (value) => {
|
|
150
|
+
const { computedName, intrinsicName } = getState();
|
|
151
|
+
|
|
152
|
+
if (value === '') {
|
|
153
|
+
if (computedName != null || intrinsicName != null) {
|
|
154
|
+
throw new ErrorProductionDesignPendingError(
|
|
155
|
+
`Unexpected file name reference. Expected one of "${computedName}", "${intrinsicName}", got: ""`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (value === intrinsicName) {
|
|
161
|
+
return computedName ?? intrinsicName;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (value === computedName) {
|
|
165
|
+
return computedName;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new ErrorProductionDesignPendingError(
|
|
169
|
+
`Unexpected file name reference. Expected one of "${computedName}", "${intrinsicName}", got: "${value}"`
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getValue = createMemo(() => {
|
|
174
|
+
const { computedName, file: currentFile } = getState();
|
|
175
|
+
|
|
176
|
+
if (computedName == null) {
|
|
177
|
+
return currentFile;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new File([currentFile], computedName, {
|
|
181
|
+
type: currentFile.type,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
const setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue> = (value) => {
|
|
185
|
+
const updatedState = instanceAttachmentState(context, {
|
|
186
|
+
nodeId,
|
|
187
|
+
file: value,
|
|
188
|
+
writtenAt: new Date(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return setState(updatedState).file;
|
|
192
|
+
};
|
|
193
|
+
const valueState = [getValue, setValue] as const;
|
|
194
|
+
|
|
195
|
+
const getFileName = createMemo(() => {
|
|
196
|
+
const { computedName, intrinsicName } = getState();
|
|
197
|
+
|
|
198
|
+
return computedName ?? intrinsicName;
|
|
199
|
+
});
|
|
200
|
+
const getInstanceValue = createMemo(() => getFileName() ?? '');
|
|
201
|
+
|
|
202
|
+
return InstanceAttachment.init(context, {
|
|
203
|
+
getFileName,
|
|
204
|
+
getInstanceValue,
|
|
205
|
+
decodeInstanceValue,
|
|
206
|
+
|
|
207
|
+
getValue,
|
|
208
|
+
setValue,
|
|
209
|
+
valueState,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FetchResourceResponse } from '../client/resources.ts';
|
|
2
|
+
|
|
3
|
+
const CONTENT_TYPE_CHARSET_PATTERN = /\s*;\s*charset\s*=.*$/;
|
|
4
|
+
|
|
5
|
+
const stripContentTypeCharset = (contentType: string): string => {
|
|
6
|
+
return contentType.replace(CONTENT_TYPE_CHARSET_PATTERN, '');
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getResponseContentType = (response: FetchResourceResponse): string | null => {
|
|
10
|
+
const { headers } = response;
|
|
11
|
+
|
|
12
|
+
if (headers == null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const contentType = headers.get('content-type');
|
|
17
|
+
|
|
18
|
+
if (contentType != null) {
|
|
19
|
+
return stripContentTypeCharset(contentType);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (headers instanceof Headers) {
|
|
23
|
+
return contentType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const [header, value] of headers) {
|
|
27
|
+
if (header.toLowerCase() === 'content-type') {
|
|
28
|
+
return stripContentTypeCharset(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
};
|
package/src/parse/XFormDOM.ts
CHANGED
|
@@ -104,9 +104,7 @@ const normalizeDefaultMetaBindings = (
|
|
|
104
104
|
let meta = getMetaElement(primaryInstanceRoot);
|
|
105
105
|
let instanceID = getMetaChildElement(meta, 'instanceID');
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
meta = createNamespacedChildElement(primaryInstanceRoot, OPENROSA_XFORMS_NAMESPACE_URI, 'meta');
|
|
109
|
-
}
|
|
107
|
+
meta ??= createNamespacedChildElement(primaryInstanceRoot, OPENROSA_XFORMS_NAMESPACE_URI, 'meta');
|
|
110
108
|
|
|
111
109
|
if (instanceID == null) {
|
|
112
110
|
instanceID = createNamespacedChildElement(meta, meta.namespaceURI, 'instanceID');
|
|
@@ -207,6 +207,10 @@ export class BodyDefinition extends DependencyContext implements BodyElementPare
|
|
|
207
207
|
return this.elementsByReference.get(reference) ?? null;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
getBodyElementType(reference: BodyElementReference): AnyBodyElementType | null {
|
|
211
|
+
return this.elementsByReference.getBodyElementType(reference);
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
getChildElementDefinitions(
|
|
211
215
|
form: XFormDefinition,
|
|
212
216
|
parent: BodyElementParentContext,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UploadMediaOptions, UploadNodeOptions } from '../../../client/UploadNode.ts';
|
|
2
|
+
import { ErrorProductionDesignPendingError } from '../../../error/ErrorProductionDesignPendingError.ts';
|
|
1
3
|
import type { XFormDefinition } from '../../XFormDefinition.ts';
|
|
2
4
|
import {
|
|
3
5
|
unknownAppearanceParser,
|
|
@@ -6,6 +8,44 @@ import {
|
|
|
6
8
|
import type { BodyElementParentContext } from '../BodyDefinition.ts';
|
|
7
9
|
import { ControlDefinition } from './ControlDefinition.ts';
|
|
8
10
|
|
|
11
|
+
const MEDIATYPE_PATTERN = /^([^/]+)\/([^/]+)$/;
|
|
12
|
+
|
|
13
|
+
// prettier-ignore
|
|
14
|
+
type MediaTypeMatches =
|
|
15
|
+
| readonly [$0: string, $1: string, $2: string];
|
|
16
|
+
|
|
17
|
+
const parseUploadMediaOptions = (element: Element): UploadMediaOptions => {
|
|
18
|
+
const mediaType = element.getAttribute('mediatype')?.trim();
|
|
19
|
+
|
|
20
|
+
if (mediaType == null || mediaType === '') {
|
|
21
|
+
return {
|
|
22
|
+
accept: '*',
|
|
23
|
+
type: null,
|
|
24
|
+
subtype: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const matches = MEDIATYPE_PATTERN.exec(mediaType) as MediaTypeMatches | null;
|
|
29
|
+
|
|
30
|
+
if (matches == null) {
|
|
31
|
+
throw new ErrorProductionDesignPendingError(`Unsupported mediatype: ${mediaType}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [accept, type, subtype] = matches;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
accept,
|
|
38
|
+
type,
|
|
39
|
+
subtype,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const parseUploadNodeOptions = (element: Element): UploadNodeOptions => {
|
|
44
|
+
const media = parseUploadMediaOptions(element);
|
|
45
|
+
|
|
46
|
+
return { media };
|
|
47
|
+
};
|
|
48
|
+
|
|
9
49
|
export class UploadControlDefinition extends ControlDefinition<'upload'> {
|
|
10
50
|
static override isCompatible(localName: string): boolean {
|
|
11
51
|
return localName === 'upload';
|
|
@@ -13,11 +53,13 @@ export class UploadControlDefinition extends ControlDefinition<'upload'> {
|
|
|
13
53
|
|
|
14
54
|
readonly type = 'upload';
|
|
15
55
|
readonly appearances: UnknownAppearanceDefinition;
|
|
56
|
+
readonly options: UploadNodeOptions;
|
|
16
57
|
|
|
17
58
|
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
18
59
|
super(form, parent, element);
|
|
19
60
|
|
|
20
61
|
this.appearances = unknownAppearanceParser.parseFrom(element, 'appearance');
|
|
62
|
+
this.options = parseUploadNodeOptions(element);
|
|
21
63
|
}
|
|
22
64
|
|
|
23
65
|
override toJSON(): object {
|
|
@@ -80,7 +80,7 @@ export class BindDefinition<T extends BindType = BindType> extends DependencyCon
|
|
|
80
80
|
) {
|
|
81
81
|
super();
|
|
82
82
|
|
|
83
|
-
this.type = BindTypeDefinition.from(bindElement);
|
|
83
|
+
this.type = BindTypeDefinition.from(form, nodeset, bindElement);
|
|
84
84
|
|
|
85
85
|
const parentNodeset = nodeset.replace(/\/[^/]+$/, '');
|
|
86
86
|
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { XSD_NAMESPACE_URI, XSD_PREFIX } from '@getodk/common/constants/xmlns.ts';
|
|
2
|
+
import type { AnyBodyElementType } from '../body/BodyDefinition.ts';
|
|
3
|
+
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
2
4
|
import { parseQualifiedNameExpression } from '../xpath/semantic-analysis.ts';
|
|
3
5
|
import type { BindElement } from './BindElement.ts';
|
|
4
6
|
|
|
5
|
-
/**
|
|
6
|
-
* As specified by {@link https://getodk.github.io/xforms-spec/#bind-attributes}
|
|
7
|
-
*/
|
|
8
|
-
export const DEFAULT_BIND_TYPE = 'string';
|
|
9
|
-
|
|
10
|
-
export type DefaultBindType = typeof DEFAULT_BIND_TYPE;
|
|
11
|
-
|
|
12
7
|
/**
|
|
13
8
|
* As specified by {@link https://getodk.github.io/xforms-spec/#data-types}
|
|
14
9
|
*/
|
|
15
10
|
const BIND_TYPES = [
|
|
16
|
-
|
|
11
|
+
'string',
|
|
17
12
|
'int',
|
|
18
13
|
'boolean',
|
|
19
14
|
'decimal',
|
|
@@ -32,6 +27,47 @@ type BindTypes = typeof BIND_TYPES;
|
|
|
32
27
|
|
|
33
28
|
export type BindType = BindTypes[number];
|
|
34
29
|
|
|
30
|
+
type BindTypeDefaultOverridesByBodyType = Partial<Readonly<Record<AnyBodyElementType, BindType>>>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* As specified by {@link https://getodk.github.io/xforms-spec/#bind-attributes}
|
|
34
|
+
*/
|
|
35
|
+
const SPEC_BIND_TYPE_DEFAULT = 'string' satisfies BindType;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* If an `<upload>` is not explicitly associated with any `<bind type>`, we can do one of two things:
|
|
39
|
+
*
|
|
40
|
+
* - correct: default to {@link SPEC_BIND_TYPE_DEFAULT | the spec default regardless of control type}
|
|
41
|
+
* - more useful and robust: default to "binary", which is the only type the spec allows for an `<upload>` control
|
|
42
|
+
*
|
|
43
|
+
* Asked which would be preferable, @lognaturel responded:
|
|
44
|
+
*
|
|
45
|
+
* > I think we should assume in the spirit of doing the most useful thing!
|
|
46
|
+
*/
|
|
47
|
+
const UPLOAD_BIND_TYPE_DEFAULT = 'binary' satisfies BindType;
|
|
48
|
+
|
|
49
|
+
const BODY_BIND_TYPE_DEFAULT_OVERRIDES = {
|
|
50
|
+
upload: UPLOAD_BIND_TYPE_DEFAULT,
|
|
51
|
+
} as const satisfies BindTypeDefaultOverridesByBodyType;
|
|
52
|
+
|
|
53
|
+
type BodyBindTypeDefaultOverrides = typeof BODY_BIND_TYPE_DEFAULT_OVERRIDES;
|
|
54
|
+
|
|
55
|
+
type BodyBindTypeDefaultOverride = keyof BodyBindTypeDefaultOverrides;
|
|
56
|
+
|
|
57
|
+
const isBodyBindTypeDefaultOverride = (
|
|
58
|
+
bodyElementType: AnyBodyElementType | null
|
|
59
|
+
): bodyElementType is BodyBindTypeDefaultOverride => {
|
|
60
|
+
return bodyElementType != null && bodyElementType in BODY_BIND_TYPE_DEFAULT_OVERRIDES;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const resolveDefaultBindType = (bodyElementType: AnyBodyElementType | null): BindType => {
|
|
64
|
+
if (isBodyBindTypeDefaultOverride(bodyElementType)) {
|
|
65
|
+
return BODY_BIND_TYPE_DEFAULT_OVERRIDES[bodyElementType];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return SPEC_BIND_TYPE_DEFAULT;
|
|
69
|
+
};
|
|
70
|
+
|
|
35
71
|
const isBindType = (value: string): value is BindType => {
|
|
36
72
|
return BIND_TYPES.includes(value as BindType);
|
|
37
73
|
};
|
|
@@ -42,14 +78,10 @@ const resolveSupportedBindType = (sourceType: string): BindType | null => {
|
|
|
42
78
|
|
|
43
79
|
type BindTypeAliasMapping = Readonly<Record<string, BindType>>;
|
|
44
80
|
|
|
45
|
-
/**
|
|
46
|
-
* @todo should we make these aliases explicit (rather than relying on {@link resolveUnsupportedBindType})?
|
|
47
|
-
*
|
|
48
|
-
* - select1
|
|
49
|
-
* - rank
|
|
50
|
-
* - odk:rank
|
|
51
|
-
*/
|
|
52
81
|
const BIND_TYPE_ALIASES: BindTypeAliasMapping = {
|
|
82
|
+
select1: SPEC_BIND_TYPE_DEFAULT,
|
|
83
|
+
rank: SPEC_BIND_TYPE_DEFAULT,
|
|
84
|
+
'odk:rank': SPEC_BIND_TYPE_DEFAULT,
|
|
53
85
|
integer: 'int',
|
|
54
86
|
};
|
|
55
87
|
|
|
@@ -65,7 +97,7 @@ const resolveAliasedBindType = (sourceType: string): BindType | null => {
|
|
|
65
97
|
* @todo Should we warn on this fallback?
|
|
66
98
|
*/
|
|
67
99
|
const resolveUnsupportedBindType = (_unsupportedType: string): BindType => {
|
|
68
|
-
return
|
|
100
|
+
return SPEC_BIND_TYPE_DEFAULT;
|
|
69
101
|
};
|
|
70
102
|
|
|
71
103
|
interface BindDataTypeNamespaceResolver {
|
|
@@ -144,7 +176,15 @@ const resolveNamespacedBindType = (
|
|
|
144
176
|
return null;
|
|
145
177
|
};
|
|
146
178
|
|
|
147
|
-
const resolveBindType = (
|
|
179
|
+
const resolveBindType = (
|
|
180
|
+
bodyElementType: AnyBodyElementType | null,
|
|
181
|
+
bindElement: BindElement,
|
|
182
|
+
sourceType: string | null
|
|
183
|
+
): BindType => {
|
|
184
|
+
if (sourceType == null) {
|
|
185
|
+
return resolveDefaultBindType(bodyElementType);
|
|
186
|
+
}
|
|
187
|
+
|
|
148
188
|
return (
|
|
149
189
|
resolveSupportedBindType(sourceType) ??
|
|
150
190
|
resolveAliasedBindType(sourceType) ??
|
|
@@ -154,20 +194,22 @@ const resolveBindType = (bindElement: BindElement, sourceType: string): BindType
|
|
|
154
194
|
};
|
|
155
195
|
|
|
156
196
|
export class BindTypeDefinition<T extends BindType = BindType> {
|
|
157
|
-
static from<T extends BindType>(
|
|
197
|
+
static from<T extends BindType>(
|
|
198
|
+
form: XFormDefinition,
|
|
199
|
+
nodeset: string,
|
|
200
|
+
bindElement: BindElement
|
|
201
|
+
): BindTypeDefinition<T> {
|
|
202
|
+
const bodyElementType = form.body.getBodyElementType(nodeset);
|
|
158
203
|
const sourceType = bindElement.getAttribute('type');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const resolved = resolveBindType(bindElement, sourceType);
|
|
204
|
+
const resolved = resolveBindType(
|
|
205
|
+
bodyElementType,
|
|
206
|
+
bindElement,
|
|
207
|
+
sourceType
|
|
208
|
+
) satisfies BindType as T;
|
|
165
209
|
|
|
166
210
|
return new this(sourceType, resolved);
|
|
167
211
|
}
|
|
168
212
|
|
|
169
|
-
private constructor(source: null, resolved: DefaultBindType);
|
|
170
|
-
private constructor(source: string, resolved: BindType);
|
|
171
213
|
private constructor(
|
|
172
214
|
readonly source: string | null,
|
|
173
215
|
readonly resolved: T
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
2
2
|
import { BindDefinition } from './BindDefinition.ts';
|
|
3
3
|
import type { BindElement, BindNodeset } from './BindElement.ts';
|
|
4
|
-
import { DEFAULT_BIND_TYPE } from './BindTypeDefinition.ts';
|
|
5
4
|
import type { ModelDefinition } from './ModelDefinition.ts';
|
|
6
5
|
|
|
7
6
|
class ArtificialBindElement implements BindElement {
|
|
@@ -16,10 +15,6 @@ class ArtificialBindElement implements BindElement {
|
|
|
16
15
|
return this.ancestorNodeset;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
if (name === 'type') {
|
|
20
|
-
return DEFAULT_BIND_TYPE;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
return null;
|
|
24
19
|
}
|
|
25
20
|
|
|
@@ -2,6 +2,7 @@ import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts
|
|
|
2
2
|
import type { MissingResourceBehavior } from '../../../../client/constants.ts';
|
|
3
3
|
import type { FetchResource, FetchResourceResponse } from '../../../../client/resources.ts';
|
|
4
4
|
import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError.ts';
|
|
5
|
+
import { getResponseContentType } from '../../../../lib/resource-helpers.ts';
|
|
5
6
|
import { FormAttachmentResource } from '../../../attachments/FormAttachmentResource.ts';
|
|
6
7
|
import type { ExternalSecondaryInstanceSourceFormat } from './SecondaryInstanceSource.ts';
|
|
7
8
|
|
|
@@ -13,36 +14,6 @@ const assertResponseSuccess = (resourceURL: JRResourceURL, response: FetchResour
|
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
const stripContentTypeCharset = (contentType: string): string => {
|
|
17
|
-
return contentType.replace(/;charset=.*$/, '');
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const getResponseContentType = (response: FetchResourceResponse): string | null => {
|
|
21
|
-
const { headers } = response;
|
|
22
|
-
|
|
23
|
-
if (headers == null) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const contentType = headers.get('content-type');
|
|
28
|
-
|
|
29
|
-
if (contentType != null) {
|
|
30
|
-
return stripContentTypeCharset(contentType);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (headers instanceof Headers) {
|
|
34
|
-
return contentType;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const [header, value] of headers) {
|
|
38
|
-
if (header.toLowerCase() === 'content-type') {
|
|
39
|
-
return stripContentTypeCharset(value);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return null;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
17
|
interface ExternalSecondaryInstanceResourceMetadata<
|
|
47
18
|
Format extends ExternalSecondaryInstanceSourceFormat = ExternalSecondaryInstanceSourceFormat,
|
|
48
19
|
> {
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { UnknownAppearanceDefinition } from '../../parse/body/appearance/unknownAppearanceParser.ts';
|
|
2
|
-
import { UploadControlDefinition } from '../../parse/body/control/UploadControlDefinition.ts';
|
|
3
|
-
import { LeafNodeDefinition } from '../../parse/model/LeafNodeDefinition.ts';
|
|
4
|
-
import { BaseNode, BaseNodeState } from '../BaseNode.ts';
|
|
5
|
-
import { RootNode } from '../RootNode.ts';
|
|
6
|
-
import { GeneralParentNode } from '../hierarchy.ts';
|
|
7
|
-
import { UnsupportedControlNodeType } from '../node-types.ts';
|
|
8
|
-
import { LeafNodeValidationState } from '../validation.ts';
|
|
9
|
-
export interface UnsupportedControlNodeState extends BaseNodeState {
|
|
10
|
-
get children(): null;
|
|
11
|
-
get valueOptions(): unknown;
|
|
12
|
-
get value(): unknown;
|
|
13
|
-
}
|
|
14
|
-
export type UnsupportedControlElementDefinition = UploadControlDefinition;
|
|
15
|
-
export interface UnsupportedControlDefinition extends LeafNodeDefinition {
|
|
16
|
-
readonly bodyElement: UnsupportedControlElementDefinition;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Stub node, for form controls pending further engine support.
|
|
20
|
-
*/
|
|
21
|
-
export interface UnsupportedControlNode extends BaseNode {
|
|
22
|
-
readonly nodeType: UnsupportedControlNodeType;
|
|
23
|
-
readonly appearances: UnknownAppearanceDefinition;
|
|
24
|
-
readonly definition: UnsupportedControlDefinition;
|
|
25
|
-
readonly root: RootNode;
|
|
26
|
-
readonly parent: GeneralParentNode;
|
|
27
|
-
readonly currentState: UnsupportedControlNodeState;
|
|
28
|
-
readonly validationState: LeafNodeValidationState;
|
|
29
|
-
setValue?(value: never): never;
|
|
30
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { UploadControlDefinition } from '../../parse/body/control/UploadControlDefinition.ts';
|
|
2
|
-
import { UnsupportedControlDefinition, UnsupportedControlNode } from './UnsupportedControlNode.ts';
|
|
3
|
-
export interface UploadNodeDefinition extends UnsupportedControlDefinition {
|
|
4
|
-
readonly bodyElement: UploadControlDefinition;
|
|
5
|
-
}
|
|
6
|
-
export interface UploadNode extends UnsupportedControlNode {
|
|
7
|
-
readonly nodeType: 'upload';
|
|
8
|
-
readonly definition: UploadNodeDefinition;
|
|
9
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { XPathNodeKindKey } from '@getodk/xpath';
|
|
2
|
-
import { Accessor } from 'solid-js';
|
|
3
|
-
import { TextRange } from '../../client/TextRange.ts';
|
|
4
|
-
import { ValueType } from '../../client/ValueType.ts';
|
|
5
|
-
import { UploadNode, UploadNodeDefinition } from '../../client/unsupported/UploadNode.ts';
|
|
6
|
-
import { XFormsXPathElement } from '../../integration/xpath/adapter/XFormsXPathNode.ts';
|
|
7
|
-
import { StaticLeafElement } from '../../integration/xpath/static-dom/StaticElement.ts';
|
|
8
|
-
import { TempUnsupportedInputValue, TempUnsupportedRuntimeValue } from '../../lib/codecs/TempUnsupportedControlCodec.ts';
|
|
9
|
-
import { CurrentState } from '../../lib/reactivity/node-state/createCurrentState.ts';
|
|
10
|
-
import { EngineState } from '../../lib/reactivity/node-state/createEngineState.ts';
|
|
11
|
-
import { SharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
|
|
12
|
-
import { UnknownAppearanceDefinition } from '../../parse/body/appearance/unknownAppearanceParser.ts';
|
|
13
|
-
import { ValueNode, ValueNodeStateSpec } from '../abstract/ValueNode.ts';
|
|
14
|
-
import { GeneralParentNode } from '../hierarchy.ts';
|
|
15
|
-
import { EvaluationContext } from '../internal-api/EvaluationContext.ts';
|
|
16
|
-
import { ValidationContext } from '../internal-api/ValidationContext.ts';
|
|
17
|
-
import { ClientReactiveSerializableValueNode } from '../internal-api/serialization/ClientReactiveSerializableValueNode.ts';
|
|
18
|
-
interface UploadControlStateSpec extends ValueNodeStateSpec<TempUnsupportedRuntimeValue> {
|
|
19
|
-
readonly label: Accessor<TextRange<'label'> | null>;
|
|
20
|
-
readonly hint: Accessor<TextRange<'hint'> | null>;
|
|
21
|
-
readonly valueOptions: null;
|
|
22
|
-
}
|
|
23
|
-
export declare class UploadControl extends ValueNode<ValueType, UploadNodeDefinition, TempUnsupportedRuntimeValue, TempUnsupportedInputValue> implements UploadNode, XFormsXPathElement, EvaluationContext, ValidationContext, ClientReactiveSerializableValueNode {
|
|
24
|
-
readonly [XPathNodeKindKey] = "element";
|
|
25
|
-
protected readonly state: SharedNodeState<UploadControlStateSpec>;
|
|
26
|
-
protected readonly engineState: EngineState<UploadControlStateSpec>;
|
|
27
|
-
readonly nodeType = "upload";
|
|
28
|
-
readonly appearances: UnknownAppearanceDefinition;
|
|
29
|
-
readonly nodeOptions: null;
|
|
30
|
-
readonly currentState: CurrentState<UploadControlStateSpec>;
|
|
31
|
-
constructor(parent: GeneralParentNode, instanceNode: StaticLeafElement | null, definition: UploadNodeDefinition);
|
|
32
|
-
setValue(_: never): never;
|
|
33
|
-
}
|
|
34
|
-
export {};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ValueType } from '../../client/ValueType.ts';
|
|
2
|
-
import { ValueCodec } from './ValueCodec.ts';
|
|
3
|
-
export type TempUnsupportedRuntimeValue = unknown;
|
|
4
|
-
export type TempUnsupportedInputValue = unknown;
|
|
5
|
-
export declare class TempUnsupportedControlCodec<V extends ValueType> extends ValueCodec<V, TempUnsupportedRuntimeValue, TempUnsupportedInputValue> {
|
|
6
|
-
constructor(valueType: V);
|
|
7
|
-
}
|