1
1
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' ;
3
4
import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError.ts' ;
4
5
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' ;
9
7
import type { ExternalSecondaryInstanceSourceFormat } from './SecondaryInstanceSource.ts' ;
10
8
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
+ > {
12
20
readonly contentType : string ;
13
- readonly format : ExternalSecondaryInstanceSourceFormat ;
21
+ readonly format : Format ;
14
22
}
15
23
16
24
const inferSecondaryInstanceResourceMetadata = (
@@ -20,15 +28,17 @@ const inferSecondaryInstanceResourceMetadata = (
20
28
) : ExternalSecondaryInstanceResourceMetadata => {
21
29
const url = resourceURL . href ;
22
30
23
- let format : ExternalSecondaryInstanceSourceFormat ;
31
+ let format : ExternalSecondaryInstanceSourceFormat | null = null ;
24
32
25
33
if ( url . endsWith ( '.xml' ) && data . startsWith ( '<' ) ) {
26
34
format = 'xml' ;
27
35
} else if ( url . endsWith ( '.csv' ) ) {
28
36
format = 'csv' ;
29
37
} else if ( url . endsWith ( '.geojson' ) && data . startsWith ( '{' ) ) {
30
38
format = 'geojson' ;
31
- } else {
39
+ }
40
+
41
+ if ( format == null ) {
32
42
throw new ErrorProductionDesignPendingError (
33
43
`Failed to infer external secondary instance format/content type for resource ${ url } (response content type: ${ contentType } , data: ${ data } )`
34
44
) ;
@@ -51,7 +61,7 @@ const detectSecondaryInstanceResourceMetadata = (
51
61
return inferSecondaryInstanceResourceMetadata ( resourceURL , contentType , data ) ;
52
62
}
53
63
54
- let format : ExternalSecondaryInstanceSourceFormat ;
64
+ let format : ExternalSecondaryInstanceSourceFormat | null = null ;
55
65
56
66
switch ( contentType ) {
57
67
case 'text/csv' :
@@ -65,11 +75,12 @@ const detectSecondaryInstanceResourceMetadata = (
65
75
case 'text/xml' :
66
76
format = 'xml' ;
67
77
break ;
78
+ }
68
79
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
+ ) ;
73
84
}
74
85
75
86
return {
@@ -78,27 +89,41 @@ const detectSecondaryInstanceResourceMetadata = (
78
89
} ;
79
90
} ;
80
91
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
+
81
105
interface ExternalSecondaryInstanceResourceOptions {
82
106
readonly isExplicitlyBlank ?: boolean ;
83
107
}
84
108
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
+ }
90
117
91
- export class ExternalSecondaryInstanceResource extends FormAttachmentResource {
92
- static async load (
118
+ private static createBlankResource (
93
119
instanceId : string ,
94
120
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' ) {
100
125
return new this (
101
- response . status ?? null ,
126
+ response . status ,
102
127
instanceId ,
103
128
resourceURL ,
104
129
{
@@ -110,57 +135,55 @@ export class ExternalSecondaryInstanceResource extends FormAttachmentResource {
110
135
) ;
111
136
}
112
137
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
+ ) ;
118
141
}
119
142
120
- private static async fetch (
143
+ static async load (
144
+ instanceId : string ,
121
145
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 ) ;
126
149
127
150
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 ) ;
132
152
}
133
153
134
- this . assertResponseSuccess ( resourceURL , response ) ;
154
+ assertResponseSuccess ( resourceURL , response ) ;
135
155
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 ;
137
162
}
138
163
139
- readonly format : ExternalSecondaryInstanceSourceFormat ;
140
- readonly data : string ;
164
+ readonly format : Format ;
141
165
readonly isBlank : boolean ;
142
166
143
167
private constructor (
144
168
readonly responseStatus : number | null ,
145
169
readonly instanceId : string ,
146
170
resourceURL : JRResourceURL ,
147
- metadata : ExternalSecondaryInstanceResourceMetadata ,
171
+ metadata : ExternalSecondaryInstanceResourceMetadata < Format > ,
148
172
data : string ,
149
173
options : ExternalSecondaryInstanceResourceOptions
150
174
) {
151
175
const { contentType, format } = metadata ;
152
176
153
- super ( resourceURL , contentType ) ;
177
+ super ( 'secondary-instance' , resourceURL , contentType , data ) ;
154
178
155
179
this . format = format ;
156
- this . data = data ;
157
180
158
181
if ( data === '' ) {
159
182
if ( options . isExplicitlyBlank ) {
160
183
this . isBlank = true ;
161
184
} else {
162
185
throw new ErrorProductionDesignPendingError (
163
- `Failed to load blank external secondary instance ${ resourceURL . href } `
186
+ `Failed to load blank external secndary instance ${ resourceURL . href } `
164
187
) ;
165
188
}
166
189
} else {
0 commit comments