Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

engine (fix): Forms load with any jr:preload or jr:preloadParams value #320

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-moose-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@getodk/xforms-engine': patch
---

Fix: relax parsing of `jr:preload` and `jr:preloadParams`. Any value for either attribute is accepted. Known (specified in ODK XForms, at time of writing) values are provided as type hints, similarly to how known appearances are specified.

This file was deleted.

94 changes: 28 additions & 66 deletions packages/xforms-engine/src/parse/model/BindPreloadDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,29 @@
import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
import { getPropertyKeys } from '@getodk/common/lib/objects/structure.ts';
import { UnknownPreloadAttributeValueNotice } from '../../error/UnknownPreloadAttributeValueNotice.ts';
import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
import type { BindElement } from './BindElement.ts';

const preloadParametersByType = {
uid: [null],
date: ['today'],
timestamp: ['start', 'end'],
property: ['deviceid', 'email', 'username', 'phonenumber'],
} as const;

const preloadParameterTypes = getPropertyKeys(preloadParametersByType);

type PreloadParametersByType = typeof preloadParametersByType;

type PreloadType = keyof PreloadParametersByType;

type AssertPreloadType = (type: string) => asserts type is PreloadType;

const assertPreloadType: AssertPreloadType = (type) => {
if (!preloadParameterTypes.includes(type as PreloadType)) {
throw new UnknownPreloadAttributeValueNotice('jr:preload', preloadParameterTypes, type);
}
};

const getPreloadType = (bindElement: BindElement): PreloadType | null => {
const type = bindElement.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'preload');

if (type == null) {
return null;
}

assertPreloadType(type);

return type;
};

type PreloadParameter<Type extends PreloadType> = PreloadParametersByType[Type][number];

type AssertPreloadParameter = <Type extends PreloadType>(
type: Type,
parameter: string | null
) => asserts parameter is PreloadParameter<Type>;
type PartiallyKnownPreloadParameter<Known extends string> =
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
PartiallyKnownString<NonNullable<Known>> | Extract<Known, null>;

const assertPreloadParameter: AssertPreloadParameter = <Type extends PreloadType>(
type: Type,
parameter: string | null
) => {
const parameters: ReadonlyArray<PreloadParameter<Type>> = preloadParametersByType[type];
interface PreloadParametersByType {
readonly uid: string | null;
readonly date: PartiallyKnownPreloadParameter<'today'>;
readonly timestamp: PartiallyKnownPreloadParameter<'end' | 'start'>;

if (!parameters.includes(parameter as PreloadParameter<Type>)) {
throw new UnknownPreloadAttributeValueNotice('jr:preloadParams', parameters, parameter);
}
};

const getPreloadParameter = <Type extends PreloadType>(
bindElement: BindElement,
type: Type
): PreloadParameter<Type> => {
const parameter = bindElement.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'preloadParams');
readonly property: PartiallyKnownPreloadParameter<
// prettier-ignore
'deviceid' | 'email' | 'phonenumber' | 'username'
>;
}

assertPreloadParameter(type, parameter);
type PreloadType = PartiallyKnownString<keyof PreloadParametersByType>;

return parameter;
};
// prettier-ignore
type PreloadParameter<Type extends PreloadType> =
Type extends keyof PreloadParametersByType
? PreloadParametersByType[Type]
: string | null;

interface PreloadInput<Type extends PreloadType> {
readonly type: Type;
Expand All @@ -75,20 +35,21 @@ type AnyPreloadInput = {
}[PreloadType];

const getPreloadInput = (bindElement: BindElement): AnyPreloadInput | null => {
const type = getPreloadType(bindElement);
const type = bindElement.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'preload');

if (type == null) {
return null;
}

type Type = typeof type;

const parameter: PreloadParameter<Type> = getPreloadParameter(bindElement, type);
const parameter: PreloadParameter<typeof type> = bindElement.getAttributeNS(
JAVAROSA_NAMESPACE_URI,
'preloadParams'
);

return {
type,
parameter,
} satisfies PreloadInput<Type> as AnyPreloadInput;
};
};

/**
Expand Down Expand Up @@ -118,7 +79,7 @@ export class BindPreloadDefinition<Type extends PreloadType> implements PreloadI
return null;
}

return new this(input) satisfies BindPreloadDefinition<PreloadType> as AnyBindPreloadDefinition;
return new this(input);
}

readonly type: Type;
Expand All @@ -135,4 +96,5 @@ export type AnyBindPreloadDefinition =
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
| BindPreloadDefinition<'uid'>
| BindPreloadDefinition<'timestamp'>
| BindPreloadDefinition<'property'>;
| BindPreloadDefinition<'property'>
| BindPreloadDefinition<string>;
Loading