Mundarija (22)
- Bu darsda
- 1. Nega bu kerak?
- 2. Query konvensiyasi
- 3. Saralash
- 3.1 sort=-yaratildi,matn
- 3.2 Nega oq ro'yxat
- 4. Maydon tanlash (sparse fieldsets)
- 5. Massiv parametrlar va parser
- 5.1 Takrorlangan kalit
- 5.2 "extended" parser va uning xavfi
- 6. Noma'lum parametr — 400
- 7. API qadami: filtr, sort va fields (holat 22, 4-qism)
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): mantiqiy va butun
- 3-mashq (qiyin): to'liq royxatOl
- 4-mashq: API qadami
- 11. Real ishda
- Xulosa
- Manbalar
REST API'da filtrlash, saralash va maydon tanlash: query parametrlar
Qisqacha: Ro'yxat endpoint'i query parametrlar bilan boshqariladi: filtr (
?bajarildi=true&teg=uy), qidiruv (?q=non), saralash (?sort=-yaratildi,matn— minus kamayish tartibi) va maydon tanlash (?fields=matn,teglar). Har parametr oq ro'yxat bilan tekshiriladi: saralash maydoni ro'yxatda bo'lmasa — 400, noma'lum parametr (?sahifa=2) ham 400, jim e'tiborsiz emas. Express 5 sukut bo'yicha "simple" parser ishlatadi — takrorlangan kalit massiv bo'ladi,filtr[x]esa obyektga aylanmaydi. Bu xavfsizroq.
Bu darsda
- Filtr, qidiruv, saralash va maydon tanlash uchun query konvensiyasini yozasiz.
sort=-maydonni tahlil qilib, barqaror saralash yasaysiz.- Oq ro'yxat nega majburiyligini va noma'lum parametrga nega 400 kerakligini bilasiz.
qs("extended") parser xavfini ko'rasiz.vazifalar-apiningGET /vazifalariga filtr,sortvafieldsni qo'shasiz.
Oldin bilishingiz kerak: Status kodlarni to'g'ri tanlash, Qo'lda routing va query parametrlar, URL va URLSearchParams, req obyekti.
1. Nega bu kerak?
vazifalar frontend'ida "Bajarilmaganlar" tugmasi, teg bo'yicha filtr va qidiruv maydoni bor. Hozirgacha bularni frontend o'zi qilardi: hamma vazifalarni olib, brauzerda filter qilardi. Vazifalar 20 ta bo'lganda bu yaxshi ishlaydi. 10 000 ta bo'lganda esa har sahifa ochilishida megabaytlab JSON tarmoqdan o'tadi va telefon xotirasi to'ladi.
Yechim — tanlashni serverga berish: "menga faqat #uy tegli, bajarilmagan, oxirgi yaratilganlari birinchi, faqat matni" deb so'rash. Server bazadan aynan shuni oladi.
O'xshatish: kutubxonachi. "Hamma kitoblarni bering, o'zim topaman" demaysiz. "Tarix bo'limidan, 2020 dan keyingi, eng yangisi oldin" deysiz. Lekin kutubxonachi faqat o'zi biladigan shartlarni qabul qiladi. "Muallifning uy manzili bo'yicha saralang" desangiz — rad etadi. Oq ro'yxat aynan shu.
2. Query konvensiyasi
vazifalar-api konvensiyasi (holat 22):
| Parametr | Misol | Ma'nosi |
|---|---|---|
bajarildi |
true / false |
holat bo'yicha filtr |
teg |
teg=uy&teg=bozor |
takrorlanadi — hammasi bo'lsin (AND) |
q |
q=non |
matn ichida qidiruv, 1–100 belgi |
sort |
-yaratildi,matn |
vergul bilan kalitlar, - — kamayish |
fields |
matn,teglar |
faqat shu maydonlar (id doim) |
limit |
20 |
1–100 (sahifalash) |
offset / cursor |
sahifa (sahifalash) | |
jami |
true |
umumiy sonni ham qaytar |
Kanon curl chiqishlari (tekshiruv-22):
$ curl $A/vazifalar?teg=uy&fields=matn,teglar
{"vazifalar":[{"id":1,"matn":"Non olish #uy","teglar":["uy"]},
{"id":3,"matn":"Sut #uy #bozor","teglar":["uy","bozor"]}],
"sahifa":{"limit":20,"keyingi":null}}
$ curl $A/vazifalar?bajarildi=true&sort=-yangilandi&fields=matn,yangilandi
{"vazifalar":[{"id":4,"matn":"Kitob o'qish #o'quv","yangilandi":"…32.899Z"},
{"id":2,"matn":"Hisobot yozish #ish","yangilandi":"…32.635Z"}], …}
$ curl $A/vazifalar?q=OLISH
{"vazifalar":[{"id":1,"matn":"Non olish #uy",…}], …}(Uzun JSON qatorlari o'qish uchun bo'lingan.) Uchinchi so'rovga qarang: q=OLISH katta harf bilan, lekin Non olish topildi — qidiruv katta-kichik harfga sezgir emas.
Konvensiyani tanlashda yagona "to'g'ri" yo'l yo'q. Boshqa mashhur uslublar: ?filter[bajarildi]=true (JSON:API), ?bajarildi=eq:true, ?$filter=bajarildi eq true (OData). Muhimi — bitta API ichida bitta uslub va hujjat.
3. Saralash
3.1 sort=-yaratildi,matn
Format: vergul bilan ajratilgan maydonlar, oldida - bo'lsa — kamayish (yangisi oldin). sort=-bajarildi,matn — avval bajarilganlar, ular ichida alifbo bo'yicha.
Tahlil va saralashni toza JS'da yozamiz. «Ishga tushir» bilan sinang:
const SARALASH = ["id", "matn", "bajarildi", "yaratildi"];
function saralashniOqi(x) {
if (x === undefined) return [];
return x.split(",").map((qism) => {
const kamayish = qism.startsWith("-");
const maydon = kamayish ? qism.slice(1) : qism;
if (!SARALASH.includes(maydon)) {
throw new Error(`sort: noma'lum maydon "${maydon}"`);
}
return { maydon, kamayish };
});
}
function sarala(royxat, kalitlar) {
// oxiriga doim id — teng qiymatlarda ham tartib barqaror
const k = [...kalitlar.filter((s) => s.maydon !== "id"),
kalitlar.find((s) => s.maydon === "id") ?? { maydon: "id" }];
return [...royxat].sort((a, b) => {
for (const { maydon, kamayish } of k) {
if (a[maydon] === b[maydon]) continue;
const n = a[maydon] < b[maydon] ? -1 : 1;
return kamayish ? -n : n;
}
return 0;
});
}
const vazifalar = [
{ id: 1, matn: "Non olish", bajarildi: false },
{ id: 2, matn: "Hisobot", bajarildi: true },
{ id: 3, matn: "Sut", bajarildi: false },
{ id: 4, matn: "Kitob o'qish", bajarildi: true },
];
const tartib = sarala(vazifalar, saralashniOqi("-bajarildi,matn"));
console.log(tartib.map((v) => v.id).join(", "));
try {
saralashniOqi("-parol");
} catch (xato) {
console.log(xato.message);
}Konsolda:
2, 4, 1, 3
sort: noma'lum maydon "parol"Ikki muhim tafsilot:
- Oxirgi kalit —
id. Ikki vazifaningbajarildivamatni bir xil bo'lsa, tartib nimaga qarab? Kalitsiz — tasodifga yoki bazaning ichki tartibiga. Natijada bir xil so'rov ikki marta har xil tartib berishi mumkin. Sahifalashda bu qator takrorlanishiga yoki yo'qolishiga olib keladi (Pagination darsi). Kanonkalitlar()funksiyasi shuning uchun oxiriga doimidqo'shadi. - Satrlar kod nuqtasi bo'yicha (
a < b),localeCompareemas. Natija deterministik, lekin "o'zbekcha alifbo" emas: katta harflar kichiklardan oldin keladi. Postgres'daORDER BY matncollation bo'yicha tartiblaydi — tartib boshqacha bo'lishi mumkin. Bu SQL qismida hal qilinadi.
3.2 Nega oq ro'yxat
Kanonda sort=parol → 400:
$ curl $A/vazifalar?sort=parol
{"xato":"sort: noma'lum maydon \"parol\" (mumkin: id, matn, bajarildi, yaratildi, yangilandi)"}Uch sabab:
- Ma'lumot sizishi. Foydalanuvchida
parol_hashmaydoni bo'lsa va saralashga ruxsat bo'lsa, hujumchi tartibga qarab hash'ni bitta-bitta "taxmin qila" oladi. Maydonni ko'rsatmasangiz ham, u bo'yicha saralash ma'lumot beradi. - SQL injection. Ustun nomini SQL'ga parametr sifatida berib bo'lmaydi:
ORDER BY $1da$1ustun nomi emas, oddiy qiymat bo'lib qoladi va saralash ishlamaydi. Satrni yopishtirsangiz —sort=matn;DROP TABLExavfi. Oq ro'yxatdagi nomlar esa sizning kodingizdagi doimiylar (SQL qismida). - Tezlik. Har maydon bo'yicha saralash indeks talab qiladi. Ruxsat etilgan maydonlar — indeksi bor maydonlar.
Kanon SARALASH_MAYDONLARI ni ombor interfeysida saqlaydi — "SQL'da ustun nomi bo'ladi, foydalanuvchi matni emas".
Tekshirib ko'ring:
sort=matn,-matnkeldi. Nima qilish kerak?
Javob
Kanon 400 qaytaradi: sort: "matn" takrorlangan. Bir maydon ikki yo'nalishda — ma'nosiz so'rov, ehtimol mijoz kodidagi xato. Jim "oxirgisi ustun" qilish xatoni yashiradi.
4. Maydon tanlash (sparse fieldsets)
Maydon tanlash (sparse fieldsets) — mijoz faqat kerakli maydonlarni so'raydi: ?fields=matn,teglar. Mobil ilova ro'yxat ekranida faqat matnni ko'rsatadi — yaratildi, yangilandi ni yuklash shart emas.
Kanon qoidalari: noma'lum maydon — 400; id doim qaytadi — mijoz keyin shu vazifaga murojaat qila olishi kerak.
const MAYDONLAR = ["id", "matn", "bajarildi", "teglar", "yaratildi"];
function maydonlarniOqi(x) {
if (x === undefined) return undefined;
const m = x.split(",");
for (const nom of m) {
if (!MAYDONLAR.includes(nom)) throw new Error(`fields: "${nom}"`);
}
return [...new Set(["id", ...m])]; // id doim, takrorsiz
}
const proyeksiya = (v, maydonlar) => maydonlar === undefined
? v : Object.fromEntries(maydonlar.map((m) => [m, v[m]]));
const v = { id: 7, matn: "Non olish #uy", bajarildi: false,
teglar: ["uy"], yaratildi: "2026-10-24T05:04:51.972Z" };
for (const fields of ["matn,teglar", "matn,id,matn"]) {
const natija = proyeksiya(v, maydonlarniOqi(fields));
console.log(JSON.stringify(natija));
}Konsolda:
{"id":7,"matn":"Non olish #uy","teglar":["uy"]}
{"id":7,"matn":"Non olish #uy"}Maydon tanlash qachon foydali? Resursda katta maydonlar bo'lsa (uzun matn, rasm havolalari ro'yxati) va mijozlar har xil bo'lsa. Kichik resursda foydasi kam — baribir bitta so'rov. GraphQL bu g'oyani oxirigacha olib boradi: mijoz har doim maydonlarni o'zi tanlaydi.
5. Massiv parametrlar va parser
5.1 Takrorlangan kalit
Bir parametrga bir nechta qiymat qanday beriladi? Uch uslub:
?teg=uy&teg=bozor takrorlangan kalit (vazifalar-api)
?teg=uy,bozor vergul bilan
?teg[]=uy&teg[]=bozor qavs bilan (PHP, qs)Express 5 sukut bo'yicha "simple" parser (node:querystring) ishlatadi. Uning bitta yashirin chegarasi bor: 1 000 dan ortiq kalit jim tashlab yuboriladi — buni qo'lda routing darsida 5 000 parametrli so'rov bilan ko'rgansiz. Kanon t10:
GET /q?teg=uy&teg=ish&sahifa=2 → {"teg":["uy","ish"],"sahifa":"2"}
GET /q?filtr[bajarildi]=true → {"filtr[bajarildi]":"true"}
GET /q?a=1&a=2&a=3 → {"a":["1","2","3"]}Takrorlangan kalit massiv bo'ladi, qavs esa oddiy belgi — filtr[bajarildi] kalit nomining bir qismi. Bu tuzoqqa olib keladi: ?teg=uy da req.query.teg — satr, ?teg=uy&teg=ish da — massiv. Kod ikkalasini ham ko'tarishi kerak:
const teglar = (Array.isArray(teg) ? teg : [teg])
.map((t) => (typeof t === "string"
? t.replace(/^#/, "").toLowerCase() : ""));Teskarisi ham bor: bitta qiymat kutilgan parametr (q) ikki marta kelsa. Kanon bitta() funksiyasi massivni rad etadi — ?q=a&q=b → 400 q bir marta berilishi kerak. Aks holda req.query.q.trim() massivda TypeError beradi va mijoz 500 oladi.
5.2 "extended" parser va uning xavfi
app.set("query parser", "extended") — qs kutubxonasi: qavslar ichma-ich obyekt yasaydi. Biz sinadik (express 5.3.0):
extended ?parol[$ne]=x → {"parol":{"$ne":"x"}}
extended ?teg[]=uy&teg[]=ish → {"teg":["uy","ish"]}
extended ?teg[0]=uy&teg[25]=ish → {"teg":{"0":"uy","25":"ish"}}
extended ?a[b][c][d][e][f][g]=1 → {"a":{"b":{"c":{"d":{"e":{"f":{"[g]":"1"}}}}}}}Birinchi qator — klassik NoSQL injection naqshi. Kod db.users.find({ parol: req.query.parol }) qilsa va MongoDB'ga { "$ne": "x" } borsa, bu "parol x ga teng bo'lmagan har qanday foydalanuvchi" degani — parolsiz kirish. Simple parser'da bu kalit oddiy satr "parol[$ne]" bo'lib qoladi va zarar qilmaydi.
Uchinchi va to'rtinchi qatorlar — qs himoyasi: 20 dan katta indeks massiv emas, obyekt bo'ladi; 5 darajadan chuqur ichma-ichlik to'xtatiladi. Bu xotirani to'ldirish hujumlariga qarshi. Lekin sizning kodingiz massiv kutib, obyekt olishi mumkin.
Qaror: vazifalar-api "simple" da qoladi. Murakkab filtr kerak bo'lsa — aniq nomli tekis parametrlar.
6. Noma'lum parametr — 400
Kanondagi eng qattiq qoida:
$ curl $A/vazifalar?sahifa=2
{"xato":"noma'lum parametr: sahifa"}Nega jim e'tiborsiz qoldirmaslik kerak? Mijoz ?bajarlidi=true (harf almashgan) yozdi deylik. Server uni e'tiborsiz qoldirsa, filtrlanmagan ro'yxat qaytadi. Mijoz "bajarilganlar shu ekan" deb o'ylaydi — xato jim yashirinadi. 400 esa xatoni darhol ko'rsatadi. Kanon testi buni aynan shu misol bilan tekshiradi (?bajarlidi=true → 400).
Kamchiligi ham bor. Kelajakda yangi parametr qo'shsangiz, eski server versiyasi uni rad etadi. Ochiq API'larda ba'zan "noma'lumni e'tiborsiz qoldir" tanlanadi. Ichki API va bitta frontend uchun qattiq qoida yaxshiroq.
flowchart LR
Q["req.query"] --> U{"Hamma kalit<br/>ro'yxatdami?"}
U -- "yo'q" --> E["400"]
U -- "ha" --> P["Har parametr:<br/>tur, oraliq, oq ro'yxat"]
P -- "xato" --> E
P -- "ok" --> R["RoyxatSorovi"]
R --> O["ombor.royxat()"]Diagrammadagi oqimga qarang: xom req.query omborga hech qachon to'g'ridan bormaydi. Avval tekshiriladi va tipli RoyxatSorovi obyektiga aylanadi. Ombor faqat tekshirilgan, toza ma'lumot oladi — 26-qismda u SQL yozganda bu juda muhim.
Tekshirib ko'ring: Nega
?teg=%23ISHham#ishtegli vazifalarni topadi?
Javob
%23 — URL'da kodlangan # belgisi (# URL'da fragment boshlanishini bildiradi, shuning uchun kodlanadi). Parser uni #ISH ga ochadi. Kanon tekshiruvi # ni olib tashlaydi va kichik harfga o'tkazadi: replace(/^#/, "").toLowerCase() → ish. Teglar bazada ham kichik harfda saqlanadi. Kanon testi: ?teg=%23ISH&bajarildi=true → [2].
7. API qadami: filtr, sort va fields (holat 22, 4-qism)
Hali commit qilmang — commit keyingi darsda. Bu dars va keyingisi bitta kesim: sorov.ts kursor bilan ham ishlaydi, shuning uchun npm run check keyingi darsdagi kursor.ts va sahifala qo'shilgach yashil bo'ladi. Kanon fayllari to'liq — telefon uchun uzun qatorlar bo'lingan, kod va ma'no aynan.
src/vazifalar/ombor.ts— interfeys to'liq:
// ombor.ts — repository interfeysi: xizmat saqlash qanday
// ishlashini bilmaydi. Hamma metod async — 26-qismda Postgres (pg) bilan
// almashganda chaqiruvchi kod o'zgarmaydi
import type { Vazifa, VazifaOzgarishi, YangiVazifa }
from "./vazifa.ts";
/** Saralash mumkin bo'lgan maydonlar (oq ro'yxat — SQL'da ustun
* nomi bo'ladi, foydalanuvchi matni emas) */
export const SARALASH_MAYDONLARI =
["id", "matn", "bajarildi", "yaratildi", "yangilandi"] as const;
export type SaralashMaydoni = (typeof SARALASH_MAYDONLARI)[number];
export type Saralash = { maydon: SaralashMaydoni; kamayish: boolean };
export type Filtr = {
bajarildi?: boolean;
/** hammasi bo'lishi kerak (AND), kichik harfda */
teglar?: string[];
/** matn ichida, katta-kichik harfsiz */
qidiruv?: string;
};
/** Keyset kursori: oxirgi qatorning saralash kalitlari qiymatlari
* (saralash tartibida) + id */
export type KursorQiymati = (string | number | boolean)[];
export type Sahifalash =
| { tur: "offset"; offset: number }
| { tur: "kursor"; keyin?: KursorQiymati };
export type RoyxatSorovi = {
filtr: Filtr;
/** bo'sh bo'lsa — id bo'yicha o'sish; ombor oxiriga doim id
* qo'shadi (barqaror tartib) */
saralash: Saralash[];
limit: number;
sahifa: Sahifalash;
/** jami sonini ham hisoblash (Postgres'da alohida COUNT —
* qimmat) */
jami?: boolean;
};
export type RoyxatNatija = {
vazifalar: Vazifa[];
/** keyingi sahifa bormi — kursor rejimida keyingi kursor
* qiymatlari */
keyingi?: KursorQiymati;
borKeyingi: boolean;
jami?: number;
};
export type TegSoni = { teg: string; soni: number };
export interface VazifaOmbori {
royxat(sorov: RoyxatSorovi): Promise<RoyxatNatija>;
/** filtrga mos vazifalar soni (limit va sahifasiz) */
soni(filtr: Filtr): Promise<number>;
ol(id: number): Promise<Vazifa | undefined>;
yarat(yangi: YangiVazifa): Promise<Vazifa>;
/** @returns undefined — bunday id yo'q */
yangila(id: number, ozgarish: VazifaOzgarishi):
Promise<Vazifa | undefined>;
/** @returns false — bunday id yo'q */
ochir(id: number): Promise<boolean>;
/** teglar va nechta vazifada — ko'pidan kamiga */
teglar(): Promise<TegSoni[]>;
}as const va (typeof …)[number] — massivdan union tip yasash (keyof, typeof va indexed access). Bitta ro'yxat ham ishga tushganda tekshiruv uchun, ham tip uchun ishlaydi. Sahifalash tiplari (KursorQiymati, Sahifalash, RoyxatNatija) keyingi darsda tushuntiriladi.
src/vazifalar/sorov.ts— to'liq (sahifalash qismi keyingi darsda tahlil qilinadi):
// sorov.ts — GET /vazifalar query parametrlarini tekshirib
// RoyxatSorovi ga aylantirish (qo'lda; #30 da Zod). Konvensiya:
// ?bajarildi=true&teg=uy&teg=ish&q=non&sort=-yaratildi,matn
// &fields=id,matn&limit=20&offset=40 | &cursor=...&jami=true
import { HttpXato } from "../umumiy/http-xato.ts";
import { kursorOqi } from "../umumiy/kursor.ts";
import { SARALASH_MAYDONLARI } from "./ombor.ts";
import type { Filtr, RoyxatSorovi, Saralash, SaralashMaydoni }
from "./ombor.ts";
import type { Vazifa } from "./vazifa.ts";
export const SUKUT_LIMIT = 20;
export const MAKS_LIMIT = 100;
export const MAKS_OFFSET = 10_000;
const MAYDONLAR = ["id", "matn", "bajarildi", "teglar", "yaratildi",
"yangilandi"] as const;
export type Maydon = (typeof MAYDONLAR)[number];
const RUXSAT = new Set(["bajarildi", "teg", "q", "sort", "fields",
"limit", "offset", "cursor", "jami"]);
const TEG = /^[\p{L}\p{N}_'‘’]{1,50}$/u;
export type TekshirilganSorov =
{ sorov: RoyxatSorovi; maydonlar?: Maydon[]; imzo: string };
type Query = Record<string, unknown>;
function bitta(q: Query, nom: string): string | undefined {
const x = q[nom];
if (x === undefined) return undefined;
if (typeof x !== "string") {
throw new HttpXato(400, `${nom} bir marta berilishi kerak`);
}
return x;
}
function mantiqiy(q: Query, nom: string): boolean | undefined {
const x = bitta(q, nom);
if (x === undefined) return undefined;
if (x !== "true" && x !== "false") {
throw new HttpXato(400, `${nom} — true yoki false`);
}
return x === "true";
}
function butun(q: Query, nom: string, min: number,
max: number): number | undefined {
const x = bitta(q, nom);
if (x === undefined) return undefined;
if (!/^\d{1,6}$/.test(x) || Number(x) < min || Number(x) > max) {
throw new HttpXato(400,
`${nom} — ${String(min)}..${String(max)} ` +
"oralig'idagi butun son");
}
return Number(x);
}
function saralashniOqi(x: string | undefined): Saralash[] {
if (x === undefined) return [];
const natija: Saralash[] = [];
for (const qism of x.split(",")) {
const kamayish = qism.startsWith("-");
const maydon = kamayish ? qism.slice(1) : qism;
if (!(SARALASH_MAYDONLARI as readonly string[])
.includes(maydon)) {
throw new HttpXato(400, `sort: noma'lum maydon "${maydon}" ` +
`(mumkin: ${SARALASH_MAYDONLARI.join(", ")})`);
}
if (natija.some((s) => s.maydon === maydon)) {
throw new HttpXato(400, `sort: "${maydon}" takrorlangan`);
}
natija.push({ maydon: maydon as SaralashMaydoni, kamayish });
}
return natija;
}
function maydonlarniOqi(x: string | undefined): Maydon[] | undefined {
if (x === undefined) return undefined;
const m = x.split(",");
for (const nom of m) {
if (!(MAYDONLAR as readonly string[]).includes(nom)) {
throw new HttpXato(400, `fields: noma'lum maydon "${nom}"`);
}
}
// id doim — mijoz keyin shu vazifaga murojaat qila olsin
return [...new Set(["id", ...m])] as Maydon[];
}
export function royxatSorovi(q: Query): TekshirilganSorov {
for (const k of Object.keys(q)) {
if (!RUXSAT.has(k)) {
throw new HttpXato(400, `noma'lum parametr: ${k}`);
}
}
const filtr: Filtr = {};
const bajarildi = mantiqiy(q, "bajarildi");
if (bajarildi !== undefined) filtr.bajarildi = bajarildi;
const teg = q.teg;
if (teg !== undefined) {
const teglar = (Array.isArray(teg) ? teg : [teg]).map((t) =>
(typeof t === "string"
? t.replace(/^#/, "").toLowerCase() : ""));
if (!teglar.every((t) => TEG.test(t))) {
throw new HttpXato(400,
"teg — harf, raqam, _ (50 belgigacha)");
}
filtr.teglar = [...new Set(teglar)];
}
const qidiruv = bitta(q, "q");
if (qidiruv !== undefined) {
if (qidiruv.trim() === "" || qidiruv.length > 100) {
throw new HttpXato(400, "q — 1..100 belgi");
}
filtr.qidiruv = qidiruv.trim();
}
const saralash = saralashniOqi(bitta(q, "sort"));
const limit = butun(q, "limit", 1, MAKS_LIMIT) ?? SUKUT_LIMIT;
const offset = butun(q, "offset", 0, MAKS_OFFSET);
const kursor = bitta(q, "cursor");
// Imzo — kursor boshqa filtr/saralash bilan qayta ishlatilmasin
const imzo = JSON.stringify([saralash, filtr]);
let sahifa: RoyxatSorovi["sahifa"] =
{ tur: "offset", offset: offset ?? 0 };
if (kursor !== undefined) {
if (offset !== undefined) {
throw new HttpXato(400, "offset va cursor birga berilmaydi");
}
const keyin = kursorOqi(kursor, imzo);
if (keyin === undefined) {
throw new HttpXato(400,
"cursor yaroqsiz yoki boshqa saralash/filtr uchun");
}
sahifa = { tur: "kursor", keyin };
} else if (offset === undefined) {
// Parametrsiz — kursor rejimi (birinchi sahifa);
// offset faqat aniq so'ralganda
sahifa = { tur: "kursor" };
}
const sorov: RoyxatSorovi = { filtr, saralash, limit, sahifa };
const jami = mantiqiy(q, "jami");
if (jami !== undefined) sorov.jami = jami;
const maydonlar = maydonlarniOqi(bitta(q, "fields"));
return maydonlar === undefined
? { sorov, imzo } : { sorov, maydonlar, imzo };
}
/** Sparse fieldsets: faqat so'ralgan maydonlar */
export function proyeksiya(v: Vazifa,
maydonlar: readonly Maydon[] | undefined): Partial<Vazifa> {
if (maydonlar === undefined) return v;
return Object.fromEntries(maydonlar.map((m) => [m, v[m]]));
}exactOptionalPropertyTypes yoqilgan (Express + TypeScript) — shuning uchun filtr.bajarildi = undefined yozilmaydi, maydon faqat qiymat bo'lsa qo'shiladi. TEG regex'idagi ‘’ — frontend (22-qism) teglaridagi o'zbekcha tutuq belgisi variantlari.
src/vazifalar/royxat-mantiq.ts— fayl boshi:filtrlavakalitlar(qolgani —sahifala— keyingi darsda):
// royxat-mantiq.ts — xotiradagi (va fayldagi) omborlar uchun filtr,
// saralash, offset va keyset sahifalash. Toza funksiyalar: massiv
// kiradi — natija chiqadi. Postgres ombori (26-qism) buni SQL'da
// qiladi (WHERE, ORDER BY, LIMIT)
import type { Filtr, KursorQiymati, RoyxatNatija, RoyxatSorovi,
Saralash } from "./ombor.ts";
import type { Vazifa } from "./vazifa.ts";
export function filtrla(vazifalar: readonly Vazifa[],
filtr: Filtr): Vazifa[] {
const qidiruv = filtr.qidiruv?.toLowerCase();
return vazifalar.filter(
(v) =>
(filtr.bajarildi === undefined ||
v.bajarildi === filtr.bajarildi) &&
(filtr.teglar === undefined ||
filtr.teglar.every((t) => v.teglar.includes(t))) &&
(qidiruv === undefined ||
v.matn.toLowerCase().includes(qidiruv)),
);
}
/** Saralash kalitlari + id (oxirgi kalit — tenglikda ham tartib
* barqaror bo'lsin) */
export function kalitlar(saralash: readonly Saralash[]): Saralash[] {
const k = saralash.filter((s) => s.maydon !== "id");
const id = saralash.find((s) => s.maydon === "id");
return [...k, id ?? { maydon: "id", kamayish: false }];
}XotiraOmbori da soni(filtr) — filtrla(...).length, royxat(sorov) — sahifala(...). Postgres ombori (26-qism) xuddi shu RoyxatSorovi ni oladi va uni WHERE, ORDER BY ga aylantiradi — xizmat va router o'zgarmaydi.
- Router —
royxatSorovi(req.query)va javobdaproyeksiya(v, maydonlar);GET /vazifalarhandler'i to'liq keyingi darsda.
8. Hujumchi nigohi
| Hujum | Misol | Himoya |
|---|---|---|
| Yashirin maydon bo'yicha saralash | sort=parol_hash |
Saralash oq ro'yxati |
| ORDER BY injection | sort=matn;DROP TABLE… |
Ustun nomi — kod doimiysi |
| NoSQL operator | ?parol[$ne]=x (qs) |
"simple" parser, tur tekshiruvi |
| Turi kutilmagan | ?q=a&q=b → massiv → 500 |
bitta() → 400 |
| Og'ir qidiruv | q = 1 MB matn |
q 1–100 belgi |
| Yashirin maydonni so'rash | fields=parol |
Maydonlar oq ro'yxati |
9. Ko'p uchraydigan xatolar
| Xato | Oqibat | To'g'risi |
|---|---|---|
| Noma'lum parametr jim e'tiborsiz | Harf xatosi — filtrsiz ro'yxat | 400 |
req.query.teg ni doim satr deb bilish |
Ikki teg — TypeError |
Massiv yoki satr |
Saralashda id yo'q |
Teng qiymatlar tartibi tasodifiy | Oxirgi kalit — id |
fields da id ni tashlab yuborish |
Mijoz elementga murojaat qila olmaydi | id doim |
extended parser "qulaylik uchun" |
Operator in'ektsiyasi | "simple" |
"true" satrini Boolean() bilan |
Boolean("false") — true |
Faqat "true"/"false" |
10. Mashqlar
Yechimlar kurs/mashqlar/25/21-filtr-saralash/ da.
1-mashq (oson): to'ldiring
- Yaratilish vaqti bo'yicha, yangisi oldin —
sort= #uyva#bozortegli vazifalar —?teg=uy&- Faqat
matnmaydoni (vaid) —?=matn - Noma'lum parametrga status —
Yechim
- -yaratildi — minus kamayish tartibi.
- teg=bozor — takrorlangan kalit, AND ma'nosida.
- fields — sparse fieldsets;
idserver o'zi qo'shadi. - 400 — jim e'tiborsiz qoldirish xatoni yashiradi.
2-mashq (o'rta): mantiqiy va butun
Ikki funksiya yozing (URLSearchParams bilan, Express'siz): mantiqiy(p, nom) — faqat "true"/"false", aks holda Error; parametr yo'q — undefined; ikki marta kelsa — Error. butun(p, nom, min, max) — faqat raqamlar (/^\d{1,6}$/) va oraliq ichida. Sinang: ?bajarildi=true&limit=20, ?bajarildi=ha, ?limit=500, ?limit=1e3, ?bajarildi=true&bajarildi=false. Yordam: p.getAll(nom) — URLSearchParams.
Yechim
function bitta(p, nom) {
const hammasi = p.getAll(nom);
if (hammasi.length === 0) return undefined;
if (hammasi.length > 1) throw new Error(`${nom} bir marta`);
return hammasi[0];
}
function mantiqiy(p, nom) {
const x = bitta(p, nom);
if (x === undefined) return undefined;
if (x !== "true" && x !== "false") {
throw new Error(`${nom}: true/false`);
}
return x === "true";
}
function butun(p, nom, min, max) {
const x = bitta(p, nom);
if (x === undefined) return undefined;
if (!/^\d{1,6}$/.test(x) || Number(x) < min || Number(x) > max) {
throw new Error(`${nom}: ${min}..${max}`);
}
return Number(x);
}
const sorovlar = ["bajarildi=true&limit=20", "bajarildi=ha",
"limit=500", "limit=1e3", "bajarildi=true&bajarildi=false"];
for (const qs of sorovlar) {
const p = new URLSearchParams(qs);
try {
const natija = [mantiqiy(p, "bajarildi"),
butun(p, "limit", 1, 100)];
console.log(qs, "→", JSON.stringify(natija));
} catch (xato) {
console.log(qs, "→ 400:", xato.message);
}
}Konsolda:
bajarildi=true&limit=20 → [true,20]
bajarildi=ha → 400: bajarildi: true/false
limit=500 → 400: limit: 1..100
limit=1e3 → 400: limit: 1..100
bajarildi=true&bajarildi=false → 400: bajarildi bir martalimit=1e3 — Number("1e3") 1000 beradi, lekin regex uni oldinroq rad etadi. Number() ga to'g'ridan ishonmang: Number("") — 0, Number(" 5 ") — 5.
3-mashq (qiyin): to'liq royxatOl
royxatOl(vazifalar, queryString) yozing: bajarildi, teg (takrorlanadi, # va katta harf normallanadi), q, sort, fields ni qo'llasin; noma'lum parametr — Error. Kanon testidagi olti vazifa bilan uch so'rov: teg=uy, teg=%23ISH&bajarildi=true, sort=-bajarildi,matn&fields=matn. Yordam: «Saralash» va «Maydon tanlash» bo'limlaridagi funksiyalar.
Yechim
const RUXSAT = ["bajarildi", "teg", "q", "sort", "fields"];
const SARALASH = ["id", "matn", "bajarildi"];
function royxatOl(vazifalar, qs) {
const p = new URLSearchParams(qs);
for (const k of p.keys()) {
if (!RUXSAT.includes(k)) {
throw new Error(`noma'lum parametr: ${k}`);
}
}
const b = p.get("bajarildi");
const teglar = p.getAll("teg")
.map((t) => t.replace(/^#/, "").toLowerCase());
const q = p.get("q")?.toLowerCase();
let natija = vazifalar.filter((v) =>
(b === null || v.bajarildi === (b === "true")) &&
teglar.every((t) => v.teglar.includes(t)) &&
(q === undefined || v.matn.toLowerCase().includes(q)));
const kalitlar = (p.get("sort") ?? "").split(",").filter(Boolean)
.map((s) => ({ m: s.replace(/^-/, ""), kam: s.startsWith("-") }));
for (const { m } of kalitlar) {
if (!SARALASH.includes(m)) throw new Error(`sort: ${m}`);
}
kalitlar.push({ m: "id", kam: false });
natija = natija.sort((x, y) => {
for (const { m, kam } of kalitlar) {
if (x[m] === y[m]) continue;
return (x[m] < y[m] ? -1 : 1) * (kam ? -1 : 1);
}
return 0;
});
const fields = p.get("fields");
if (fields === null) return natija.map((v) => v.id);
const m = ["id", ...fields.split(",")];
return natija.map((v) =>
Object.fromEntries(m.map((k) => [k, v[k]])));
}
const matnlar = ["Non olish #uy", "Hisobot #ish", "Sut #uy #bozor",
"Kitob o'qish #o'quv", "Yig'ilish #ish", "Olma #bozor"];
const vazifalar = matnlar.map((matn, i) => ({
id: i + 1, matn, bajarildi: i % 2 === 1,
teglar: [...matn.matchAll(/#([\p{L}']+)/gu)].map((x) => x[1]),
}));
console.log(JSON.stringify(royxatOl(vazifalar, "teg=uy")));
console.log(JSON.stringify(royxatOl(vazifalar,
"teg=%23ISH&bajarildi=true")));
const r = royxatOl(vazifalar, "sort=-bajarildi,matn&fields=matn");
console.log(r.map((v) => v.id).join(", "));
console.log(r.slice(0, 3).map((v) => v.matn).join(" | "));Konsolda:
[1,3]
[2]
2, 4, 6, 1, 3, 5
Hisobot #ish | Kitob o'qish #o'quv | Olma #bozorNatijalar kanon testidagi bilan aynan bir xil: [1, 3], [2], [2, 4, 6, 1, 3, 5] va uchta bajarilgan matn.
4-mashq: API qadami
sorov.ts ni yozing (RUXSAT, bitta, mantiqiy, butun, saralashniOqi, maydonlarniOqi, royxatSorovi, proyeksiya), ombor interfeysini RoyxatSorovi ga o'tkazing, royxat-mantiq.ts da filtrla va kalitlar ni yozing. Testlar: filtrlar (teg AND, %23ISH, q), ?bajarlidi=true → 400, ?q=a&q=b → 400, sort=-bajarildi,matn&fields=matn tartibi, sort=parol → 400. Commit qilmang.
Yechim
Kanon testlari (test/rest.test.ts, qisqartirilgan):
const idlar = async (yol: string) =>
(await ol(yol)).tana.vazifalar.map((v) => v.id);
assert.deepEqual(await idlar("/vazifalar?teg=uy"), [1, 3]);
assert.deepEqual(await idlar("/vazifalar?teg=uy&teg=bozor"), [3]);
assert.deepEqual(
await idlar("/vazifalar?teg=%23ISH&bajarildi=true"), [2]);
assert.equal((await ol("/vazifalar?bajarlidi=true")).holat, 400);
assert.equal((await ol("/vazifalar?q=a&q=b")).holat, 400);
assert.equal((await ol("/vazifalar?sort=parol")).holat, 400);Sahifalash (limit, offset, cursor), kursor.ts va sahifala — keyingi darsda; npm run check o'shanda yashil bo'ladi (bitta kesim).
11. Real ishda
- Har admin panel va katalog — filtr, saralash, qidiruv. Uzum Market, Olx kabi saytlarning manzil satriga qarang:
?sort=price_asc&category=…— aynan shu konvensiyalar. - Xavfsizlik auditida saralash va filtr parametrlari — SQL/NoSQL injection uchun birinchi tekshiriladigan joy.
- Intervyu: "Sort parametrini qanday xavfsiz qilasiz?", "Sparse fieldsets nima?", "Noma'lum query parametrga nima qaytarasiz?".
Xulosa
- Konvensiya:
?bajarildi=true&teg=uy&q=non&sort=-yaratildi,matn&fields=matn— bitta API'da bitta uslub. - Saralash:
-— kamayish; oq ro'yxat (sizish, injection, indeks); oxirgi kalit —id(barqaror tartib). fields: noma'lum — 400,iddoim.- Express 5 "simple" parser: takrorlangan kalit — massiv, qavs — oddiy belgi; "extended" (
qs) —[$ne]xavfi. - Noma'lum parametr va noto'g'ri tur — 400;
req.queryomborga tekshirilganRoyxatSorovibo'lib boradi.
Keyingi dars: Pagination: offset, cursor, keyset — katta ro'yxatni sahifalash, Link sarlavhasi va holat 22 ning commiti.
Manbalar
- JSON:API — Sparse Fieldsets va Sorting: https://jsonapi.org/format/#fetching-sparse-fieldsets (2026-10-24)
- Microsoft REST API Guidelines — filtrlash va saralash: https://github.com/microsoft/api-guidelines
qskutubxonasi (arrayLimit 20, depth 5): https://github.com/ljharb/qs- OWASP — NoSQL injection: https://owasp.org/www-project-web-security-testing-guide/
- Kanon: holat 22 (
sorov.ts,royxat-mantiq.ts,ombor.ts, testlar),tekshiruv-22(curl),t10(simple parser). "extended" parser chiqishi — biz ishga tushirdik (express 5.3.0, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!