From b26a659d0c0d3016b4b2ed5b764119ba6ef51107 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 29 Jan 2026 16:41:55 +0200 Subject: [PATCH] chore: init --- .editorconfig | 8 ++ .gitignore | 34 ++++++++ .prettierrc | 7 ++ AGENTS.md | 1 + README.md | 12 +++ bun.lock | 78 +++++++++++++++++ jsconfig.json | 29 +++++++ package.json | 23 +++++ src/Client/index.ts | 23 +++++ src/Client/types.ts | 150 +++++++++++++++++++++++++++++++++ src/index.ts | 2 + src/login/index.ts | 43 ++++++++++ src/login/playwright.ts | 22 +++++ src/util/AuthClient.ts | 67 +++++++++++++++ src/util/Mutex.ts | 27 ++++++ src/util/NameUtil.ts | 10 +++ tests/EklaseAuthClient.test.ts | 16 ++++ tests/NameUtil.test.ts | 21 +++++ tsconfig.json | 14 +++ 19 files changed, 587 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 src/Client/index.ts create mode 100644 src/Client/types.ts create mode 100644 src/index.ts create mode 100644 src/login/index.ts create mode 100644 src/login/playwright.ts create mode 100644 src/util/AuthClient.ts create mode 100644 src/util/Mutex.ts create mode 100644 src/util/NameUtil.ts create mode 100644 tests/EklaseAuthClient.test.ts create mode 100644 tests/NameUtil.test.ts create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5fb5a0f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5b6fcbd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "none", + "tabWidth": 4, + "useTabs": true, + "semi": true, + "singleQuote": false +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fc63f4d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +Do not use "Fake Axios". Only production APIs, even in tests. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b597735 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Atruna + +Šis ir **NEOFICIĀLS** E-Klases API wrapperis. + +Šis projekts **nav jebkādā veidā saistīts ar** SIA "Izglītības sistēmas" vai +E-Klasi un tās operatoriem. Visi zīmoli un pakalpojumu nosaukumi pieder to +attiecīgajiem īpašniekiem. + +PROGRAMMATŪRA TIEK NODROŠINĀTA **"KĀ IR"**, BEZ JEBKĀDĀM GARANTIJĀM. ES +NEUZŅEMOS ATBILDĪBU JA TAVU KONTU NOBLOĶĒ. + +Tu pats esi atbildīgs par to, kā šo lieto! diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2d6aaef --- /dev/null +++ b/bun.lock @@ -0,0 +1,78 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "eklase-api-wrapper", + "dependencies": { + "axios": "^1.13.2", + "playwright-core": "^1.58.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..146fe4e --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc32f35 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "eklase-api-wrapper", + "version": "0.1.0", + "main": "dist/index.js", + "dependencies": { + "axios": "^1.13.2", + "playwright-core": "^1.58.0" + }, + "description": "TypeScript Bun wrapper for the e-klase authentication API with tests.", + "license": "MIT", + "scripts": { + "build": "bun build", + "test": "bun test" + }, + "types": "dist/index.d.ts", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/src/Client/index.ts b/src/Client/index.ts new file mode 100644 index 0000000..a5b45c7 --- /dev/null +++ b/src/Client/index.ts @@ -0,0 +1,23 @@ +import { EklaseAuthClient } from "../util/AuthClient"; +import { Evaluation, EvaluationDetails, LessonTime, UserInfo } from "./types"; + +export class APIClient extends EklaseAuthClient { + + async getUser(): Promise { + return await this.get("https://family.e-klase.lv/api/user") + } + + async getEvaluations(): Promise { + return await this.get("https://family.e-klase.lv/api/evaluations") + } + + async getEvaluationsDetails(evaluationId: number): Promise { + return await this.get(`https://family.e-klase.lv/api/evaluations/${evaluationId}`) + } + + + async getLessonTimes(): Promise { + return await this.get("https://family.e-klase.lv/api/lesson-times") + } + +} diff --git a/src/Client/types.ts b/src/Client/types.ts new file mode 100644 index 0000000..796be17 --- /dev/null +++ b/src/Client/types.ts @@ -0,0 +1,150 @@ +export type UserInfo = { + firstName: string; + lastName: string; + personType: "Student"; + personTypeName: "skolēns" | string; + personTypeId: number; + school: { + id: number; + tenantId: string; + name: string; + regionId: number; + }, + class: { + id: number; + name: string; + level: number; + }, + studyYearId: number; + activeStudyYears: { + id: number; + name: `${number}./${number}.` | string; + isCurrent: boolean + }[], + userPublicId: string; + username: string; + /** phone # without +371 */ + phoneNumber: string; + isArchived: boolean; + userSettings: Record; + /** ģimenes komplekts */ + premiumSubscription?: { + isActive?: boolean, + endDate?: string, + hasDiscount?: boolean, + isActiveTillEndOfYear?: boolean + } +} + +export type Evaluation = { + id: number, + /** bad grade: 1-3; (1-4 if professional) */ + value: string, + /** iso 8601 */ + timeCreated: string, + /** class name e.g. math */ + disciplineName: string, + isNonAttendance: boolean + lesson?: { + /** iso 8601 */ + date: string, + type?: { + name: string, + abbreviation: string, + /** ltid_Test = PD */ + lessonTypeId: string + }, + isTest: boolean, + /** html-like rich text markup */ + subject?: boolean + }, + isWeighted: boolean, + isNew: boolean, + hasComments: boolean, + editedEvaluation?: { + /** bad grade: 1-3; (1-4 if professional) */ + value: string, + isNonAttendance: boolean + } +} + +export type LessonTime = { + name: string, + from: `${number}:${number}` + to: `${number}:${number}` +} + +export type EvaluationDetails = { + evaluation: EvaluationDetailsDetailed; + lesson: EvaluationLessonDetails; + editedEvaluations: EditedEvaluation[]; + statistics: EvaluationStatistics; +}; + +export type EvaluationDetailsDetailed = { + id: number; + /** bad grade: 1-3; (1-4 if professional) */ + value: string; + /** Vārds Uzvārds */ + authorName: string; + /** iso 8601 */ + timeCreated: string; + testNotes: string; +}; + +export type EvaluationLessonDetails = { + type: LessonType; + /** iso 8601 midnight */ + lessonDate: string; + lessonSubject: LessonSubject; + disciplineName: string; + testId: number; + lessonNumberInDiary: number; + creation: LessonAudit; + lastModification: LessonModification; +}; + +export type LessonType = { + name: string; + abbreviation: string; + /** e.g. ltid_Test */ + lessonTypeId: string; +}; + +export type LessonSubject = { + /** html / rich text */ + text: string; + attachments: LessonAttachment[]; +}; + +export type LessonAttachment = Record; + + +export type LessonAudit = { + authorName: string; + /** iso 8601 */ + timeCreated: string; +}; + +export type LessonModification = { + authorName: string; + /** iso 8601 */ + timeModified: string; +}; + +export type EditedEvaluation = { + value: string; + authorName: string; + /** iso 8601 */ + timeCreated: string; + /** e.g. Student_Improved */ + reasonForEdit: string; +}; + +export type EvaluationStatistics = { + chartItems: number[]; + /** 1st, 2nd, 3rd and so on */ + studentPlace: number; + indexOfStudentEvaluation: number; + averageClassEvaluation: string; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c303c9a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from "./Client"; +export * from "./util/NameUtil"; diff --git a/src/login/index.ts b/src/login/index.ts new file mode 100644 index 0000000..1e5b695 --- /dev/null +++ b/src/login/index.ts @@ -0,0 +1,43 @@ +import { sleep } from "bun"; +import { getBrowserContext } from "./playwright"; + +export const LOGIN_URL = "https://family.e-klase.lv/"; + +export async function loginAndGetToken({ + username, + password +}: { + username: string; + password: string; +}): Promise<{ + expiry: Date; + token: string; +}> { + const ctx = await getBrowserContext(); + + const page = await ctx.newPage(); + + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto(LOGIN_URL); + + await sleep(2000); + + await page.locator("#username").fill(username, { force: true }); + await page.locator("#password").fill(password, { force: true }); + await page.locator("#login-button").click(); + + const req = await page.waitForRequest( + (request) => + request.method() === "GET" && + request.url() === "https://family.e-klase.lv/api/user" + ); + + const authHeader = req.headers()["authorization"]; + + await ctx.close(); + + return { + token: authHeader, + expiry: new Date(Date.now() + 15 * 60 * 1000) + }; +} diff --git a/src/login/playwright.ts b/src/login/playwright.ts new file mode 100644 index 0000000..1b747b5 --- /dev/null +++ b/src/login/playwright.ts @@ -0,0 +1,22 @@ +import { Browser, BrowserContext, chromium } from "playwright-core"; + +const userAgents = [ + "Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0" +]; + +let _globalBrowserCtx: Browser; + +export async function getBrowserContext(): Promise { + if (!_globalBrowserCtx) { + _globalBrowserCtx = await chromium.launch({ + executablePath: + Bun.which("google-chrome") || + Bun.which("chrome") || + Bun.which("google-chrome-stable")!, + headless: true + }); + } + return await _globalBrowserCtx.newContext({ + userAgent: userAgents[Math.floor(Math.random() * userAgents.length)] + }); +} diff --git a/src/util/AuthClient.ts b/src/util/AuthClient.ts new file mode 100644 index 0000000..929040b --- /dev/null +++ b/src/util/AuthClient.ts @@ -0,0 +1,67 @@ +import { loginAndGetToken } from "../login"; + +export type AuthClientOptions = { + username: string; + password: string; +}; + +export type TokenResponse = { + expiry: Date; + token: string; +}; + +export class EklaseAuthClient { + #username: string; + #password: string; + #token?: TokenResponse; + + constructor(options: AuthClientOptions) { + this.#username = options.username; + this.#password = options.password; + } + + async getToken(): Promise { + if (!this.#token || new Date() > this.#token.expiry) { + const r = await loginAndGetToken({ + username: this.#username, + password: this.#password + }); + this.#token = r; + return r; + } + return this.#token; + } + + async apiRequest( + input: RequestInfo, + init: RequestInit = {} + ): Promise { + const tokenResponse = await this.getToken(); + const headers = new Headers(init.headers ?? {}); + if (!headers.has("authorization")) { + headers.set("authorization", tokenResponse.token); + } + + return fetch(input, { + ...init, + headers + }); + } + + async get( + input: RequestInfo, + init: RequestInit = {} + ): Promise { + const tokenResponse = await this.getToken(); + const headers = new Headers(init.headers ?? {}); + if (!headers.has("authorization")) { + headers.set("authorization", tokenResponse.token); + } + + const r = await fetch(input, { + ...init, + headers + }); + return await r.json() as T; + } +} diff --git a/src/util/Mutex.ts b/src/util/Mutex.ts new file mode 100644 index 0000000..8ccedd2 --- /dev/null +++ b/src/util/Mutex.ts @@ -0,0 +1,27 @@ +export class Mutex { + private promise: Promise = Promise.resolve(); + private resolve: () => void = () => {}; + + async withMutex(f: () => Promise): Promise { + this.lock(); + let r; + try { + r = await f(); + } catch (e_) { + this.unlock(); + throw new Error(); + } + this.unlock(); + return r; + } + + async lock(): Promise { + const oldPromise = this.promise; + this.promise = new Promise((resolve) => (this.resolve = resolve)); + await oldPromise; + } + + unlock(): void { + this.resolve(); + } +} diff --git a/src/util/NameUtil.ts b/src/util/NameUtil.ts new file mode 100644 index 0000000..1ae8ec4 --- /dev/null +++ b/src/util/NameUtil.ts @@ -0,0 +1,10 @@ +/** + * Normalizes a name from the Latvian Government form (LastName FirstName) or (LastName-OtherLastName FirstName) + */ +export function normalizeName(name: string): { + firstName: string; + lastName: string; +} { + const [lastName, firstName] = name.split(" "); + return { lastName, firstName }; +} diff --git a/tests/EklaseAuthClient.test.ts b/tests/EklaseAuthClient.test.ts new file mode 100644 index 0000000..89d0503 --- /dev/null +++ b/tests/EklaseAuthClient.test.ts @@ -0,0 +1,16 @@ +import { test, expect } from "bun:test"; +import { APIClient } from "../src/Client"; + +test.todo("Authenticated API request", async () => { + const client = new APIClient({ + username: process.env.USERNAME!, + password: process.env.PASSWORD! + }); + + const token = await client.getToken(); + expect(typeof token.token).toBe("string"); + console.log(token); + + const user = await client.apiRequest("https://family.e-klase.lv/api/user"); + console.log(await user.json()); +}); diff --git a/tests/NameUtil.test.ts b/tests/NameUtil.test.ts new file mode 100644 index 0000000..0aa3bd9 --- /dev/null +++ b/tests/NameUtil.test.ts @@ -0,0 +1,21 @@ +import { test, expect } from "bun:test"; +import { normalizeName } from "../src"; + +// LV/EN placeholder names +test.each([ + // https://en.wikipedia.org/wiki/List_of_placeholder_names#Latvian + { + fn: "Andris", + ln: "Paraudziņš" + }, + // https://en.wikipedia.org/wiki/List_of_placeholder_names#English + { + fn: "John", + ln: "Doe" + } +])("Full government name normalization info FN/LN", (v) => { + // why last name first? + const x = normalizeName(`${v.ln} ${v.fn}`); + expect(x.firstName).toBe(v.fn); + expect(x.lastName).toBe(v.ln); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..837ce7b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src", "tests"] +}