@ultimat3/entity 16.0.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -2
- package/package.json +5 -5
- package/src/errors.ts +17 -1
- package/src/plan.ts +6 -4
- package/src/query.ts +3 -3
package/CLAUDE.md
CHANGED
|
@@ -245,10 +245,12 @@ Columns + invariants; the row type is derived from the columns. Tier 2.
|
|
|
245
245
|
number, answers the read they did. `limit(rows)` was `next({ limit: rows })` and nothing else —
|
|
246
246
|
no integer check, no positivity check, no ceiling — so an action taking `pageSize` as input and
|
|
247
247
|
passing it through bound whatever a client sent, and one request could ask for five million rows.
|
|
248
|
-
`
|
|
248
|
+
`assertFinitePageSize` is `assertBatchable`'s three refusals in the other call, deliberately under the
|
|
249
249
|
same code (`X_INVARIANT_VIOLATED`) because `limit(0)` and `inBatches(0)` are one mistake in two
|
|
250
250
|
places. Called from **both** `limit()` on the chain (so the refusal lands on the line the author
|
|
251
|
-
wrote) and `
|
|
251
|
+
wrote) and both `plan()` builders, on the RESOLVED page — so `findMany({ limit })` straight at the
|
|
252
|
+
repository cannot route around it, and `bun run finite-bounds` can see the repair, which it could
|
|
253
|
+
not while the screen was spelled `assertPageSize` and took a parameter called `rows` —
|
|
252
254
|
and `MAX_PAGE_SIZE` bounds `inBatches(size)` too — a batch IS a page, so the ceiling belongs to
|
|
253
255
|
the range and not to one of the two calls.
|
|
254
256
|
- **A repository pinned to its own client refuses to run inside a transaction**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/entity",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "A table + its domain type + invariants the database also enforces",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
35
|
-
"@ultimat3/db": "
|
|
36
|
-
"@ultimat3/schema": "
|
|
37
|
-
"@ultimat3/time": "
|
|
34
|
+
"@ultimat3/core": "17.0.0",
|
|
35
|
+
"@ultimat3/db": "17.0.0",
|
|
36
|
+
"@ultimat3/schema": "17.0.0",
|
|
37
|
+
"@ultimat3/time": "17.0.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/errors.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// that fixes the situation — `X_DB_DRIFT` is the flagship: it names the table, the
|
|
3
3
|
// column and the generator invocation.
|
|
4
4
|
import { registerErrorCodes, UltimateError } from '@ultimat3/core';
|
|
5
|
+
import { shellInertIdentifier } from '@ultimat3/db';
|
|
5
6
|
|
|
6
7
|
/** Codes this package declares and owns. */
|
|
7
8
|
export const ENTITY_OWNED_ERROR_CODES = [
|
|
@@ -293,11 +294,26 @@ export const repoClientPinned = (entityName: string): EntityError =>
|
|
|
293
294
|
fix: `setDbClient(client) at boot and build the repository with no client: — postgresDriver() then resolves the open transaction through db() — or run this call outside withTransaction()`,
|
|
294
295
|
});
|
|
295
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The contract's pinned wording. Mirror of `@ultimat3/db`'s `dbDrift()` (`drift-errors.ts`) —
|
|
299
|
+
* keep in sync; both screen through the same `shellInertIdentifier`, so the two lines are one
|
|
300
|
+
* text on both sides of the tier seam, and `errors.test.ts` asserts it rather than asking.
|
|
301
|
+
*
|
|
302
|
+
* The column is the CATALOG's, so it is data, and `x db gen "add C"` puts it inside SHELL DOUBLE
|
|
303
|
+
* QUOTES where `$(…)` and a backtick substitute before `x` is reached at all. The argument is a
|
|
304
|
+
* migration DESCRIPTION, not an identifier, so no quoted form makes a hostile name safe to pass —
|
|
305
|
+
* a refused one is left OUT of the command rather than escaped into it, and read off `cause`.
|
|
306
|
+
*/
|
|
296
307
|
export const dbDrift = (tableName: string, columnName: string): EntityError =>
|
|
297
308
|
new EntityError({
|
|
298
309
|
code: 'X_DB_DRIFT',
|
|
299
310
|
cause: `table "${tableName}" has column "${columnName}" not present in any migration`,
|
|
300
|
-
fix:
|
|
311
|
+
fix:
|
|
312
|
+
shellInertIdentifier(columnName) === null
|
|
313
|
+
? 'x db gen "add the column named in this error" # its name carries a backtick, a ' +
|
|
314
|
+
'dollar sign, a quote, a backslash or whitespace, so it is in the cause and not in ' +
|
|
315
|
+
'this command'
|
|
316
|
+
: `x db gen "add ${columnName}"`,
|
|
301
317
|
});
|
|
302
318
|
|
|
303
319
|
export const notFound = (entityName: string, id: string): EntityError =>
|
package/src/plan.ts
CHANGED
|
@@ -88,8 +88,8 @@ export const totalOrder = <Row>(
|
|
|
88
88
|
* query, a third-party driver's caller — cannot route around it. One function, so the two can
|
|
89
89
|
* never disagree about what a page may be.
|
|
90
90
|
*/
|
|
91
|
-
export const
|
|
92
|
-
if (Number.isSafeInteger(rows) && rows >= 1 && rows <= MAX_PAGE_SIZE) return;
|
|
91
|
+
export const assertFinitePageSize = (entityName: string, rows: number): number => {
|
|
92
|
+
if (Number.isSafeInteger(rows) && rows >= 1 && rows <= MAX_PAGE_SIZE) return rows;
|
|
93
93
|
throw new EntityError({
|
|
94
94
|
code: 'X_INVARIANT_VIOLATED',
|
|
95
95
|
cause: `${entityName}.limit(${String(rows)}) — a page is a whole number of rows, at least one and at most ${MAX_PAGE_SIZE}`,
|
|
@@ -108,7 +108,6 @@ export const assertPageSize = (entityName: string, rows: number): void => {
|
|
|
108
108
|
* `cursorFor` and `seekFrom` still call it: they are reached from a driver directly.
|
|
109
109
|
*/
|
|
110
110
|
export const planFor = <Row>(entity: EntityCore<Row>, args: FindManyArgs): QueryPlan => {
|
|
111
|
-
if (args.limit !== undefined) assertPageSize(entity.$name, args.limit);
|
|
112
111
|
const orderBy = totalOrder(entity, args.orderBy ?? []);
|
|
113
112
|
assertSeekable(entity, orderBy);
|
|
114
113
|
const scoped =
|
|
@@ -119,7 +118,10 @@ export const planFor = <Row>(entity: EntityCore<Row>, args: FindManyArgs): Query
|
|
|
119
118
|
entity: entity.$name,
|
|
120
119
|
where: [...(args.where ?? []), ...scoped],
|
|
121
120
|
orderBy,
|
|
122
|
-
|
|
121
|
+
// Screened on the RESOLVED page, so `findMany({ limit })` straight at the repository — a
|
|
122
|
+
// generated client, a query, a third-party driver's caller — cannot route around the rule the
|
|
123
|
+
// chain's own `limit()` applies.
|
|
124
|
+
limit: assertFinitePageSize(entity.$name, args.limit ?? DEFAULT_PAGE_SIZE),
|
|
123
125
|
...(args.cursor === undefined || args.cursor === null ? {} : { cursor: args.cursor }),
|
|
124
126
|
...(args.select === undefined ? {} : { select: args.select }),
|
|
125
127
|
};
|
package/src/query.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { assertBatchable, batchIterator } from './batch';
|
|
|
9
9
|
import { entityNow } from './clock';
|
|
10
10
|
import type { EntityCore } from './entity';
|
|
11
11
|
import { searchUndeclared } from './feature-errors';
|
|
12
|
-
import {
|
|
12
|
+
import { assertFinitePageSize, DEFAULT_PAGE_SIZE, namedColumns } from './plan';
|
|
13
13
|
import type { RelatedTables } from './preload';
|
|
14
14
|
import { preloaded } from './preload';
|
|
15
15
|
import type { Relation } from './relations';
|
|
@@ -305,7 +305,7 @@ const builder = <Source, Row>(
|
|
|
305
305
|
// sized. `planFor` applies the identical guard, so a caller reaching the repository directly
|
|
306
306
|
// cannot go round it.
|
|
307
307
|
limit: (rows) => {
|
|
308
|
-
|
|
308
|
+
assertFinitePageSize(entity.$name, rows);
|
|
309
309
|
return next({ limit: rows });
|
|
310
310
|
},
|
|
311
311
|
|
|
@@ -400,7 +400,7 @@ const builder = <Source, Row>(
|
|
|
400
400
|
where: state.where,
|
|
401
401
|
orderBy: state.orderBy,
|
|
402
402
|
// The page that will actually run, so an unnamed one still reads as the bound it has.
|
|
403
|
-
limit: state.limit ?? DEFAULT_PAGE_SIZE,
|
|
403
|
+
limit: assertFinitePageSize(entity.$name, state.limit ?? DEFAULT_PAGE_SIZE),
|
|
404
404
|
...(state.cursor === null ? {} : { cursor: state.cursor }),
|
|
405
405
|
// The projection actually sent, preload keys included: a plan that is safe to log is only
|
|
406
406
|
// useful if it is the plan that ran.
|