Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/web/app/api/v1/szakasz/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import { szakaszApi } from "@/lib/api/szakasz";
import { REST } from "@/lib/api/semak";
import { apiRoute } from "@/lib/api/valasz";
import { szakaszCacheMp } from "@/lib/api/cache-ido";

export const dynamic = "force-dynamic";

// konkrét dátum = múltbeli, változatlan állapot → egy napig cache-elhető
export const GET = apiRoute(REST.szakasz, szakaszApi, (p) => (p.datum ? 86_400 : 3_600));
// csak a mai napnál korábbi dátum végleges → egy napig cache-elhető; a mai/jövőbeli
// datumot még érintheti a napi delta, ezért rövid cache (lásd cache-ido.ts)
export const GET = apiRoute(REST.szakasz, szakaszApi, (p) => szakaszCacheMp(p.datum));
24 changes: 24 additions & 0 deletions apps/web/lib/api/cache-ido.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// `szakaszCacheMp` egységteszt — a modul import nélküli tiszta függvény, ezért
// node:test-tel közvetlenül futtatható: node --test lib/api/cache-ido.test.ts

import test from "node:test";
import assert from "node:assert/strict";
import { szakaszCacheMp } from "./cache-ido.ts";

test("datum nélkül közepes cache", () => {
assert.equal(szakaszCacheMp(undefined, "2026-10-23"), 3_600);
});

test("múltbeli datum egy napig cache-elhető", () => {
assert.equal(szakaszCacheMp("2026-10-22", "2026-10-23"), 86_400);
assert.equal(szakaszCacheMp("2024-01-01", "2026-10-23"), 86_400);
});

test("mai datum rövid cache — a napi delta még módosíthatja", () => {
assert.equal(szakaszCacheMp("2026-10-23", "2026-10-23"), 900);
});

test("jövőbeli datum rövid cache", () => {
assert.equal(szakaszCacheMp("2026-10-24", "2026-10-23"), 900);
assert.equal(szakaszCacheMp("2027-01-01", "2026-10-23"), 900);
});
12 changes: 12 additions & 0 deletions apps/web/lib/api/cache-ido.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Szakasz-válasz cache-ideje (mp). Csak a mai napnál korábbi datum végleges
// (napi delta már nem érinti) → egy napig cache-elhető; a mai vagy jövőbeli
// datumra a napi delta még módosíthat, ezért ugyanaz a rövid 15 perc,
// amit a valtozasok route is használ.

export function szakaszCacheMp(
datum: string | undefined,
ma: string = new Date().toISOString().slice(0, 10),
): number {
if (!datum) return 3_600;
return datum < ma ? 86_400 : 900;
}
23 changes: 23 additions & 0 deletions apps/web/lib/api/datum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// `ervenyesDatum` egységteszt — a modul import nélküli tiszta függvény, ezért
// node:test-tel közvetlenül futtatható: node --test lib/api/datum.test.ts

import test from "node:test";
import assert from "node:assert/strict";
import { ervenyesDatum } from "./datum.ts";

test("valódi naptári dátum igaz", () => {
assert.equal(ervenyesDatum("2026-10-01"), true);
assert.equal(ervenyesDatum("2024-02-29"), true); // szökőév
});

test("alakilag jó, de nem létező nap hamis", () => {
for (const d of ["2026-13-01", "2026-02-30", "2026-10-00", "2025-02-29", "2026-04-31"]) {
assert.equal(ervenyesDatum(d), false, d);
}
});

test("rossz alak hamis", () => {
for (const d of ["2024-2-1", "", "2026-10-01T00:00:00Z"]) {
assert.equal(ervenyesDatum(d), false, d);
}
});
14 changes: 14 additions & 0 deletions apps/web/lib/api/datum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Dátum-ellenőrzés: az alak (YYYY-MM-DD) mellett a naptárban is létező nap
// kell — a „2026-02-30" alakilag jó, de nem valódi dátum. Nincs import, hogy
// tiszta függvényként tesztelhető legyen.

const DATUM_MINTA = /^\d{4}-\d{2}-\d{2}$/;

/** igaz, ha `d` YYYY-MM-DD alakú és valódi naptári dátum */
export function ervenyesDatum(d: string): boolean {
if (!DATUM_MINTA.test(d)) return false;
// UTC oda-vissza alakítás: a túlcsorduló napot (febr. 30) a Date átrollozza,
// a képtelen hónapot Invalid Date-ként adja — mindkettő elbukik az összehasonlításon.
const t = new Date(`${d}T00:00:00Z`);
return !Number.isNaN(t.getTime()) && t.toISOString().slice(0, 10) === d;
}
6 changes: 4 additions & 2 deletions apps/web/lib/api/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
type SzakaszValtozas,
} from "@gitjog/szoveg";
import { getAllapotokSlug, getSzovegAt } from "@/lib/adat";
import { ApiHiba, DATUM_MINTA, diffUrl, jogszabalyMeta, jogszabalyVagy404, MEGJEGYZES } from "./kozos";
import { ApiHiba, diffUrl, jogszabalyMeta, jogszabalyVagy404, MEGJEGYZES } from "./kozos";
import { ervenyesDatum } from "./datum";

export interface DiffParam {
slug: string;
Expand Down Expand Up @@ -37,7 +38,8 @@ export async function diffApi(p: DiffParam): Promise<DiffValasz> {
["tol", p.tol],
["ig", p.ig],
] as const) {
if (!DATUM_MINTA.test(ertek)) throw new ApiHiba(400, `A \`${mezo}\` alakja YYYY-MM-DD`, mezo);
if (!ervenyesDatum(ertek))
throw new ApiHiba(400, `A \`${mezo}\` alakja YYYY-MM-DD (valódi naptári dátum)`, mezo);
}
if (p.tol >= p.ig) throw new ApiHiba(400, "A `tol` legyen korábbi az `ig`-nél", "tol");

Expand Down
2 changes: 0 additions & 2 deletions apps/web/lib/api/kozos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ export function jogszabalyMeta(t: JogszabalyTetel): JogszabalyMeta {
};
}

export const DATUM_MINTA = /^\d{4}-\d{2}-\d{2}$/;

/** legfeljebb `n` párhuzamos feldolgozás — a raw GitHub 429-e ellen */
export async function korlatozottParhuzam<T, R>(
tetelek: T[],
Expand Down
6 changes: 5 additions & 1 deletion apps/web/lib/api/semak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// REST query-string mindent szövegként — ezért két alak, közös leírásokkal.

import { z } from "zod";
import { ervenyesDatum } from "./datum";
import { KORLAT } from "./kozos";

const L = {
Expand Down Expand Up @@ -31,7 +32,10 @@ const MAX = 200;
const szoveg = () => z.string().max(MAX);
const cursor = z.string().max(40);

const datum = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD");
const datum = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD")
.refine(ervenyesDatum, "valódi naptári dátum");
const restBool = z
.enum(["true", "false", "1", "0"])
.transform((v) => v === "true" || v === "1");
Expand Down
5 changes: 3 additions & 2 deletions apps/web/lib/api/szakasz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import { paragrafusKivag, szakaszKeres, szakaszokraBont } from "@gitjog/szoveg";
import { getAllapotokSlug, getSzoveg, getSzovegAt, type Allapot } from "@/lib/adat";
import { ervenyesDatum } from "./datum";
import {
ApiHiba,
DATUM_MINTA,
jogszabalyMeta,
jogszabalyUrl,
jogszabalyVagy404,
Expand Down Expand Up @@ -46,7 +46,8 @@ export interface JogszabalyValasz extends JogszabalyMeta {
export function idoallapotValaszt(allapotok: Allapot[], datum?: string): Allapot {
if (allapotok.length === 0) throw new ApiHiba(404, "A jogszabálynak nincs időállapota");
if (!datum) return allapotok.at(-1)!;
if (!DATUM_MINTA.test(datum)) throw new ApiHiba(400, "A `datum` alakja YYYY-MM-DD", "datum");
if (!ervenyesDatum(datum))
throw new ApiHiba(400, "A `datum` alakja YYYY-MM-DD (valódi naptári dátum)", "datum");
const talalt = [...allapotok].reverse().find((a) => a.datum <= datum);
if (!talalt) {
throw new ApiHiba(
Expand Down
5 changes: 3 additions & 2 deletions apps/web/lib/api/valtozasok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
import {
ApiHiba,
csonkol,
DATUM_MINTA,
diffUrl,
jogszabalyTerkep,
jogszabalyUrl,
Expand All @@ -27,6 +26,7 @@ import {
MEGJEGYZES,
normalizal,
} from "./kozos";
import { ervenyesDatum } from "./datum";

export interface ValtozasokParam {
felveve_utan?: string;
Expand Down Expand Up @@ -188,7 +188,8 @@ export async function valtozasokApi(p: ValtozasokParam): Promise<ValtozasokValas
["since", p.since],
["until", p.until],
] as const) {
if (ertek && !DATUM_MINTA.test(ertek)) throw new ApiHiba(400, `A \`${mezo}\` alakja YYYY-MM-DD`, mezo);
if (ertek && !ervenyesDatum(ertek))
throw new ApiHiba(400, `A \`${mezo}\` alakja YYYY-MM-DD (valódi naptári dátum)`, mezo);
}
if (p.felveve_utan && Number.isNaN(Date.parse(p.felveve_utan))) {
throw new ApiHiba(400, "A `felveve_utan` ISO-8601 időbélyeg (pl. 2026-09-15T00:00:00Z)", "felveve_utan");
Expand Down
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"test": "node --test \"lib/**/*.test.ts\""
},
"dependencies": {
"@gitjog/szoveg": "workspace:*",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"**/*.test.ts"
]
}
12 changes: 10 additions & 2 deletions packages/szoveg/src/paragrafus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ export interface ParagrafusReszlet {
const PARAGRAFUS_SOR = /^(\d+(?::\d+)?(?:\/[A-Z])?)\. §(?=\s|$)/;
const HEADING = /^#{2,4} (.+)$/;

/** „18. §" → „18"; a szövegbeli sor elejével vetjük össze */
/**
* „18. §", „18 §" → „18"; a szövegbeli sor elejével vetjük össze. Regex helyett
* sztringművelet: a `\.?\s*§` minta sok szóközön polinomiális (ReDoS).
*/
function szam(paragrafus: string): string {
return paragrafus.replace(/\.\s*§\s*$/, "").trim().toUpperCase();
let s = paragrafus.trim();
if (s.endsWith("§")) {
s = s.slice(0, -1).trimEnd();
if (s.endsWith(".")) s = s.slice(0, -1);
}
return s.trim().toUpperCase();
}

export function paragrafusKivag(md: string, paragrafus: string): ParagrafusReszlet | undefined {
Expand Down
13 changes: 13 additions & 0 deletions packages/szoveg/test/paragrafus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ describe("paragrafusKivag", () => {
expect(paragrafusKivag(MD, "18/A. §")?.szoveg).toBe("18/A. § Betűs paragrafus.");
expect(paragrafusKivag(MD, "20. §")?.horgony).toBe("7-hatosagi-jovahagyas");
});
it("a § előtti pont opcionális: „18 §” és „18.§” ugyanaz, mint „18. §”", () => {
const kanonikus = paragrafusKivag(MD, "18. §")?.szoveg;
expect(kanonikus?.startsWith("18. § (1) A föld eladása")).toBe(true);
expect(paragrafusKivag(MD, "18 §")?.szoveg).toBe(kanonikus);
expect(paragrafusKivag(MD, "18.§")?.szoveg).toBe(kanonikus);
expect(paragrafusKivag(MD, "18/A §")?.szoveg).toBe("18/A. § Betűs paragrafus.");
});
it("sok szóközös bemenet lineáris időben fut (nincs ReDoS)", () => {
const kezdet = performance.now();
expect(paragrafusKivag(MD, `18${" ".repeat(100_000)}x`)).toBeUndefined();
expect(paragrafusKivag(MD, `18${" ".repeat(100_000)}§`)?.paragrafus).toBe("18. §");
expect(performance.now() - kezdet).toBeLessThan(500);
});
it("hiányzó §-ra undefined; a 1. § nem illeszkedik a 18. §-ra", () => {
expect(paragrafusKivag(MD, "1. §")).toBeUndefined();
expect(paragrafusKivag(MD, "8. §")).toBeUndefined();
Expand Down
Loading