@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 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
- `assertPageSize` is `assertBatchable`'s three refusals in the other call, deliberately under the
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 `planFor` (so `findMany({ limit })` straight at the repository cannot route around it),
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": "16.0.0",
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": "16.0.0",
35
- "@ultimat3/db": "16.0.0",
36
- "@ultimat3/schema": "16.0.0",
37
- "@ultimat3/time": "16.0.0"
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: `x db gen "add ${columnName}"`,
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 assertPageSize = (entityName: string, rows: number): void => {
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
- limit: args.limit ?? DEFAULT_PAGE_SIZE,
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 { assertPageSize, DEFAULT_PAGE_SIZE, namedColumns } from './plan';
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
- assertPageSize(entity.$name, rows);
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.