Skip to content

Commit e9c2ad2

Browse files
committed
Revert changes related to resources
1 parent 3a4e769 commit e9c2ad2

11 files changed

+124
-90
lines changed

packages/common/src/jr-resources/JRResource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { XFormAttachmentFixture } from '../fixtures/xform-attachments.ts';
22
import { JRResourceURL } from './JRResourceURL.ts';
33

4-
type JRResourceLoader = (this: void) => Promise<Blob | string>;
4+
type JRResourceLoader = (this: void) => Promise<string>;
55

66
export interface JRResourceSource {
77
readonly url: JRResourceURL;
Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
2-
import type { FetchResource, FetchResourceResponse, MissingResourceBehavior } from '../../client';
3-
import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
42

5-
export interface FormAttachmentResourceOptions {
6-
readonly fetchResource: FetchResource<JRResourceURL>;
7-
readonly missingResourceBehavior: MissingResourceBehavior;
8-
}
9-
10-
export abstract class FormAttachmentResource {
11-
protected constructor(
12-
readonly resourceURL: JRResourceURL,
13-
readonly contentType: string
14-
) {}
3+
export type FormAttachmentDataType = 'media' | 'secondary-instance';
154

16-
protected static isMissingResource(response: FetchResourceResponse) {
17-
return response.status === 404;
18-
}
5+
/**
6+
* @todo This type anticipates work to support media form attachments, which
7+
* will tend to be associated with binary data. The
8+
* expectation is that:
9+
*
10+
* - {@link Blob} would be appropriate for representing data from attachment
11+
* resources which are conventionally loaded to completion (where network
12+
* conditions are favorable), such as images
13+
*
14+
* - {@link MediaSource} or {@link ReadableStream} may be more appropriate for
15+
* representing data from resources which are conventionally streamed in a
16+
* browser context (often regardless of network conditions), such as video and
17+
* audio
18+
*/
19+
// prettier-ignore
20+
export type FormAttachmentMediaData =
21+
| Blob
22+
| MediaSource
23+
| ReadableStream<unknown>;
1924

20-
protected static handleMissingResource(resourceURL: JRResourceURL) {
21-
throw new ErrorProductionDesignPendingError(`Resource not found: ${resourceURL.href}`);
22-
}
25+
export type FormAttachmentSecondaryInstanceData = string;
2326

24-
protected static assertResponseSuccess(
25-
resourceURL: JRResourceURL,
26-
response: FetchResourceResponse
27-
) {
28-
const { ok = true, status = 200 } = response;
27+
// prettier-ignore
28+
type FormAttachmentData<DataType extends FormAttachmentDataType> =
29+
DataType extends 'media'
30+
? FormAttachmentMediaData
31+
: FormAttachmentSecondaryInstanceData;
2932

30-
if (!ok || status !== 200) {
31-
throw new ErrorProductionDesignPendingError(`Failed to load resource: ${resourceURL.href}`);
32-
}
33-
}
33+
export abstract class FormAttachmentResource<DataType extends FormAttachmentDataType> {
34+
protected constructor(
35+
readonly dataType: DataType,
36+
readonly resourceURL: JRResourceURL,
37+
readonly contentType: string,
38+
readonly data: FormAttachmentData<DataType>
39+
) {}
3440
}

packages/xforms-engine/src/parse/model/SecondaryInstance/SecondaryInstancesDefinition.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { ErrorProductionDesignPendingError } from '../../../error/ErrorProductio
99
import type { EngineXPathNode } from '../../../integration/xpath/adapter/kind.ts';
1010
import type { StaticDocument } from '../../../integration/xpath/static-dom/StaticDocument.ts';
1111
import type { StaticElement } from '../../../integration/xpath/static-dom/StaticElement.ts';
12-
import type { FormAttachmentResourceOptions } from '../../attachments/FormAttachmentResource.ts';
1312
import type { XFormDOM } from '../../XFormDOM.ts';
1413
import { BlankSecondaryInstanceSource } from './sources/BlankSecondaryInstanceSource.ts';
1514
import { CSVExternalSecondaryInstanceSource } from './sources/CSVExternalSecondaryInstance.ts';
15+
import type { ExternalSecondaryInstanceResourceLoadOptions } from './sources/ExternalSecondaryInstanceResource.ts';
1616
import { ExternalSecondaryInstanceResource } from './sources/ExternalSecondaryInstanceResource.ts';
1717
import { GeoJSONExternalSecondaryInstanceSource } from './sources/GeoJSONExternalSecondaryInstance.ts';
1818
import { InternalSecondaryInstanceSource } from './sources/InternalSecondaryInstanceSource.ts';
@@ -62,7 +62,7 @@ export class SecondaryInstancesDefinition
6262

6363
static async load(
6464
xformDOM: XFormDOM,
65-
options: FormAttachmentResourceOptions
65+
options: ExternalSecondaryInstanceResourceLoadOptions
6666
): Promise<SecondaryInstancesDefinition> {
6767
const { secondaryInstanceElements } = xformDOM;
6868

@@ -103,7 +103,7 @@ export class SecondaryInstancesDefinition
103103
return new XMLExternalSecondaryInstanceSource(domElement, resource);
104104

105105
default:
106-
throw new UnreachableError(resource as never);
106+
throw new UnreachableError(resource);
107107
}
108108
})
109109
);

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/BlankSecondaryInstanceSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { defineSecondaryInstance } from '../defineSecondaryInstance.ts';
44
import type { SecondaryInstanceDefinition } from '../SecondaryInstancesDefinition.ts';
55
import { SecondaryInstanceSource } from './SecondaryInstanceSource.ts';
66

7-
export class BlankSecondaryInstanceSource extends SecondaryInstanceSource {
7+
export class BlankSecondaryInstanceSource extends SecondaryInstanceSource<'blank'> {
88
constructor(
99
instanceId: string,
1010
resourceURL: JRResourceURL,

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const csvExternalSecondaryInstanceDefinition = (
119119
return defineSecondaryInstance(instanceId, rootChildOption(items));
120120
};
121121

122-
export class CSVExternalSecondaryInstanceSource extends ExternalSecondaryInstanceSource {
122+
export class CSVExternalSecondaryInstanceSource extends ExternalSecondaryInstanceSource<'csv'> {
123123
/**
124124
* Based on
125125
* {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L79} (and {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L13}).

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/ExternalSecondaryInstanceResource.ts

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
2-
import type { FetchResourceResponse } from '../../../../client/resources.ts';
2+
import type { MissingResourceBehavior } from '../../../../client/constants.ts';
3+
import type { FetchResource, FetchResourceResponse } from '../../../../client/resources.ts';
34
import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError.ts';
45
import { getResponseContentType } from '../../../../lib/resource-helpers.ts';
5-
import {
6-
FormAttachmentResource,
7-
type FormAttachmentResourceOptions,
8-
} from '../../../attachments/FormAttachmentResource.ts';
6+
import { FormAttachmentResource } from '../../../attachments/FormAttachmentResource.ts';
97
import type { ExternalSecondaryInstanceSourceFormat } from './SecondaryInstanceSource.ts';
108

11-
interface ExternalSecondaryInstanceResourceMetadata {
9+
const assertResponseSuccess = (resourceURL: JRResourceURL, response: FetchResourceResponse) => {
10+
const { ok = true, status = 200 } = response;
11+
12+
if (!ok || status !== 200) {
13+
throw new ErrorProductionDesignPendingError(`Failed to load ${resourceURL.href}`);
14+
}
15+
};
16+
17+
interface ExternalSecondaryInstanceResourceMetadata<
18+
Format extends ExternalSecondaryInstanceSourceFormat = ExternalSecondaryInstanceSourceFormat,
19+
> {
1220
readonly contentType: string;
13-
readonly format: ExternalSecondaryInstanceSourceFormat;
21+
readonly format: Format;
1422
}
1523

1624
const inferSecondaryInstanceResourceMetadata = (
@@ -20,15 +28,17 @@ const inferSecondaryInstanceResourceMetadata = (
2028
): ExternalSecondaryInstanceResourceMetadata => {
2129
const url = resourceURL.href;
2230

23-
let format: ExternalSecondaryInstanceSourceFormat;
31+
let format: ExternalSecondaryInstanceSourceFormat | null = null;
2432

2533
if (url.endsWith('.xml') && data.startsWith('<')) {
2634
format = 'xml';
2735
} else if (url.endsWith('.csv')) {
2836
format = 'csv';
2937
} else if (url.endsWith('.geojson') && data.startsWith('{')) {
3038
format = 'geojson';
31-
} else {
39+
}
40+
41+
if (format == null) {
3242
throw new ErrorProductionDesignPendingError(
3343
`Failed to infer external secondary instance format/content type for resource ${url} (response content type: ${contentType}, data: ${data})`
3444
);
@@ -51,7 +61,7 @@ const detectSecondaryInstanceResourceMetadata = (
5161
return inferSecondaryInstanceResourceMetadata(resourceURL, contentType, data);
5262
}
5363

54-
let format: ExternalSecondaryInstanceSourceFormat;
64+
let format: ExternalSecondaryInstanceSourceFormat | null = null;
5565

5666
switch (contentType) {
5767
case 'text/csv':
@@ -65,11 +75,12 @@ const detectSecondaryInstanceResourceMetadata = (
6575
case 'text/xml':
6676
format = 'xml';
6777
break;
78+
}
6879

69-
default:
70-
throw new ErrorProductionDesignPendingError(
71-
`Failed to detect external secondary instance format for resource ${resourceURL.href} (response content type: ${contentType}, data: ${data})`
72-
);
80+
if (format == null) {
81+
throw new ErrorProductionDesignPendingError(
82+
`Failed to detect external secondary instance format for resource ${resourceURL.href} (response content type: ${contentType}, data: ${data})`
83+
);
7384
}
7485

7586
return {
@@ -78,27 +89,41 @@ const detectSecondaryInstanceResourceMetadata = (
7889
};
7990
};
8091

92+
interface MissingResourceResponse extends FetchResourceResponse {
93+
readonly status: 404;
94+
}
95+
96+
export interface ExternalSecondaryInstanceResourceLoadOptions {
97+
readonly fetchResource: FetchResource<JRResourceURL>;
98+
readonly missingResourceBehavior: MissingResourceBehavior;
99+
}
100+
101+
type LoadedExternalSecondaryInstanceResource = {
102+
[Format in ExternalSecondaryInstanceSourceFormat]: ExternalSecondaryInstanceResource<Format>;
103+
}[ExternalSecondaryInstanceSourceFormat];
104+
81105
interface ExternalSecondaryInstanceResourceOptions {
82106
readonly isExplicitlyBlank?: boolean;
83107
}
84108

85-
interface ExternalSecondaryInstanceLoadResult {
86-
response: FetchResourceResponse;
87-
data: string;
88-
isBlank: boolean;
89-
}
109+
export class ExternalSecondaryInstanceResource<
110+
Format extends ExternalSecondaryInstanceSourceFormat = ExternalSecondaryInstanceSourceFormat,
111+
> extends FormAttachmentResource<'secondary-instance'> {
112+
private static isMissingResource(
113+
response: FetchResourceResponse
114+
): response is MissingResourceResponse {
115+
return response.status === 404;
116+
}
90117

91-
export class ExternalSecondaryInstanceResource extends FormAttachmentResource {
92-
static async load(
118+
private static createBlankResource(
93119
instanceId: string,
94120
resourceURL: JRResourceURL,
95-
options: FormAttachmentResourceOptions
96-
): Promise<ExternalSecondaryInstanceResource> {
97-
const { response, data, isBlank } = await this.fetch(resourceURL, options);
98-
99-
if (isBlank) {
121+
response: MissingResourceResponse,
122+
options: ExternalSecondaryInstanceResourceLoadOptions
123+
) {
124+
if (options.missingResourceBehavior === 'BLANK') {
100125
return new this(
101-
response.status ?? null,
126+
response.status,
102127
instanceId,
103128
resourceURL,
104129
{
@@ -110,57 +135,55 @@ export class ExternalSecondaryInstanceResource extends FormAttachmentResource {
110135
);
111136
}
112137

113-
const metadata = detectSecondaryInstanceResourceMetadata(resourceURL, response, data);
114-
115-
return new this(response.status ?? null, instanceId, resourceURL, metadata, data, {
116-
isExplicitlyBlank: false,
117-
});
138+
throw new ErrorProductionDesignPendingError(
139+
`Failed to load resource: ${resourceURL.href}: resource is missing (status: ${response.status})`
140+
);
118141
}
119142

120-
private static async fetch(
143+
static async load(
144+
instanceId: string,
121145
resourceURL: JRResourceURL,
122-
options: FormAttachmentResourceOptions
123-
): Promise<ExternalSecondaryInstanceLoadResult> {
124-
const { fetchResource, missingResourceBehavior } = options;
125-
const response = await fetchResource(resourceURL);
146+
options: ExternalSecondaryInstanceResourceLoadOptions
147+
): Promise<LoadedExternalSecondaryInstanceResource> {
148+
const response = await options.fetchResource(resourceURL);
126149

127150
if (this.isMissingResource(response)) {
128-
if (missingResourceBehavior === 'BLANK') {
129-
return { response, isBlank: true, data: '' };
130-
}
131-
throw new ErrorProductionDesignPendingError(`Resource not found: ${resourceURL.href}`);
151+
return this.createBlankResource(instanceId, resourceURL, response, options);
132152
}
133153

134-
this.assertResponseSuccess(resourceURL, response);
154+
assertResponseSuccess(resourceURL, response);
135155

136-
return { response, isBlank: false, data: await response.text() };
156+
const data = await response.text();
157+
const metadata = detectSecondaryInstanceResourceMetadata(resourceURL, response, data);
158+
159+
return new this(response.status ?? null, instanceId, resourceURL, metadata, data, {
160+
isExplicitlyBlank: false,
161+
}) satisfies ExternalSecondaryInstanceResource as LoadedExternalSecondaryInstanceResource;
137162
}
138163

139-
readonly format: ExternalSecondaryInstanceSourceFormat;
140-
readonly data: string;
164+
readonly format: Format;
141165
readonly isBlank: boolean;
142166

143167
private constructor(
144168
readonly responseStatus: number | null,
145169
readonly instanceId: string,
146170
resourceURL: JRResourceURL,
147-
metadata: ExternalSecondaryInstanceResourceMetadata,
171+
metadata: ExternalSecondaryInstanceResourceMetadata<Format>,
148172
data: string,
149173
options: ExternalSecondaryInstanceResourceOptions
150174
) {
151175
const { contentType, format } = metadata;
152176

153-
super(resourceURL, contentType);
177+
super('secondary-instance', resourceURL, contentType, data);
154178

155179
this.format = format;
156-
this.data = data;
157180

158181
if (data === '') {
159182
if (options.isExplicitlyBlank) {
160183
this.isBlank = true;
161184
} else {
162185
throw new ErrorProductionDesignPendingError(
163-
`Failed to load blank external secondary instance ${resourceURL.href}`
186+
`Failed to load blank external secndary instance ${resourceURL.href}`
164187
);
165188
}
166189
} else {

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/ExternalSecondaryInstanceSource.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
22
import type { DOMSecondaryInstanceElement } from '../../../XFormDOM.ts';
33
import type { ExternalSecondaryInstanceResource } from './ExternalSecondaryInstanceResource.ts';
4+
import type { ExternalSecondaryInstanceSourceFormat } from './SecondaryInstanceSource.ts';
45
import { SecondaryInstanceSource } from './SecondaryInstanceSource.ts';
56

6-
export abstract class ExternalSecondaryInstanceSource extends SecondaryInstanceSource {
7+
export abstract class ExternalSecondaryInstanceSource<
8+
Format extends ExternalSecondaryInstanceSourceFormat = ExternalSecondaryInstanceSourceFormat,
9+
> extends SecondaryInstanceSource<Format> {
710
override readonly resourceURL: JRResourceURL;
811

912
constructor(
1013
domElement: DOMSecondaryInstanceElement,
11-
protected readonly resource: ExternalSecondaryInstanceResource
14+
protected readonly resource: ExternalSecondaryInstanceResource<Format>
1215
) {
1316
const { format, instanceId, resourceURL } = resource;
1417

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/GeoJSONExternalSecondaryInstance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ const geoJSONExternalSecondaryInstanceDefinition = (
330330
return defineSecondaryInstance(instanceId, rootChildOption(featureCollection));
331331
};
332332

333-
export class GeoJSONExternalSecondaryInstanceSource extends ExternalSecondaryInstanceSource {
333+
export class GeoJSONExternalSecondaryInstanceSource extends ExternalSecondaryInstanceSource<'geojson'> {
334334
parseDefinition(): SecondaryInstanceDefinition {
335335
const { data } = this.resource;
336336
const value = JSON.parse(data) as unknown;

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/InternalSecondaryInstanceSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { assertSecondaryInstanceDefinition } from '../assertSecondaryInstanceDef
44
import type { SecondaryInstanceDefinition } from '../SecondaryInstancesDefinition.ts';
55
import { SecondaryInstanceSource } from './SecondaryInstanceSource.ts';
66

7-
export class InternalSecondaryInstanceSource extends SecondaryInstanceSource {
7+
export class InternalSecondaryInstanceSource extends SecondaryInstanceSource<'internal'> {
88
constructor(instanceId: string, resourceURL: null, domElement: DOMSecondaryInstanceElement) {
99
super('internal', instanceId, resourceURL, domElement);
1010
}

packages/xforms-engine/src/parse/model/SecondaryInstance/sources/SecondaryInstanceSource.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ export type ExternalSecondaryInstanceSourceFormat =
1010

1111
// prettier-ignore
1212
export type SecondaryInstanceSourceFormat =
13-
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
13+
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
1414
| ExternalSecondaryInstanceSourceFormat
1515
| 'internal'
1616
| 'blank';
1717

18-
export abstract class SecondaryInstanceSource {
18+
export abstract class SecondaryInstanceSource<
19+
Format extends SecondaryInstanceSourceFormat = SecondaryInstanceSourceFormat,
20+
> {
1921
constructor(
20-
readonly format: SecondaryInstanceSourceFormat,
22+
readonly format: Format,
2123
readonly instanceId: string,
2224
readonly resourceURL: JRResourceURL | null,
2325
readonly domElement: DOMSecondaryInstanceElement

0 commit comments

Comments
 (0)