@ultimat3/query
Advanced tools
+6
-6
| { | ||
| "name": "@ultimat3/query", | ||
| "version": "9.0.0", | ||
| "version": "10.0.0", | ||
| "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher", | ||
@@ -34,8 +34,8 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@ultimat3/cache": "9.0.0", | ||
| "@ultimat3/core": "9.0.0", | ||
| "@ultimat3/http": "9.0.0", | ||
| "@ultimat3/policy": "9.0.0", | ||
| "@ultimat3/schema": "9.0.0" | ||
| "@ultimat3/cache": "10.0.0", | ||
| "@ultimat3/core": "10.0.0", | ||
| "@ultimat3/http": "10.0.0", | ||
| "@ultimat3/policy": "10.0.0", | ||
| "@ultimat3/schema": "10.0.0" | ||
| } | ||
| } |
+12
-14
@@ -5,3 +5,8 @@ /** Every failure @ultimat3/query can produce, one subclass per stable code. */ | ||
| const docs = (code: string): string => `https://ultimate.dev/errors/${code}`; | ||
| // No `docs:` on the classes below, with one exception noted at `QueryRequestFailedError`. | ||
| // `UltimateError` fills it from `describeErrorCode(code).docs`, which is `@ultimat3/core`'s | ||
| // `ERROR_DOCS_URL` — one page for every code, never one per code, because `wiki/` is the | ||
| // framework's only public documentation surface and a code lives there in a TABLE ROW, which has | ||
| // no anchor. The `https://ultimate.dev/errors/<code>` links this file built until 9.x answered | ||
| // 404, host included, on every read this package has ever refused. | ||
@@ -80,3 +85,2 @@ /** One class, one code: core owns the cursor codec, so core owns `X_CURSOR_INVALID`. */ | ||
| fix: `x policy explain ${query} --json # shows which clause decided and why`, | ||
| docs: docs(code), | ||
| }); | ||
@@ -94,3 +98,2 @@ this.denial = denial; | ||
| fix: "call registerQueries(await import('./live')) at boot, before serving reads", | ||
| docs: docs('X_QUERY_UNREGISTERED'), | ||
| }); | ||
@@ -112,3 +115,2 @@ } | ||
| fix: "declare it as `export const name = query({ input, policy, sql })` from '@ultimat3/query'", | ||
| docs: docs('X_QUERY_FOREIGN'), | ||
| }); | ||
@@ -132,3 +134,2 @@ } | ||
| fix: 'flatten the key into scalar arguments (status: t.string, limit: t.number), spell an absent value as `.optional()` rather than `t.nullable(...)`, or declare it as an action() if it really needs a JSON body', | ||
| docs: docs('X_QUERY_INPUT_UNENCODABLE'), | ||
| }); | ||
@@ -156,3 +157,2 @@ } | ||
| fix: 'set `cache: { ttlMs: 60_000 }` to a positive whole number of milliseconds, or drop ttlMs to take the read cache default', | ||
| docs: docs('X_QUERY_CACHE_TTL_INVALID'), | ||
| meta: { ttlMs }, | ||
@@ -169,3 +169,2 @@ }); | ||
| fix: 'rename one export — query names are globally unique: x queries list --json', | ||
| docs: docs('X_QUERY_DUPLICATE'), | ||
| }); | ||
@@ -189,3 +188,2 @@ } | ||
| fix: `add \`policy: can('<resource>:<verb>')\` to the query() that exports "${name}" — a permission your definePermissions() call declares, never the query's own name — or \`allow('<resource>:<verb>')\` to state that the read is public`, | ||
| docs: docs('X_QUERY_POLICY_MISSING'), | ||
| }); | ||
@@ -207,3 +205,2 @@ } | ||
| fix: `edit \`deprecated: { ${field}: … }\` on ${name} to an ISO-8601 instant — e.g. '2026-12-31T23:59:59Z'`, | ||
| docs: docs('X_QUERY_DEPRECATION_INVALID'), | ||
| meta: { query: name, field, value }, | ||
@@ -226,3 +223,2 @@ }); | ||
| fix: `return the primary key from the query's sql: db.${entity ?? 'rows'}.select({ id: true, … })`, | ||
| docs: docs('X_QUERY_NOT_PAGEABLE'), | ||
| }); | ||
@@ -250,3 +246,2 @@ } | ||
| fix: 'order by a scalar column — .orderBy("createdAt") or .orderBy("id") — and project the composite value into the row instead', | ||
| docs: docs('X_CURSOR_VALUE_UNSUPPORTED'), | ||
| }); | ||
@@ -263,3 +258,2 @@ } | ||
| fix: `set \`live: false\` and poll, or reshape the query to equality filters + orderBy + limit`, | ||
| docs: docs('X_MATCHER_UNSUPPORTED'), | ||
| }); | ||
@@ -275,3 +269,2 @@ } | ||
| fix: `x queries describe ${name} --json # prints the expected input schema`, | ||
| docs: docs('X_INPUT_INVALID'), | ||
| }); | ||
@@ -299,2 +292,3 @@ } | ||
| const code = text(problem.code) ?? 'X_RPC_FAILED'; | ||
| const served = text(problem.docs); | ||
| super({ | ||
@@ -306,3 +300,7 @@ code, | ||
| `check the gateway in front of the app, then: x queries describe ${name} --json`, | ||
| docs: text(problem.docs) ?? docs(code), | ||
| // The one place a `docs` is passed: the SERVER's, re-thrown verbatim beside the cause and | ||
| // fix it came with. An app that documents its own codes somewhere else is entitled to say | ||
| // so, and overwriting it with this framework's page would bury the answer. Absent, the | ||
| // constructor resolves `ERROR_DOCS_URL` like every other error here. | ||
| ...(served === undefined ? {} : { docs: served }), | ||
| }); | ||
@@ -309,0 +307,0 @@ } |
+19
-5
@@ -18,3 +18,3 @@ /** | ||
| import type { QueryShape, SeekKey } from './shape'; | ||
| import { seekKeyOf } from './shape'; | ||
| import { compareRows, seekKeyOf, totalOrder } from './shape'; | ||
| import type { SqlSource } from './source'; | ||
@@ -74,3 +74,3 @@ import { isAfterKey } from './source'; | ||
| const executed = await source.execute(); | ||
| const scoped = base.seek === undefined ? sliceAfter(executed, after, shape) : executed; | ||
| const scoped = base.seek === undefined ? inTotalOrder(executed, after, shape) : executed; | ||
| // The source came from this query's own `sql()`, so its rows are TRow. | ||
@@ -97,4 +97,16 @@ const rows = scoped.slice(0, args.first) as unknown as readonly TRow[]; | ||
| * silent restart, which is the failure keyset pagination exists to make impossible. | ||
| * | ||
| * **It SORTS first, and that half was missing.** `isAfterKey` breaks a tie on the declared keys by | ||
| * `id` — it has to, or the cut is not a position at all — while a foreign `SqlSource` ordered its | ||
| * rows by the DECLARED keys alone. So a tie group arrived in an order the cut does not describe, | ||
| * and the cut fell in the middle of it: page one served `a(10), d(20)`, the cursor named `(20, d)`, | ||
| * and rows `b(20)` and `c(20)` matched no page in the listing. Rows VANISH — silently, and only | ||
| * where two rows share a sort key. | ||
| * | ||
| * `Builder` has always done this: `execute()` sorts by `servedOrder()`, which appends `id` once a | ||
| * read asked to be seekable. This is that rule applied to the path a `Builder` does not take, and | ||
| * it is the ordering the cursor arithmetic already assumes on both sides. Reachable only for a | ||
| * hand-written `SqlSource` with no `seek()` — the branch this whole function exists for. | ||
| */ | ||
| function sliceAfter( | ||
| function inTotalOrder( | ||
| rows: readonly object[], | ||
@@ -104,4 +116,6 @@ after: SeekKey | null, | ||
| ): readonly object[] { | ||
| if (after === null) return rows; | ||
| return rows.filter((row) => isAfterKey(row, after, shape.orderBy)); | ||
| const keys = totalOrder(shape.orderBy); | ||
| const ordered = [...rows].sort((left, right) => compareRows(left, right, keys)); | ||
| if (after === null) return ordered; | ||
| return ordered.filter((row) => isAfterKey(row, after, shape.orderBy)); | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
204237
0.72%2916
0.41%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated