bsuir-iis-api 0.9.0 → 0.10.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 +34 -8
- package/README.md +9 -2
- package/dist/_tsup-dts-rollup.d.ts +285 -106
- package/dist/index.js +239 -210
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
package/dist/index.js
CHANGED
|
@@ -177,25 +177,7 @@ function isAbortError(error) {
|
|
|
177
177
|
return false;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
// src/client/http.ts
|
|
181
|
-
var RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
182
|
-
function buildUrl(baseUrl, path, query) {
|
|
183
|
-
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
184
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
185
|
-
const url = new URL(`${normalizedBase}${normalizedPath}`);
|
|
186
|
-
if (query) {
|
|
187
|
-
for (const [key, value] of Object.entries(query)) {
|
|
188
|
-
if (value === void 0 || value === null) {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
url.searchParams.set(key, String(value));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return url.toString();
|
|
195
|
-
}
|
|
196
|
-
function sleep(ms) {
|
|
197
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
-
}
|
|
180
|
+
// src/client/http/cache.ts
|
|
199
181
|
function tryReadCache(config, key) {
|
|
200
182
|
const entry = config.responseCache.get(key);
|
|
201
183
|
if (!entry) {
|
|
@@ -213,6 +195,7 @@ function setCache(config, key, value) {
|
|
|
213
195
|
return;
|
|
214
196
|
}
|
|
215
197
|
const now = Date.now();
|
|
198
|
+
config.responseCache.delete(key);
|
|
216
199
|
config.responseCache.set(key, {
|
|
217
200
|
value,
|
|
218
201
|
expiresAt: now + config.cacheTtlMs,
|
|
@@ -227,22 +210,93 @@ function setCache(config, key, value) {
|
|
|
227
210
|
config.responseCache.delete(k);
|
|
228
211
|
}
|
|
229
212
|
}
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
213
|
+
if (config.responseCache.size > config.cacheMaxEntries) {
|
|
214
|
+
const byLeastRecentlyUsed = [...config.responseCache.entries()].sort(
|
|
215
|
+
(a, b) => a[1].accessedAt - b[1].accessedAt
|
|
216
|
+
);
|
|
217
|
+
for (const [k] of byLeastRecentlyUsed) {
|
|
218
|
+
if (config.responseCache.size <= config.cacheMaxEntries) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
config.responseCache.delete(k);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/client/http/response.ts
|
|
227
|
+
async function readBodyTextWithLimit(response, maxResponseBytes) {
|
|
228
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
229
|
+
if (contentLengthHeader) {
|
|
230
|
+
const parsedLength = Number(contentLengthHeader);
|
|
231
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxResponseBytes) {
|
|
232
|
+
throw new BsuirApiError(
|
|
233
|
+
`Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
|
|
234
|
+
response.status,
|
|
235
|
+
response.url,
|
|
236
|
+
null
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
241
|
+
const fallbackText = await response.text();
|
|
242
|
+
if (new TextEncoder().encode(fallbackText).byteLength > maxResponseBytes) {
|
|
243
|
+
throw new BsuirApiError(
|
|
244
|
+
`Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
|
|
245
|
+
response.status,
|
|
246
|
+
response.url,
|
|
247
|
+
null
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return fallbackText;
|
|
251
|
+
}
|
|
252
|
+
const reader = response.body.getReader();
|
|
253
|
+
const decoder = new TextDecoder();
|
|
254
|
+
let bytesRead = 0;
|
|
255
|
+
let text = "";
|
|
256
|
+
for (; ; ) {
|
|
257
|
+
const { done, value } = await reader.read();
|
|
258
|
+
if (done) {
|
|
233
259
|
break;
|
|
234
260
|
}
|
|
235
|
-
|
|
261
|
+
bytesRead += value.byteLength;
|
|
262
|
+
if (bytesRead > maxResponseBytes) {
|
|
263
|
+
await reader.cancel();
|
|
264
|
+
throw new BsuirApiError(
|
|
265
|
+
`Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
|
|
266
|
+
response.status,
|
|
267
|
+
response.url,
|
|
268
|
+
null
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
text += decoder.decode(value, { stream: true });
|
|
236
272
|
}
|
|
273
|
+
text += decoder.decode();
|
|
274
|
+
return text;
|
|
237
275
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
276
|
+
async function parseBody(response, maxResponseBytes) {
|
|
277
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
278
|
+
const declaredJson = contentType.includes("application/json");
|
|
279
|
+
const text = await readBodyTextWithLimit(response, maxResponseBytes);
|
|
280
|
+
if (text.length === 0) {
|
|
281
|
+
if (declaredJson) {
|
|
282
|
+
throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
|
|
283
|
+
}
|
|
284
|
+
return "";
|
|
241
285
|
}
|
|
242
|
-
|
|
243
|
-
return
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
if (declaredJson) {
|
|
290
|
+
throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
|
|
291
|
+
}
|
|
292
|
+
return text;
|
|
244
293
|
}
|
|
245
|
-
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/client/http/retry.ts
|
|
297
|
+
var RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
298
|
+
function sleep(ms) {
|
|
299
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
300
|
}
|
|
247
301
|
function parseRetryAfterMs(retryAfter) {
|
|
248
302
|
if (!retryAfter || retryAfter.trim().length === 0) {
|
|
@@ -276,24 +330,32 @@ function getRetryDelayMs(config, attempt, retryAfterHeader) {
|
|
|
276
330
|
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
277
331
|
return Math.floor(baseDelay * jitterFactor);
|
|
278
332
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
333
|
+
|
|
334
|
+
// src/client/http/url.ts
|
|
335
|
+
function buildUrl(baseUrl, path, query) {
|
|
336
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
337
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
338
|
+
const url = new URL(`${normalizedBase}${normalizedPath}`);
|
|
339
|
+
if (query) {
|
|
340
|
+
for (const [key, value] of Object.entries(query)) {
|
|
341
|
+
if (value === void 0 || value === null) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
url.searchParams.set(key, String(value));
|
|
286
345
|
}
|
|
287
|
-
return "";
|
|
288
346
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return
|
|
347
|
+
return url.toString();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/client/http/requestJson.ts
|
|
351
|
+
function combineAbortSignals(first, second) {
|
|
352
|
+
if (!first) {
|
|
353
|
+
return second;
|
|
296
354
|
}
|
|
355
|
+
if (!second) {
|
|
356
|
+
return first;
|
|
357
|
+
}
|
|
358
|
+
return mergeSignals([first, second]);
|
|
297
359
|
}
|
|
298
360
|
function baseHookContext(method, path, endpoint, attempt, maxAttempts, query) {
|
|
299
361
|
return {
|
|
@@ -327,8 +389,9 @@ async function requestJson(config, path, options = {}) {
|
|
|
327
389
|
headers.set("Content-Type", "application/json");
|
|
328
390
|
}
|
|
329
391
|
const cacheKey = endpoint;
|
|
330
|
-
const
|
|
331
|
-
const
|
|
392
|
+
const hasActiveSignal = options.signal != null || config.signal?.aborted === true;
|
|
393
|
+
const canUseCaching = config.cacheTtlMs !== void 0 && method === "GET" && !hasActiveSignal;
|
|
394
|
+
const canUseDedup = config.dedupeInFlight && method === "GET" && !hasActiveSignal;
|
|
332
395
|
if (canUseCaching) {
|
|
333
396
|
const cached = tryReadCache(config, cacheKey);
|
|
334
397
|
if (cached !== void 0) {
|
|
@@ -361,7 +424,7 @@ async function requestJson(config, path, options = {}) {
|
|
|
361
424
|
}
|
|
362
425
|
const response = await config.fetchImpl(endpoint, requestInit);
|
|
363
426
|
if (!response.ok) {
|
|
364
|
-
const errorBody = await parseBody(response);
|
|
427
|
+
const errorBody = await parseBody(response, config.maxResponseBytes);
|
|
365
428
|
if (attempt < maxRetries && RETRIABLE_STATUS_CODES.has(response.status)) {
|
|
366
429
|
const delayMs = getRetryDelayMs(config, attempt, response.headers.get("retry-after"));
|
|
367
430
|
const retryCtx = {
|
|
@@ -388,7 +451,7 @@ async function requestJson(config, path, options = {}) {
|
|
|
388
451
|
config.hooks.onError?.(errorCtx);
|
|
389
452
|
throw apiError;
|
|
390
453
|
}
|
|
391
|
-
const parsed = await parseBody(response);
|
|
454
|
+
const parsed = await parseBody(response, config.maxResponseBytes);
|
|
392
455
|
const responseCtx = {
|
|
393
456
|
...hookCtx,
|
|
394
457
|
status: response.status,
|
|
@@ -590,119 +653,47 @@ function createAnnouncementsModule(config) {
|
|
|
590
653
|
};
|
|
591
654
|
}
|
|
592
655
|
|
|
593
|
-
// src/modules/
|
|
594
|
-
function
|
|
656
|
+
// src/modules/createListModule.ts
|
|
657
|
+
function createListModule(config, endpoint) {
|
|
595
658
|
return {
|
|
596
659
|
/**
|
|
597
|
-
* Returns
|
|
598
|
-
|
|
599
|
-
*
|
|
600
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
601
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
602
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
603
|
-
*/
|
|
660
|
+
* Returns all items from the configured endpoint.
|
|
661
|
+
*/
|
|
604
662
|
async listAll(options = {}) {
|
|
605
|
-
const payload = await requestJson(config,
|
|
663
|
+
const payload = await requestJson(config, endpoint, {
|
|
606
664
|
signal: options.signal
|
|
607
665
|
});
|
|
608
666
|
if (config.validateResponses) {
|
|
609
|
-
assertArrayResponse(payload,
|
|
667
|
+
assertArrayResponse(payload, endpoint);
|
|
610
668
|
}
|
|
611
669
|
return payload;
|
|
612
670
|
}
|
|
613
671
|
};
|
|
614
672
|
}
|
|
615
673
|
|
|
674
|
+
// src/modules/auditories.ts
|
|
675
|
+
function createAuditoriesModule(config) {
|
|
676
|
+
return createListModule(config, "/auditories");
|
|
677
|
+
}
|
|
678
|
+
|
|
616
679
|
// src/modules/departments.ts
|
|
617
680
|
function createDepartmentsModule(config) {
|
|
618
|
-
return
|
|
619
|
-
/**
|
|
620
|
-
* Returns the full list of departments from `/departments`.
|
|
621
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
622
|
-
*
|
|
623
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
624
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
625
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
626
|
-
*/
|
|
627
|
-
async listAll(options = {}) {
|
|
628
|
-
const payload = await requestJson(config, "/departments", {
|
|
629
|
-
signal: options.signal
|
|
630
|
-
});
|
|
631
|
-
if (config.validateResponses) {
|
|
632
|
-
assertArrayResponse(payload, "/departments");
|
|
633
|
-
}
|
|
634
|
-
return payload;
|
|
635
|
-
}
|
|
636
|
-
};
|
|
681
|
+
return createListModule(config, "/departments");
|
|
637
682
|
}
|
|
638
683
|
|
|
639
684
|
// src/modules/employees.ts
|
|
640
685
|
function createEmployeesModule(config) {
|
|
641
|
-
return
|
|
642
|
-
/**
|
|
643
|
-
* Returns the full list of employees from `/employees/all`.
|
|
644
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
645
|
-
*
|
|
646
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
647
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
648
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
649
|
-
*/
|
|
650
|
-
async listAll(options = {}) {
|
|
651
|
-
const payload = await requestJson(config, "/employees/all", {
|
|
652
|
-
signal: options.signal
|
|
653
|
-
});
|
|
654
|
-
if (config.validateResponses) {
|
|
655
|
-
assertArrayResponse(payload, "/employees/all");
|
|
656
|
-
}
|
|
657
|
-
return payload;
|
|
658
|
-
}
|
|
659
|
-
};
|
|
686
|
+
return createListModule(config, "/employees/all");
|
|
660
687
|
}
|
|
661
688
|
|
|
662
689
|
// src/modules/faculties.ts
|
|
663
690
|
function createFacultiesModule(config) {
|
|
664
|
-
return
|
|
665
|
-
/**
|
|
666
|
-
* Returns the full list of faculties from `/faculties`.
|
|
667
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
668
|
-
*
|
|
669
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
670
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
671
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
672
|
-
*/
|
|
673
|
-
async listAll(options = {}) {
|
|
674
|
-
const payload = await requestJson(config, "/faculties", {
|
|
675
|
-
signal: options.signal
|
|
676
|
-
});
|
|
677
|
-
if (config.validateResponses) {
|
|
678
|
-
assertArrayResponse(payload, "/faculties");
|
|
679
|
-
}
|
|
680
|
-
return payload;
|
|
681
|
-
}
|
|
682
|
-
};
|
|
691
|
+
return createListModule(config, "/faculties");
|
|
683
692
|
}
|
|
684
693
|
|
|
685
694
|
// src/modules/groups.ts
|
|
686
695
|
function createGroupsModule(config) {
|
|
687
|
-
return
|
|
688
|
-
/**
|
|
689
|
-
* Returns the full list of student groups from `/student-groups`.
|
|
690
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
691
|
-
*
|
|
692
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
693
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
694
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
695
|
-
*/
|
|
696
|
-
async listAll(options = {}) {
|
|
697
|
-
const payload = await requestJson(config, "/student-groups", {
|
|
698
|
-
signal: options.signal
|
|
699
|
-
});
|
|
700
|
-
if (config.validateResponses) {
|
|
701
|
-
assertArrayResponse(payload, "/student-groups");
|
|
702
|
-
}
|
|
703
|
-
return payload;
|
|
704
|
-
}
|
|
705
|
-
};
|
|
696
|
+
return createListModule(config, "/student-groups");
|
|
706
697
|
}
|
|
707
698
|
|
|
708
699
|
// src/utils/week.ts
|
|
@@ -741,7 +732,58 @@ function parseCurrentWeekInternal(payload, depth) {
|
|
|
741
732
|
throw new BsuirValidationError("'currentWeek' response payload must be a positive integer");
|
|
742
733
|
}
|
|
743
734
|
|
|
744
|
-
// src/modules/
|
|
735
|
+
// src/modules/scheduleFilter.ts
|
|
736
|
+
function lessonAuditories(item) {
|
|
737
|
+
const { auditories } = item;
|
|
738
|
+
return Array.isArray(auditories) ? auditories : [];
|
|
739
|
+
}
|
|
740
|
+
function matchesFilter(item, filter) {
|
|
741
|
+
if (filter.source && item.source !== filter.source) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
if (filter.weekday && item.day !== filter.weekday) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
if (typeof filter.weekNumber === "number") {
|
|
748
|
+
if (!Array.isArray(item.weekNumber) || !item.weekNumber.includes(filter.weekNumber)) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (typeof filter.subgroup === "number" && item.numSubgroup !== filter.subgroup) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
if (filter.lessonTypeAbbrev) {
|
|
756
|
+
const types = Array.isArray(filter.lessonTypeAbbrev) ? filter.lessonTypeAbbrev : [filter.lessonTypeAbbrev];
|
|
757
|
+
if (!item.lessonTypeAbbrev || !types.includes(item.lessonTypeAbbrev)) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (filter.subjectQuery) {
|
|
762
|
+
const query = filter.subjectQuery.toLowerCase();
|
|
763
|
+
const haystack = `${item.subject} ${item.subjectFullName} ${item.note ?? ""}`.toLowerCase();
|
|
764
|
+
if (!haystack.includes(query)) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (filter.employeeUrlId) {
|
|
769
|
+
const employeeMatch = item.employees?.some((employee) => employee.urlId === filter.employeeUrlId);
|
|
770
|
+
if (!employeeMatch) {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (filter.auditory && !lessonAuditories(item).includes(filter.auditory)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
function filterLessons(response, filter) {
|
|
780
|
+
if (typeof filter.weekNumber === "number") {
|
|
781
|
+
assertPositiveInt(filter.weekNumber, "filter.weekNumber");
|
|
782
|
+
}
|
|
783
|
+
return response.lessons.filter((item) => matchesFilter(item, filter));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/modules/scheduleNormalize.ts
|
|
745
787
|
var WEEKDAYS = [
|
|
746
788
|
"\u041F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A",
|
|
747
789
|
"\u0412\u0442\u043E\u0440\u043D\u0438\u043A",
|
|
@@ -750,7 +792,7 @@ var WEEKDAYS = [
|
|
|
750
792
|
"\u041F\u044F\u0442\u043D\u0438\u0446\u0430",
|
|
751
793
|
"\u0421\u0443\u0431\u0431\u043E\u0442\u0430"
|
|
752
794
|
];
|
|
753
|
-
function
|
|
795
|
+
function lessonAuditories2(item) {
|
|
754
796
|
const { auditories } = item;
|
|
755
797
|
return Array.isArray(auditories) ? auditories : [];
|
|
756
798
|
}
|
|
@@ -766,7 +808,7 @@ function normalizeSchedule(response) {
|
|
|
766
808
|
for (const day of WEEKDAYS) {
|
|
767
809
|
const dayItems = safeSchedules[day] ?? [];
|
|
768
810
|
const flattenedDayItems = dayItems.map((item) => {
|
|
769
|
-
const auditories =
|
|
811
|
+
const auditories = lessonAuditories2(item);
|
|
770
812
|
return {
|
|
771
813
|
...item,
|
|
772
814
|
auditories,
|
|
@@ -779,12 +821,11 @@ function normalizeSchedule(response) {
|
|
|
779
821
|
scheduleLessons.push(...flattenedDayItems);
|
|
780
822
|
}
|
|
781
823
|
for (const exam of safeExams) {
|
|
782
|
-
const auditories =
|
|
824
|
+
const auditories = lessonAuditories2(exam);
|
|
783
825
|
const flattenedExam = {
|
|
784
826
|
...exam,
|
|
785
827
|
auditories,
|
|
786
828
|
day: null,
|
|
787
|
-
// Exams are not grouped by weekday in API response.
|
|
788
829
|
source: "exams"
|
|
789
830
|
};
|
|
790
831
|
lessons.push(flattenedExam);
|
|
@@ -800,51 +841,8 @@ function normalizeSchedule(response) {
|
|
|
800
841
|
examLessons
|
|
801
842
|
};
|
|
802
843
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return false;
|
|
806
|
-
}
|
|
807
|
-
if (filter.weekday && item.day !== filter.weekday) {
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
if (typeof filter.weekNumber === "number") {
|
|
811
|
-
if (!Array.isArray(item.weekNumber) || !item.weekNumber.includes(filter.weekNumber)) {
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (typeof filter.subgroup === "number" && item.numSubgroup !== filter.subgroup) {
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
if (filter.lessonTypeAbbrev) {
|
|
819
|
-
const types = Array.isArray(filter.lessonTypeAbbrev) ? filter.lessonTypeAbbrev : [filter.lessonTypeAbbrev];
|
|
820
|
-
if (!item.lessonTypeAbbrev || !types.includes(item.lessonTypeAbbrev)) {
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
if (filter.subjectQuery) {
|
|
825
|
-
const query = filter.subjectQuery.toLowerCase();
|
|
826
|
-
const haystack = `${item.subject} ${item.subjectFullName} ${item.note ?? ""}`.toLowerCase();
|
|
827
|
-
if (!haystack.includes(query)) {
|
|
828
|
-
return false;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (filter.employeeUrlId) {
|
|
832
|
-
const employeeMatch = item.employees?.some((employee) => employee.urlId === filter.employeeUrlId);
|
|
833
|
-
if (!employeeMatch) {
|
|
834
|
-
return false;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
if (filter.auditory && !lessonAuditories(item).includes(filter.auditory)) {
|
|
838
|
-
return false;
|
|
839
|
-
}
|
|
840
|
-
return true;
|
|
841
|
-
}
|
|
842
|
-
function filterLessons(response, filter) {
|
|
843
|
-
if (typeof filter.weekNumber === "number") {
|
|
844
|
-
assertPositiveInt(filter.weekNumber, "filter.weekNumber");
|
|
845
|
-
}
|
|
846
|
-
return response.lessons.filter((item) => matchesFilter(item, filter));
|
|
847
|
-
}
|
|
844
|
+
|
|
845
|
+
// src/modules/scheduleApi.ts
|
|
848
846
|
function createScheduleModule(config) {
|
|
849
847
|
async function getGroup(groupNumber, options = {}) {
|
|
850
848
|
assertGroupNumber(groupNumber, "groupNumber");
|
|
@@ -889,24 +887,35 @@ function createScheduleModule(config) {
|
|
|
889
887
|
getEmployee,
|
|
890
888
|
getGroupFiltered,
|
|
891
889
|
getEmployeeFiltered,
|
|
890
|
+
/**
|
|
891
|
+
* Returns exams for a group.
|
|
892
|
+
*/
|
|
892
893
|
async getGroupExams(groupNumber, options = {}) {
|
|
893
894
|
return getGroupFiltered(groupNumber, { source: "exams" }, options);
|
|
894
895
|
},
|
|
896
|
+
/**
|
|
897
|
+
* Returns exams for an employee.
|
|
898
|
+
*/
|
|
895
899
|
async getEmployeeExams(urlId, options = {}) {
|
|
896
900
|
return getEmployeeFiltered(urlId, { source: "exams" }, options);
|
|
897
901
|
},
|
|
902
|
+
/**
|
|
903
|
+
* Returns regular schedule lessons of a specific subgroup for a group.
|
|
904
|
+
*/
|
|
898
905
|
async getGroupBySubgroup(groupNumber, subgroup, options = {}) {
|
|
899
906
|
assertPositiveInt(subgroup, "subgroup");
|
|
900
907
|
return getGroupFiltered(groupNumber, { source: "schedules", subgroup }, options);
|
|
901
908
|
},
|
|
909
|
+
/**
|
|
910
|
+
* Returns regular schedule lessons of a specific subgroup for an employee.
|
|
911
|
+
*/
|
|
902
912
|
async getEmployeeBySubgroup(urlId, subgroup, options = {}) {
|
|
903
913
|
assertPositiveInt(subgroup, "subgroup");
|
|
904
914
|
return getEmployeeFiltered(urlId, { source: "schedules", subgroup }, options);
|
|
905
915
|
},
|
|
906
916
|
getCurrentWeek,
|
|
907
917
|
/**
|
|
908
|
-
* Calls IIS `/last-update-date/student-group`.
|
|
909
|
-
* it may return an error for newer group numbers (e.g. six-digit `524404`).
|
|
918
|
+
* Calls IIS `/last-update-date/student-group`.
|
|
910
919
|
*/
|
|
911
920
|
async getLastUpdateByGroup(params, options = {}) {
|
|
912
921
|
let query;
|
|
@@ -927,8 +936,7 @@ function createScheduleModule(config) {
|
|
|
927
936
|
return payload;
|
|
928
937
|
},
|
|
929
938
|
/**
|
|
930
|
-
* Calls IIS `/last-update-date/employee`.
|
|
931
|
-
* not relying on it for critical cache logic.
|
|
939
|
+
* Calls IIS `/last-update-date/employee`.
|
|
932
940
|
*/
|
|
933
941
|
async getLastUpdateByEmployee(params, options = {}) {
|
|
934
942
|
let query;
|
|
@@ -953,29 +961,13 @@ function createScheduleModule(config) {
|
|
|
953
961
|
|
|
954
962
|
// src/modules/specialities.ts
|
|
955
963
|
function createSpecialitiesModule(config) {
|
|
956
|
-
return
|
|
957
|
-
/**
|
|
958
|
-
* Returns the full list of specialities from `/specialities`.
|
|
959
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
960
|
-
*
|
|
961
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
962
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
963
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
964
|
-
*/
|
|
965
|
-
async listAll(options = {}) {
|
|
966
|
-
const payload = await requestJson(config, "/specialities", {
|
|
967
|
-
signal: options.signal
|
|
968
|
-
});
|
|
969
|
-
if (config.validateResponses) {
|
|
970
|
-
assertArrayResponse(payload, "/specialities");
|
|
971
|
-
}
|
|
972
|
-
return payload;
|
|
973
|
-
}
|
|
974
|
-
};
|
|
964
|
+
return createListModule(config, "/specialities");
|
|
975
965
|
}
|
|
976
966
|
|
|
977
967
|
// src/client/createClient.ts
|
|
978
968
|
var DEFAULT_BASE_URL = "https://iis.bsuir.by/api/v1";
|
|
969
|
+
var DEFAULT_ALLOWED_BASE_URL_HOSTS = ["iis.bsuir.by"];
|
|
970
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 5e6;
|
|
979
971
|
var MAX_TIMEOUT_MS = 3e5;
|
|
980
972
|
function resolveFetch(customFetch) {
|
|
981
973
|
if (customFetch) {
|
|
@@ -999,6 +991,39 @@ function assertIntegerOption(value, name, minInclusive) {
|
|
|
999
991
|
}
|
|
1000
992
|
return value;
|
|
1001
993
|
}
|
|
994
|
+
function normalizeBaseUrl(rawBaseUrl, allowInsecureHttp, allowedHosts) {
|
|
995
|
+
let parsed;
|
|
996
|
+
try {
|
|
997
|
+
parsed = new URL(rawBaseUrl);
|
|
998
|
+
} catch {
|
|
999
|
+
throw new BsuirConfigurationError("'baseUrl' must be a valid absolute URL");
|
|
1000
|
+
}
|
|
1001
|
+
if (parsed.username || parsed.password) {
|
|
1002
|
+
throw new BsuirConfigurationError("'baseUrl' must not include credentials");
|
|
1003
|
+
}
|
|
1004
|
+
if (parsed.search || parsed.hash) {
|
|
1005
|
+
throw new BsuirConfigurationError("'baseUrl' must not include query string or hash");
|
|
1006
|
+
}
|
|
1007
|
+
if (parsed.protocol !== "https:" && !(allowInsecureHttp && parsed.protocol === "http:")) {
|
|
1008
|
+
throw new BsuirConfigurationError(
|
|
1009
|
+
"'baseUrl' must use HTTPS. Set 'allowInsecureHttp: true' only for trusted local/test endpoints."
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
const normalizedAllowedHosts = new Set(
|
|
1013
|
+
allowedHosts.map((host2) => host2.trim().toLowerCase()).filter((host2) => host2.length > 0)
|
|
1014
|
+
);
|
|
1015
|
+
if (normalizedAllowedHosts.size === 0) {
|
|
1016
|
+
throw new BsuirConfigurationError("'allowedBaseUrlHosts' must contain at least one hostname");
|
|
1017
|
+
}
|
|
1018
|
+
const host = parsed.hostname.toLowerCase();
|
|
1019
|
+
if (!normalizedAllowedHosts.has(host)) {
|
|
1020
|
+
throw new BsuirConfigurationError(
|
|
1021
|
+
`'baseUrl' host '${host}' is not allowed. Allowed hosts: ${[...normalizedAllowedHosts].join(", ")}`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, "");
|
|
1025
|
+
return `${parsed.origin}${normalizedPath}`;
|
|
1026
|
+
}
|
|
1002
1027
|
function createInternalConfig(options) {
|
|
1003
1028
|
const timeoutMs = assertIntegerOption(options.timeoutMs, "timeoutMs", 1) ?? 1e4;
|
|
1004
1029
|
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
@@ -1011,13 +1036,16 @@ function createInternalConfig(options) {
|
|
|
1011
1036
|
const retryMaxDelayMs = assertIntegerOption(options.retryMaxDelayMs, "retryMaxDelayMs", 0) ?? 3e3;
|
|
1012
1037
|
const cacheTtlMs = assertIntegerOption(options.cache?.ttlMs, "cache.ttlMs", 1);
|
|
1013
1038
|
const cacheMaxEntries = assertIntegerOption(options.cache?.maxEntries, "cache.maxEntries", 1) ?? 200;
|
|
1039
|
+
const maxResponseBytes = assertIntegerOption(options.maxResponseBytes, "maxResponseBytes", 1) ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
1040
|
+
const allowInsecureHttp = options.allowInsecureHttp ?? false;
|
|
1041
|
+
const allowedBaseUrlHosts = options.allowedBaseUrlHosts ?? DEFAULT_ALLOWED_BASE_URL_HOSTS;
|
|
1014
1042
|
if (retryDelayMs > retryMaxDelayMs) {
|
|
1015
1043
|
throw new BsuirConfigurationError(
|
|
1016
1044
|
"'retryDelayMs' must be less than or equal to 'retryMaxDelayMs'"
|
|
1017
1045
|
);
|
|
1018
1046
|
}
|
|
1019
1047
|
return {
|
|
1020
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
1048
|
+
baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL, allowInsecureHttp, allowedBaseUrlHosts),
|
|
1021
1049
|
fetchImpl: resolveFetch(options.fetch),
|
|
1022
1050
|
signal: options.signal,
|
|
1023
1051
|
timeoutMs,
|
|
@@ -1029,7 +1057,8 @@ function createInternalConfig(options) {
|
|
|
1029
1057
|
cacheTtlMs,
|
|
1030
1058
|
cacheMaxEntries,
|
|
1031
1059
|
dedupeInFlight: options.dedupeInFlight ?? true,
|
|
1032
|
-
|
|
1060
|
+
maxResponseBytes,
|
|
1061
|
+
validateResponses: options.validateResponses ?? true,
|
|
1033
1062
|
hooks: options.hooks ?? {},
|
|
1034
1063
|
responseCache: /* @__PURE__ */ new Map(),
|
|
1035
1064
|
inFlightRequests: /* @__PURE__ */ new Map(),
|