commit b26a659d0c0d3016b4b2ed5b764119ba6ef51107 Author: Kris Date: Thu Jan 29 16:41:55 2026 +0200 chore: init 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"] +}