@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.
- package/LICENSE +21 -0
- package/README.md +126 -40
- package/package.json +4 -3
- package/src/column.d.ts +28 -0
- package/src/column.d.ts.map +1 -0
- package/src/column.js +75 -0
- package/src/column.js.map +1 -0
- package/src/column.ts +134 -0
- package/src/columns.d.ts +39 -0
- package/src/columns.d.ts.map +1 -0
- package/src/columns.js +136 -0
- package/src/columns.js.map +1 -0
- package/src/columns.ts +164 -217
- package/src/cursor.ts +187 -0
- package/src/database.d.ts +21 -0
- package/src/database.d.ts.map +1 -0
- package/src/database.js +38 -0
- package/src/database.js.map +1 -0
- package/src/database.ts +62 -0
- package/src/describe.d.ts +16 -0
- package/src/describe.d.ts.map +1 -0
- package/src/describe.js +79 -0
- package/src/describe.js.map +1 -0
- package/src/describe.ts +106 -0
- package/src/entity.d.ts +58 -0
- package/src/entity.d.ts.map +1 -0
- package/src/entity.js +160 -0
- package/src/entity.js.map +1 -0
- package/src/entity.ts +246 -99
- package/src/errors.d.ts +18 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +59 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +27 -6
- package/src/expr.d.ts +41 -0
- package/src/expr.d.ts.map +1 -0
- package/src/expr.js +94 -0
- package/src/expr.js.map +1 -0
- package/src/expr.ts +231 -0
- package/src/index.d.ts +23 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +36 -20
- package/src/invariants.d.ts +36 -0
- package/src/invariants.d.ts.map +1 -0
- package/src/invariants.js +53 -0
- package/src/invariants.js.map +1 -0
- package/src/invariants.ts +53 -49
- package/src/pg-driver.ts +154 -0
- package/src/pg-row.ts +110 -0
- package/src/pg-sql.ts +162 -0
- package/src/plan.ts +82 -0
- package/src/query.d.ts +30 -0
- package/src/query.d.ts.map +1 -0
- package/src/query.js +74 -0
- package/src/query.js.map +1 -0
- package/src/query.ts +144 -0
- package/src/registry.d.ts +45 -0
- package/src/registry.d.ts.map +1 -0
- package/src/registry.js +26 -0
- package/src/registry.js.map +1 -0
- package/src/registry.ts +8 -5
- package/src/repo.d.ts +54 -0
- package/src/repo.d.ts.map +1 -0
- package/src/repo.js +203 -0
- package/src/repo.js.map +1 -0
- package/src/repo.ts +0 -0
- package/src/seed.d.ts +20 -0
- package/src/seed.d.ts.map +1 -0
- package/src/seed.js +43 -0
- package/src/seed.js.map +1 -0
- package/src/seed.ts +69 -0
- package/src/tenancy.d.ts +41 -0
- package/src/tenancy.d.ts.map +1 -0
- package/src/tenancy.js +57 -0
- package/src/tenancy.js.map +1 -0
- package/src/tenancy.ts +68 -19
- package/src/types.d.ts +99 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +8 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +94 -44
- package/src/view.ts +97 -0
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Postgres types the blessed builders emit. `money` expands to `bigint` + `char(3)`. */
|
|
2
|
+
export type ColumnKind = 'uuid' | 'text' | 'char' | 'boolean' | 'integer' | 'bigint' | 'timestamptz' | 'jsonb' | 'money';
|
|
3
|
+
export type ColumnDefault = {
|
|
4
|
+
readonly kind: 'value';
|
|
5
|
+
readonly value: string | number | boolean | null;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'generated';
|
|
8
|
+
readonly by: 'uuid-v7' | 'now';
|
|
9
|
+
};
|
|
10
|
+
export type OnDelete = 'cascade' | 'restrict' | 'set null';
|
|
11
|
+
export interface ReferenceOptions {
|
|
12
|
+
readonly onDelete?: OnDelete;
|
|
13
|
+
}
|
|
14
|
+
/** The single value a money column puts on the row. Two physical columns back it. */
|
|
15
|
+
export interface MoneyValue {
|
|
16
|
+
readonly minor: bigint;
|
|
17
|
+
readonly currency: string;
|
|
18
|
+
}
|
|
19
|
+
/** What a writer may hand a money column. An integer `number` widens; a float throws. */
|
|
20
|
+
export interface MoneyInput {
|
|
21
|
+
readonly minor: bigint | number;
|
|
22
|
+
readonly currency: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What the author declared. Where it landed — table, property key, physical name — is the
|
|
26
|
+
* binding `entity()` records (see `column.ts`), so a name is never written twice.
|
|
27
|
+
*/
|
|
28
|
+
export interface ColumnMeta {
|
|
29
|
+
readonly kind: ColumnKind;
|
|
30
|
+
readonly notNull: boolean;
|
|
31
|
+
readonly primaryKey: boolean;
|
|
32
|
+
readonly unique: boolean;
|
|
33
|
+
readonly index: boolean;
|
|
34
|
+
/** Presence of a tenant column is what turns tenancy on. See `tenancy.ts`. */
|
|
35
|
+
readonly tenant: boolean;
|
|
36
|
+
readonly length?: number;
|
|
37
|
+
readonly values?: readonly string[];
|
|
38
|
+
readonly default?: ColumnDefault;
|
|
39
|
+
readonly onUpdate?: ColumnDefault;
|
|
40
|
+
/** Takes the physical name, so a CHECK can be written before that name is known. */
|
|
41
|
+
readonly check?: (column: string) => string;
|
|
42
|
+
/** A thunk: schema modules reference each other in a cycle. */
|
|
43
|
+
readonly references?: () => AnyColumn;
|
|
44
|
+
readonly onDelete?: OnDelete;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `Optional` is the phantom that says "this column has a default", so an insert may omit it.
|
|
48
|
+
* It is a real boolean at runtime too, so nothing has to be re-derived to check it.
|
|
49
|
+
*/
|
|
50
|
+
export interface Column<T, Optional extends boolean = false> {
|
|
51
|
+
readonly $meta: ColumnMeta;
|
|
52
|
+
/** Runtime guard AND the carrier of the column's TypeScript type. */
|
|
53
|
+
readonly $parse: (value: unknown) => T;
|
|
54
|
+
readonly $optional: Optional;
|
|
55
|
+
/** `boolean`: only a uuid key carries a generated default, so only it becomes optional. */
|
|
56
|
+
primaryKey(): Column<T, boolean>;
|
|
57
|
+
nullable(): Column<T | null, Optional>;
|
|
58
|
+
unique(): Column<T, Optional>;
|
|
59
|
+
/** Marks the tenant column; a query without an org predicate then throws. */
|
|
60
|
+
tenant(): Column<T, Optional>;
|
|
61
|
+
references(target: () => AnyColumn, options?: ReferenceOptions): Column<T, Optional>;
|
|
62
|
+
default(value: T): Column<T, true>;
|
|
63
|
+
}
|
|
64
|
+
/** A uuid primary key is generated (v7) when omitted, which is why it narrows to `true`. */
|
|
65
|
+
export interface UuidColumn<Optional extends boolean = false> extends Column<string, Optional> {
|
|
66
|
+
primaryKey(): Column<string, true>;
|
|
67
|
+
}
|
|
68
|
+
export interface TimestampColumn<Optional extends boolean = false> extends Column<Date, Optional> {
|
|
69
|
+
defaultNow(): TimestampColumn<true>;
|
|
70
|
+
onUpdateNow(): TimestampColumn<Optional>;
|
|
71
|
+
}
|
|
72
|
+
export type AnyColumn = Column<unknown, boolean>;
|
|
73
|
+
export type ColumnMap = Readonly<Record<string, AnyColumn>>;
|
|
74
|
+
export type TypeOf<C> = C extends Column<infer T, boolean> ? T : never;
|
|
75
|
+
/** The row type a column set describes. This derivation is why the package exists. */
|
|
76
|
+
export type RowOf<C extends ColumnMap> = {
|
|
77
|
+
readonly [K in keyof C]: TypeOf<C[K]>;
|
|
78
|
+
};
|
|
79
|
+
type DefaultedKeys<C extends ColumnMap> = {
|
|
80
|
+
[K in keyof C]-?: C[K]['$optional'] extends true ? K : never;
|
|
81
|
+
}[keyof C];
|
|
82
|
+
/** Money is the one column whose write shape is wider than its row shape. */
|
|
83
|
+
type InputOf<T> = T extends MoneyValue ? MoneyInput : T;
|
|
84
|
+
/** What an insert must supply: every column except the ones carrying a default. */
|
|
85
|
+
export type Insertable<C extends ColumnMap> = {
|
|
86
|
+
readonly [K in Exclude<keyof C, DefaultedKeys<C>>]: InputOf<TypeOf<C[K]>>;
|
|
87
|
+
} & {
|
|
88
|
+
readonly [K in DefaultedKeys<C>]?: InputOf<TypeOf<C[K]>>;
|
|
89
|
+
};
|
|
90
|
+
export interface IndexDef {
|
|
91
|
+
readonly name: string;
|
|
92
|
+
readonly columns: readonly string[];
|
|
93
|
+
readonly unique: boolean;
|
|
94
|
+
readonly order?: 'asc' | 'desc';
|
|
95
|
+
/** Partial index predicate — a soft-deleted row is excluded with this. */
|
|
96
|
+
readonly where?: string;
|
|
97
|
+
}
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The structural vocabulary of a column. Drizzle is the production backing for the physical
|
|
2
|
+
// layer (see README); declaring the shape we consume instead of depending on an ORM keeps the
|
|
3
|
+
// emitted SQL readable and keeps this package free of a dependency an agent must learn to read.
|
|
4
|
+
//
|
|
5
|
+
// A column carries its TypeScript type in `$parse`, which is what lets the row type be derived
|
|
6
|
+
// from the column set instead of being written a second time as a hand-maintained schema.
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
package/src/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,8FAA8F;AAC9F,gGAAgG;AAChG,EAAE;AACF,+FAA+F;AAC/F,0FAA0F"}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The structural vocabulary of a column. The physical layer is this package's own hand-written
|
|
2
|
+
// `postgresDriver()` (`pg-driver.ts` / `pg-sql.ts`), not an ORM; declaring the narrow shape we
|
|
3
|
+
// consume keeps the emitted SQL readable and keeps this package free of a dependency an agent
|
|
4
|
+
// must learn to read.
|
|
5
|
+
//
|
|
6
|
+
// A column carries its TypeScript type in `$parse`, which is what lets the row type be derived
|
|
7
|
+
// from the column set instead of being written a second time as a hand-maintained schema.
|
|
5
8
|
|
|
6
|
-
/** Postgres types the blessed
|
|
9
|
+
/** Postgres types the blessed builders emit. `money` expands to `bigint` + `char(3)`. */
|
|
7
10
|
export type ColumnKind =
|
|
8
11
|
| 'uuid'
|
|
9
12
|
| 'text'
|
|
@@ -11,67 +14,114 @@ export type ColumnKind =
|
|
|
11
14
|
| 'boolean'
|
|
12
15
|
| 'integer'
|
|
13
16
|
| 'bigint'
|
|
14
|
-
| 'numeric'
|
|
15
17
|
| 'timestamptz'
|
|
16
|
-
| '
|
|
17
|
-
| '
|
|
18
|
+
| 'jsonb'
|
|
19
|
+
| 'money';
|
|
18
20
|
|
|
19
21
|
export type ColumnDefault =
|
|
20
|
-
| { readonly kind: 'sql'; readonly expression: string }
|
|
21
22
|
| { readonly kind: 'value'; readonly value: string | number | boolean | null }
|
|
22
23
|
| { readonly kind: 'generated'; readonly by: 'uuid-v7' | 'now' };
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
readonly onDelete?:
|
|
25
|
+
export type OnDelete = 'cascade' | 'restrict' | 'set null';
|
|
26
|
+
|
|
27
|
+
export interface ReferenceOptions {
|
|
28
|
+
readonly onDelete?: OnDelete;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly
|
|
31
|
+
/** The single value a money column puts on the row. Two physical columns back it. */
|
|
32
|
+
export interface MoneyValue {
|
|
33
|
+
readonly minor: bigint;
|
|
34
|
+
readonly currency: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What a writer may hand a money column. An integer `number` widens; a float throws. */
|
|
38
|
+
export interface MoneyInput {
|
|
39
|
+
readonly minor: bigint | number;
|
|
40
|
+
readonly currency: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What the author declared. Where it landed — table, property key, physical name — is the
|
|
45
|
+
* binding `entity()` records (see `column.ts`), so a name is never written twice.
|
|
46
|
+
*/
|
|
47
|
+
export interface ColumnMeta {
|
|
33
48
|
readonly kind: ColumnKind;
|
|
34
49
|
readonly notNull: boolean;
|
|
35
50
|
readonly primaryKey: boolean;
|
|
36
51
|
readonly unique: boolean;
|
|
52
|
+
readonly index: boolean;
|
|
53
|
+
/** Presence of a tenant column is what turns tenancy on. See `tenancy.ts`. */
|
|
54
|
+
readonly tenant: boolean;
|
|
37
55
|
readonly length?: number;
|
|
56
|
+
readonly values?: readonly string[];
|
|
38
57
|
readonly default?: ColumnDefault;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
readonly
|
|
42
|
-
|
|
43
|
-
readonly
|
|
44
|
-
|
|
45
|
-
* Runtime guard AND the carrier of the column's TypeScript type. Every write goes
|
|
46
|
-
* through it, which is how `money()` can refuse a float instead of rounding it.
|
|
47
|
-
*/
|
|
48
|
-
readonly parse: (value: unknown) => T;
|
|
58
|
+
readonly onUpdate?: ColumnDefault;
|
|
59
|
+
/** Takes the physical name, so a CHECK can be written before that name is known. */
|
|
60
|
+
readonly check?: (column: string) => string;
|
|
61
|
+
/** A thunk: schema modules reference each other in a cycle. */
|
|
62
|
+
readonly references?: () => AnyColumn;
|
|
63
|
+
readonly onDelete?: OnDelete;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
/**
|
|
67
|
+
* `Optional` is the phantom that says "this column has a default", so an insert may omit it.
|
|
68
|
+
* It is a real boolean at runtime too, so nothing has to be re-derived to check it.
|
|
69
|
+
*/
|
|
70
|
+
export interface Column<T, Optional extends boolean = false> {
|
|
71
|
+
readonly $meta: ColumnMeta;
|
|
72
|
+
/** Runtime guard AND the carrier of the column's TypeScript type. */
|
|
73
|
+
readonly $parse: (value: unknown) => T;
|
|
74
|
+
readonly $optional: Optional;
|
|
75
|
+
/** `boolean`: only a uuid key carries a generated default, so only it becomes optional. */
|
|
76
|
+
primaryKey(): Column<T, boolean>;
|
|
77
|
+
nullable(): Column<T | null, Optional>;
|
|
78
|
+
unique(): Column<T, Optional>;
|
|
79
|
+
/** Marks the tenant column; a query without an org predicate then throws. */
|
|
80
|
+
tenant(): Column<T, Optional>;
|
|
81
|
+
references(target: () => AnyColumn, options?: ReferenceOptions): Column<T, Optional>;
|
|
82
|
+
default(value: T): Column<T, true>;
|
|
83
|
+
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
readonly unique: boolean;
|
|
57
|
-
/** Partial index predicate — soft-deleted rows are excluded with this. */
|
|
58
|
-
readonly where?: string;
|
|
85
|
+
/** A uuid primary key is generated (v7) when omitted, which is why it narrows to `true`. */
|
|
86
|
+
export interface UuidColumn<Optional extends boolean = false> extends Column<string, Optional> {
|
|
87
|
+
primaryKey(): Column<string, true>;
|
|
59
88
|
}
|
|
60
89
|
|
|
61
|
-
export interface
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
readonly primaryKey: readonly string[];
|
|
65
|
-
readonly indexes: readonly IndexDef[];
|
|
90
|
+
export interface TimestampColumn<Optional extends boolean = false> extends Column<Date, Optional> {
|
|
91
|
+
defaultNow(): TimestampColumn<true>;
|
|
92
|
+
onUpdateNow(): TimestampColumn<Optional>;
|
|
66
93
|
}
|
|
67
94
|
|
|
68
|
-
|
|
95
|
+
export type AnyColumn = Column<unknown, boolean>;
|
|
96
|
+
|
|
97
|
+
export type ColumnMap = Readonly<Record<string, AnyColumn>>;
|
|
98
|
+
|
|
99
|
+
export type TypeOf<C> = C extends Column<infer T, boolean> ? T : never;
|
|
100
|
+
|
|
101
|
+
/** The row type a column set describes. This derivation is why the package exists. */
|
|
69
102
|
export type RowOf<C extends ColumnMap> = {
|
|
70
|
-
readonly [K in keyof C]: C[K]
|
|
103
|
+
readonly [K in keyof C]: TypeOf<C[K]>;
|
|
71
104
|
};
|
|
72
105
|
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
type DefaultedKeys<C extends ColumnMap> = {
|
|
107
|
+
[K in keyof C]-?: C[K]['$optional'] extends true ? K : never;
|
|
108
|
+
}[keyof C];
|
|
109
|
+
|
|
110
|
+
/** Money is the one column whose write shape is wider than its row shape. */
|
|
111
|
+
type InputOf<T> = T extends MoneyValue ? MoneyInput : T;
|
|
112
|
+
|
|
113
|
+
/** What an insert must supply: every column except the ones carrying a default. */
|
|
114
|
+
export type Insertable<C extends ColumnMap> = {
|
|
115
|
+
readonly [K in Exclude<keyof C, DefaultedKeys<C>>]: InputOf<TypeOf<C[K]>>;
|
|
116
|
+
} & {
|
|
117
|
+
readonly [K in DefaultedKeys<C>]?: InputOf<TypeOf<C[K]>>;
|
|
118
|
+
};
|
|
75
119
|
|
|
76
|
-
export
|
|
77
|
-
|
|
120
|
+
export interface IndexDef {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
readonly columns: readonly string[];
|
|
123
|
+
readonly unique: boolean;
|
|
124
|
+
readonly order?: 'asc' | 'desc';
|
|
125
|
+
/** Partial index predicate — a soft-deleted row is excluded with this. */
|
|
126
|
+
readonly where?: string;
|
|
127
|
+
}
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// `posts.$view(['id', 'title'])` — the hop between an entity and an action's `output`: a Standard
|
|
2
|
+
// Schema over a subset of the row, so `output: PostView` never re-declares a shape the columns
|
|
3
|
+
// already describe. Values are validated by the entity's own column parsers; an unknown key is a
|
|
4
|
+
// declaration-time failure, not a surprise on the first request.
|
|
5
|
+
|
|
6
|
+
import type { StandardSchemaV1 } from '@ultimat3/schema';
|
|
7
|
+
import { invariantViolated } from './errors';
|
|
8
|
+
import type { AnyColumn, ColumnMap } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A row projection, usable anywhere a schema is. `$row` is the phantom that carries the type
|
|
12
|
+
* (`type PostView = typeof PostView.$row`); `$name` is how a manifest or an OpenAPI document
|
|
13
|
+
* identifies it.
|
|
14
|
+
*/
|
|
15
|
+
export interface EntityView<Row, K extends keyof Row & string>
|
|
16
|
+
extends StandardSchemaV1<unknown, Pick<Row, K>> {
|
|
17
|
+
readonly $name: string;
|
|
18
|
+
readonly $keys: readonly K[];
|
|
19
|
+
/** Phantom: `type PostView = typeof PostView.$row`. Reading it at runtime throws. */
|
|
20
|
+
readonly $row: Pick<Row, K>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dots and underscores only, so the name is a legal `components.schemas` key unescaped. */
|
|
24
|
+
const viewName = (entityName: string, keys: readonly string[]): string =>
|
|
25
|
+
`${entityName}.view.${keys.join('_')}`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bound to an entity as `$view`; never exported as a free `view(entity, keys)`, because two ways
|
|
29
|
+
* to write the same projection is exactly the ambiguity the `$`-prefixed surface exists to avoid.
|
|
30
|
+
*/
|
|
31
|
+
export const viewFor = <Row, K extends keyof Row & string>(
|
|
32
|
+
entityName: string,
|
|
33
|
+
columns: ColumnMap,
|
|
34
|
+
keys: readonly K[],
|
|
35
|
+
): EntityView<Row, K> => {
|
|
36
|
+
// Resolved once, at declaration: a key naming no column is the author's typo, and the columns
|
|
37
|
+
// are listed because the agent reading the error is the one that has to pick the right key.
|
|
38
|
+
const picked: readonly (readonly [K, AnyColumn])[] = keys.map((key) => {
|
|
39
|
+
const column = columns[key];
|
|
40
|
+
if (column === undefined) {
|
|
41
|
+
throw invariantViolated(
|
|
42
|
+
entityName,
|
|
43
|
+
'view',
|
|
44
|
+
`$view(['${key}']) names no column — pick from: ${Object.keys(columns).join(', ')}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return [key, column] as const;
|
|
48
|
+
});
|
|
49
|
+
const name = viewName(entityName, keys);
|
|
50
|
+
|
|
51
|
+
const parse = (value: unknown): Pick<Row, K> => {
|
|
52
|
+
if (typeof value !== 'object' || value === null) {
|
|
53
|
+
throw invariantViolated(entityName, 'view', `expected an object, got ${String(value)}`);
|
|
54
|
+
}
|
|
55
|
+
const input = value as Readonly<Record<string, unknown>>;
|
|
56
|
+
const projected = {} as Record<K, unknown>;
|
|
57
|
+
for (const [key, column] of picked) {
|
|
58
|
+
const given = input[key];
|
|
59
|
+
if (given === undefined || given === null) {
|
|
60
|
+
// No default is filled in: a view projects a row that already exists, so an absent
|
|
61
|
+
// required column is missing data, never a value the projection may invent.
|
|
62
|
+
if (column.$meta.notNull) throw invariantViolated(entityName, `view.${key}`, 'is required');
|
|
63
|
+
projected[key] = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
projected[key] = column.$parse(given);
|
|
67
|
+
}
|
|
68
|
+
// Every picked key went through its own column's parser, so this is the derived projection.
|
|
69
|
+
return projected as Pick<Row, K>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'~standard': {
|
|
74
|
+
version: 1,
|
|
75
|
+
vendor: 'ultimate',
|
|
76
|
+
validate: (value) => {
|
|
77
|
+
try {
|
|
78
|
+
return { value: parse(value) };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
issues: [{ message: error instanceof Error ? error.message : String(error) }],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
$name: name,
|
|
87
|
+
$keys: keys,
|
|
88
|
+
get $row(): Pick<Row, K> {
|
|
89
|
+
// Type-only, exactly as on the entity. Reading it means a type was meant.
|
|
90
|
+
throw invariantViolated(
|
|
91
|
+
entityName,
|
|
92
|
+
'view.$row',
|
|
93
|
+
'$row is a type, not a value — use typeof x.$row',
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|