bsuir-iis-api 0.9.1 → 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 +24 -0
- package/README.md +9 -2
- package/dist/_tsup-dts-rollup.d.ts +114 -87
- package/dist/index.js +227 -204
- 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) {
|
|
@@ -240,14 +222,81 @@ function setCache(config, key, value) {
|
|
|
240
222
|
}
|
|
241
223
|
}
|
|
242
224
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
239
|
}
|
|
247
|
-
if (!
|
|
248
|
-
|
|
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) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
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 });
|
|
249
272
|
}
|
|
250
|
-
|
|
273
|
+
text += decoder.decode();
|
|
274
|
+
return text;
|
|
275
|
+
}
|
|
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 "";
|
|
285
|
+
}
|
|
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;
|
|
293
|
+
}
|
|
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));
|
|
251
300
|
}
|
|
252
301
|
function parseRetryAfterMs(retryAfter) {
|
|
253
302
|
if (!retryAfter || retryAfter.trim().length === 0) {
|
|
@@ -281,24 +330,32 @@ function getRetryDelayMs(config, attempt, retryAfterHeader) {
|
|
|
281
330
|
const jitterFactor = 0.75 + Math.random() * 0.5;
|
|
282
331
|
return Math.floor(baseDelay * jitterFactor);
|
|
283
332
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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));
|
|
291
345
|
}
|
|
292
|
-
return "";
|
|
293
346
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return
|
|
347
|
+
return url.toString();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/client/http/requestJson.ts
|
|
351
|
+
function combineAbortSignals(first, second) {
|
|
352
|
+
if (!first) {
|
|
353
|
+
return second;
|
|
354
|
+
}
|
|
355
|
+
if (!second) {
|
|
356
|
+
return first;
|
|
301
357
|
}
|
|
358
|
+
return mergeSignals([first, second]);
|
|
302
359
|
}
|
|
303
360
|
function baseHookContext(method, path, endpoint, attempt, maxAttempts, query) {
|
|
304
361
|
return {
|
|
@@ -367,7 +424,7 @@ async function requestJson(config, path, options = {}) {
|
|
|
367
424
|
}
|
|
368
425
|
const response = await config.fetchImpl(endpoint, requestInit);
|
|
369
426
|
if (!response.ok) {
|
|
370
|
-
const errorBody = await parseBody(response);
|
|
427
|
+
const errorBody = await parseBody(response, config.maxResponseBytes);
|
|
371
428
|
if (attempt < maxRetries && RETRIABLE_STATUS_CODES.has(response.status)) {
|
|
372
429
|
const delayMs = getRetryDelayMs(config, attempt, response.headers.get("retry-after"));
|
|
373
430
|
const retryCtx = {
|
|
@@ -394,7 +451,7 @@ async function requestJson(config, path, options = {}) {
|
|
|
394
451
|
config.hooks.onError?.(errorCtx);
|
|
395
452
|
throw apiError;
|
|
396
453
|
}
|
|
397
|
-
const parsed = await parseBody(response);
|
|
454
|
+
const parsed = await parseBody(response, config.maxResponseBytes);
|
|
398
455
|
const responseCtx = {
|
|
399
456
|
...hookCtx,
|
|
400
457
|
status: response.status,
|
|
@@ -596,119 +653,47 @@ function createAnnouncementsModule(config) {
|
|
|
596
653
|
};
|
|
597
654
|
}
|
|
598
655
|
|
|
599
|
-
// src/modules/
|
|
600
|
-
function
|
|
656
|
+
// src/modules/createListModule.ts
|
|
657
|
+
function createListModule(config, endpoint) {
|
|
601
658
|
return {
|
|
602
659
|
/**
|
|
603
|
-
* Returns
|
|
604
|
-
|
|
605
|
-
*
|
|
606
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
607
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
608
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
609
|
-
*/
|
|
660
|
+
* Returns all items from the configured endpoint.
|
|
661
|
+
*/
|
|
610
662
|
async listAll(options = {}) {
|
|
611
|
-
const payload = await requestJson(config,
|
|
663
|
+
const payload = await requestJson(config, endpoint, {
|
|
612
664
|
signal: options.signal
|
|
613
665
|
});
|
|
614
666
|
if (config.validateResponses) {
|
|
615
|
-
assertArrayResponse(payload,
|
|
667
|
+
assertArrayResponse(payload, endpoint);
|
|
616
668
|
}
|
|
617
669
|
return payload;
|
|
618
670
|
}
|
|
619
671
|
};
|
|
620
672
|
}
|
|
621
673
|
|
|
674
|
+
// src/modules/auditories.ts
|
|
675
|
+
function createAuditoriesModule(config) {
|
|
676
|
+
return createListModule(config, "/auditories");
|
|
677
|
+
}
|
|
678
|
+
|
|
622
679
|
// src/modules/departments.ts
|
|
623
680
|
function createDepartmentsModule(config) {
|
|
624
|
-
return
|
|
625
|
-
/**
|
|
626
|
-
* Returns the full list of departments from `/departments`.
|
|
627
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
628
|
-
*
|
|
629
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
630
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
631
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
632
|
-
*/
|
|
633
|
-
async listAll(options = {}) {
|
|
634
|
-
const payload = await requestJson(config, "/departments", {
|
|
635
|
-
signal: options.signal
|
|
636
|
-
});
|
|
637
|
-
if (config.validateResponses) {
|
|
638
|
-
assertArrayResponse(payload, "/departments");
|
|
639
|
-
}
|
|
640
|
-
return payload;
|
|
641
|
-
}
|
|
642
|
-
};
|
|
681
|
+
return createListModule(config, "/departments");
|
|
643
682
|
}
|
|
644
683
|
|
|
645
684
|
// src/modules/employees.ts
|
|
646
685
|
function createEmployeesModule(config) {
|
|
647
|
-
return
|
|
648
|
-
/**
|
|
649
|
-
* Returns the full list of employees from `/employees/all`.
|
|
650
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
651
|
-
*
|
|
652
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
653
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
654
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
655
|
-
*/
|
|
656
|
-
async listAll(options = {}) {
|
|
657
|
-
const payload = await requestJson(config, "/employees/all", {
|
|
658
|
-
signal: options.signal
|
|
659
|
-
});
|
|
660
|
-
if (config.validateResponses) {
|
|
661
|
-
assertArrayResponse(payload, "/employees/all");
|
|
662
|
-
}
|
|
663
|
-
return payload;
|
|
664
|
-
}
|
|
665
|
-
};
|
|
686
|
+
return createListModule(config, "/employees/all");
|
|
666
687
|
}
|
|
667
688
|
|
|
668
689
|
// src/modules/faculties.ts
|
|
669
690
|
function createFacultiesModule(config) {
|
|
670
|
-
return
|
|
671
|
-
/**
|
|
672
|
-
* Returns the full list of faculties from `/faculties`.
|
|
673
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
674
|
-
*
|
|
675
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
676
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
677
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
678
|
-
*/
|
|
679
|
-
async listAll(options = {}) {
|
|
680
|
-
const payload = await requestJson(config, "/faculties", {
|
|
681
|
-
signal: options.signal
|
|
682
|
-
});
|
|
683
|
-
if (config.validateResponses) {
|
|
684
|
-
assertArrayResponse(payload, "/faculties");
|
|
685
|
-
}
|
|
686
|
-
return payload;
|
|
687
|
-
}
|
|
688
|
-
};
|
|
691
|
+
return createListModule(config, "/faculties");
|
|
689
692
|
}
|
|
690
693
|
|
|
691
694
|
// src/modules/groups.ts
|
|
692
695
|
function createGroupsModule(config) {
|
|
693
|
-
return
|
|
694
|
-
/**
|
|
695
|
-
* Returns the full list of student groups from `/student-groups`.
|
|
696
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
697
|
-
*
|
|
698
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
699
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
700
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
701
|
-
*/
|
|
702
|
-
async listAll(options = {}) {
|
|
703
|
-
const payload = await requestJson(config, "/student-groups", {
|
|
704
|
-
signal: options.signal
|
|
705
|
-
});
|
|
706
|
-
if (config.validateResponses) {
|
|
707
|
-
assertArrayResponse(payload, "/student-groups");
|
|
708
|
-
}
|
|
709
|
-
return payload;
|
|
710
|
-
}
|
|
711
|
-
};
|
|
696
|
+
return createListModule(config, "/student-groups");
|
|
712
697
|
}
|
|
713
698
|
|
|
714
699
|
// src/utils/week.ts
|
|
@@ -747,7 +732,58 @@ function parseCurrentWeekInternal(payload, depth) {
|
|
|
747
732
|
throw new BsuirValidationError("'currentWeek' response payload must be a positive integer");
|
|
748
733
|
}
|
|
749
734
|
|
|
750
|
-
// 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
|
|
751
787
|
var WEEKDAYS = [
|
|
752
788
|
"\u041F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A",
|
|
753
789
|
"\u0412\u0442\u043E\u0440\u043D\u0438\u043A",
|
|
@@ -756,7 +792,7 @@ var WEEKDAYS = [
|
|
|
756
792
|
"\u041F\u044F\u0442\u043D\u0438\u0446\u0430",
|
|
757
793
|
"\u0421\u0443\u0431\u0431\u043E\u0442\u0430"
|
|
758
794
|
];
|
|
759
|
-
function
|
|
795
|
+
function lessonAuditories2(item) {
|
|
760
796
|
const { auditories } = item;
|
|
761
797
|
return Array.isArray(auditories) ? auditories : [];
|
|
762
798
|
}
|
|
@@ -772,7 +808,7 @@ function normalizeSchedule(response) {
|
|
|
772
808
|
for (const day of WEEKDAYS) {
|
|
773
809
|
const dayItems = safeSchedules[day] ?? [];
|
|
774
810
|
const flattenedDayItems = dayItems.map((item) => {
|
|
775
|
-
const auditories =
|
|
811
|
+
const auditories = lessonAuditories2(item);
|
|
776
812
|
return {
|
|
777
813
|
...item,
|
|
778
814
|
auditories,
|
|
@@ -785,12 +821,11 @@ function normalizeSchedule(response) {
|
|
|
785
821
|
scheduleLessons.push(...flattenedDayItems);
|
|
786
822
|
}
|
|
787
823
|
for (const exam of safeExams) {
|
|
788
|
-
const auditories =
|
|
824
|
+
const auditories = lessonAuditories2(exam);
|
|
789
825
|
const flattenedExam = {
|
|
790
826
|
...exam,
|
|
791
827
|
auditories,
|
|
792
828
|
day: null,
|
|
793
|
-
// Exams are not grouped by weekday in API response.
|
|
794
829
|
source: "exams"
|
|
795
830
|
};
|
|
796
831
|
lessons.push(flattenedExam);
|
|
@@ -806,51 +841,8 @@ function normalizeSchedule(response) {
|
|
|
806
841
|
examLessons
|
|
807
842
|
};
|
|
808
843
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
return false;
|
|
812
|
-
}
|
|
813
|
-
if (filter.weekday && item.day !== filter.weekday) {
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
if (typeof filter.weekNumber === "number") {
|
|
817
|
-
if (!Array.isArray(item.weekNumber) || !item.weekNumber.includes(filter.weekNumber)) {
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (typeof filter.subgroup === "number" && item.numSubgroup !== filter.subgroup) {
|
|
822
|
-
return false;
|
|
823
|
-
}
|
|
824
|
-
if (filter.lessonTypeAbbrev) {
|
|
825
|
-
const types = Array.isArray(filter.lessonTypeAbbrev) ? filter.lessonTypeAbbrev : [filter.lessonTypeAbbrev];
|
|
826
|
-
if (!item.lessonTypeAbbrev || !types.includes(item.lessonTypeAbbrev)) {
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
if (filter.subjectQuery) {
|
|
831
|
-
const query = filter.subjectQuery.toLowerCase();
|
|
832
|
-
const haystack = `${item.subject} ${item.subjectFullName} ${item.note ?? ""}`.toLowerCase();
|
|
833
|
-
if (!haystack.includes(query)) {
|
|
834
|
-
return false;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
if (filter.employeeUrlId) {
|
|
838
|
-
const employeeMatch = item.employees?.some((employee) => employee.urlId === filter.employeeUrlId);
|
|
839
|
-
if (!employeeMatch) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
if (filter.auditory && !lessonAuditories(item).includes(filter.auditory)) {
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
return true;
|
|
847
|
-
}
|
|
848
|
-
function filterLessons(response, filter) {
|
|
849
|
-
if (typeof filter.weekNumber === "number") {
|
|
850
|
-
assertPositiveInt(filter.weekNumber, "filter.weekNumber");
|
|
851
|
-
}
|
|
852
|
-
return response.lessons.filter((item) => matchesFilter(item, filter));
|
|
853
|
-
}
|
|
844
|
+
|
|
845
|
+
// src/modules/scheduleApi.ts
|
|
854
846
|
function createScheduleModule(config) {
|
|
855
847
|
async function getGroup(groupNumber, options = {}) {
|
|
856
848
|
assertGroupNumber(groupNumber, "groupNumber");
|
|
@@ -895,24 +887,35 @@ function createScheduleModule(config) {
|
|
|
895
887
|
getEmployee,
|
|
896
888
|
getGroupFiltered,
|
|
897
889
|
getEmployeeFiltered,
|
|
890
|
+
/**
|
|
891
|
+
* Returns exams for a group.
|
|
892
|
+
*/
|
|
898
893
|
async getGroupExams(groupNumber, options = {}) {
|
|
899
894
|
return getGroupFiltered(groupNumber, { source: "exams" }, options);
|
|
900
895
|
},
|
|
896
|
+
/**
|
|
897
|
+
* Returns exams for an employee.
|
|
898
|
+
*/
|
|
901
899
|
async getEmployeeExams(urlId, options = {}) {
|
|
902
900
|
return getEmployeeFiltered(urlId, { source: "exams" }, options);
|
|
903
901
|
},
|
|
902
|
+
/**
|
|
903
|
+
* Returns regular schedule lessons of a specific subgroup for a group.
|
|
904
|
+
*/
|
|
904
905
|
async getGroupBySubgroup(groupNumber, subgroup, options = {}) {
|
|
905
906
|
assertPositiveInt(subgroup, "subgroup");
|
|
906
907
|
return getGroupFiltered(groupNumber, { source: "schedules", subgroup }, options);
|
|
907
908
|
},
|
|
909
|
+
/**
|
|
910
|
+
* Returns regular schedule lessons of a specific subgroup for an employee.
|
|
911
|
+
*/
|
|
908
912
|
async getEmployeeBySubgroup(urlId, subgroup, options = {}) {
|
|
909
913
|
assertPositiveInt(subgroup, "subgroup");
|
|
910
914
|
return getEmployeeFiltered(urlId, { source: "schedules", subgroup }, options);
|
|
911
915
|
},
|
|
912
916
|
getCurrentWeek,
|
|
913
917
|
/**
|
|
914
|
-
* Calls IIS `/last-update-date/student-group`.
|
|
915
|
-
* it may return an error for newer group numbers (e.g. six-digit `524404`).
|
|
918
|
+
* Calls IIS `/last-update-date/student-group`.
|
|
916
919
|
*/
|
|
917
920
|
async getLastUpdateByGroup(params, options = {}) {
|
|
918
921
|
let query;
|
|
@@ -933,8 +936,7 @@ function createScheduleModule(config) {
|
|
|
933
936
|
return payload;
|
|
934
937
|
},
|
|
935
938
|
/**
|
|
936
|
-
* Calls IIS `/last-update-date/employee`.
|
|
937
|
-
* not relying on it for critical cache logic.
|
|
939
|
+
* Calls IIS `/last-update-date/employee`.
|
|
938
940
|
*/
|
|
939
941
|
async getLastUpdateByEmployee(params, options = {}) {
|
|
940
942
|
let query;
|
|
@@ -959,29 +961,13 @@ function createScheduleModule(config) {
|
|
|
959
961
|
|
|
960
962
|
// src/modules/specialities.ts
|
|
961
963
|
function createSpecialitiesModule(config) {
|
|
962
|
-
return
|
|
963
|
-
/**
|
|
964
|
-
* Returns the full list of specialities from `/specialities`.
|
|
965
|
-
* If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
|
|
966
|
-
*
|
|
967
|
-
* @throws {BsuirApiError} When the API returns a non-success HTTP status
|
|
968
|
-
* @throws {BsuirNetworkError} On transport failures after retries
|
|
969
|
-
* @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
|
|
970
|
-
*/
|
|
971
|
-
async listAll(options = {}) {
|
|
972
|
-
const payload = await requestJson(config, "/specialities", {
|
|
973
|
-
signal: options.signal
|
|
974
|
-
});
|
|
975
|
-
if (config.validateResponses) {
|
|
976
|
-
assertArrayResponse(payload, "/specialities");
|
|
977
|
-
}
|
|
978
|
-
return payload;
|
|
979
|
-
}
|
|
980
|
-
};
|
|
964
|
+
return createListModule(config, "/specialities");
|
|
981
965
|
}
|
|
982
966
|
|
|
983
967
|
// src/client/createClient.ts
|
|
984
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;
|
|
985
971
|
var MAX_TIMEOUT_MS = 3e5;
|
|
986
972
|
function resolveFetch(customFetch) {
|
|
987
973
|
if (customFetch) {
|
|
@@ -1005,6 +991,39 @@ function assertIntegerOption(value, name, minInclusive) {
|
|
|
1005
991
|
}
|
|
1006
992
|
return value;
|
|
1007
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
|
+
}
|
|
1008
1027
|
function createInternalConfig(options) {
|
|
1009
1028
|
const timeoutMs = assertIntegerOption(options.timeoutMs, "timeoutMs", 1) ?? 1e4;
|
|
1010
1029
|
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
@@ -1017,13 +1036,16 @@ function createInternalConfig(options) {
|
|
|
1017
1036
|
const retryMaxDelayMs = assertIntegerOption(options.retryMaxDelayMs, "retryMaxDelayMs", 0) ?? 3e3;
|
|
1018
1037
|
const cacheTtlMs = assertIntegerOption(options.cache?.ttlMs, "cache.ttlMs", 1);
|
|
1019
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;
|
|
1020
1042
|
if (retryDelayMs > retryMaxDelayMs) {
|
|
1021
1043
|
throw new BsuirConfigurationError(
|
|
1022
1044
|
"'retryDelayMs' must be less than or equal to 'retryMaxDelayMs'"
|
|
1023
1045
|
);
|
|
1024
1046
|
}
|
|
1025
1047
|
return {
|
|
1026
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
1048
|
+
baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL, allowInsecureHttp, allowedBaseUrlHosts),
|
|
1027
1049
|
fetchImpl: resolveFetch(options.fetch),
|
|
1028
1050
|
signal: options.signal,
|
|
1029
1051
|
timeoutMs,
|
|
@@ -1035,7 +1057,8 @@ function createInternalConfig(options) {
|
|
|
1035
1057
|
cacheTtlMs,
|
|
1036
1058
|
cacheMaxEntries,
|
|
1037
1059
|
dedupeInFlight: options.dedupeInFlight ?? true,
|
|
1038
|
-
|
|
1060
|
+
maxResponseBytes,
|
|
1061
|
+
validateResponses: options.validateResponses ?? true,
|
|
1039
1062
|
hooks: options.hooks ?? {},
|
|
1040
1063
|
responseCache: /* @__PURE__ */ new Map(),
|
|
1041
1064
|
inFlightRequests: /* @__PURE__ */ new Map(),
|