arxiv-api-wrapper 1.1.1 → 2.0.1
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/README.md +69 -1
- package/package.json +2 -2
- package/src/index.ts +45 -0
- package/src/oaiClient.ts +475 -0
- package/src/oaiParser.ts +264 -0
- package/src/oaiToArxiv.ts +204 -0
- package/src/oaiTypes.ts +260 -0
- package/tests/oai.integration.test.ts +247 -0
- package/tests/oai.test.ts +296 -0
- package/tests/oaiToArxiv.test.ts +131 -0
package/src/oaiTypes.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAI-PMH types for the arXiv OAI interface.
|
|
3
|
+
* @see https://info.arxiv.org/help/oa/index.html#open-archives-initiative-oai
|
|
4
|
+
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ArxivRateLimitConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
/** OAI-PMH error codes. */
|
|
10
|
+
export type OaiErrorCode =
|
|
11
|
+
| 'badArgument'
|
|
12
|
+
| 'badResumptionToken'
|
|
13
|
+
| 'badVerb'
|
|
14
|
+
| 'cannotDisseminateFormat'
|
|
15
|
+
| 'idDoesNotExist'
|
|
16
|
+
| 'noMetadataFormats'
|
|
17
|
+
| 'noRecordsMatch'
|
|
18
|
+
| 'noSetHierarchy';
|
|
19
|
+
|
|
20
|
+
/** Options shared by all OAI request functions. */
|
|
21
|
+
export interface OaiRequestOptions {
|
|
22
|
+
/** Request timeout in milliseconds (default: 10000). */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
/** Number of retry attempts for failed requests (default: 3). */
|
|
25
|
+
retries?: number;
|
|
26
|
+
/** Custom User-Agent header for requests. */
|
|
27
|
+
userAgent?: string;
|
|
28
|
+
/** Rate limiting configuration. */
|
|
29
|
+
rateLimit?: ArxivRateLimitConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Response to the Identify verb. */
|
|
33
|
+
export interface OaiIdentifyResponse {
|
|
34
|
+
repositoryName: string;
|
|
35
|
+
baseURL: string;
|
|
36
|
+
protocolVersion: string;
|
|
37
|
+
adminEmail: string[];
|
|
38
|
+
earliestDatestamp: string;
|
|
39
|
+
deletedRecord: 'no' | 'persistent' | 'transient';
|
|
40
|
+
granularity: 'YYYY-MM-DD' | 'YYYY-MM-DDThh:mm:ssZ';
|
|
41
|
+
compression?: string[];
|
|
42
|
+
description?: unknown[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A metadata format from ListMetadataFormats. */
|
|
46
|
+
export interface OaiMetadataFormat {
|
|
47
|
+
metadataPrefix: OaiMetadataPrefix;
|
|
48
|
+
schema: string;
|
|
49
|
+
metadataNamespace: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A set from ListSets (for selective harvesting). */
|
|
53
|
+
export interface OaiSet {
|
|
54
|
+
setSpec: string;
|
|
55
|
+
setName: string;
|
|
56
|
+
setDescription?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resumption token for paginated list responses. */
|
|
60
|
+
export interface OaiResumptionToken {
|
|
61
|
+
/** Opaque token value to pass to the next request. */
|
|
62
|
+
value: string;
|
|
63
|
+
/** When the token expires (UTC). */
|
|
64
|
+
expirationDate?: string;
|
|
65
|
+
/** Total size of the complete list (may be approximate). */
|
|
66
|
+
completeListSize?: number;
|
|
67
|
+
/** Cursor position (number of elements returned so far). */
|
|
68
|
+
cursor?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Record header (identifier, datestamp, setSpecs, optional deleted status). */
|
|
72
|
+
export interface OaiHeader {
|
|
73
|
+
identifier: string;
|
|
74
|
+
datestamp: string;
|
|
75
|
+
setSpec: string[];
|
|
76
|
+
/** Present and 'deleted' when the record has been withdrawn. */
|
|
77
|
+
status?: 'deleted';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** arXiv OAI metadata prefixes supported by the repository. */
|
|
81
|
+
export type OaiMetadataPrefix = 'oai_dc' | 'arXiv' | 'arXivOld' | 'arXivRaw';
|
|
82
|
+
|
|
83
|
+
type OneOrMany<T> = T | T[];
|
|
84
|
+
|
|
85
|
+
/** oai_dc metadata (Dublin Core). */
|
|
86
|
+
export interface OaiDcMetadata {
|
|
87
|
+
dc: {
|
|
88
|
+
title?: OneOrMany<string>;
|
|
89
|
+
creator?: OneOrMany<string>;
|
|
90
|
+
subject?: OneOrMany<string>;
|
|
91
|
+
description?: OneOrMany<string>;
|
|
92
|
+
publisher?: OneOrMany<string>;
|
|
93
|
+
contributor?: OneOrMany<string>;
|
|
94
|
+
date?: OneOrMany<string>;
|
|
95
|
+
type?: OneOrMany<string>;
|
|
96
|
+
format?: OneOrMany<string>;
|
|
97
|
+
identifier?: OneOrMany<string>;
|
|
98
|
+
source?: OneOrMany<string>;
|
|
99
|
+
language?: OneOrMany<string>;
|
|
100
|
+
relation?: OneOrMany<string>;
|
|
101
|
+
coverage?: OneOrMany<string>;
|
|
102
|
+
rights?: OneOrMany<string>;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** arXiv author in the arXiv metadata format. */
|
|
107
|
+
export interface OaiArxivAuthor {
|
|
108
|
+
keyname: string;
|
|
109
|
+
forenames?: string;
|
|
110
|
+
suffix?: string;
|
|
111
|
+
affiliation?: OneOrMany<string>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** arXiv metadata (latest-version focused metadata). */
|
|
115
|
+
export interface OaiArxivMetadata {
|
|
116
|
+
arXiv: {
|
|
117
|
+
id: string;
|
|
118
|
+
created?: string;
|
|
119
|
+
updated?: string;
|
|
120
|
+
authors?: {
|
|
121
|
+
author: OneOrMany<OaiArxivAuthor>;
|
|
122
|
+
};
|
|
123
|
+
title?: string;
|
|
124
|
+
'msc-class'?: string;
|
|
125
|
+
'acm-class'?: string;
|
|
126
|
+
'report-no'?: string;
|
|
127
|
+
'journal-ref'?: string;
|
|
128
|
+
comments?: string;
|
|
129
|
+
abstract?: string;
|
|
130
|
+
categories?: string;
|
|
131
|
+
doi?: string;
|
|
132
|
+
proxy?: string;
|
|
133
|
+
license?: string;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** arXivOld metadata (legacy arXiv internal format). */
|
|
138
|
+
export interface OaiArxivOldMetadata {
|
|
139
|
+
arXivOld: {
|
|
140
|
+
id: string;
|
|
141
|
+
title?: string;
|
|
142
|
+
authors?: string;
|
|
143
|
+
categories?: string;
|
|
144
|
+
comments?: string;
|
|
145
|
+
proxy?: string;
|
|
146
|
+
'report-no'?: string;
|
|
147
|
+
'msc-class'?: string;
|
|
148
|
+
'acm-class'?: string;
|
|
149
|
+
'journal-ref'?: string;
|
|
150
|
+
doi?: string;
|
|
151
|
+
abstract?: string;
|
|
152
|
+
license?: string;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Version entry in arXivRaw metadata. */
|
|
157
|
+
export interface OaiArxivRawVersion {
|
|
158
|
+
version?: string;
|
|
159
|
+
date: string;
|
|
160
|
+
size?: string;
|
|
161
|
+
source_type?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** arXivRaw metadata (close to arXiv internal metadata with version history). */
|
|
165
|
+
export interface OaiArxivRawMetadata {
|
|
166
|
+
arXivRaw: {
|
|
167
|
+
id: string;
|
|
168
|
+
submitter: string;
|
|
169
|
+
version: OneOrMany<OaiArxivRawVersion>;
|
|
170
|
+
title?: string;
|
|
171
|
+
authors?: string;
|
|
172
|
+
categories: string;
|
|
173
|
+
comments?: string;
|
|
174
|
+
proxy?: string;
|
|
175
|
+
'report-no'?: string;
|
|
176
|
+
'acm-class'?: string;
|
|
177
|
+
'msc-class'?: string;
|
|
178
|
+
'journal-ref'?: string;
|
|
179
|
+
doi?: string;
|
|
180
|
+
license?: string;
|
|
181
|
+
abstract?: string;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Mapping from metadataPrefix to metadata payload shape. */
|
|
186
|
+
export interface OaiMetadataByPrefix {
|
|
187
|
+
oai_dc: OaiDcMetadata;
|
|
188
|
+
arXiv: OaiArxivMetadata;
|
|
189
|
+
arXivOld: OaiArxivOldMetadata;
|
|
190
|
+
arXivRaw: OaiArxivRawMetadata;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Metadata part of a record (format-dependent: oai_dc, arXiv, arXivOld, arXivRaw). */
|
|
194
|
+
export type OaiMetadata = OaiMetadataByPrefix[keyof OaiMetadataByPrefix];
|
|
195
|
+
|
|
196
|
+
/** A single OAI record (header + optional metadata and about). */
|
|
197
|
+
export interface OaiRecord {
|
|
198
|
+
header: OaiHeader;
|
|
199
|
+
/** Omitted for deleted records. */
|
|
200
|
+
metadata?: OaiMetadata;
|
|
201
|
+
/** Optional about containers (e.g. provenance, rights). */
|
|
202
|
+
about?: unknown[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Result of ListIdentifiers (headers + optional resumption). */
|
|
206
|
+
export interface OaiListIdentifiersResult {
|
|
207
|
+
headers: OaiHeader[];
|
|
208
|
+
resumptionToken?: OaiResumptionToken;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Result of ListRecords (records + optional resumption). */
|
|
212
|
+
export interface OaiListRecordsResult {
|
|
213
|
+
records: OaiRecord[];
|
|
214
|
+
resumptionToken?: OaiResumptionToken;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Result of ListSets (sets + optional resumption). */
|
|
218
|
+
export interface OaiListSetsResult {
|
|
219
|
+
sets: OaiSet[];
|
|
220
|
+
resumptionToken?: OaiResumptionToken;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type OaiSelectiveHarvestOptions = {
|
|
224
|
+
/** Lower bound for datestamp-based selective harvesting (UTC). */
|
|
225
|
+
from?: string;
|
|
226
|
+
/** Upper bound for datestamp-based selective harvesting (UTC). */
|
|
227
|
+
until?: string;
|
|
228
|
+
/** Set spec for selective harvesting (e.g. cs:cs:AI, physics:hep-th). */
|
|
229
|
+
set?: string;
|
|
230
|
+
/** Resumption token must not be provided for an initial selective request. */
|
|
231
|
+
resumptionToken?: undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
type OaiResumptionTokenOnlyOptions = {
|
|
235
|
+
/** Resumption token from a previous incomplete list response. */
|
|
236
|
+
resumptionToken: string;
|
|
237
|
+
/** Selective harvesting parameters are not allowed together with resumptionToken. */
|
|
238
|
+
from?: never;
|
|
239
|
+
until?: never;
|
|
240
|
+
set?: never;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Options for ListIdentifiers and ListRecords. */
|
|
244
|
+
export type OaiListOptions = OaiRequestOptions &
|
|
245
|
+
(OaiSelectiveHarvestOptions | OaiResumptionTokenOnlyOptions);
|
|
246
|
+
|
|
247
|
+
/** Error thrown when the OAI repository returns an error element. */
|
|
248
|
+
export class OaiError extends Error {
|
|
249
|
+
readonly code: OaiErrorCode;
|
|
250
|
+
readonly messageText: string;
|
|
251
|
+
|
|
252
|
+
constructor(code: OaiErrorCode, messageText: string = '') {
|
|
253
|
+
const msg = messageText ? `${code}: ${messageText}` : code;
|
|
254
|
+
super(msg);
|
|
255
|
+
this.name = 'OaiError';
|
|
256
|
+
this.code = code;
|
|
257
|
+
this.messageText = messageText;
|
|
258
|
+
Object.setPrototypeOf(this, OaiError.prototype);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the arXiv OAI-PMH interface (real HTTP calls).
|
|
3
|
+
* Conservative request size and rate; same pattern as arxivAPI.integration.test.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
oaiIdentify,
|
|
8
|
+
oaiListRecords,
|
|
9
|
+
oaiListRecordsAsyncIterator,
|
|
10
|
+
oaiListRecordsAll,
|
|
11
|
+
oaiListIdentifiersAsyncIterator,
|
|
12
|
+
oaiListIdentifiersAll,
|
|
13
|
+
oaiListSetsAsyncIterator,
|
|
14
|
+
oaiListSetsAll,
|
|
15
|
+
} from '../src/oaiClient.js';
|
|
16
|
+
import { OaiError } from '../src/oaiTypes.js';
|
|
17
|
+
|
|
18
|
+
const OAI_OPTIONS = {
|
|
19
|
+
timeoutMs: 15000,
|
|
20
|
+
retries: 2,
|
|
21
|
+
rateLimit: { tokensPerInterval: 1, intervalMs: 1000 },
|
|
22
|
+
userAgent: 'arxiv-api-wrapper-tests/1.0',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('OAI-PMH integration', () => {
|
|
26
|
+
it('oaiIdentify returns repository info and protocol version 2.0', async () => {
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = await oaiIdentify(OAI_OPTIONS);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('oaiIdentify failed:', error);
|
|
32
|
+
throw new Error(
|
|
33
|
+
`OAI Identify failed: ${error instanceof Error ? error.message : String(error)}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
expect(result.repositoryName).toBeTruthy();
|
|
37
|
+
expect(result.baseURL).toContain('oaipmh.arxiv.org');
|
|
38
|
+
expect(result.protocolVersion).toBe('2.0');
|
|
39
|
+
expect(Array.isArray(result.adminEmail)).toBe(true);
|
|
40
|
+
expect(result.earliestDatestamp).toBeTruthy();
|
|
41
|
+
}, 30000);
|
|
42
|
+
|
|
43
|
+
it('oaiListRecords returns one page of records with header and metadata', async () => {
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
result = await oaiListRecords('oai_dc', {
|
|
47
|
+
...OAI_OPTIONS,
|
|
48
|
+
from: '2024-01-01',
|
|
49
|
+
until: '2024-01-02',
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('oaiListRecords failed:', error);
|
|
53
|
+
throw new Error(
|
|
54
|
+
`OAI ListRecords failed: ${error instanceof Error ? error.message : String(error)}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
expect(Array.isArray(result.records)).toBe(true);
|
|
58
|
+
if (result.records.length > 0) {
|
|
59
|
+
const rec = result.records[0];
|
|
60
|
+
expect(rec.header).toBeTruthy();
|
|
61
|
+
expect(rec.header.identifier).toBeTruthy();
|
|
62
|
+
expect(rec.header.datestamp).toBeTruthy();
|
|
63
|
+
expect(rec.metadata).toBeDefined();
|
|
64
|
+
expect(typeof rec.metadata).toBe('object');
|
|
65
|
+
}
|
|
66
|
+
// May or may not have resumptionToken depending on result size
|
|
67
|
+
if (result.resumptionToken) {
|
|
68
|
+
expect(result.resumptionToken.value).toBeTruthy();
|
|
69
|
+
}
|
|
70
|
+
}, 30000);
|
|
71
|
+
|
|
72
|
+
it('oaiListRecords continuation requests work with resumptionToken-only options', async () => {
|
|
73
|
+
const firstPage = await oaiListRecords('oai_dc', OAI_OPTIONS);
|
|
74
|
+
expect(firstPage.resumptionToken?.value).toBeTruthy();
|
|
75
|
+
|
|
76
|
+
const secondPage = await oaiListRecords('oai_dc', {
|
|
77
|
+
...OAI_OPTIONS,
|
|
78
|
+
resumptionToken: firstPage.resumptionToken!.value,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(Array.isArray(secondPage.records)).toBe(true);
|
|
82
|
+
}, 30000);
|
|
83
|
+
|
|
84
|
+
it('throws a local OaiError for resumptionToken combined with selective params', async () => {
|
|
85
|
+
const invalidOptions = {
|
|
86
|
+
...OAI_OPTIONS,
|
|
87
|
+
from: '2024-01-01',
|
|
88
|
+
resumptionToken: 'fake-token',
|
|
89
|
+
} as unknown as Parameters<typeof oaiListRecords>[1];
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
oaiListRecords('oai_dc', invalidOptions)
|
|
93
|
+
).rejects.toBeInstanceOf(OaiError);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('oaiListRecordsAll returns records across all pages within a small date range', async () => {
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await oaiListRecordsAll('oai_dc', {
|
|
100
|
+
...OAI_OPTIONS,
|
|
101
|
+
from: '2024-01-01',
|
|
102
|
+
until: '2024-01-02',
|
|
103
|
+
maxRecords: 200,
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('oaiListRecordsAll failed:', error);
|
|
107
|
+
throw new Error(
|
|
108
|
+
`OAI ListRecordsAll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(Array.isArray(result.records)).toBe(true);
|
|
113
|
+
if (result.records.length > 0) {
|
|
114
|
+
const rec = result.records[0];
|
|
115
|
+
expect(rec.header).toBeTruthy();
|
|
116
|
+
expect(rec.header.identifier).toBeTruthy();
|
|
117
|
+
expect(rec.header.datestamp).toBeTruthy();
|
|
118
|
+
expect(rec.metadata).toBeDefined();
|
|
119
|
+
expect(typeof rec.metadata).toBe('object');
|
|
120
|
+
}
|
|
121
|
+
}, 30000);
|
|
122
|
+
|
|
123
|
+
it('oaiListRecordsAsyncIterator yields records and matches oaiListRecordsAll count for the same cap', async () => {
|
|
124
|
+
let iteratedRecords: unknown[] = [];
|
|
125
|
+
let allResult;
|
|
126
|
+
try {
|
|
127
|
+
const maxRecords = 25;
|
|
128
|
+
for await (const record of oaiListRecordsAsyncIterator('oai_dc', {
|
|
129
|
+
...OAI_OPTIONS,
|
|
130
|
+
from: '2024-01-01',
|
|
131
|
+
until: '2024-01-02',
|
|
132
|
+
maxRecords,
|
|
133
|
+
})) {
|
|
134
|
+
iteratedRecords.push(record);
|
|
135
|
+
}
|
|
136
|
+
allResult = await oaiListRecordsAll('oai_dc', {
|
|
137
|
+
...OAI_OPTIONS,
|
|
138
|
+
from: '2024-01-01',
|
|
139
|
+
until: '2024-01-02',
|
|
140
|
+
maxRecords,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('oaiListRecordsAsyncIterator failed:', error);
|
|
144
|
+
throw new Error(
|
|
145
|
+
`OAI ListRecordsAsyncIterator failed: ${error instanceof Error ? error.message : String(error)}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
expect(Array.isArray(iteratedRecords)).toBe(true);
|
|
150
|
+
expect(iteratedRecords.length).toBeLessThanOrEqual(25);
|
|
151
|
+
expect(iteratedRecords.length).toBe(allResult.records.length);
|
|
152
|
+
}, 30000);
|
|
153
|
+
|
|
154
|
+
it('oaiListIdentifiersAll returns headers across pages with maxHeaders cap', async () => {
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
result = await oaiListIdentifiersAll('oai_dc', {
|
|
158
|
+
...OAI_OPTIONS,
|
|
159
|
+
from: '2024-01-01',
|
|
160
|
+
until: '2024-01-02',
|
|
161
|
+
maxHeaders: 50,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('oaiListIdentifiersAll failed:', error);
|
|
165
|
+
throw new Error(
|
|
166
|
+
`OAI ListIdentifiersAll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
expect(Array.isArray(result.headers)).toBe(true);
|
|
170
|
+
expect(result.headers.length).toBeLessThanOrEqual(50);
|
|
171
|
+
if (result.headers.length > 0) {
|
|
172
|
+
expect(result.headers[0].identifier).toBeTruthy();
|
|
173
|
+
expect(result.headers[0].datestamp).toBeTruthy();
|
|
174
|
+
}
|
|
175
|
+
}, 30000);
|
|
176
|
+
|
|
177
|
+
it('oaiListIdentifiersAsyncIterator yields headers and honors maxHeaders', async () => {
|
|
178
|
+
const headers = [];
|
|
179
|
+
try {
|
|
180
|
+
for await (const header of oaiListIdentifiersAsyncIterator('oai_dc', {
|
|
181
|
+
...OAI_OPTIONS,
|
|
182
|
+
from: '2024-01-01',
|
|
183
|
+
until: '2024-01-02',
|
|
184
|
+
maxHeaders: 20,
|
|
185
|
+
})) {
|
|
186
|
+
headers.push(header);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error('oaiListIdentifiersAsyncIterator failed:', error);
|
|
190
|
+
throw new Error(
|
|
191
|
+
`OAI ListIdentifiersAsyncIterator failed: ${error instanceof Error ? error.message : String(error)}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
expect(Array.isArray(headers)).toBe(true);
|
|
196
|
+
expect(headers.length).toBeLessThanOrEqual(20);
|
|
197
|
+
if (headers.length > 0) {
|
|
198
|
+
expect(headers[0].identifier).toBeTruthy();
|
|
199
|
+
expect(headers[0].datestamp).toBeTruthy();
|
|
200
|
+
}
|
|
201
|
+
}, 30000);
|
|
202
|
+
|
|
203
|
+
it('oaiListSetsAll returns sets with maxSets cap', async () => {
|
|
204
|
+
let result;
|
|
205
|
+
try {
|
|
206
|
+
result = await oaiListSetsAll({
|
|
207
|
+
...OAI_OPTIONS,
|
|
208
|
+
maxSets: 20,
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('oaiListSetsAll failed:', error);
|
|
212
|
+
throw new Error(
|
|
213
|
+
`OAI ListSetsAll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
expect(Array.isArray(result.sets)).toBe(true);
|
|
217
|
+
expect(result.sets.length).toBeLessThanOrEqual(20);
|
|
218
|
+
if (result.sets.length > 0) {
|
|
219
|
+
expect(result.sets[0].setSpec).toBeTruthy();
|
|
220
|
+
expect(result.sets[0].setName).toBeTruthy();
|
|
221
|
+
}
|
|
222
|
+
}, 30000);
|
|
223
|
+
|
|
224
|
+
it('oaiListSetsAsyncIterator yields sets and honors maxSets', async () => {
|
|
225
|
+
const sets = [];
|
|
226
|
+
try {
|
|
227
|
+
for await (const set of oaiListSetsAsyncIterator({
|
|
228
|
+
...OAI_OPTIONS,
|
|
229
|
+
maxSets: 10,
|
|
230
|
+
})) {
|
|
231
|
+
sets.push(set);
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('oaiListSetsAsyncIterator failed:', error);
|
|
235
|
+
throw new Error(
|
|
236
|
+
`OAI ListSetsAsyncIterator failed: ${error instanceof Error ? error.message : String(error)}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(Array.isArray(sets)).toBe(true);
|
|
241
|
+
expect(sets.length).toBeLessThanOrEqual(10);
|
|
242
|
+
if (sets.length > 0) {
|
|
243
|
+
expect(sets[0].setSpec).toBeTruthy();
|
|
244
|
+
expect(sets[0].setName).toBeTruthy();
|
|
245
|
+
}
|
|
246
|
+
}, 30000);
|
|
247
|
+
});
|