@ultimat3/entity 29.4.0 → 30.1.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.
Files changed (65) hide show
  1. package/CLAUDE.md +66 -96
  2. package/README.md +118 -8
  3. package/package.json +15 -5
  4. package/src/append-only.ts +7 -0
  5. package/src/batch-read.ts +12 -4
  6. package/src/bulk-write-send.ts +49 -0
  7. package/src/bulk-write.ts +48 -11
  8. package/src/column.ts +29 -4
  9. package/src/counter.ts +109 -0
  10. package/src/cross-tenant.ts +13 -0
  11. package/src/cursor.ts +17 -1
  12. package/src/database.ts +16 -10
  13. package/src/describe-tenancy.ts +25 -0
  14. package/src/describe.ts +144 -3
  15. package/src/entity-error.ts +33 -0
  16. package/src/entity-parse.ts +89 -0
  17. package/src/entity-patch.ts +42 -0
  18. package/src/entity.ts +90 -76
  19. package/src/errors.ts +8 -0
  20. package/src/history-actor.ts +22 -0
  21. package/src/history.ts +86 -0
  22. package/src/index.ts +20 -1
  23. package/src/jit-preload.ts +35 -25
  24. package/src/lifecycle.ts +100 -0
  25. package/src/lock-key.ts +27 -0
  26. package/src/lock-unsupported.ts +11 -0
  27. package/src/memory-aggregates.ts +99 -0
  28. package/src/memory-batch.ts +69 -0
  29. package/src/memory-journal.ts +68 -0
  30. package/src/memory-lifecycle.ts +106 -0
  31. package/src/memory-lock.ts +144 -0
  32. package/src/memory-observed.ts +121 -0
  33. package/src/memory-repo.ts +199 -186
  34. package/src/memory-same-tenant.ts +83 -0
  35. package/src/memory-seek.ts +44 -0
  36. package/src/memory-transactor.ts +94 -0
  37. package/src/memory-unique.ts +19 -4
  38. package/src/n-plus-one.ts +28 -10
  39. package/src/numeric-compare.ts +43 -0
  40. package/src/pg-aggregates.ts +185 -0
  41. package/src/pg-driver.ts +79 -166
  42. package/src/pg-lock.ts +50 -0
  43. package/src/pg-row.ts +29 -2
  44. package/src/pg-sql-aggregate.ts +31 -0
  45. package/src/pg-sql.ts +17 -1
  46. package/src/query.ts +26 -105
  47. package/src/read-aggregates.ts +138 -0
  48. package/src/read-capture.ts +22 -0
  49. package/src/registry.ts +44 -1
  50. package/src/repo.ts +34 -0
  51. package/src/retain.ts +71 -0
  52. package/src/row-observer.ts +22 -4
  53. package/src/row-security-settings.ts +20 -0
  54. package/src/sealed-repo.ts +15 -0
  55. package/src/seed.ts +43 -7
  56. package/src/series-unsupported.ts +11 -0
  57. package/src/table-locks.ts +41 -0
  58. package/src/test-driver.ts +90 -0
  59. package/src/time-bucket.ts +147 -0
  60. package/src/touch.ts +33 -0
  61. package/src/types.ts +20 -0
  62. package/src/view-entity.ts +78 -0
  63. package/src/view-issue.ts +47 -0
  64. package/src/view-repo.ts +38 -0
  65. package/src/view.ts +41 -27
package/src/view.ts CHANGED
@@ -3,10 +3,16 @@
3
3
  // already describe. Values are validated by the entity's own column parsers; an unknown key is a
4
4
  // declaration-time failure, not a surprise on the first request.
5
5
 
6
- import { renderThrowable } from '@ultimat3/core';
7
- import { describeValue, type SchemaNode, type StandardSchemaV1 } from '@ultimat3/schema';
6
+ import {
7
+ describeValue,
8
+ type SchemaNode,
9
+ type StandardIssue,
10
+ type StandardSchemaV1,
11
+ } from '@ultimat3/schema';
12
+ import { viewKeyUnknown } from './entity-error';
8
13
  import { invariantViolated } from './errors';
9
14
  import type { AnyColumn, ColumnMap } from './types';
15
+ import { columnIssue, missingIssue } from './view-issue';
10
16
 
11
17
  /** What `bigint()` puts on a row: the digits, as a string — `columns-data.ts` parses by this. */
12
18
  const BIGINT_PATTERN = '^-?\\d+$';
@@ -104,11 +110,7 @@ export const viewFor = <Row, K extends keyof Row & string>(
104
110
  const picked: readonly (readonly [K, AnyColumn])[] = keys.map((key) => {
105
111
  const column = columns[key];
106
112
  if (column === undefined) {
107
- throw invariantViolated(
108
- entityName,
109
- 'view',
110
- `$view(['${key}']) names no column — pick from: ${Object.keys(columns).join(', ')}`,
111
- );
113
+ throw viewKeyUnknown(entityName, key, Object.keys(columns));
112
114
  }
113
115
  return [key, column] as const;
114
116
  });
@@ -120,47 +122,59 @@ export const viewFor = <Row, K extends keyof Row & string>(
120
122
  properties: Object.fromEntries(picked.map(([key, column]) => [key, columnNode(column)])),
121
123
  };
122
124
 
123
- const parse = (value: unknown): Pick<Row, K> => {
125
+ const nodes = node.properties ?? {};
126
+
127
+ /**
128
+ * Every picked key through its own column's parser, each refusal an issue AT ITS KEY: a form
129
+ * binds `path: ['title']` to the control, where one wrapped `EntityError` line at `path: ''`
130
+ * bound to nothing. All keys are read, so a form shows every refused field at once.
131
+ */
132
+ const check = (
133
+ value: unknown,
134
+ ): { readonly value: Pick<Row, K> } | { readonly issues: readonly StandardIssue[] } => {
124
135
  if (typeof value !== 'object' || value === null) {
125
136
  // Shape, never content — the same renderer `columns.ts` uses, for the same reason: a
126
137
  // view issue is folded into `X_BODY_INVALID` and reaches the caller and the log line.
127
- throw invariantViolated(
128
- entityName,
129
- 'view',
130
- `expected an object, got ${describeValue(value)}`,
131
- );
138
+ return {
139
+ issues: [
140
+ {
141
+ path: [],
142
+ message: `expected an object, got ${describeValue(value)}`,
143
+ rule: 'type',
144
+ params: { type: 'object' },
145
+ },
146
+ ],
147
+ };
132
148
  }
133
149
  const input = value as Readonly<Record<string, unknown>>;
134
150
  const projected = {} as Record<K, unknown>;
151
+ const issues: StandardIssue[] = [];
135
152
  for (const [key, column] of picked) {
136
153
  const given = input[key];
137
154
  if (given === undefined || given === null) {
138
155
  // No default is filled in: a view projects a row that already exists, so an absent
139
156
  // required column is missing data, never a value the projection may invent.
140
- if (column.$meta.notNull) throw invariantViolated(entityName, `view.${key}`, 'is required');
141
- projected[key] = null;
157
+ if (column.$meta.notNull) issues.push(missingIssue(key));
158
+ else projected[key] = null;
142
159
  continue;
143
160
  }
144
- projected[key] = column.$parse(given);
161
+ try {
162
+ projected[key] = column.$parse(given);
163
+ } catch (error) {
164
+ issues.push(columnIssue(key, column, nodes[key] ?? { kind: 'unknown' }, error));
165
+ }
145
166
  }
146
167
  // Every picked key went through its own column's parser, so this is the derived projection.
147
- return projected as Pick<Row, K>;
168
+ return issues.length > 0 ? { issues } : { value: projected as Pick<Row, K> };
148
169
  };
149
170
 
150
171
  return {
151
172
  '~standard': {
152
173
  version: 1,
153
174
  vendor: 'ultimate',
154
- validate: (value) => {
155
- try {
156
- return { value: parse(value) };
157
- } catch (error) {
158
- // The entity's rule, for the same reason: `instanceof` and `String()` are both reads of
159
- // a caught value, and a throwable that fights being read turned a rejected projection
160
- // into an uncatchable `TypeError`. `renderThrowable` is total.
161
- return { issues: [{ message: renderThrowable(error) }] };
162
- }
163
- },
175
+ // Never throws: `columnIssue` reads a caught value through `renderThrowable`, which is
176
+ // total — `instanceof` and `String()` are reads a hostile throwable refuses.
177
+ validate: check,
164
178
  },
165
179
  // `node` is the duck-typed key `nodeOf()` reads — the same one every `t.*` schema carries.
166
180
  node,