fms-odata-js 0.3.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/CHANGELOG.md +160 -0
- package/LICENSE +22 -0
- package/README.md +381 -0
- package/dist/batch.d.ts +154 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/client.d.ts +160 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/containers.d.ts +190 -0
- package/dist/containers.d.ts.map +1 -0
- package/dist/entity.d.ts +149 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fms-odata.esm.js +2201 -0
- package/dist/fms-odata.esm.min.js +6 -0
- package/dist/fms-odata.iife.min.js +6 -0
- package/dist/http.d.ts +51 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/metadata.d.ts +85 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/query.d.ts +198 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/scripts.d.ts +114 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/url.d.ts +49 -0
- package/dist/url.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/batch.ts +477 -0
- package/src/client.ts +268 -0
- package/src/containers.ts +457 -0
- package/src/entity.ts +334 -0
- package/src/errors.ts +134 -0
- package/src/http.ts +190 -0
- package/src/index.ts +81 -0
- package/src/metadata.ts +354 -0
- package/src/query.ts +442 -0
- package/src/scripts.ts +279 -0
- package/src/types.ts +30 -0
- package/src/url.ts +98 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export { FMSOData } from './client.js'
|
|
2
|
+
export { Batch, Changeset } from './batch.js'
|
|
3
|
+
export type {
|
|
4
|
+
BatchHandle,
|
|
5
|
+
BatchOpResult,
|
|
6
|
+
BatchReadOp,
|
|
7
|
+
BatchResult,
|
|
8
|
+
} from './batch.js'
|
|
9
|
+
export {
|
|
10
|
+
buildContainerJsonBody,
|
|
11
|
+
ContainerRef,
|
|
12
|
+
FM_CONTAINER_SUPPORTED_MIME_TYPES,
|
|
13
|
+
sniffContainerMime,
|
|
14
|
+
toBase64,
|
|
15
|
+
} from './containers.js'
|
|
16
|
+
export type {
|
|
17
|
+
ContainerDownload,
|
|
18
|
+
ContainerJsonValue,
|
|
19
|
+
ContainerUploadInput,
|
|
20
|
+
FMContainerMimeType,
|
|
21
|
+
} from './containers.js'
|
|
22
|
+
export { EntityRef } from './entity.js'
|
|
23
|
+
export type { EntityWriteOptions, EntityRefInfo } from './entity.js'
|
|
24
|
+
export { FMSODataError, FMScriptError, isFMSODataError, isFMScriptError } from './errors.js'
|
|
25
|
+
export type { ODataErrorBody } from './errors.js'
|
|
26
|
+
export { basicAuth, fmidAuth } from './http.js'
|
|
27
|
+
export type { FMAuthScheme, FMAuthToken, FMAuthTokenProvider } from './http.js'
|
|
28
|
+
export { MetadataFetcher } from './metadata.js'
|
|
29
|
+
export type {
|
|
30
|
+
EdmAction,
|
|
31
|
+
EdmEntitySet,
|
|
32
|
+
EdmEntityType,
|
|
33
|
+
EdmProperty,
|
|
34
|
+
MetadataOptions,
|
|
35
|
+
ODataMetadata,
|
|
36
|
+
} from './metadata.js'
|
|
37
|
+
export { Filter, Query, filterFactory } from './query.js'
|
|
38
|
+
export type { FilterFactory, FilterInput, OrderDir, QueryResult } from './query.js'
|
|
39
|
+
export type { AggregateFunction } from '@fms-odata/spec-ts'
|
|
40
|
+
export { ScriptInvoker } from './scripts.js'
|
|
41
|
+
export type { ScriptOptions, ScriptResult, ScriptScope, ScriptIdentifier } from './scripts.js'
|
|
42
|
+
export type { FMSODataOptions, TokenProvider, RequestOptions } from './types.js'
|
|
43
|
+
export type { ODataLiteral } from './url.js'
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Spec alignment: re-export version and feature-flag helpers from @fms-odata/spec-ts
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
FM_VERSION_NAMES,
|
|
51
|
+
FM_VERSION_MATRIX,
|
|
52
|
+
ODATA_PROTOCOL_VERSION,
|
|
53
|
+
hasFeature,
|
|
54
|
+
hasQueryOption,
|
|
55
|
+
minVersionForFeature,
|
|
56
|
+
parseServerVersion,
|
|
57
|
+
parseVersionString,
|
|
58
|
+
} from '@fms-odata/spec-ts'
|
|
59
|
+
export type {
|
|
60
|
+
FMVersionMajor,
|
|
61
|
+
FMVersionStatus,
|
|
62
|
+
FMFeatureFlags,
|
|
63
|
+
FMQueryOptionFlags,
|
|
64
|
+
FMVersionInfo,
|
|
65
|
+
FMServerVersion,
|
|
66
|
+
} from '@fms-odata/spec-ts'
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Deprecated aliases (kept for backward compatibility with fm-odata-js).
|
|
70
|
+
// These re-export the renamed symbols under their old names and will be
|
|
71
|
+
// removed in a future major version. New code should use the FMSOData* names.
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/** @deprecated Use `FMSOData` instead. */
|
|
75
|
+
export { FMSOData as FMOData } from './client.js'
|
|
76
|
+
/** @deprecated Use `FMSODataError` instead. */
|
|
77
|
+
export { FMSODataError as FMODataError } from './errors.js'
|
|
78
|
+
/** @deprecated Use `isFMSODataError` instead. */
|
|
79
|
+
export { isFMSODataError as isFMODataError } from './errors.js'
|
|
80
|
+
/** @deprecated Use `FMSODataOptions` instead. */
|
|
81
|
+
export type { FMSODataOptions as FMODataOptions } from './types.js'
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* $metadata (EDMX/CSDL) fetch and lightweight parser.
|
|
3
|
+
*
|
|
4
|
+
* FMS returns a subset of the full OData CSDL spec. This module parses just
|
|
5
|
+
* what FMS emits: EntityTypes, EntitySets, and Actions (FileMaker scripts).
|
|
6
|
+
* No inheritance, complex types, or enums are expected.
|
|
7
|
+
*
|
|
8
|
+
* The parser is intentionally dependency-free and uses simple regex-based
|
|
9
|
+
* extraction to stay within the bundle budget (~1 KB gzipped).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FMSODataError } from './errors.js'
|
|
13
|
+
import { executeRequest, type HttpClientContext } from './http.js'
|
|
14
|
+
import type { RequestOptions } from './types.js'
|
|
15
|
+
import { parseServerVersion, type FMServerVersion } from '@fms-odata/spec-ts'
|
|
16
|
+
|
|
17
|
+
/** Property on an entity type (field in FileMaker). */
|
|
18
|
+
export interface EdmProperty {
|
|
19
|
+
name: string
|
|
20
|
+
type: string
|
|
21
|
+
nullable: boolean
|
|
22
|
+
maxLength?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Entity type (FileMaker table occurrence). */
|
|
26
|
+
export interface EdmEntityType {
|
|
27
|
+
name: string
|
|
28
|
+
keys: string[]
|
|
29
|
+
properties: EdmProperty[]
|
|
30
|
+
navigationProperties: { name: string; target: string; collection: boolean }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Entity set (exposed layout/table in OData). */
|
|
34
|
+
export interface EdmEntitySet {
|
|
35
|
+
name: string
|
|
36
|
+
entityType: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Action (FileMaker script exposed as OData Action). */
|
|
40
|
+
export interface EdmAction {
|
|
41
|
+
name: string
|
|
42
|
+
boundTo?: string
|
|
43
|
+
parameters: { name: string; type: string }[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parsed OData metadata document. */
|
|
47
|
+
export interface ODataMetadata {
|
|
48
|
+
namespace: string
|
|
49
|
+
entityTypes: EdmEntityType[]
|
|
50
|
+
entitySets: EdmEntitySet[]
|
|
51
|
+
actions: EdmAction[]
|
|
52
|
+
/** Product version string extracted from Org.OData.Core.V1.ProductVersion annotation (if present). */
|
|
53
|
+
productVersion?: string
|
|
54
|
+
/** Full parsed server version (major.minor.patch + raw), if detected from $metadata. */
|
|
55
|
+
serverVersion?: FMServerVersion
|
|
56
|
+
/** Original XML, for debugging and forward compatibility. */
|
|
57
|
+
raw: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Options for metadata fetch. */
|
|
61
|
+
export interface MetadataOptions extends RequestOptions {
|
|
62
|
+
/** Force refetch, bypassing the internal cache. */
|
|
63
|
+
refresh?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @internal — parse a boolean XML attribute value. */
|
|
67
|
+
function parseBoolAttr(value: string | undefined, defaultValue: boolean): boolean {
|
|
68
|
+
if (value === undefined) return defaultValue
|
|
69
|
+
return value === 'true'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @internal — extract the value of an XML attribute by name from a tag string. */
|
|
73
|
+
function getAttr(text: string, name: string): string | undefined {
|
|
74
|
+
const re = new RegExp(`${name}="([^"]*)"`, 'i')
|
|
75
|
+
const m = text.match(re)
|
|
76
|
+
return m?.[1]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @internal — extract all attributes as a record from a tag open. */
|
|
80
|
+
function getAttrs(text: string): Record<string, string> {
|
|
81
|
+
const out: Record<string, string> = {}
|
|
82
|
+
const re = /(\w+)="([^"]*)"/g
|
|
83
|
+
let m: RegExpExecArray | null
|
|
84
|
+
while ((m = re.exec(text)) !== null) {
|
|
85
|
+
// m[1] and m[2] are always defined when the regex matches
|
|
86
|
+
out[m[1]!] = m[2]!
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @internal — find all top-level elements with the given tag name, handling namespace prefixes. */
|
|
92
|
+
function findElements(xml: string, tagName: string): string[] {
|
|
93
|
+
const results: string[] = []
|
|
94
|
+
// Match both <Tag ...> and <prefix:Tag ...> (self-closing or with content)
|
|
95
|
+
// [\\w]+ matches word characters (properly escaped for RegExp string)
|
|
96
|
+
const openRe = new RegExp(`<([\\w]+:)?${tagName}\\b[^>]*>`, 'gi')
|
|
97
|
+
let m: RegExpExecArray | null
|
|
98
|
+
while ((m = openRe.exec(xml)) !== null) {
|
|
99
|
+
const openTag = m[0]
|
|
100
|
+
// Self-closing tag: <... />
|
|
101
|
+
if (/\/>\s*$/.test(openTag)) {
|
|
102
|
+
results.push(openTag)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
// Find matching close tag; naive but adequate for FMS CSDL which isn't deeply nested.
|
|
106
|
+
const startIdx = m.index
|
|
107
|
+
const closeRe = new RegExp(`</([\\w]+:)?${tagName}>`, 'gi')
|
|
108
|
+
closeRe.lastIndex = openRe.lastIndex
|
|
109
|
+
const closeM = closeRe.exec(xml)
|
|
110
|
+
if (closeM) {
|
|
111
|
+
results.push(xml.slice(startIdx, closeM.index + closeM[0].length))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return results
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @internal — extract inner XML between the outermost matching open/close tags (handles namespace prefixes). */
|
|
118
|
+
function elementContent(xml: string, tagName: string): string | undefined {
|
|
119
|
+
// Match tag with optional namespace prefix: <tag>, <prefix:tag>
|
|
120
|
+
const openRe = new RegExp(`<(?:[\\w]+:)?${tagName}\\b[^>]*>`, 'i')
|
|
121
|
+
const openM = openRe.exec(xml)
|
|
122
|
+
if (!openM) return undefined
|
|
123
|
+
const startIdx = openM.index + openM[0].length
|
|
124
|
+
// Match closing tag with optional namespace prefix
|
|
125
|
+
const closeRe = new RegExp(`</(?:[\\w]+:)?${tagName}>`, 'i')
|
|
126
|
+
closeRe.lastIndex = startIdx
|
|
127
|
+
const closeM = closeRe.exec(xml)
|
|
128
|
+
if (!closeM) return undefined
|
|
129
|
+
return xml.slice(startIdx, closeM.index)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @internal — parse <Property Name="..." Type="..." Nullable="..." [MaxLength="..."] /> */
|
|
133
|
+
function parseProperty(xml: string): EdmProperty {
|
|
134
|
+
const attrs = getAttrs(xml)
|
|
135
|
+
const nullable = parseBoolAttr(attrs.Nullable, true)
|
|
136
|
+
const out: EdmProperty = {
|
|
137
|
+
name: attrs.Name ?? '',
|
|
138
|
+
type: attrs.Type ?? '',
|
|
139
|
+
nullable,
|
|
140
|
+
}
|
|
141
|
+
if (attrs.MaxLength !== undefined) {
|
|
142
|
+
const n = parseInt(attrs.MaxLength, 10)
|
|
143
|
+
if (!Number.isNaN(n)) out.maxLength = n
|
|
144
|
+
}
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @internal — parse <NavigationProperty Name="..." Type="..." /> */
|
|
149
|
+
function parseNavigationProperty(xml: string): { name: string; target: string; collection: boolean } {
|
|
150
|
+
const attrs = getAttrs(xml)
|
|
151
|
+
const type = attrs.Type ?? ''
|
|
152
|
+
const collection = type.startsWith('Collection(')
|
|
153
|
+
const target = collection ? type.slice(11, -1) : type
|
|
154
|
+
return {
|
|
155
|
+
name: attrs.Name ?? '',
|
|
156
|
+
target,
|
|
157
|
+
collection,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @internal — parse <Key><PropertyRef Name="..." /></Key> */
|
|
162
|
+
function parseKey(xml: string): string[] {
|
|
163
|
+
const keys: string[] = []
|
|
164
|
+
const refs = findElements(xml, 'PropertyRef')
|
|
165
|
+
for (const ref of refs) {
|
|
166
|
+
const name = getAttr(ref, 'Name')
|
|
167
|
+
if (name) keys.push(name)
|
|
168
|
+
}
|
|
169
|
+
return keys
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @internal — parse <EntityType Name="...">...</EntityType> */
|
|
173
|
+
function parseEntityType(xml: string): EdmEntityType {
|
|
174
|
+
const name = getAttr(xml, 'Name') ?? ''
|
|
175
|
+
const keys: string[] = []
|
|
176
|
+
const properties: EdmProperty[] = []
|
|
177
|
+
const navigationProperties: EdmEntityType['navigationProperties'] = []
|
|
178
|
+
|
|
179
|
+
const inner = elementContent(xml, 'EntityType') ?? xml
|
|
180
|
+
|
|
181
|
+
// Key
|
|
182
|
+
const keyEl = findElements(inner, 'Key')[0]
|
|
183
|
+
if (keyEl) {
|
|
184
|
+
keys.push(...parseKey(keyEl))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Properties
|
|
188
|
+
for (const prop of findElements(inner, 'Property')) {
|
|
189
|
+
properties.push(parseProperty(prop))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Navigation Properties
|
|
193
|
+
for (const nav of findElements(inner, 'NavigationProperty')) {
|
|
194
|
+
navigationProperties.push(parseNavigationProperty(nav))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { name, keys, properties, navigationProperties }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @internal — parse <EntitySet Name="..." EntityType="..." /> */
|
|
201
|
+
function parseEntitySet(xml: string): EdmEntitySet {
|
|
202
|
+
const attrs = getAttrs(xml)
|
|
203
|
+
return {
|
|
204
|
+
name: attrs.Name ?? '',
|
|
205
|
+
entityType: attrs.EntityType ?? '',
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @internal — extract the FileMaker Server version from the metadata XML using
|
|
211
|
+
* the multi-strategy parser from @fms-odata/spec-ts.
|
|
212
|
+
*
|
|
213
|
+
* Detection strategies (in priority order):
|
|
214
|
+
* 1a. Org.OData.Core.V1.ProductVersion annotation with String attribute
|
|
215
|
+
* 1b. Same as 1a but with reversed attribute order
|
|
216
|
+
* 1c. ServerVersion annotation (FM 26+, e.g. "OData Engine 26.0.1")
|
|
217
|
+
* 1d. Same as 1c but with reversed attribute order
|
|
218
|
+
* 2. Generic fallback: any annotation term containing "Version" with major >= 17
|
|
219
|
+
*
|
|
220
|
+
* Returns the full FMServerVersion object, or undefined if not found.
|
|
221
|
+
*/
|
|
222
|
+
function extractServerVersion(xml: string): FMServerVersion | undefined {
|
|
223
|
+
return parseServerVersion(xml) ?? undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @internal — parse <Action Name="...">[...parameters...]</Action> */
|
|
227
|
+
function parseAction(xml: string): EdmAction {
|
|
228
|
+
const name = getAttr(xml, 'Name') ?? ''
|
|
229
|
+
const attrs = getAttrs(xml)
|
|
230
|
+
const boundTo = attrs['IsBound'] === 'true' ? attrs['EntityType'] : undefined
|
|
231
|
+
|
|
232
|
+
const parameters: EdmAction['parameters'] = []
|
|
233
|
+
const inner = elementContent(xml, 'Action') ?? xml
|
|
234
|
+
for (const p of findElements(inner, 'Parameter')) {
|
|
235
|
+
const pAttrs = getAttrs(p)
|
|
236
|
+
parameters.push({
|
|
237
|
+
name: pAttrs.Name ?? '',
|
|
238
|
+
type: pAttrs.Type ?? '',
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return boundTo !== undefined ? { name, boundTo, parameters } : { name, parameters }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @internal — parse raw CSDL XML into ODataMetadata.
|
|
247
|
+
* Throws FMSODataError for malformed XML.
|
|
248
|
+
*/
|
|
249
|
+
export function parseMetadata(xml: string): ODataMetadata {
|
|
250
|
+
try {
|
|
251
|
+
// Extract Edmx/DataServices/Schema
|
|
252
|
+
const dataServices = elementContent(xml, 'DataServices')
|
|
253
|
+
if (!dataServices) {
|
|
254
|
+
throw new Error('Missing <edmx:DataServices> in metadata')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const schema = findElements(dataServices, 'Schema')[0]
|
|
258
|
+
if (!schema) {
|
|
259
|
+
throw new Error('Missing <Schema> in metadata')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const namespace = getAttr(schema, 'Namespace') ?? ''
|
|
263
|
+
const schemaInner = elementContent(schema, 'Schema') ?? schema
|
|
264
|
+
|
|
265
|
+
// Parse EntityTypes
|
|
266
|
+
const entityTypes: EdmEntityType[] = []
|
|
267
|
+
for (const et of findElements(schemaInner, 'EntityType')) {
|
|
268
|
+
entityTypes.push(parseEntityType(et))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Parse EntitySets (inside EntityContainer)
|
|
272
|
+
const entitySets: EdmEntitySet[] = []
|
|
273
|
+
const container = findElements(schemaInner, 'EntityContainer')[0]
|
|
274
|
+
if (container) {
|
|
275
|
+
const containerInner = elementContent(container, 'EntityContainer') ?? container
|
|
276
|
+
for (const es of findElements(containerInner, 'EntitySet')) {
|
|
277
|
+
entitySets.push(parseEntitySet(es))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Parse Actions (FileMaker scripts surface here)
|
|
282
|
+
const actions: EdmAction[] = []
|
|
283
|
+
for (const a of findElements(schemaInner, 'Action')) {
|
|
284
|
+
actions.push(parseAction(a))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Extract server version using the multi-strategy parser from @fms-odata/spec-ts.
|
|
288
|
+
// Handles ProductVersion, ServerVersion (FM 26+), reversed attribute order,
|
|
289
|
+
// and a generic fallback for non-standard annotation terms.
|
|
290
|
+
const serverVersion = extractServerVersion(xml)
|
|
291
|
+
|
|
292
|
+
const result: ODataMetadata = {
|
|
293
|
+
namespace,
|
|
294
|
+
entityTypes,
|
|
295
|
+
entitySets,
|
|
296
|
+
actions,
|
|
297
|
+
raw: xml,
|
|
298
|
+
}
|
|
299
|
+
if (serverVersion) {
|
|
300
|
+
result.serverVersion = serverVersion
|
|
301
|
+
// Keep productVersion for backward compatibility (raw string only)
|
|
302
|
+
result.productVersion = serverVersion.raw
|
|
303
|
+
}
|
|
304
|
+
return result
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
307
|
+
throw new FMSODataError(`Failed to parse $metadata: ${message}`, {
|
|
308
|
+
status: 0,
|
|
309
|
+
odataError: xml.slice(0, 500),
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @internal — fetch raw $metadata XML.
|
|
316
|
+
*/
|
|
317
|
+
export async function fetchMetadataXml(
|
|
318
|
+
ctx: HttpClientContext,
|
|
319
|
+
baseUrl: string,
|
|
320
|
+
opts: RequestOptions = {},
|
|
321
|
+
): Promise<string> {
|
|
322
|
+
const res = await executeRequest(ctx, `${baseUrl}/$metadata`, {
|
|
323
|
+
method: 'GET',
|
|
324
|
+
accept: 'xml',
|
|
325
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
326
|
+
})
|
|
327
|
+
return res.text()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Metadata fetcher bound to a client. Caches by default; pass `refresh: true` to bypass.
|
|
332
|
+
* @internal — exported for testing; use via `FMSOData#metadata()`.
|
|
333
|
+
*/
|
|
334
|
+
export class MetadataFetcher {
|
|
335
|
+
private _cache: Promise<ODataMetadata> | undefined
|
|
336
|
+
|
|
337
|
+
constructor(
|
|
338
|
+
private _ctx: HttpClientContext,
|
|
339
|
+
private _baseUrl: string,
|
|
340
|
+
) {}
|
|
341
|
+
|
|
342
|
+
async fetchXml(opts: RequestOptions = {}): Promise<string> {
|
|
343
|
+
return fetchMetadataXml(this._ctx, this._baseUrl, opts)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async fetch(opts: MetadataOptions = {}): Promise<ODataMetadata> {
|
|
347
|
+
if (!opts.refresh && this._cache) {
|
|
348
|
+
return this._cache
|
|
349
|
+
}
|
|
350
|
+
const promise = this.fetchXml(opts).then(parseMetadata)
|
|
351
|
+
this._cache = promise
|
|
352
|
+
return promise
|
|
353
|
+
}
|
|
354
|
+
}
|