@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 developerz.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,60 +1,144 @@
|
|
|
1
1
|
# @ultimat3/entity 🗄️
|
|
2
2
|
|
|
3
|
-
An entity is **
|
|
4
|
-
|
|
5
|
-
one
|
|
3
|
+
An entity is **columns + the invariants that hold for every row**. The row type is derived from
|
|
4
|
+
the columns: `type Post = typeof posts.$row`. Declare the shape once — repos, migrations, the
|
|
5
|
+
admin screen, cache tags and the manifest are all projections of that one call.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
import {
|
|
9
|
+
entity, enumerated, integer, invariant, text, timestamp, url, uuid,
|
|
10
|
+
} from '@ultimat3/entity';
|
|
11
|
+
|
|
12
|
+
export const posts = entity('posts', {
|
|
13
|
+
tenant: 'orgId',
|
|
14
|
+
columns: {
|
|
15
|
+
id: uuid().primaryKey(),
|
|
16
|
+
orgId: uuid().references(() => orgs.id, { onDelete: 'cascade' }),
|
|
17
|
+
slug: text({ max: 80 }),
|
|
18
|
+
title: text({ max: 120 }),
|
|
19
|
+
coverUrl: url().nullable(),
|
|
20
|
+
status: enumerated(POST_STATUSES).default('draft'),
|
|
21
|
+
likeCount: integer().default(0),
|
|
22
|
+
createdAt: timestamp().defaultNow(),
|
|
23
|
+
updatedAt: timestamp().defaultNow().onUpdateNow(),
|
|
24
|
+
},
|
|
25
|
+
invariants: [
|
|
26
|
+
invariant('post_title_present', (c) => c.title.trimmed().minLength(1)),
|
|
27
|
+
invariant('post_slug_unique_per_org', (c) => c.unique(['orgId', 'slug'])),
|
|
28
|
+
invariant('post_like_count_non_negative', (c) => c.likeCount.atLeast(0)),
|
|
29
|
+
],
|
|
30
|
+
indexes: [{ on: ['orgId', 'createdAt'], order: 'desc', where: (c) => c.status.eq('published') }],
|
|
17
31
|
});
|
|
32
|
+
|
|
33
|
+
export type Post = typeof posts.$row;
|
|
34
|
+
|
|
35
|
+
export const PostView = posts.$view(['id', 'title', 'coverUrl', 'status']);
|
|
36
|
+
export type PostView = typeof PostView.$row;
|
|
18
37
|
```
|
|
19
38
|
|
|
39
|
+
## `$view` is what leaves the server
|
|
40
|
+
|
|
41
|
+
`posts.$view([...])` returns a Standard Schema over a subset of the row, so an action names it
|
|
42
|
+
directly — `output: PostView` — and the projected type flows on to the client and the component.
|
|
43
|
+
|
|
44
|
+
| Rule | Detail |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Keys are checked twice | unknown key ⇒ `tsc` error, and `X_INVARIANT_VIOLATED` at declaration for a JS caller |
|
|
47
|
+
| Values are the columns' | each key is parsed by the column that declared it; no second copy of the rule |
|
|
48
|
+
| Nothing is invented | a view projects a row that exists — an absent required key is missing data, not a default |
|
|
49
|
+
| `$name` | `posts.view.id_title_coverUrl_status` — stable, and legal as an OpenAPI `components.schemas` key |
|
|
50
|
+
|
|
51
|
+
There is no free `view(posts, [...])` function: a projection is reached through the entity, and
|
|
52
|
+
every framework member is `$`-prefixed so a column may still be called `name`, `view` or `tenant`.
|
|
53
|
+
|
|
54
|
+
A view the columns cannot express — a joined `authorName`, a computed `excerpt` — is a hand-written
|
|
55
|
+
`t.object({...})`. `t` is re-exported here, the same object `@ultimat3/schema` exports, so that file
|
|
56
|
+
still imports one package: `import { entity, t } from '@ultimat3/entity'`.
|
|
57
|
+
|
|
20
58
|
## Blessed columns
|
|
21
59
|
|
|
22
|
-
|
|
|
60
|
+
| Builder | Emits | Why it is the only way |
|
|
23
61
|
|---|---|---|
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `money(
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
once or not at all.
|
|
62
|
+
| `uuid()` | `uuid`; `.primaryKey()` defaults to v7 | time-ordered keys keep the pk index append-friendly |
|
|
63
|
+
| `timestamp()` | `timestamptz` | UTC storage is not a per-table decision; there is no naive variant |
|
|
64
|
+
| `money()` | `<name>_minor bigint` + `<name>_currency char(3)` | never a float, never one implied currency |
|
|
65
|
+
| `enumerated(v)` | `text` + CHECK | a variant is a one-line migration, not `ALTER TYPE` |
|
|
66
|
+
| `tz(zones)`, `locale(tags)` | `text` + CHECK, `Intl`-validated at declaration | an offset is not a time zone |
|
|
67
|
+
| `text({ max })`, `integer()`, `boolean()`, `url()` | `text`/`integer`/`boolean` + CHECK | format is enforced by the database too |
|
|
68
|
+
|
|
69
|
+
Chain: `.primaryKey()` · `.nullable()` · `.unique()` · `.default(v)` · `.defaultNow()` ·
|
|
70
|
+
`.onUpdateNow()` · `.references(() => other.id, { onDelete })` · `.tenant()`. Physical names are
|
|
71
|
+
derived from the property key (`orgId` → `org_id`); a name is written once, or never.
|
|
35
72
|
|
|
36
73
|
## Invariants run twice
|
|
37
74
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
import or a `psql` session hits the same rule.
|
|
75
|
+
One declaration, two enforcement points: the app checks it on every write, and the migration
|
|
76
|
+
emits it. A bulk import or a `psql` session hits the same rule.
|
|
41
77
|
|
|
42
78
|
```sql
|
|
43
|
-
ALTER TABLE "posts" ADD CONSTRAINT "
|
|
79
|
+
ALTER TABLE "posts" ADD CONSTRAINT "posts_post_like_count_non_negative_check" CHECK (like_count >= 0);
|
|
80
|
+
CREATE UNIQUE INDEX "posts_post_slug_unique_per_org_key" ON "posts" ("org_id", "slug");
|
|
44
81
|
```
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
A rule written as a JS predicate — `c.slug.matches(isValidSlug)`, `c.satisfies(fn, [...])` —
|
|
84
|
+
still runs on write, reports `kind: 'assert'` and `sql: null`, and is what `x verify` warns
|
|
85
|
+
about: a rule the database does not know is a rule a migration script can violate.
|
|
86
|
+
|
|
87
|
+
## One typed handle
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export const db = database({ orgs, posts });
|
|
91
|
+
|
|
92
|
+
db.posts.where({ orgId }).orderBy('createdAt').limit(50).page(); // { rows, nextCursor }
|
|
93
|
+
```
|
|
47
94
|
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
`db.posts` exists because `posts` was declared. Pagination is **cursor-only**: `OFFSET` is wrong
|
|
96
|
+
under concurrent writes, because an insert before the offset shifts every later page and the
|
|
97
|
+
client silently skips and repeats rows.
|
|
98
|
+
|
|
99
|
+
`nextCursor` is signed by `@ultimat3/core` and scoped to the plan that produced it — this entity,
|
|
100
|
+
these filters, this sort order. A tampered cursor, or one taken from another listing, is
|
|
101
|
+
`X_CURSOR_INVALID` rather than a silent page one. The page size is deliberately outside the scope:
|
|
102
|
+
asking for a bigger next page is the same query.
|
|
103
|
+
|
|
104
|
+
## Two drivers, one meaning
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
database({ orgs, posts }); // memoryDriver() — the default
|
|
108
|
+
database({ orgs, posts }, { driver: postgresDriver() }); // production
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| | `memoryDriver()` | `postgresDriver()` |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| Rows live | in a `Map` | in Postgres |
|
|
114
|
+
| For | tests, `x dev` before the first migration | production |
|
|
115
|
+
| Transaction | `memoryTransactor()` — undo closures | `postgresTransactor()` — real `BEGIN`/`COMMIT` |
|
|
116
|
+
|
|
117
|
+
They are not two implementations of an idea. They share the plan (scope, sort order, page size),
|
|
118
|
+
the cursor codec and the `Repo<T>` contract, so a page taken in a test means the same thing as a
|
|
119
|
+
page taken in production. `postgresDriver()` takes no connection: `db()` from `@ultimat3/db`
|
|
120
|
+
returns the open transaction when there is one, so a repository call inside `withTransaction`
|
|
121
|
+
joins it without being told — which is how a job's outbox row lands atomically with the write
|
|
122
|
+
that enqueued it.
|
|
123
|
+
|
|
124
|
+
Every value is bound to `$n` and every identifier is resolved through the entity, so a column
|
|
125
|
+
name can only be one the entity declared and a row value can never become SQL.
|
|
53
126
|
|
|
54
127
|
## Tenancy is a guard
|
|
55
128
|
|
|
56
|
-
|
|
57
|
-
|
|
129
|
+
`tenant: 'orgId'` on the entity names the column outright. Omit it and it is inferred — a
|
|
130
|
+
`.tenant()` column, else one named `orgId` — so an entity never becomes unscoped by forgetting the
|
|
131
|
+
key; name a column that does not exist and the declaration fails with `X_INVARIANT_VIOLATED`.
|
|
132
|
+
|
|
133
|
+
Either way, every read then needs an org predicate. Without one: `X_TENANCY_UNSCOPED`, at the
|
|
134
|
+
seam, every time. Writes are reads: `update(id, patch)` and `delete(id)` build the same plan, so
|
|
135
|
+
an id alone never addresses a row, and another tenant's id is `X_NOT_FOUND` rather than theirs.
|
|
136
|
+
|
|
137
|
+
## Seeds
|
|
138
|
+
|
|
139
|
+
`defineSeed('dev', async ({ insert, id }) => …)`. `id('post:tenancy')` is a UUID v5 of the label,
|
|
140
|
+
so the same fixture graph gets the same ids on every machine. Rows go through the columns and
|
|
141
|
+
the invariants, which makes a seed a test of the schema as well.
|
|
58
142
|
|
|
59
143
|
## Errors
|
|
60
144
|
|
|
@@ -63,7 +147,9 @@ predicate. Without one: `X_TENANCY_UNSCOPED`, at the seam, every time.
|
|
|
63
147
|
|
|
64
148
|
## Boundaries
|
|
65
149
|
|
|
66
|
-
Tier 2. Imports `@ultimat3/core` and `@ultimat3/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
150
|
+
Tier 2. Imports `@ultimat3/core`, `@ultimat3/schema` and `@ultimat3/db` only — `db` is tier 1
|
|
151
|
+
(it imports `core` and nothing else), which is what keeps `Driver` and its production
|
|
152
|
+
implementation in one package instead of two. No `drizzle-orm` dependency:
|
|
153
|
+
`types.ts` declares the narrow structural column vocabulary this package consumes, so the
|
|
154
|
+
generated SQL stays readable and an agent can self-correct against it. `@ultimat3/cache`
|
|
155
|
+
invalidates by the `entity:<name>` tag.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/entity",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A table + its domain type + invariants the database also enforces",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"test": "bun test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@ultimat3/core": "
|
|
34
|
-
"@ultimat3/
|
|
33
|
+
"@ultimat3/core": "1.0.0",
|
|
34
|
+
"@ultimat3/db": "1.0.0",
|
|
35
|
+
"@ultimat3/schema": "1.0.0"
|
|
35
36
|
}
|
|
36
37
|
}
|
package/src/column.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnyColumn, Column, ColumnDefault, ColumnMeta, TimestampColumn } from './types';
|
|
2
|
+
export declare const snake: (value: string) => string;
|
|
3
|
+
export declare const GENERATED_UUID: ColumnDefault;
|
|
4
|
+
export declare const GENERATED_NOW: ColumnDefault;
|
|
5
|
+
/** Every column starts here: not null, no key, no index. */
|
|
6
|
+
export declare const BARE: Omit<ColumnMeta, 'kind'>;
|
|
7
|
+
export interface Binding {
|
|
8
|
+
readonly table: string;
|
|
9
|
+
/** camelCase key on the row. */
|
|
10
|
+
readonly property: string;
|
|
11
|
+
/** snake_case physical column name. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Called once per column by `entity()`. A column object belongs to exactly one table: sharing
|
|
16
|
+
* one between two entities would give it two physical names and silently mis-name a foreign
|
|
17
|
+
* key, so a second binding is a declaration-time error.
|
|
18
|
+
*/
|
|
19
|
+
export declare const bindColumn: (column: AnyColumn, table: string, property: string) => Binding;
|
|
20
|
+
export declare const bindingOf: (column: AnyColumn) => Binding | undefined;
|
|
21
|
+
export declare const makeColumn: <T, Optional extends boolean>(meta: ColumnMeta, parse: (value: unknown) => T, optional: Optional) => Column<T, Optional>;
|
|
22
|
+
export declare const column: <T>(kind: ColumnMeta['kind'], parse: (value: unknown) => T, extra?: Partial<ColumnMeta>) => Column<T>;
|
|
23
|
+
/**
|
|
24
|
+
* `timestamptz`, always. There is no naive-timestamp builder and there will not be one: a
|
|
25
|
+
* `timestamp without time zone` is a bug that only surfaces twice a year.
|
|
26
|
+
*/
|
|
27
|
+
export declare const makeTimestamp: <Optional extends boolean>(meta: ColumnMeta, parse: (value: unknown) => Date, optional: Optional) => TimestampColumn<Optional>;
|
|
28
|
+
//# sourceMappingURL=column.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column.d.ts","sourceRoot":"","sources":["column.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE7F,eAAO,MAAM,KAAK,UAAW,MAAM,KAAG,MACsB,CAAC;AAE7D,eAAO,MAAM,cAAc,EAAE,aAAoD,CAAC;AAClF,eAAO,MAAM,aAAa,EAAE,aAAgD,CAAC;AAE7E,4DAA4D;AAC5D,eAAO,MAAM,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAMzC,CAAC;AAEF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAID;;;;GAIG;AACH,eAAO,MAAM,UAAU,WAAY,SAAS,SAAS,MAAM,YAAY,MAAM,KAAG,OAa/E,CAAC;AAEF,eAAO,MAAM,SAAS,WAAY,SAAS,KAAG,OAAO,GAAG,SAAiC,CAAC;AAc1F,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,QAAQ,SAAS,OAAO,QAC9C,UAAU,SACT,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,YAClB,QAAQ,KACjB,MAAM,CAAC,CAAC,EAAE,QAAQ,CAuCnB,CAAC;AAEH,eAAO,MAAM,MAAM,GAAI,CAAC,QAChB,UAAU,CAAC,MAAM,CAAC,SACjB,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,UACrB,OAAO,CAAC,UAAU,CAAC,KACzB,MAAM,CAAC,CAAC,CAAoE,CAAC;AAEhF;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,SAAS,OAAO,QAC9C,UAAU,SACT,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,YACrB,QAAQ,KACjB,eAAe,CAAC,QAAQ,CAIzB,CAAC"}
|
package/src/column.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The chain every column builder is made of. Each link returns a new column, so a chain reads
|
|
2
|
+
// in declaration order and a builder is never mutated behind someone's back.
|
|
3
|
+
//
|
|
4
|
+
// Where a column landed (table, property key, physical name) is recorded in a binding rather
|
|
5
|
+
// than on the column: the author writes the property key once, `entity()` derives `orgId` ->
|
|
6
|
+
// `org_id` from it, and a lazy `.references(() => orgs.id)` can still resolve the target's
|
|
7
|
+
// physical name even though two schema modules import each other in a cycle.
|
|
8
|
+
import { invariantViolated } from './errors';
|
|
9
|
+
export const snake = (value) => value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
10
|
+
export const GENERATED_UUID = { kind: 'generated', by: 'uuid-v7' };
|
|
11
|
+
export const GENERATED_NOW = { kind: 'generated', by: 'now' };
|
|
12
|
+
/** Every column starts here: not null, no key, no index. */
|
|
13
|
+
export const BARE = {
|
|
14
|
+
notNull: true,
|
|
15
|
+
primaryKey: false,
|
|
16
|
+
unique: false,
|
|
17
|
+
index: false,
|
|
18
|
+
tenant: false,
|
|
19
|
+
};
|
|
20
|
+
const bindings = new WeakMap();
|
|
21
|
+
/**
|
|
22
|
+
* Called once per column by `entity()`. A column object belongs to exactly one table: sharing
|
|
23
|
+
* one between two entities would give it two physical names and silently mis-name a foreign
|
|
24
|
+
* key, so a second binding is a declaration-time error.
|
|
25
|
+
*/
|
|
26
|
+
export const bindColumn = (column, table, property) => {
|
|
27
|
+
const existing = bindings.get(column);
|
|
28
|
+
if (existing !== undefined && existing.table !== table) {
|
|
29
|
+
throw invariantViolated(table, property, `this column is already bound to ${existing.table}.${existing.name}; ` +
|
|
30
|
+
'build a new column instead of sharing one between entities');
|
|
31
|
+
}
|
|
32
|
+
const binding = { table, property, name: snake(property) };
|
|
33
|
+
bindings.set(column, binding);
|
|
34
|
+
return binding;
|
|
35
|
+
};
|
|
36
|
+
export const bindingOf = (column) => bindings.get(column);
|
|
37
|
+
const literal = (value) => {
|
|
38
|
+
if (value === null)
|
|
39
|
+
return { kind: 'value', value: null };
|
|
40
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
41
|
+
return { kind: 'value', value };
|
|
42
|
+
}
|
|
43
|
+
throw invariantViolated('column', 'default', `a default must be a literal; got ${typeof value}. For an instant use timestamp().defaultNow()`);
|
|
44
|
+
};
|
|
45
|
+
export const makeColumn = (meta, parse, optional) => ({
|
|
46
|
+
$meta: meta,
|
|
47
|
+
$parse: parse,
|
|
48
|
+
$optional: optional,
|
|
49
|
+
primaryKey: () => {
|
|
50
|
+
// Only a uuid key can be generated for the caller; any other key must be supplied.
|
|
51
|
+
const generated = meta.kind === 'uuid' && meta.default === undefined;
|
|
52
|
+
return makeColumn({ ...meta, primaryKey: true, ...(generated ? { default: GENERATED_UUID } : {}) }, parse, generated || meta.default !== undefined);
|
|
53
|
+
},
|
|
54
|
+
nullable: () => makeColumn({ ...meta, notNull: false }, (value) => (value === null || value === undefined ? null : parse(value)), optional),
|
|
55
|
+
unique: () => makeColumn({ ...meta, unique: true }, parse, optional),
|
|
56
|
+
tenant: () => makeColumn({ ...meta, tenant: true, index: true }, parse, optional),
|
|
57
|
+
references: (target, options = {}) => makeColumn({
|
|
58
|
+
...meta,
|
|
59
|
+
references: target,
|
|
60
|
+
index: true,
|
|
61
|
+
...(options.onDelete === undefined ? {} : { onDelete: options.onDelete }),
|
|
62
|
+
}, parse, optional),
|
|
63
|
+
default: (value) => makeColumn({ ...meta, default: literal(value) }, parse, true),
|
|
64
|
+
});
|
|
65
|
+
export const column = (kind, parse, extra = {}) => makeColumn({ ...BARE, ...extra, kind }, parse, false);
|
|
66
|
+
/**
|
|
67
|
+
* `timestamptz`, always. There is no naive-timestamp builder and there will not be one: a
|
|
68
|
+
* `timestamp without time zone` is a bug that only surfaces twice a year.
|
|
69
|
+
*/
|
|
70
|
+
export const makeTimestamp = (meta, parse, optional) => ({
|
|
71
|
+
...makeColumn(meta, parse, optional),
|
|
72
|
+
defaultNow: () => makeTimestamp({ ...meta, default: GENERATED_NOW }, parse, true),
|
|
73
|
+
onUpdateNow: () => makeTimestamp({ ...meta, onUpdate: GENERATED_NOW }, parse, optional),
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=column.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column.js","sourceRoot":"","sources":["column.ts"],"names":[],"mappings":"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"}
|
package/src/column.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The chain every column builder is made of. Each link returns a new column, so a chain reads
|
|
2
|
+
// in declaration order and a builder is never mutated behind someone's back.
|
|
3
|
+
//
|
|
4
|
+
// Where a column landed (table, property key, physical name) is recorded in a binding rather
|
|
5
|
+
// than on the column: the author writes the property key once, `entity()` derives `orgId` ->
|
|
6
|
+
// `org_id` from it, and a lazy `.references(() => orgs.id)` can still resolve the target's
|
|
7
|
+
// physical name even though two schema modules import each other in a cycle.
|
|
8
|
+
|
|
9
|
+
import { invariantViolated } from './errors';
|
|
10
|
+
import type { AnyColumn, Column, ColumnDefault, ColumnMeta, TimestampColumn } from './types';
|
|
11
|
+
|
|
12
|
+
export const snake = (value: string): string =>
|
|
13
|
+
value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
14
|
+
|
|
15
|
+
export const GENERATED_UUID: ColumnDefault = { kind: 'generated', by: 'uuid-v7' };
|
|
16
|
+
export const GENERATED_NOW: ColumnDefault = { kind: 'generated', by: 'now' };
|
|
17
|
+
|
|
18
|
+
/** Every column starts here: not null, no key, no index. */
|
|
19
|
+
export const BARE: Omit<ColumnMeta, 'kind'> = {
|
|
20
|
+
notNull: true,
|
|
21
|
+
primaryKey: false,
|
|
22
|
+
unique: false,
|
|
23
|
+
index: false,
|
|
24
|
+
tenant: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface Binding {
|
|
28
|
+
readonly table: string;
|
|
29
|
+
/** camelCase key on the row. */
|
|
30
|
+
readonly property: string;
|
|
31
|
+
/** snake_case physical column name. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const bindings = new WeakMap<AnyColumn, Binding>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Called once per column by `entity()`. A column object belongs to exactly one table: sharing
|
|
39
|
+
* one between two entities would give it two physical names and silently mis-name a foreign
|
|
40
|
+
* key, so a second binding is a declaration-time error.
|
|
41
|
+
*/
|
|
42
|
+
export const bindColumn = (column: AnyColumn, table: string, property: string): Binding => {
|
|
43
|
+
const existing = bindings.get(column);
|
|
44
|
+
if (existing !== undefined && existing.table !== table) {
|
|
45
|
+
throw invariantViolated(
|
|
46
|
+
table,
|
|
47
|
+
property,
|
|
48
|
+
`this column is already bound to ${existing.table}.${existing.name}; ` +
|
|
49
|
+
'build a new column instead of sharing one between entities',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const binding: Binding = { table, property, name: snake(property) };
|
|
53
|
+
bindings.set(column, binding);
|
|
54
|
+
return binding;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const bindingOf = (column: AnyColumn): Binding | undefined => bindings.get(column);
|
|
58
|
+
|
|
59
|
+
const literal = (value: unknown): ColumnDefault => {
|
|
60
|
+
if (value === null) return { kind: 'value', value: null };
|
|
61
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
62
|
+
return { kind: 'value', value };
|
|
63
|
+
}
|
|
64
|
+
throw invariantViolated(
|
|
65
|
+
'column',
|
|
66
|
+
'default',
|
|
67
|
+
`a default must be a literal; got ${typeof value}. For an instant use timestamp().defaultNow()`,
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const makeColumn = <T, Optional extends boolean>(
|
|
72
|
+
meta: ColumnMeta,
|
|
73
|
+
parse: (value: unknown) => T,
|
|
74
|
+
optional: Optional,
|
|
75
|
+
): Column<T, Optional> => ({
|
|
76
|
+
$meta: meta,
|
|
77
|
+
$parse: parse,
|
|
78
|
+
$optional: optional,
|
|
79
|
+
|
|
80
|
+
primaryKey: () => {
|
|
81
|
+
// Only a uuid key can be generated for the caller; any other key must be supplied.
|
|
82
|
+
const generated = meta.kind === 'uuid' && meta.default === undefined;
|
|
83
|
+
return makeColumn<T, boolean>(
|
|
84
|
+
{ ...meta, primaryKey: true, ...(generated ? { default: GENERATED_UUID } : {}) },
|
|
85
|
+
parse,
|
|
86
|
+
generated || meta.default !== undefined,
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
nullable: () =>
|
|
91
|
+
makeColumn<T | null, Optional>(
|
|
92
|
+
{ ...meta, notNull: false },
|
|
93
|
+
(value) => (value === null || value === undefined ? null : parse(value)),
|
|
94
|
+
optional,
|
|
95
|
+
),
|
|
96
|
+
|
|
97
|
+
unique: () => makeColumn<T, Optional>({ ...meta, unique: true }, parse, optional),
|
|
98
|
+
|
|
99
|
+
tenant: () => makeColumn<T, Optional>({ ...meta, tenant: true, index: true }, parse, optional),
|
|
100
|
+
|
|
101
|
+
references: (target, options = {}) =>
|
|
102
|
+
makeColumn<T, Optional>(
|
|
103
|
+
{
|
|
104
|
+
...meta,
|
|
105
|
+
references: target,
|
|
106
|
+
index: true,
|
|
107
|
+
...(options.onDelete === undefined ? {} : { onDelete: options.onDelete }),
|
|
108
|
+
},
|
|
109
|
+
parse,
|
|
110
|
+
optional,
|
|
111
|
+
),
|
|
112
|
+
|
|
113
|
+
default: (value) => makeColumn<T, true>({ ...meta, default: literal(value) }, parse, true),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const column = <T>(
|
|
117
|
+
kind: ColumnMeta['kind'],
|
|
118
|
+
parse: (value: unknown) => T,
|
|
119
|
+
extra: Partial<ColumnMeta> = {},
|
|
120
|
+
): Column<T> => makeColumn<T, false>({ ...BARE, ...extra, kind }, parse, false);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `timestamptz`, always. There is no naive-timestamp builder and there will not be one: a
|
|
124
|
+
* `timestamp without time zone` is a bug that only surfaces twice a year.
|
|
125
|
+
*/
|
|
126
|
+
export const makeTimestamp = <Optional extends boolean>(
|
|
127
|
+
meta: ColumnMeta,
|
|
128
|
+
parse: (value: unknown) => Date,
|
|
129
|
+
optional: Optional,
|
|
130
|
+
): TimestampColumn<Optional> => ({
|
|
131
|
+
...makeColumn<Date, Optional>(meta, parse, optional),
|
|
132
|
+
defaultNow: () => makeTimestamp({ ...meta, default: GENERATED_NOW }, parse, true),
|
|
133
|
+
onUpdateNow: () => makeTimestamp({ ...meta, onUpdate: GENERATED_NOW }, parse, optional),
|
|
134
|
+
});
|
package/src/columns.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Column, MoneyValue, TimestampColumn, UuidColumn } from './types';
|
|
2
|
+
/** uuid v7: time-ordered, so a primary key index stays append-friendly. */
|
|
3
|
+
export declare const newId: () => string;
|
|
4
|
+
export declare const uuid: () => UuidColumn;
|
|
5
|
+
export interface TextOptions {
|
|
6
|
+
/** Emits `char_length(<column>) <= max`, so Postgres refuses an over-long string too. */
|
|
7
|
+
readonly max?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const text: (options?: TextOptions) => Column<string>;
|
|
10
|
+
export declare const integer: () => Column<number>;
|
|
11
|
+
export declare const boolean: () => Column<boolean>;
|
|
12
|
+
/** Always `timestamptz`. UTC storage is not a per-table decision. */
|
|
13
|
+
export declare const timestamp: () => TimestampColumn;
|
|
14
|
+
/**
|
|
15
|
+
* A closed set of strings, emitted as a CHECK rather than a Postgres `ENUM` type: adding a
|
|
16
|
+
* variant is then a one-line migration instead of `ALTER TYPE`, which cannot run inside a
|
|
17
|
+
* transaction on older servers.
|
|
18
|
+
*/
|
|
19
|
+
export declare const enumerated: <const V extends readonly string[]>(values: V) => Column<V[number]>;
|
|
20
|
+
/**
|
|
21
|
+
* An absolute http(s) URL, validated on write rather than on render: a bad URL stored once is
|
|
22
|
+
* served to every reader, and `<img src>` fails silently in the browser.
|
|
23
|
+
*/
|
|
24
|
+
export declare const url: () => Column<string>;
|
|
25
|
+
/**
|
|
26
|
+
* IANA identifiers, checked against `Intl` when the column is declared — a typo or a UTC offset
|
|
27
|
+
* is a startup error rather than a row nobody can format. An offset is wrong twice a year.
|
|
28
|
+
*/
|
|
29
|
+
export declare const tz: <const Z extends readonly string[]>(zones: Z) => Column<Z[number]>;
|
|
30
|
+
export declare const locale: <const L extends readonly string[]>(locales: L) => Column<L[number]>;
|
|
31
|
+
/**
|
|
32
|
+
* One property, two physical columns: `<name>_minor bigint` and `<name>_currency char(3)`.
|
|
33
|
+
* A single implied currency is a migration nobody wants to write later, and a float is a
|
|
34
|
+
* rounding bug nobody wants to debug.
|
|
35
|
+
*/
|
|
36
|
+
export declare const money: () => Column<MoneyValue>;
|
|
37
|
+
/** The CHECK that stops a psql session writing a currency the app would refuse. */
|
|
38
|
+
export declare const currencyCheck: (currencyColumn: string) => string;
|
|
39
|
+
//# sourceMappingURL=columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["columns.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAc,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAM3F,2EAA2E;AAC3E,eAAO,MAAM,KAAK,QAAO,MAAkB,CAAC;AAS5C,eAAO,MAAM,IAAI,QAAO,UAUtB,CAAC;AAEH,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,IAAI,aAAa,WAAW,KAAQ,MAAM,CAAC,MAAM,CAQ3D,CAAC;AAEJ,eAAO,MAAM,OAAO,QAAO,MAAM,CAAC,MAAM,CAKrC,CAAC;AAEJ,eAAO,MAAM,OAAO,QAAO,MAAM,CAAC,OAAO,CAGtC,CAAC;AAWJ,qEAAqE;AACrE,eAAO,MAAM,SAAS,QAAO,eACgD,CAAC;AAS9E;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,UAAU,CAAC,KAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAUzF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,GAAG,QAAO,MAAM,CAAC,MAAM,CAejC,CAAC;AAWJ;;;GAGG;AACH,eAAO,MAAM,EAAE,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAahF,CAAC;AAIF,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,WAAW,CAAC,KAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAatF,CAAC;AA8BF;;;;GAIG;AACH,eAAO,MAAM,KAAK,QAAO,MAAM,CAAC,UAAU,CAA4C,CAAC;AAEvF,mFAAmF;AACnF,eAAO,MAAM,aAAa,mBAAoB,MAAM,KAAG,MAA4C,CAAC"}
|