@ultimat3/entity 0.0.1 → 1.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -40
  3. package/package.json +4 -3
  4. package/src/column.d.ts +28 -0
  5. package/src/column.d.ts.map +1 -0
  6. package/src/column.js +75 -0
  7. package/src/column.js.map +1 -0
  8. package/src/column.ts +134 -0
  9. package/src/columns.d.ts +39 -0
  10. package/src/columns.d.ts.map +1 -0
  11. package/src/columns.js +136 -0
  12. package/src/columns.js.map +1 -0
  13. package/src/columns.ts +164 -217
  14. package/src/cursor.ts +187 -0
  15. package/src/database.d.ts +21 -0
  16. package/src/database.d.ts.map +1 -0
  17. package/src/database.js +38 -0
  18. package/src/database.js.map +1 -0
  19. package/src/database.ts +62 -0
  20. package/src/describe.d.ts +16 -0
  21. package/src/describe.d.ts.map +1 -0
  22. package/src/describe.js +79 -0
  23. package/src/describe.js.map +1 -0
  24. package/src/describe.ts +106 -0
  25. package/src/entity.d.ts +58 -0
  26. package/src/entity.d.ts.map +1 -0
  27. package/src/entity.js +160 -0
  28. package/src/entity.js.map +1 -0
  29. package/src/entity.ts +246 -99
  30. package/src/errors.d.ts +18 -0
  31. package/src/errors.d.ts.map +1 -0
  32. package/src/errors.js +59 -0
  33. package/src/errors.js.map +1 -0
  34. package/src/errors.ts +27 -6
  35. package/src/expr.d.ts +41 -0
  36. package/src/expr.d.ts.map +1 -0
  37. package/src/expr.js +94 -0
  38. package/src/expr.js.map +1 -0
  39. package/src/expr.ts +231 -0
  40. package/src/index.d.ts +23 -0
  41. package/src/index.d.ts.map +1 -0
  42. package/src/index.js +12 -0
  43. package/src/index.js.map +1 -0
  44. package/src/index.ts +36 -20
  45. package/src/invariants.d.ts +36 -0
  46. package/src/invariants.d.ts.map +1 -0
  47. package/src/invariants.js +53 -0
  48. package/src/invariants.js.map +1 -0
  49. package/src/invariants.ts +53 -49
  50. package/src/pg-driver.ts +154 -0
  51. package/src/pg-row.ts +110 -0
  52. package/src/pg-sql.ts +162 -0
  53. package/src/plan.ts +82 -0
  54. package/src/query.d.ts +30 -0
  55. package/src/query.d.ts.map +1 -0
  56. package/src/query.js +74 -0
  57. package/src/query.js.map +1 -0
  58. package/src/query.ts +144 -0
  59. package/src/registry.d.ts +45 -0
  60. package/src/registry.d.ts.map +1 -0
  61. package/src/registry.js +26 -0
  62. package/src/registry.js.map +1 -0
  63. package/src/registry.ts +8 -5
  64. package/src/repo.d.ts +54 -0
  65. package/src/repo.d.ts.map +1 -0
  66. package/src/repo.js +203 -0
  67. package/src/repo.js.map +1 -0
  68. package/src/repo.ts +0 -0
  69. package/src/seed.d.ts +20 -0
  70. package/src/seed.d.ts.map +1 -0
  71. package/src/seed.js +43 -0
  72. package/src/seed.js.map +1 -0
  73. package/src/seed.ts +69 -0
  74. package/src/tenancy.d.ts +41 -0
  75. package/src/tenancy.d.ts.map +1 -0
  76. package/src/tenancy.js +57 -0
  77. package/src/tenancy.js.map +1 -0
  78. package/src/tenancy.ts +68 -19
  79. package/src/types.d.ts +99 -0
  80. package/src/types.d.ts.map +1 -0
  81. package/src/types.js +8 -0
  82. package/src/types.js.map +1 -0
  83. package/src/types.ts +94 -44
  84. package/src/view.ts +97 -0
package/src/entity.js ADDED
@@ -0,0 +1,160 @@
1
+ // `entity(name, { columns })` is the first primitive. The row type is DERIVED from the columns —
2
+ // there is no second declaration of the same shape to keep in sync — and everything downstream
3
+ // (the typed db handle, migrations, cache tags, the admin UI, the manifest) is projected from
4
+ // this one call.
5
+ import { bindColumn, snake } from './column';
6
+ import { newId } from './columns';
7
+ import { describeEntity } from './describe';
8
+ import { invariantViolated } from './errors';
9
+ import { invariantColumns } from './expr';
10
+ import { assertInvariants, bindInvariant, invariantsToSql } from './invariants';
11
+ import { registerEntity } from './registry';
12
+ import { tenantColumnOf } from './tenancy';
13
+ /** Presence of this column is what makes an entity soft-deletable — not a flag. */
14
+ export const SOFT_DELETE_COLUMN = 'deletedAt';
15
+ const MONEY_PARTS = new Set(['minor', 'currency']);
16
+ const indexName = (table, columns, unique) => `${table}_${columns.join('_')}_${unique ? 'key' : 'idx'}`;
17
+ const defaultValue = (meta) => {
18
+ const declared = meta.default;
19
+ if (declared === undefined)
20
+ return undefined;
21
+ if (declared.kind === 'value')
22
+ return declared.value;
23
+ return declared.by === 'uuid-v7' ? newId() : new Date();
24
+ };
25
+ export const entity = (name, init) => {
26
+ const entries = Object.entries(init.columns);
27
+ for (const [property, column] of entries)
28
+ bindColumn(column, name, property);
29
+ const cacheTag = `entity:${name}`;
30
+ const softDelete = Object.hasOwn(init.columns, SOFT_DELETE_COLUMN);
31
+ const tenantColumn = tenantColumnOf(init.columns);
32
+ const primaryKey = init.primaryKey ?? entries.filter(([, column]) => column.$meta.primaryKey).map(([key]) => key);
33
+ if (primaryKey.length === 0) {
34
+ throw invariantViolated(name, 'primary-key', 'no primary key: mark a column .primaryKey() or pass primaryKey: [...] for a composite one');
35
+ }
36
+ // Property path -> physical name. The one place `orgId` becomes `org_id`.
37
+ const resolve = (path) => {
38
+ const [property, part] = path;
39
+ const column = property === undefined ? undefined : init.columns[property];
40
+ if (property === undefined || column === undefined) {
41
+ throw invariantViolated(name, 'invariant', `no column "${String(property)}"`);
42
+ }
43
+ const isMoney = column.$meta.kind === 'money';
44
+ if (part === undefined) {
45
+ if (isMoney) {
46
+ throw invariantViolated(name, property, `${property} is money: name ${property}.minor or ${property}.currency`);
47
+ }
48
+ return snake(property);
49
+ }
50
+ if (!isMoney || !MONEY_PARTS.has(part)) {
51
+ throw invariantViolated(name, property, `${property} has no part "${part}"`);
52
+ }
53
+ return `${snake(property)}_${part}`;
54
+ };
55
+ const columnsExpr = invariantColumns(name, entries.map(([property]) => property));
56
+ const partialWhere = softDelete ? `${snake(SOFT_DELETE_COLUMN)} is null` : undefined;
57
+ const invariants = (init.invariants ?? []).map((def) => bindInvariant(def, columnsExpr, resolve, partialWhere));
58
+ const declared = [
59
+ ...entries.flatMap(([property, column]) => {
60
+ const meta = column.$meta;
61
+ if (!meta.unique && !meta.index)
62
+ return [];
63
+ const physical = [meta.kind === 'money' ? `${snake(property)}_minor` : snake(property)];
64
+ return [
65
+ { name: indexName(name, physical, meta.unique), columns: physical, unique: meta.unique },
66
+ ];
67
+ }),
68
+ ...(init.indexes ?? []).map((index) => {
69
+ const columns = index.on.map((property) => resolve([property]));
70
+ const unique = index.unique === true;
71
+ const where = index.where?.(columnsExpr).toSql(resolve) ?? null;
72
+ if (index.where !== undefined && where === null) {
73
+ throw invariantViolated(name, 'index', 'a partial index predicate must be expressible in SQL; a JS predicate cannot be one');
74
+ }
75
+ return {
76
+ name: indexName(name, columns, unique),
77
+ columns,
78
+ unique,
79
+ ...(index.order === undefined ? {} : { order: index.order }),
80
+ ...(where === null ? {} : { where }),
81
+ };
82
+ }),
83
+ ];
84
+ // A foreign key already indexes its column; naming it again in `indexes` is not two indexes.
85
+ const indexes = declared.filter((index, position) => declared.findIndex((other) => other.name === index.name) === position);
86
+ const tags = [cacheTag, ...(init.tags ?? [])];
87
+ const describe = () => describeEntity({
88
+ name,
89
+ columns: entries,
90
+ primaryKey,
91
+ invariants,
92
+ indexes,
93
+ tags,
94
+ cacheTag,
95
+ softDelete,
96
+ tenantColumn,
97
+ });
98
+ const parse = (value) => {
99
+ if (typeof value !== 'object' || value === null) {
100
+ throw invariantViolated(name, 'row', `expected an object, got ${String(value)}`);
101
+ }
102
+ const input = value;
103
+ const row = {};
104
+ for (const [property, column] of entries) {
105
+ const given = input[property] ?? defaultValue(column.$meta);
106
+ if (given === undefined || given === null) {
107
+ if (column.$meta.notNull) {
108
+ throw invariantViolated(name, property, 'is required and has no default');
109
+ }
110
+ row[property] = null;
111
+ continue;
112
+ }
113
+ row[property] = column.$parse(given);
114
+ }
115
+ // Every property was validated by its own column above, so the shape is the derived row.
116
+ return row;
117
+ };
118
+ const core = {
119
+ $name: name,
120
+ $table: name,
121
+ $columns: init.columns,
122
+ $primaryKey: primaryKey,
123
+ $indexes: indexes,
124
+ $invariants: invariants,
125
+ $tags: tags,
126
+ $cacheTag: cacheTag,
127
+ $softDelete: softDelete,
128
+ $tenantColumn: tenantColumn,
129
+ $schema: {
130
+ '~standard': {
131
+ version: 1,
132
+ vendor: 'ultimate',
133
+ validate: (value) => {
134
+ try {
135
+ return { value: parse(value) };
136
+ }
137
+ catch (error) {
138
+ return {
139
+ issues: [{ message: error instanceof Error ? error.message : String(error) }],
140
+ };
141
+ }
142
+ },
143
+ },
144
+ },
145
+ get $row() {
146
+ // Type-only. Reading it means someone expected a value where a type was meant.
147
+ throw invariantViolated(name, '$row', '$row is a type, not a value — use typeof x.$row');
148
+ },
149
+ $tagFor: (id) => `${cacheTag}:${id}`,
150
+ $parse: parse,
151
+ $assert: (row) => assertInvariants(name, invariants, row),
152
+ $migration: () => invariantsToSql(name, invariants),
153
+ $describe: describe,
154
+ };
155
+ registerEntity({ name, tableName: name, describe });
156
+ // The columns land on the entity itself so `orgs.id` is a column reference; every framework
157
+ // member is `$`-prefixed, which is why a column may be called `name`.
158
+ return Object.assign(core, init.columns);
159
+ };
160
+ //# sourceMappingURL=entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity.js","sourceRoot":"","sources":["entity.ts"],"names":[],"mappings":"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"}
package/src/entity.ts CHANGED
@@ -1,120 +1,267 @@
1
- // `entity()` is the first primitive: a table, its domain type, and the invariants
2
- // that hold for every row. Everything downstream (repo, admin UI, cache tags, the
3
- // manifest) is derived from this one declaration.
1
+ // `entity(name, { columns })` is the first primitive. The row type is DERIVED from the columns —
2
+ // there is no second declaration of the same shape to keep in sync — and everything downstream
3
+ // (the typed db handle, migrations, cache tags, the admin UI, the manifest) is projected from
4
+ // this one call.
5
+
4
6
  import type { StandardSchemaV1 } from '@ultimat3/schema';
7
+ import { bindColumn, snake } from './column';
8
+ import { newId } from './columns';
9
+ import { describeEntity } from './describe';
5
10
  import { invariantViolated } from './errors';
6
- import { assertInvariants, type Invariant, invariantsToSql } from './invariants';
7
- import { type EntityDescription, registerEntity } from './registry';
8
- import { isOrgScoped } from './tenancy';
9
- import type { ColumnMap, TableDef } from './types';
10
-
11
- export type EntitySchema<T> = StandardSchemaV1<unknown, T>;
12
-
13
- export interface EntityInit<T, C extends ColumnMap> {
14
- /** Defaults to the table name. Must be unique across the app. */
15
- readonly name?: string;
16
- readonly table: TableDef<C>;
17
- readonly type: EntitySchema<T>;
18
- readonly invariants?: readonly Invariant<T>[];
11
+ import type { Expr, InvariantColumns, Resolve } from './expr';
12
+ import { invariantColumns } from './expr';
13
+ import type { Invariant, InvariantDef } from './invariants';
14
+ import { assertInvariants, bindInvariant, invariantsToSql } from './invariants';
15
+ import type { EntityDescription } from './registry';
16
+ import { registerEntity } from './registry';
17
+ import { resolveTenantColumn } from './tenancy';
18
+ import type { AnyColumn, ColumnMap, ColumnMeta, IndexDef, RowOf } from './types';
19
+ import type { EntityView } from './view';
20
+ import { viewFor } from './view';
21
+
22
+ /** Presence of this column is what makes an entity soft-deletable — not a flag. */
23
+ export const SOFT_DELETE_COLUMN = 'deletedAt';
24
+
25
+ export interface IndexInit<C extends ColumnMap> {
26
+ readonly on: readonly (keyof C & string)[];
27
+ readonly order?: 'asc' | 'desc';
28
+ readonly unique?: boolean;
29
+ /** Partial index predicate, written in the same language as an invariant. */
30
+ readonly where?: (columns: InvariantColumns) => Expr;
31
+ }
32
+
33
+ export interface EntityInit<C extends ColumnMap> {
34
+ readonly columns: C;
35
+ /**
36
+ * The tenant column, said out loud. Omitted, it is inferred from `.tenant()` or a column named
37
+ * `orgId`, so silence never means unscoped.
38
+ */
39
+ readonly tenant?: keyof C & string;
40
+ /** Composite keys only — a single key is `uuid().primaryKey()` on the column itself. */
41
+ readonly primaryKey?: readonly (keyof C & string)[];
42
+ readonly invariants?: readonly InvariantDef[];
43
+ readonly indexes?: readonly IndexInit<C>[];
19
44
  /** Extra cache tags this entity participates in, beyond its own. */
20
45
  readonly tags?: readonly string[];
21
- /** Defaults to the presence of a `deletedAt` column. */
22
- readonly softDelete?: boolean;
23
46
  }
24
47
 
25
- export interface Entity<T, C extends ColumnMap = ColumnMap> {
26
- readonly name: string;
27
- readonly table: TableDef<C>;
28
- readonly type: EntitySchema<T>;
29
- readonly invariants: readonly Invariant<T>[];
30
- readonly tags: readonly string[];
48
+ /**
49
+ * The row-shaped half of an entity. Consumers that must name "some entity" without naming its
50
+ * row use `EntityCore`; `Entity` adds the columns themselves, so `orgs.id` is a column
51
+ * reference and `typeof orgs.$row` is the derived row type.
52
+ */
53
+ export interface EntityCore<Row = unknown, C extends ColumnMap = ColumnMap> {
54
+ readonly $name: string;
55
+ readonly $table: string;
56
+ readonly $columns: C;
57
+ readonly $primaryKey: readonly string[];
58
+ readonly $indexes: readonly IndexDef[];
59
+ readonly $invariants: readonly Invariant<Row>[];
60
+ readonly $tags: readonly string[];
31
61
  /** `entity:<name>`. `@ultimat3/cache` invalidates by this string. */
32
- readonly cacheTag: string;
33
- readonly softDelete: boolean;
34
- readonly orgScoped: boolean;
62
+ readonly $cacheTag: string;
63
+ readonly $softDelete: boolean;
64
+ /** Property key of the tenant column, or `null`. Presence is what turns tenancy on. */
65
+ readonly $tenantColumn: string | null;
66
+ /** Phantom: `type Post = typeof posts.$row`. Reading it at runtime throws. */
67
+ readonly $row: Row;
68
+ /** The Standard Schema the columns already describe — forms and actions hand input to it. */
69
+ readonly $schema: StandardSchemaV1<unknown, Row>;
35
70
  /** `entity:<name>:<id>` — row-level invalidation for live queries. */
36
- tagFor(id: string): string;
37
- /** Validates an unknown value into the domain type. Throws on failure. */
38
- parse(value: unknown): T;
71
+ $tagFor(id: string): string;
72
+ /** Fills declared defaults, then validates every column. Throws on a bad value. */
73
+ $parse(value: unknown): Row;
74
+ /**
75
+ * `const PostView = posts.$view(['id', 'title'])` — the projection an action names as its
76
+ * `output`. An unknown key is a compile error, and a declaration error for a JS caller.
77
+ */
78
+ $view<K extends keyof Row & string>(keys: readonly K[]): EntityView<Row, K>;
39
79
  /** Runs every invariant. Called by the repository on insert and update. */
40
- assert(row: T): void;
80
+ $assert(row: Row): void;
41
81
  /** The CHECK/UNIQUE statements the migration emits for this entity. */
42
- migration(): string;
43
- describe(): EntityDescription;
82
+ $migration(): string;
83
+ $describe(): EntityDescription;
44
84
  }
45
85
 
46
- interface LooseResult<T> {
47
- readonly value?: T;
48
- readonly issues?: readonly { readonly message: string }[] | undefined;
49
- }
86
+ export type Entity<Row, C extends ColumnMap = ColumnMap> = EntityCore<Row, C> & C;
50
87
 
51
- const parseWith = <T>(schema: EntitySchema<T>, name: string, value: unknown): T => {
52
- const result = schema['~standard'].validate(value);
53
- if (result instanceof Promise) {
54
- throw invariantViolated(name, 'schema', 'entity types must validate synchronously');
55
- }
56
- const outcome = result as LooseResult<T>;
57
- if (outcome.issues !== undefined && outcome.issues.length > 0) {
58
- throw invariantViolated(name, 'schema', outcome.issues.map((i) => i.message).join('; '));
59
- }
60
- return outcome.value as T;
88
+ const MONEY_PARTS = new Set(['minor', 'currency']);
89
+
90
+ const indexName = (table: string, columns: readonly string[], unique: boolean): string =>
91
+ `${table}_${columns.join('_')}_${unique ? 'key' : 'idx'}`;
92
+
93
+ const defaultValue = (meta: ColumnMeta): unknown => {
94
+ const declared = meta.default;
95
+ if (declared === undefined) return undefined;
96
+ if (declared.kind === 'value') return declared.value;
97
+ return declared.by === 'uuid-v7' ? newId() : new Date();
61
98
  };
62
99
 
63
- export const entity = <T, C extends ColumnMap>(init: EntityInit<T, C>): Entity<T, C> => {
64
- const name = init.name ?? init.table.name;
65
- const invariants = init.invariants ?? [];
66
- const softDelete = init.softDelete ?? Object.hasOwn(init.table.columns, 'deletedAt');
67
- const orgScoped = isOrgScoped(init.table);
100
+ export const entity = <const C extends ColumnMap>(
101
+ name: string,
102
+ init: EntityInit<C>,
103
+ ): Entity<RowOf<C>, C> => {
104
+ type Row = RowOf<C>;
105
+ const entries: readonly (readonly [string, AnyColumn])[] = Object.entries(init.columns);
106
+ for (const [property, column] of entries) bindColumn(column, name, property);
107
+
68
108
  const cacheTag = `entity:${name}`;
109
+ const softDelete = Object.hasOwn(init.columns, SOFT_DELETE_COLUMN);
110
+ const tenantColumn = resolveTenantColumn(name, init.columns, init.tenant);
69
111
 
70
- const describe = (): EntityDescription => ({
71
- name,
72
- table: init.table.name,
73
- primaryKey: init.table.primaryKey,
74
- columns: Object.entries(init.table.columns).map(([property, column]) => ({
75
- property,
76
- column: column.name,
77
- kind: column.kind,
78
- notNull: column.notNull,
79
- primaryKey: column.primaryKey,
80
- unique: column.unique,
81
- hasDefault: column.default !== undefined,
82
- check: column.check ?? null,
83
- references:
84
- column.references === undefined
85
- ? null
86
- : `${column.references.table}.${column.references.column}`,
87
- })),
88
- invariants: invariants.map((inv) => ({
89
- name: inv.name,
90
- kind: inv.kind,
91
- message: inv.message,
92
- sql: inv.sql,
93
- where: inv.where ?? null,
94
- })),
95
- indexes: init.table.indexes.map((index) => index.name),
96
- tags: [cacheTag, ...(init.tags ?? [])],
97
- cacheTag,
98
- softDelete,
99
- orgScoped,
100
- });
101
-
102
- const built: Entity<T, C> = {
112
+ const primaryKey =
113
+ init.primaryKey ?? entries.filter(([, column]) => column.$meta.primaryKey).map(([key]) => key);
114
+ if (primaryKey.length === 0) {
115
+ throw invariantViolated(
116
+ name,
117
+ 'primary-key',
118
+ 'no primary key: mark a column .primaryKey() or pass primaryKey: [...] for a composite one',
119
+ );
120
+ }
121
+
122
+ // Property path -> physical name. The one place `orgId` becomes `org_id`.
123
+ const resolve: Resolve = (path) => {
124
+ const [property, part] = path;
125
+ const column = property === undefined ? undefined : init.columns[property];
126
+ if (property === undefined || column === undefined) {
127
+ throw invariantViolated(name, 'invariant', `no column "${String(property)}"`);
128
+ }
129
+ const isMoney = column.$meta.kind === 'money';
130
+ if (part === undefined) {
131
+ if (isMoney) {
132
+ throw invariantViolated(
133
+ name,
134
+ property,
135
+ `${property} is money: name ${property}.minor or ${property}.currency`,
136
+ );
137
+ }
138
+ return snake(property);
139
+ }
140
+ if (!isMoney || !MONEY_PARTS.has(part)) {
141
+ throw invariantViolated(name, property, `${property} has no part "${part}"`);
142
+ }
143
+ return `${snake(property)}_${part}`;
144
+ };
145
+
146
+ const columnsExpr = invariantColumns(
103
147
  name,
104
- table: init.table,
105
- type: init.type,
106
- invariants,
107
- tags: [cacheTag, ...(init.tags ?? [])],
108
- cacheTag,
109
- softDelete,
110
- orgScoped,
111
- tagFor: (id) => `${cacheTag}:${id}`,
112
- parse: (value) => parseWith(init.type, name, value),
113
- assert: (row) => assertInvariants(name, invariants, row),
114
- migration: () => invariantsToSql(init.table.name, invariants),
115
- describe,
148
+ entries.map(([property]) => property),
149
+ );
150
+ const partialWhere = softDelete ? `${snake(SOFT_DELETE_COLUMN)} is null` : undefined;
151
+ const invariants: readonly Invariant<Row>[] = (init.invariants ?? []).map((def) =>
152
+ bindInvariant<Row>(def, columnsExpr, resolve, partialWhere),
153
+ );
154
+
155
+ const declared: readonly IndexDef[] = [
156
+ ...entries.flatMap(([property, column]) => {
157
+ const meta = column.$meta;
158
+ if (!meta.unique && !meta.index) return [];
159
+ const physical = [meta.kind === 'money' ? `${snake(property)}_minor` : snake(property)];
160
+ return [
161
+ { name: indexName(name, physical, meta.unique), columns: physical, unique: meta.unique },
162
+ ];
163
+ }),
164
+ ...(init.indexes ?? []).map((index) => {
165
+ const columns = index.on.map((property) => resolve([property]));
166
+ const unique = index.unique === true;
167
+ const where = index.where?.(columnsExpr).toSql(resolve) ?? null;
168
+ if (index.where !== undefined && where === null) {
169
+ throw invariantViolated(
170
+ name,
171
+ 'index',
172
+ 'a partial index predicate must be expressible in SQL; a JS predicate cannot be one',
173
+ );
174
+ }
175
+ return {
176
+ name: indexName(name, columns, unique),
177
+ columns,
178
+ unique,
179
+ ...(index.order === undefined ? {} : { order: index.order }),
180
+ ...(where === null ? {} : { where }),
181
+ };
182
+ }),
183
+ ];
184
+ // A foreign key already indexes its column; naming it again in `indexes` is not two indexes.
185
+ const indexes: readonly IndexDef[] = declared.filter(
186
+ (index, position) => declared.findIndex((other) => other.name === index.name) === position,
187
+ );
188
+
189
+ const tags = [cacheTag, ...(init.tags ?? [])];
190
+ const describe = (): EntityDescription =>
191
+ describeEntity({
192
+ name,
193
+ columns: entries,
194
+ primaryKey,
195
+ invariants,
196
+ indexes,
197
+ tags,
198
+ cacheTag,
199
+ softDelete,
200
+ tenantColumn,
201
+ });
202
+
203
+ const parse = (value: unknown): Row => {
204
+ if (typeof value !== 'object' || value === null) {
205
+ throw invariantViolated(name, 'row', `expected an object, got ${String(value)}`);
206
+ }
207
+ const input = value as Readonly<Record<string, unknown>>;
208
+ const row: Record<string, unknown> = {};
209
+ for (const [property, column] of entries) {
210
+ const given = input[property] ?? defaultValue(column.$meta);
211
+ if (given === undefined || given === null) {
212
+ if (column.$meta.notNull) {
213
+ throw invariantViolated(name, property, 'is required and has no default');
214
+ }
215
+ row[property] = null;
216
+ continue;
217
+ }
218
+ row[property] = column.$parse(given);
219
+ }
220
+ // Every property was validated by its own column above, so the shape is the derived row.
221
+ return row as Row;
222
+ };
223
+
224
+ const core: EntityCore<Row, C> = {
225
+ $name: name,
226
+ $table: name,
227
+ $columns: init.columns,
228
+ $primaryKey: primaryKey,
229
+ $indexes: indexes,
230
+ $invariants: invariants,
231
+ $tags: tags,
232
+ $cacheTag: cacheTag,
233
+ $softDelete: softDelete,
234
+ $tenantColumn: tenantColumn,
235
+ $schema: {
236
+ '~standard': {
237
+ version: 1,
238
+ vendor: 'ultimate',
239
+ validate: (value) => {
240
+ try {
241
+ return { value: parse(value) };
242
+ } catch (error) {
243
+ return {
244
+ issues: [{ message: error instanceof Error ? error.message : String(error) }],
245
+ };
246
+ }
247
+ },
248
+ },
249
+ },
250
+ get $row(): Row {
251
+ // Type-only. Reading it means someone expected a value where a type was meant.
252
+ throw invariantViolated(name, '$row', '$row is a type, not a value — use typeof x.$row');
253
+ },
254
+ $tagFor: (id) => `${cacheTag}:${id}`,
255
+ $parse: parse,
256
+ $view: <K extends keyof Row & string>(keys: readonly K[]) =>
257
+ viewFor<Row, K>(name, init.columns, keys),
258
+ $assert: (row) => assertInvariants(name, invariants, row),
259
+ $migration: () => invariantsToSql(name, invariants),
260
+ $describe: describe,
116
261
  };
117
262
 
118
- registerEntity({ name, tableName: init.table.name, describe });
119
- return built;
263
+ registerEntity({ name, tableName: name, describe });
264
+ // The columns land on the entity itself so `orgs.id` is a column reference; every framework
265
+ // member is `$`-prefixed, which is why a column may be called `name`.
266
+ return Object.assign(core, init.columns);
120
267
  };
@@ -0,0 +1,18 @@
1
+ import { UltimateError } from '@ultimat3/core';
2
+ export declare const ENTITY_ERROR_CODES: readonly ['X_ENTITY_DUPLICATE', 'X_INVARIANT_VIOLATED', 'X_TENANCY_UNSCOPED', 'X_DB_DRIFT', 'X_NOT_FOUND'];
3
+ export type EntityErrorCode = (typeof ENTITY_ERROR_CODES)[number];
4
+ export declare const ENTITY_ERROR_TITLES: Readonly<Record<EntityErrorCode, string>>;
5
+ export declare class EntityError extends UltimateError {
6
+ readonly name = "EntityError";
7
+ constructor(init: {
8
+ code: EntityErrorCode;
9
+ cause: string;
10
+ fix: string;
11
+ });
12
+ }
13
+ export declare const entityDuplicate: (name: string, existingTable: string) => EntityError;
14
+ export declare const invariantViolated: (entityName: string, invariantName: string, message: string) => EntityError;
15
+ export declare const tenancyUnscoped: (entityName: string, operation: string) => EntityError;
16
+ export declare const dbDrift: (tableName: string, columnName: string) => EntityError;
17
+ export declare const notFound: (entityName: string, id: string) => EntityError;
18
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEnE,eAAO,MAAM,kBAAkB,YAC7B,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,aAAa,CACL,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAMzE,CAAC;AASF,qBAAa,WAAY,SAAQ,aAAa;IAC5C,SAAkB,IAAI,iBAAiB;IAEvC,YAAY,IAAI,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAOtE;CACF;AAED,eAAO,MAAM,eAAe,SAAU,MAAM,iBAAiB,MAAM,KAAG,WAKlE,CAAC;AAEL,eAAO,MAAM,iBAAiB,eAChB,MAAM,iBACH,MAAM,WACZ,MAAM,KACd,WAKC,CAAC;AAEL,eAAO,MAAM,eAAe,eAAgB,MAAM,aAAa,MAAM,KAAG,WAKpE,CAAC;AAEL,eAAO,MAAM,OAAO,cAAe,MAAM,cAAc,MAAM,KAAG,WAK5D,CAAC;AAEL,eAAO,MAAM,QAAQ,eAAgB,MAAM,MAAM,MAAM,KAAG,WAKtD,CAAC"}
package/src/errors.js ADDED
@@ -0,0 +1,59 @@
1
+ // The entity layer's stable error codes. Each factory produces the exact command
2
+ // that fixes the situation — `X_DB_DRIFT` is the flagship: it names the table, the
3
+ // column and the generator invocation.
4
+ import { registerErrorCodes, UltimateError } from '@ultimat3/core';
5
+ export const ENTITY_ERROR_CODES = [
6
+ 'X_ENTITY_DUPLICATE',
7
+ 'X_INVARIANT_VIOLATED',
8
+ 'X_TENANCY_UNSCOPED',
9
+ 'X_DB_DRIFT',
10
+ 'X_NOT_FOUND',
11
+ ];
12
+ export const ENTITY_ERROR_TITLES = {
13
+ X_ENTITY_DUPLICATE: 'two entities claim the same name',
14
+ X_INVARIANT_VIOLATED: 'a domain invariant rejected this row',
15
+ X_TENANCY_UNSCOPED: 'a tenant-scoped query has no org predicate',
16
+ X_DB_DRIFT: 'schema differs from migrations',
17
+ X_NOT_FOUND: 'no row for that id',
18
+ };
19
+ // Registered at module load, like every other package's errors.ts. Without this the
20
+ // registry humanises the code (`X_DB_DRIFT` → "db drift") and the terminal, the overlay
21
+ // and `--json` all render a title this package never wrote.
22
+ registerErrorCodes(Object.fromEntries(Object.entries(ENTITY_ERROR_TITLES).map(([code, title]) => [code, { title }])));
23
+ export class EntityError extends UltimateError {
24
+ name = 'EntityError';
25
+ constructor(init) {
26
+ super({
27
+ code: init.code,
28
+ cause: init.cause,
29
+ fix: init.fix,
30
+ docs: `https://ultimate.dev/errors/${init.code}`,
31
+ });
32
+ }
33
+ }
34
+ export const entityDuplicate = (name, existingTable) => new EntityError({
35
+ code: 'X_ENTITY_DUPLICATE',
36
+ cause: `entity "${name}" is already registered for table "${existingTable}"`,
37
+ fix: `x entities list --json # then rename one of the two entity({ name }) declarations`,
38
+ });
39
+ export const invariantViolated = (entityName, invariantName, message) => new EntityError({
40
+ code: 'X_INVARIANT_VIOLATED',
41
+ cause: `${entityName}.${invariantName}: ${message}`,
42
+ fix: `x entity explain ${entityName} --json # shows the invariant and its SQL CHECK`,
43
+ });
44
+ export const tenancyUnscoped = (entityName, operation) => new EntityError({
45
+ code: 'X_TENANCY_UNSCOPED',
46
+ cause: `${entityName}.${operation}() was built without an org predicate but the entity has an orgId column`,
47
+ fix: `pass { orgId } to ${entityName}.${operation}(), or wrap the plan with orgScoped(entity, orgId, plan)`,
48
+ });
49
+ export const dbDrift = (tableName, columnName) => new EntityError({
50
+ code: 'X_DB_DRIFT',
51
+ cause: `table "${tableName}" has column "${columnName}" not present in any migration`,
52
+ fix: `x db gen "add ${columnName}"`,
53
+ });
54
+ export const notFound = (entityName, id) => new EntityError({
55
+ code: 'X_NOT_FOUND',
56
+ cause: `${entityName} ${id} does not exist (or is soft-deleted)`,
57
+ fix: `x db query "select id from ${entityName} limit 5" --json # confirm the id you expect`,
58
+ });
59
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,mFAAmF;AACnF,uCAAuC;AACvC,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEnE,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,oBAAoB;IACpB,sBAAsB;IACtB,oBAAoB;IACpB,YAAY;IACZ,aAAa;CACL,CAAC;AAIX,MAAM,CAAC,MAAM,mBAAmB,GAA8C;IAC5E,kBAAkB,EAAE,kCAAkC;IACtD,oBAAoB,EAAE,sCAAsC;IAC5D,kBAAkB,EAAE,4CAA4C;IAChE,UAAU,EAAE,gCAAgC;IAC5C,WAAW,EAAE,oBAAoB;CAClC,CAAC;AAEF,oFAAoF;AACpF,wFAAwF;AACxF,4DAA4D;AAC5D,kBAAkB,CAChB,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAClG,CAAC;AAEF,MAAM,OAAO,WAAY,SAAQ,aAAa;IAC1B,IAAI,GAAG,aAAa,CAAC;IAEvC,YAAY,IAA2D;QACrE,KAAK,CAAC;YACJ,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,IAAI,EAAE,+BAA+B,IAAI,CAAC,IAAI,EAAE;SACjD,CAAC,CAAC;IACL,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,aAAqB,EAAe,EAAE,CAClF,IAAI,WAAW,CAAC;IACd,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,WAAW,IAAI,sCAAsC,aAAa,GAAG;IAC5E,GAAG,EAAE,qFAAqF;CAC3F,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,UAAkB,EAClB,aAAqB,EACrB,OAAe,EACF,EAAE,CACf,IAAI,WAAW,CAAC;IACd,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,GAAG,UAAU,IAAI,aAAa,KAAK,OAAO,EAAE;IACnD,GAAG,EAAE,oBAAoB,UAAU,mDAAmD;CACvF,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,UAAkB,EAAE,SAAiB,EAAe,EAAE,CACpF,IAAI,WAAW,CAAC;IACd,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,GAAG,UAAU,IAAI,SAAS,0EAA0E;IAC3G,GAAG,EAAE,qBAAqB,UAAU,IAAI,SAAS,0DAA0D;CAC5G,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,UAAkB,EAAe,EAAE,CAC5E,IAAI,WAAW,CAAC;IACd,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,UAAU,SAAS,iBAAiB,UAAU,gCAAgC;IACrF,GAAG,EAAE,iBAAiB,UAAU,GAAG;CACpC,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,UAAkB,EAAE,EAAU,EAAe,EAAE,CACtE,IAAI,WAAW,CAAC;IACd,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,GAAG,UAAU,IAAI,EAAE,sCAAsC;IAChE,GAAG,EAAE,8BAA8B,UAAU,gDAAgD;CAC9F,CAAC,CAAC"}