diff --git a/lessons/01-query-fundamentals/02-where-conditions/lesson.mdx b/lessons/01-query-fundamentals/02-where-conditions/lesson.mdx index e712427..639a7a7 100644 --- a/lessons/01-query-fundamentals/02-where-conditions/lesson.mdx +++ b/lessons/01-query-fundamentals/02-where-conditions/lesson.mdx @@ -103,4 +103,4 @@ ORDER BY signed_up_at; Mark this lesson done — we'll just confirm the sandbox is healthy. -Up next: aggregating rows together with `GROUP BY`. +Up next: sorting results predictably and paging through them without falling into the `OFFSET` trap. diff --git a/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.mdx b/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.mdx new file mode 100644 index 0000000..bc361f8 --- /dev/null +++ b/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.mdx @@ -0,0 +1,129 @@ +Lesson 01 introduced `ORDER BY` and `LIMIT`. This lesson goes deeper: stable sorts, dropping duplicates, and the two ways to page through a result set — including why one of them quietly breaks in production. + +The seed loaded an `articles` table — 30 rows, several authors, and a few intentional ties on `published_at`. + +## `ORDER BY`: pick a direction + +`ORDER BY col` sorts ascending (small to large, oldest first). Add `DESC` to flip it. + + +SELECT title, views +FROM articles +ORDER BY views DESC +LIMIT 5; + + +The top-5 most-viewed posts. Without `LIMIT 5`, you'd get all 30, sorted. + +## Tie-breakers + +What happens when two rows have the same sort key? Postgres returns them in *some* order — but which one is implementation-defined. If the order matters, spell out a tie-breaker. + + +SELECT title, author, published_at +FROM articles +ORDER BY published_at, id; + + +`published_at` has duplicates (Ada's two articles, Grace's first two — see the seed). Adding `id` as a second sort key makes the result *deterministic*: same query, same order, every time. For pagination this isn't optional — it's a correctness requirement, as we'll see in a minute. + +## `NULLS FIRST` / `NULLS LAST` + +Postgres puts `NULL`s **last** in ascending sorts and **first** in descending sorts. Override with `NULLS FIRST` or `NULLS LAST` when you want the opposite — most often when you want "newest first, but missing dates at the bottom". + + +SELECT title, published_at +FROM articles +ORDER BY published_at DESC NULLS LAST; + + +The seed now includes a couple of `NULL` `published_at` values so you can see this directly. + +## `DISTINCT`: drop duplicate rows + +`DISTINCT` removes duplicate rows from the result. It's a post-processing step on whatever the `SELECT` list produced. + + +SELECT DISTINCT author +FROM articles +ORDER BY author; + + +Twelve articles came from a handful of repeat authors — `DISTINCT` collapses them. Note `DISTINCT` is *across all selected columns*, not just one: `SELECT DISTINCT author, published_at` would keep two rows from the same author on different days. + +### `DISTINCT ON (...)`: one row per group, Postgres-flavored + +`DISTINCT ON (col)` is a Postgres extension: "one row per distinct value of `col`, and you pick which one with `ORDER BY`". Handy for "the latest article per author": + + +SELECT DISTINCT ON (author) author, title, published_at +FROM articles +ORDER BY author, published_at DESC; + + +The first column(s) in the `ORDER BY` must match the `DISTINCT ON` list — that's the rule that lets Postgres pick "the first row per group". Inside each author, `published_at DESC` chooses the newest. + +## `LIMIT` and `OFFSET`: the obvious way to paginate + +`LIMIT N OFFSET M` says "skip M rows, then return N". The classic page-2-of-10 query: + + +SELECT id, title, published_at +FROM articles +ORDER BY published_at DESC NULLS LAST, id DESC +LIMIT 10 OFFSET 10; + + +That's page 2 (rows 11–20). Page 3 would be `OFFSET 20`. Simple, and the right tool for small result sets. + +## The `OFFSET` trap + +`OFFSET M` makes Postgres fetch *and discard* M rows before returning anything. On page 1 that's free. On page 1000 of a million-row feed, you're scanning a million rows to throw away 999,990 of them. + +There's a subtler bug too: if a new row gets inserted between requesting page 1 and page 2, page 2 will repeat a row from page 1 (because everything shifted down by one). The result set isn't stable across requests. + +For small admin tables, `OFFSET` is fine. For user-facing feeds, infinite scroll, or anything that paginates deeply, reach for keyset pagination. + +## Keyset pagination: page by `WHERE` + +Idea: instead of "skip 10,000 rows", remember the *last row you saw* and ask for "rows after that one". With a deterministic `ORDER BY`, that's just a `WHERE` clause. + +Page 1: + + +SELECT id, title, published_at +FROM articles +ORDER BY published_at DESC NULLS LAST, id DESC +LIMIT 5; + + +Note the last row's `published_at` and `id`. To get the next page, plug them into a `WHERE` filter that asks for everything strictly after that key: + + +SELECT id, title, published_at +FROM articles +WHERE (published_at, id) < ('2024-06-24 08:30:00+00', 26) +ORDER BY published_at DESC NULLS LAST, id DESC +LIMIT 5; + + +Two important details: + +1. **The tuple comparison `(a, b) < (x, y)`** does lexicographic ordering — `a < x`, OR `a = x AND b < y`. That's exactly the tie-breaker logic we wrote into `ORDER BY`. They have to match. +2. **No `OFFSET`**. Each page is a fresh `WHERE` lookup that an index on `(published_at DESC, id DESC)` can serve in constant time, no matter how deep you go. + +The downside: you can't jump to "page 42" — you walk forward one page at a time. For feeds and infinite scroll that's fine; for an admin grid with a page picker, `OFFSET` is the easier fit. + +## What you learned + +- `ORDER BY` sorts; add `DESC` and `NULLS FIRST`/`LAST` as needed. +- Always include a tie-breaker (typically the primary key) for deterministic order. +- `DISTINCT` drops duplicate rows; `DISTINCT ON (col)` picks one row per group, chosen by `ORDER BY`. +- `LIMIT N OFFSET M` is the obvious way to paginate — and gets slow and unstable on deep pages. +- Keyset pagination (`WHERE (key) < (last_seen)`) pages in constant time and survives concurrent inserts. + + +Mark this lesson done — we'll just confirm the sandbox is healthy. + + +Up next: collapsing rows into summaries with aggregations and `GROUP BY`. diff --git a/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.yaml b/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.yaml new file mode 100644 index 0000000..a434773 --- /dev/null +++ b/lessons/01-query-fundamentals/03-sorting-and-pagination/lesson.yaml @@ -0,0 +1,19 @@ +title: Sorting and pagination +summary: Order results predictably, drop duplicates with DISTINCT, and page through with LIMIT/OFFSET — and why keyset pagination is the better default. +estimatedMinutes: 12 +tags: + - order-by + - distinct + - limit + - offset + - pagination +authors: + - exekias +seed: seed.sql +checks: + - id: seed-loaded + type: row-count + description: The seeded articles table has 30 rows — click to mark this lesson done. + table: articles + expect: + rowCount: 30 diff --git a/lessons/01-query-fundamentals/03-sorting-and-pagination/seed.sql b/lessons/01-query-fundamentals/03-sorting-and-pagination/seed.sql new file mode 100644 index 0000000..fc2d0e9 --- /dev/null +++ b/lessons/01-query-fundamentals/03-sorting-and-pagination/seed.sql @@ -0,0 +1,43 @@ +-- Seed for "03-sorting-and-pagination": a small articles feed with a few +-- intentional ties on published_at so ORDER BY tie-breakers are visible, and +-- some duplicate authors so DISTINCT has something to do. + +CREATE TABLE articles ( + id serial PRIMARY KEY, + title text NOT NULL, + author text NOT NULL, + views int NOT NULL, + published_at timestamptz +); + +INSERT INTO articles (title, author, views, published_at) VALUES + ('Indexing for humans', 'Ada Lovelace', 1200, '2024-01-05 09:00:00+00'), + ('The case for MVCC', 'Alan Turing', 890, '2024-01-05 09:00:00+00'), + ('Why your query is slow', 'Grace Hopper', 4300, '2024-01-12 14:00:00+00'), + ('EXPLAIN, explained', 'Grace Hopper', 3100, '2024-01-20 10:30:00+00'), + ('B-trees from first principles','Donald Knuth', 2750, '2024-01-28 08:15:00+00'), + ('Postgres tips, vol 1', 'Ada Lovelace', 640, '2024-02-04 16:45:00+00'), + ('Postgres tips, vol 2', 'Ada Lovelace', 720, '2024-02-11 16:45:00+00'), + ('Joins by example', 'Linus Torvalds', 1810, '2024-02-19 12:00:00+00'), + ('LATERAL is fine, actually', 'Barbara Liskov', 980, '2024-02-26 11:00:00+00'), + ('When to use JSONB', 'Guido van Rossum', 2210, '2024-03-04 09:30:00+00'), + ('When not to use JSONB', 'Guido van Rossum', 1560, '2024-03-11 09:30:00+00'), + ('Window functions in anger', 'Margaret Hamilton', 3380, '2024-03-18 13:20:00+00'), + ('Reading EXPLAIN ANALYZE', 'Grace Hopper', 4710, '2024-03-25 13:20:00+00'), + ('Vacuum and bloat', 'Dennis Ritchie', 430, '2024-04-01 07:00:00+00'), + ('A small note on COLLATE', 'Bjarne Stroustrup', 210, NULL), + ('GIN vs GiST', 'Donald Knuth', 1990, '2024-04-15 10:00:00+00'), + ('Trigram search basics', 'Ken Thompson', 870, '2024-04-22 10:00:00+00'), + ('CTEs are not optimization fences anymore','Linus Torvalds', 2540, '2024-04-29 15:15:00+00'), + ('Schema migrations without tears','Barbara Liskov', 3050, '2024-05-06 11:45:00+00'), + ('Idempotent INSERTs with ON CONFLICT','Margaret Hamilton', 2890, '2024-05-13 11:45:00+00'), + ('Three flavors of UUID', 'Edsger Dijkstra', 760, '2024-05-20 09:00:00+00'), + ('Counting is harder than it looks','Ada Lovelace', 1680, '2024-05-27 14:30:00+00'), + ('Pagination, the LIMIT/OFFSET trap','Grace Hopper', 5120, '2024-06-03 14:30:00+00'), + ('Pagination, the keyset way', 'Grace Hopper', 4870, '2024-06-10 14:30:00+00'), + ('Date math in Postgres', 'Guido van Rossum', 930, '2024-06-17 08:30:00+00'), + ('Time zones, again', 'Bjarne Stroustrup', 410, '2024-06-24 08:30:00+00'), + ('Generated columns: hidden gems','Dennis Ritchie', 1240, '2024-07-01 17:00:00+00'), + ('Foreign keys revisited', 'Ken Thompson', 1080, '2024-07-08 17:00:00+00'), + ('Locking, lightly', 'Edsger Dijkstra', 1340, '2024-07-15 12:30:00+00'), + ('How autovacuum keeps you sane','Margaret Hamilton', 980, NULL); diff --git a/lessons/01-query-fundamentals/03-aggregations/lesson.mdx b/lessons/01-query-fundamentals/04-aggregations/lesson.mdx similarity index 100% rename from lessons/01-query-fundamentals/03-aggregations/lesson.mdx rename to lessons/01-query-fundamentals/04-aggregations/lesson.mdx diff --git a/lessons/01-query-fundamentals/03-aggregations/lesson.yaml b/lessons/01-query-fundamentals/04-aggregations/lesson.yaml similarity index 100% rename from lessons/01-query-fundamentals/03-aggregations/lesson.yaml rename to lessons/01-query-fundamentals/04-aggregations/lesson.yaml diff --git a/lessons/01-query-fundamentals/03-aggregations/seed.sql b/lessons/01-query-fundamentals/04-aggregations/seed.sql similarity index 100% rename from lessons/01-query-fundamentals/03-aggregations/seed.sql rename to lessons/01-query-fundamentals/04-aggregations/seed.sql diff --git a/package-lock.json b/package-lock.json index e63df2a..b2d27ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -298,7 +297,6 @@ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.11.tgz", "integrity": "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ==", "license": "MIT", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", @@ -417,7 +415,6 @@ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "^2.0.1" } @@ -425,8 +422,7 @@ "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", - "peer": true + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", @@ -487,7 +483,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -504,7 +499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -521,7 +515,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -538,7 +531,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -555,7 +547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -572,7 +563,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -589,7 +579,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -606,7 +595,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -623,7 +611,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -640,7 +627,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -657,7 +643,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -674,7 +659,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -691,7 +675,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,7 +691,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -725,7 +707,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -742,7 +723,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -759,7 +739,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -776,7 +755,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -793,7 +771,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -810,7 +787,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -827,7 +803,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -844,7 +819,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -911,7 +885,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -928,7 +901,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -945,7 +917,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -962,7 +933,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -979,7 +949,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -996,7 +965,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1013,7 +981,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1030,7 +997,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1047,7 +1013,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1064,7 +1029,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1081,7 +1045,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1098,7 +1061,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1115,7 +1077,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1132,7 +1093,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1149,7 +1109,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1166,7 +1125,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1183,7 +1141,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1200,7 +1157,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1217,7 +1173,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1234,7 +1189,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1251,7 +1205,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1268,7 +1221,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,7 +1237,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1302,7 +1253,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1319,7 +1269,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1336,7 +1285,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2153,7 +2101,6 @@ "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz", "integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=19.0.0" } @@ -2791,7 +2738,6 @@ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -2803,7 +2749,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2879,7 +2824,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -3425,7 +3369,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3852,7 +3795,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", @@ -3912,7 +3854,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4379,7 +4320,6 @@ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", @@ -4395,7 +4335,6 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -4839,7 +4778,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5025,7 +4963,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5492,7 +5429,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6452,7 +6388,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -6553,7 +6488,6 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -7789,7 +7723,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -7822,7 +7755,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", @@ -8208,7 +8140,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -8472,7 +8403,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8482,7 +8412,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9336,8 +9265,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", @@ -9394,7 +9322,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9507,7 +9434,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9524,7 +9450,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9541,7 +9466,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9558,7 +9482,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9575,7 +9498,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9592,7 +9514,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9609,7 +9530,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9626,7 +9546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9643,7 +9562,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9660,7 +9578,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9677,7 +9594,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9694,7 +9610,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9711,7 +9626,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9728,7 +9642,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9745,7 +9658,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9762,7 +9674,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9779,7 +9690,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9796,7 +9706,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9813,7 +9722,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9830,7 +9738,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9847,7 +9754,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9864,7 +9770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9881,7 +9786,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9898,7 +9802,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9915,7 +9818,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9932,7 +9834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10081,7 +9982,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10565,7 +10465,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }