IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API21/51-dars21 daqiqa
Mundarija (22)

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=-maydon ni 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-api ning GET /vazifalar iga filtr, sort va fields ni 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):

text
$ 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:

js
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:

text
2, 4, 1, 3
sort: noma'lum maydon "parol"

Ikki muhim tafsilot:

  1. Oxirgi kalit — id. Ikki vazifaning bajarildi va matn i 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). Kanon kalitlar() funksiyasi shuning uchun oxiriga doim id qo'shadi.
  2. Satrlar kod nuqtasi bo'yicha (a < b), localeCompare emas. Natija deterministik, lekin "o'zbekcha alifbo" emas: katta harflar kichiklardan oldin keladi. Postgres'da ORDER BY matn collation bo'yicha tartiblaydi — tartib boshqacha bo'lishi mumkin. Bu SQL qismida hal qilinadi.

3.2 Nega oq ro'yxat

Kanonda sort=parol → 400:

text
$ curl $A/vazifalar?sort=parol
{"xato":"sort: noma'lum maydon \"parol\" (mumkin: id, matn, bajarildi, yaratildi, yangilandi)"}

Uch sabab:

  • Ma'lumot sizishi. Foydalanuvchida parol_hash maydoni 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 $1 da $1 ustun nomi emas, oddiy qiymat bo'lib qoladi va saralash ishlamaydi. Satrni yopishtirsangiz — sort=matn;DROP TABLE xavfi. 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,-matn keldi. 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.

js
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:

text
{"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:

text
?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:

text
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:

ts
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):

text
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:

text
$ 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=%23ISH ham #ish tegli 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.

  1. src/vazifalar/ombor.ts — interfeys to'liq:
ts
// 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.

  1. src/vazifalar/sorov.ts — to'liq (sahifalash qismi keyingi darsda tahlil qilinadi):
ts
// 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.

  1. src/vazifalar/royxat-mantiq.ts — fayl boshi: filtrla va kalitlar (qolgani — sahifala — keyingi darsda):
ts
// 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.

  1. Router — royxatSorovi(req.query) va javobda proyeksiya(v, maydonlar); GET /vazifalar handler'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=
  • #uy va #bozor tegli vazifalar — ?teg=uy&
  • Faqat matn maydoni (va id) — ?=matn
  • Noma'lum parametrga status —
Yechim
  • -yaratildi — minus kamayish tartibi.
  • teg=bozor — takrorlangan kalit, AND ma'nosida.
  • fields — sparse fieldsets; id server 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
js
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:

text
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 marta

limit=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
js
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:

text
[1,3]
[2]
2, 4, 6, 1, 3, 5
Hisobot #ish | Kitob o'qish #o'quv | Olma #bozor

Natijalar 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):

ts
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, id doim.
  • Express 5 "simple" parser: takrorlangan kalit — massiv, qavs — oddiy belgi; "extended" (qs) — [$ne] xavfi.
  • Noma'lum parametr va noto'g'ri tur — 400; req.query omborga tekshirilgan RoyxatSorovi bo'lib boradi.

Keyingi dars: Pagination: offset, cursor, keyset — katta ro'yxatni sahifalash, Link sarlavhasi va holat 22 ning commiti.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
REST API'da filtrlash, saralash va maydon tanlash: query parametrlar — IlmHamroh