@ultimat3/entity 22.8.2 → 22.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/entity",
3
- "version": "22.8.2",
3
+ "version": "22.9.1",
4
4
  "description": "A table + its domain type + invariants the database also enforces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,9 +32,9 @@
32
32
  "test": "bun test"
33
33
  },
34
34
  "dependencies": {
35
- "@ultimat3/core": "22.8.2",
36
- "@ultimat3/db": "22.8.2",
37
- "@ultimat3/schema": "22.8.2",
38
- "@ultimat3/time": "22.8.2"
35
+ "@ultimat3/core": "22.9.1",
36
+ "@ultimat3/db": "22.9.1",
37
+ "@ultimat3/schema": "22.9.1",
38
+ "@ultimat3/time": "22.9.1"
39
39
  }
40
40
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { literal } from '@ultimat3/db';
6
6
  import { describeValue } from '@ultimat3/schema';
7
+ import { refuseColumn } from './refuse';
7
8
 
8
9
  /**
9
10
  * The rejected value, rendered as its SHAPE and never its content — `@ultimat3/schema`'s
@@ -35,3 +36,18 @@ export const oneOf =
35
36
  (values: readonly string[]) =>
36
37
  (name: string): string =>
37
38
  `${name} in (${values.map((value) => literal(value).text).join(', ')})`;
39
+
40
+ /**
41
+ * U+0000 is the one character a Postgres `text` value cannot hold (SQLSTATE 22021). Refused in the
42
+ * column parser for the reason every rule in this package is: the memory driver would store what
43
+ * production answers `X_DB_STATEMENT_FAILED` for. `t.string` refuses it at the wire first; this is
44
+ * the same rule for a value that reached the row by any other path — a job, a seed, an import.
45
+ */
46
+ export const refuseNul = (value: string): string =>
47
+ value.includes('\u0000')
48
+ ? refuseColumn(
49
+ 'format',
50
+ `expected text without a NUL character (U+0000), ${got(value)} that contains one`,
51
+ "strip it at the call site — value.replaceAll('\\u0000', '') — Postgres text cannot store U+0000; binary content is bytea()",
52
+ )
53
+ : value;
@@ -1,10 +1,10 @@
1
1
  // Single responsibility: the three plain scalar columns — `text`, `integer`, `boolean` — each
2
2
  // refusing in `$parse` exactly what Postgres refuses at the column, so the memory driver cannot
3
- // store a value production answers 23514 or 22003 for. Split from `columns.ts` at its ceiling.
3
+ // store a value production answers 23514, 22003 or 22021 (a NUL in text) for. Split from `columns.ts` at its ceiling.
4
4
 
5
5
  import { charCount } from '@ultimat3/schema';
6
6
  import { column } from './column';
7
- import { got } from './column-values';
7
+ import { got, refuseNul } from './column-values';
8
8
  import { refuseColumn } from './refuse';
9
9
  import type { Column } from './types';
10
10
 
@@ -43,7 +43,7 @@ export const text = (options: TextOptions = {}): Column<string> => {
43
43
  `truncate at the call site, or widen the column — text({ max: ${charCount(value)} }) — and run x db gen "widen the text"`,
44
44
  );
45
45
  }
46
- return value;
46
+ return refuseNul(value);
47
47
  },
48
48
  max === undefined ? {} : { length: max, check: (name) => `char_length(${name}) <= ${max}` },
49
49
  );
package/src/columns.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  makeColumn,
20
20
  makeTimestamp,
21
21
  } from './column';
22
- import { got, oneOf } from './column-values';
22
+ import { got, oneOf, refuseNul } from './column-values';
23
23
  import { refuseColumn } from './refuse';
24
24
  import type {
25
25
  Column,
@@ -118,6 +118,9 @@ export const url = (): Column<string> =>
118
118
  'text',
119
119
  (value) => {
120
120
  if (typeof value === 'string') {
121
+ // Before the parse, outside its `catch`: WHATWG percent-encodes a NUL in a path, so
122
+ // `new URL` accepts it, and the value stored is the caller's — NUL included.
123
+ refuseNul(value);
121
124
  try {
122
125
  const parsed = new URL(value);
123
126
  // The scheme is stored in its canonical LOWER case: the CHECK is `~ '^https?://'`, so