neoorm 0.2.2 → 0.2.4
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/README.md +104 -772
- package/dist/bin/neoorm.js +21 -15
- package/dist/bin/neoorm.js.map +1 -1
- package/dist/codegen/diff-manifest.d.ts.map +1 -1
- package/dist/codegen/diff-manifest.js +19 -10
- package/dist/codegen/diff-manifest.js.map +1 -1
- package/dist/codegen/emit-includes.d.ts.map +1 -1
- package/dist/codegen/emit-includes.js +3 -1
- package/dist/codegen/emit-includes.js.map +1 -1
- package/dist/codegen/emit-models.d.ts.map +1 -1
- package/dist/codegen/emit-models.js +4 -4
- package/dist/codegen/emit-models.js.map +1 -1
- package/dist/codegen/generate-summary.d.ts.map +1 -1
- package/dist/codegen/generate-summary.js +3 -1
- package/dist/codegen/generate-summary.js.map +1 -1
- package/dist/codegen/generate.d.ts +5 -5
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/codegen/generate.js +7 -7
- package/dist/codegen/generate.js.map +1 -1
- package/dist/codegen/manifest-relations.d.ts.map +1 -1
- package/dist/codegen/manifest-relations.js.map +1 -1
- package/dist/codegen/schema-to-manifest.d.ts +3 -3
- package/dist/codegen/schema-to-manifest.d.ts.map +1 -1
- package/dist/codegen/schema-to-manifest.js +23 -7
- package/dist/codegen/schema-to-manifest.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -6
- package/dist/config.js.map +1 -1
- package/dist/dialect/index.d.ts +1 -1
- package/dist/dialect/index.d.ts.map +1 -1
- package/dist/dialect/index.js.map +1 -1
- package/dist/dialect/postgres.d.ts.map +1 -1
- package/dist/dialect/postgres.js +5 -2
- package/dist/dialect/postgres.js.map +1 -1
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/init/scaffold.d.ts.map +1 -1
- package/dist/init/scaffold.js.map +1 -1
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js.map +1 -1
- package/dist/introspect/pull.d.ts.map +1 -1
- package/dist/introspect/pull.js +11 -5
- package/dist/introspect/pull.js.map +1 -1
- package/dist/introspect/queries.d.ts.map +1 -1
- package/dist/introspect/queries.js.map +1 -1
- package/dist/introspect/to-manifest.d.ts.map +1 -1
- package/dist/introspect/to-manifest.js +17 -9
- package/dist/introspect/to-manifest.js.map +1 -1
- package/dist/migrate/runner.d.ts.map +1 -1
- package/dist/migrate/runner.js +9 -3
- package/dist/migrate/runner.js.map +1 -1
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +6 -2
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/ensure-plugins.js +1 -1
- package/dist/plugins/ensure-plugins.js.map +1 -1
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/json/operators.js.map +1 -1
- package/dist/plugins/postgis/columns.d.ts.map +1 -1
- package/dist/plugins/postgis/columns.js.map +1 -1
- package/dist/plugins/postgis/geojson.js.map +1 -1
- package/dist/plugins/postgis/index.d.ts +2 -2
- package/dist/plugins/postgis/index.d.ts.map +1 -1
- package/dist/plugins/postgis/index.js +1 -1
- package/dist/plugins/postgis/index.js.map +1 -1
- package/dist/plugins/postgis/operators.d.ts.map +1 -1
- package/dist/plugins/postgis/operators.js +3 -1
- package/dist/plugins/postgis/operators.js.map +1 -1
- package/dist/plugins/postgis/plugin.js +1 -1
- package/dist/plugins/postgis/plugin.js.map +1 -1
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js.map +1 -1
- package/dist/plugins/types.d.ts +1 -1
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/runtime/client.d.ts +2 -2
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +17 -12
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +2 -1
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/pg-error.d.ts.map +1 -1
- package/dist/runtime/pg-error.js +3 -1
- package/dist/runtime/pg-error.js.map +1 -1
- package/dist/runtime/query/aggregate.d.ts.map +1 -1
- package/dist/runtime/query/aggregate.js.map +1 -1
- package/dist/runtime/query/compile.d.ts.map +1 -1
- package/dist/runtime/query/compile.js +14 -3
- package/dist/runtime/query/compile.js.map +1 -1
- package/dist/runtime/query/count.d.ts.map +1 -1
- package/dist/runtime/query/count.js +3 -3
- package/dist/runtime/query/count.js.map +1 -1
- package/dist/runtime/query/create.d.ts +1 -1
- package/dist/runtime/query/create.d.ts.map +1 -1
- package/dist/runtime/query/create.js +3 -1
- package/dist/runtime/query/create.js.map +1 -1
- package/dist/runtime/query/cursor-codec.d.ts.map +1 -1
- package/dist/runtime/query/cursor-codec.js +4 -1
- package/dist/runtime/query/cursor-codec.js.map +1 -1
- package/dist/runtime/query/cursor.d.ts.map +1 -1
- package/dist/runtime/query/cursor.js +10 -3
- package/dist/runtime/query/cursor.js.map +1 -1
- package/dist/runtime/query/delete.d.ts +1 -1
- package/dist/runtime/query/delete.d.ts.map +1 -1
- package/dist/runtime/query/delete.js +6 -4
- package/dist/runtime/query/delete.js.map +1 -1
- package/dist/runtime/query/execute.d.ts +1 -1
- package/dist/runtime/query/execute.d.ts.map +1 -1
- package/dist/runtime/query/execute.js +1 -1
- package/dist/runtime/query/execute.js.map +1 -1
- package/dist/runtime/query/find.d.ts.map +1 -1
- package/dist/runtime/query/find.js +31 -17
- package/dist/runtime/query/find.js.map +1 -1
- package/dist/runtime/query/paginate.d.ts +1 -1
- package/dist/runtime/query/paginate.d.ts.map +1 -1
- package/dist/runtime/query/paginate.js +1 -1
- package/dist/runtime/query/paginate.js.map +1 -1
- package/dist/runtime/query/primary-key.d.ts +2 -1
- package/dist/runtime/query/primary-key.d.ts.map +1 -1
- package/dist/runtime/query/primary-key.js +21 -1
- package/dist/runtime/query/primary-key.js.map +1 -1
- package/dist/runtime/query/relation-writes.d.ts +1 -1
- package/dist/runtime/query/relation-writes.d.ts.map +1 -1
- package/dist/runtime/query/relation-writes.js +80 -6
- package/dist/runtime/query/relation-writes.js.map +1 -1
- package/dist/runtime/query/unique.d.ts.map +1 -1
- package/dist/runtime/query/unique.js.map +1 -1
- package/dist/runtime/query/update.d.ts +1 -1
- package/dist/runtime/query/update.d.ts.map +1 -1
- package/dist/runtime/query/update.js +16 -10
- package/dist/runtime/query/update.js.map +1 -1
- package/dist/runtime/query/updated-at.d.ts.map +1 -1
- package/dist/runtime/query/updated-at.js.map +1 -1
- package/dist/runtime/query/upsert.d.ts +1 -1
- package/dist/runtime/query/upsert.d.ts.map +1 -1
- package/dist/runtime/query/upsert.js +5 -5
- package/dist/runtime/query/upsert.js.map +1 -1
- package/dist/runtime/types.d.ts +1 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/schema/column-where.d.ts.map +1 -1
- package/dist/schema/column.d.ts +1 -1
- package/dist/schema/column.d.ts.map +1 -1
- package/dist/schema/column.js +1 -1
- package/dist/schema/column.js.map +1 -1
- package/dist/schema/define-schema.d.ts.map +1 -1
- package/dist/schema/define-schema.js.map +1 -1
- package/dist/schema/index.d.ts +10 -10
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -4
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/many-to-many.d.ts.map +1 -1
- package/dist/schema/many-to-many.js.map +1 -1
- package/dist/schema/nested-relation-types.d.ts +2 -2
- package/dist/schema/nested-relation-types.d.ts.map +1 -1
- package/dist/schema/relation-types.d.ts +8 -4
- package/dist/schema/relation-types.d.ts.map +1 -1
- package/dist/schema/relation.d.ts.map +1 -1
- package/dist/schema/relation.js +3 -1
- package/dist/schema/relation.js.map +1 -1
- package/dist/schema/table.d.ts.map +1 -1
- package/dist/schema/table.js.map +1 -1
- package/dist/schema/types.d.ts +4 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/sql/builder.d.ts.map +1 -1
- package/dist/sql/builder.js.map +1 -1
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.d.ts.map +1 -1
- package/dist/sql/index.js +1 -1
- package/dist/sql/index.js.map +1 -1
- package/dist/sql/template.d.ts.map +1 -1
- package/dist/sql/template.js.map +1 -1
- package/dist/utils/case.d.ts.map +1 -1
- package/dist/utils/case.js.map +1 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +4 -2
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +77 -76
package/README.md
CHANGED
|
@@ -1,804 +1,136 @@
|
|
|
1
1
|
# NeoOrm
|
|
2
|
+
<p>
|
|
3
|
+
<strong>NeoOrm</strong> is a TypeScript-first SQL ORM built for people who want <strong>type safety without the complexity</strong>.
|
|
4
|
+
Schema → codegen → typed client — you own the SQL, we handle the boilerplate.
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
PostgreSQL dialect ships today — MySQL, SQLite, and others are on the roadmap.
|
|
7
|
+
</p>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="#quick-start"><strong>Quick start</strong></a> ·
|
|
11
|
+
<a href="docs/getting-started.md"><strong>Getting started</strong></a> ·
|
|
12
|
+
<a href="docs/cli.md"><strong>CLI reference</strong></a> ·
|
|
13
|
+
<a href="examples/blog/queries.example.ts"><strong>Example</strong></a>
|
|
14
|
+
</p>
|
|
6
15
|
|
|
7
|
-
```
|
|
16
|
+
```
|
|
8
17
|
npm install neoorm pg
|
|
9
18
|
```
|
|
10
19
|
|
|
11
|
-
Requires Node.js 20
|
|
20
|
+
Requires **Node.js 20+** (PostgreSQL driver ships now; more databases coming).
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
---
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
## Why NeoOrm?
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
npx neoorm init
|
|
19
|
-
```
|
|
26
|
+
**No `any`. No codegen runtime. No lock-in.**
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
Most ORMs force you to learn their query language, fight their type system, or ship a heavy runtime. NeoOrm takes a different approach: you write a plain TypeScript schema, it generates a typed client, and you write real SQL — amplified by types, not abstracted away.
|
|
22
29
|
|
|
23
|
-
**
|
|
30
|
+
- **Schema as code** — one source of truth for types and the database
|
|
31
|
+
- **Generated client** — zero-cost abstractions, full autocomplete
|
|
32
|
+
- **Relations done right** — nested reads, writes, and filters without N+1 footguns
|
|
33
|
+
- **PostgreSQL powered** — arrays, JSONB, PostGIS, enums, full-text, extensions — no abstraction layer that gets in the way. MySQL, SQLite, and more are coming.
|
|
34
|
+
- **Migrations built-in** — diff your schema, get SQL, deploy. Rollback supported.
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
cp .env.example .env
|
|
27
|
-
# edit DATABASE_URL in .env
|
|
28
|
-
```
|
|
36
|
+
---
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
## Quick start
|
|
31
39
|
|
|
32
40
|
```bash
|
|
33
|
-
npx neoorm
|
|
41
|
+
npx neoorm init # scaffold schema, config, client
|
|
42
|
+
npx neoorm migrate deploy # create tables
|
|
34
43
|
```
|
|
35
44
|
|
|
36
|
-
**4. Query**
|
|
37
|
-
|
|
38
45
|
```ts
|
|
39
46
|
import { db } from "./neoorm/client.js";
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
posts: { orderBy: { createdAt: "desc" }, limit: 10 },
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Manual setup
|
|
49
|
-
|
|
50
|
-
If you prefer to write files yourself instead of `neoorm init`:
|
|
51
|
-
|
|
52
|
-
**Define a schema** (`schema.ts`):
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
import { defineSchema, table, uuid, text, timestamp, fk } from "neoorm/schema";
|
|
56
|
-
|
|
57
|
-
export const schema = defineSchema({
|
|
58
|
-
users: table("users", {
|
|
59
|
-
id: uuid().primary(),
|
|
60
|
-
email: text().notNull().unique(),
|
|
61
|
-
createdAt: timestamp().notNull().defaultNow(),
|
|
62
|
-
}),
|
|
63
|
-
|
|
64
|
-
posts: table("posts", {
|
|
65
|
-
id: uuid().primary(),
|
|
66
|
-
authorId: fk("users.id", { as: "author", inverse: "posts", nullable: false }),
|
|
67
|
-
title: text().notNull(),
|
|
68
|
-
}),
|
|
69
|
-
});
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**2. Configure NeoOrm** (`neoorm.config.ts`):
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { defineConfig } from "neoorm";
|
|
76
|
-
|
|
77
|
-
export default defineConfig({
|
|
78
|
-
schema: "./schema.ts",
|
|
79
|
-
out: "./neoorm",
|
|
80
|
-
datasource: {
|
|
81
|
-
provider: "postgresql",
|
|
82
|
-
url: process.env.DATABASE_URL!,
|
|
83
|
-
schema: "public", // optional; use "tenant_template", "app", etc.
|
|
84
|
-
enum: "check", // "check" (default) | "union" | "native"
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
`datasource.schema` controls which PostgreSQL schema/namespace the CLI targets for migrations, `db push`, `db pull`, and reset. It defaults to `public`.
|
|
90
|
-
|
|
91
|
-
For tenant-per-schema isolation at runtime, create a client with the tenant schema:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
import { createNeoOrmClient } from "neoorm";
|
|
95
|
-
import { manifest } from "./neoorm/manifest.js";
|
|
96
|
-
|
|
97
|
-
const tenantDb = createNeoOrmClient(manifest, {
|
|
98
|
-
connectionString: process.env.DATABASE_URL!,
|
|
99
|
-
schema: "tenant_acme",
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
NeoOrm qualifies generated ORM table references as `"tenant_acme"."users"`. Raw `db.sql` and `db.execute` calls are not rewritten, so qualify raw SQL yourself. Treat schema names as trusted tenant metadata, not raw request input.
|
|
104
|
-
|
|
105
|
-
**3. Generate the client**
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
npx neoorm generate
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
This writes `client.ts`, `manifest.ts`, `models.ts`, `includes.ts`, and migration SQL when the schema changed.
|
|
112
|
-
|
|
113
|
-
## Schema DSL
|
|
114
|
-
|
|
115
|
-
### Tables and accessors
|
|
116
|
-
|
|
117
|
-
`defineSchema` keys are TypeScript accessors (`users`, `posts`). The first argument to `table()` is the SQL table name:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
users: table("user", { ... }) // db.users → SQL table "user"
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Column field names use camelCase in TypeScript. By default, SQL column names are snake_case (`createdAt` → `created_at`). You can change that globally or per table with `columnNaming`.
|
|
124
|
-
|
|
125
|
-
### Column types
|
|
126
|
-
|
|
127
|
-
| Builder | SQL type | TypeScript | Notes |
|
|
128
|
-
|---------|----------|------------|-------|
|
|
129
|
-
| `id.primary()` | `TEXT` | `string` | App-generated string IDs (e.g. `user_a1b2c3d4`) |
|
|
130
|
-
| `uuid()` | `UUID` | `string` | Defaults to UUID v7; pass `{ version: 4 }` for v4 |
|
|
131
|
-
| `uuid().primary()` | `UUID` | `string` | Primary key; auto-generated on create if omitted |
|
|
132
|
-
| `serial()` | `INTEGER GENERATED BY DEFAULT AS IDENTITY` | `number` | Auto-increment; omit on insert, DB assigns via `RETURNING` |
|
|
133
|
-
| `serial().primary()` | same | `number` | Integer PK with DB-generated values |
|
|
134
|
-
| `text()` | `TEXT` | `string \| null` | |
|
|
135
|
-
| `bool()` | `BOOLEAN` | `boolean \| null` | |
|
|
136
|
-
| `int()` | `INTEGER` | `number \| null` | |
|
|
137
|
-
| `timestamp()` | `TIMESTAMPTZ` | `Date \| null` | Use `.defaultNow()` for `DEFAULT NOW()`; use `.updatedAt()` for auto-update on ORM writes |
|
|
138
|
-
|
|
139
|
-
Pair audit timestamps on mutable tables:
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
createdAt: timestamp().notNull().defaultNow(),
|
|
143
|
-
updatedAt: timestamp().notNull().defaultNow().updatedAt(),
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
`.updatedAt()` is ORM metadata only (no DB trigger). `update`, `updateById`, `updateMany`, and `upsert` always set the column to `NOW()` in SQL; user-provided values in `data` are ignored.
|
|
147
|
-
| `json()` | `JSON` | `unknown \| null` | Generic: `json<MyType>()` |
|
|
148
|
-
| `jsonb()` | `JSONB` | `unknown \| null` | Generic: `jsonb<MyType>()` |
|
|
149
|
-
| `decimal()` / `numeric()` | `NUMERIC` or `NUMERIC(p,s)` | `string \| null` | Use strings to avoid float precision loss |
|
|
150
|
-
| `enumType(["a", "b"])` | mode-dependent | union literals | See [Enum columns](#enum-columns) |
|
|
151
|
-
| `bytea()` | `BYTEA` | `Buffer \| null` | Binary data |
|
|
152
|
-
| `textArray()` | `TEXT[]` | `string[] \| null` | |
|
|
153
|
-
| `intArray()` | `INTEGER[]` | `number[] \| null` | |
|
|
154
|
-
| `citext()` | `CITEXT` | `string \| null` | Case-insensitive text; requires `citext` extension |
|
|
155
|
-
|
|
156
|
-
All column builders support `.notNull()`, `.unique()`, `.default(value)`, `.defaultNow()`, `.updatedAt()` (timestamp only), `.primary()`, and `.map(name)`.
|
|
157
|
-
|
|
158
|
-
Foreign keys use `fk("target_table.target_column", { as, inverse, nullable?, onDelete? })`.
|
|
159
|
-
|
|
160
|
-
### Enum columns
|
|
161
|
-
|
|
162
|
-
Define allowed values in the schema:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
import { enumType, table } from "neoorm/schema";
|
|
166
|
-
|
|
167
|
-
posts: table("posts", {
|
|
168
|
-
status: enumType(["draft", "published", "archived"] as const)
|
|
169
|
-
.notNull()
|
|
170
|
-
.default("draft"),
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Control how enums are stored via `datasource.enum` in `neoorm.config.ts`:
|
|
175
|
-
|
|
176
|
-
| Mode | SQL | DB enforcement |
|
|
177
|
-
|------|-----|----------------|
|
|
178
|
-
| `check` (default) | `TEXT` + `CHECK (...)` | yes |
|
|
179
|
-
| `union` | `TEXT` | no (TypeScript union only) |
|
|
180
|
-
| `native` | Postgres `CREATE TYPE ... AS ENUM` | yes |
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
export default defineConfig({
|
|
184
|
-
schema: "./schema.ts",
|
|
185
|
-
out: "./neoorm",
|
|
186
|
-
datasource: {
|
|
187
|
-
provider: "postgresql",
|
|
188
|
-
url: process.env.DATABASE_URL!,
|
|
189
|
-
enum: "check", // or "union" | "native"
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Optional custom SQL type name for native mode: `enumType(["draft", "published"], { name: "post_status" })`.
|
|
195
|
-
|
|
196
|
-
Query and mutate enum columns like strings with compile-time union checking:
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
await db.posts.findMany({ where: { status: "published" } });
|
|
200
|
-
await db.posts.create({ data: { title: "Hello", status: "draft" } });
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### JSON and decimal columns
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
posts: table("posts", {
|
|
207
|
-
metadata: jsonb<{ tags: string[]; featured?: boolean }>(),
|
|
208
|
-
price: decimal({ precision: 10, scale: 2 }).default("0.00"),
|
|
209
|
-
})
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
// jsonb — exact equality on the full object (all keys must match)
|
|
214
|
-
await db.posts.findMany({
|
|
215
|
-
where: { metadata: { featured: true, category: "engineering" } },
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// jsonb — partial / subset match (@> containment)
|
|
219
|
-
await db.posts.findMany({
|
|
220
|
-
where: { metadata: { jsonContains: { featured: true } } },
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// jsonb — key existence and path filters
|
|
224
|
-
await db.posts.findMany({
|
|
225
|
-
where: {
|
|
226
|
-
metadata: {
|
|
227
|
-
hasKey: "featured",
|
|
228
|
-
hasAnyKeys: ["category", "tags"],
|
|
229
|
-
path: { segments: ["category"], equals: "engineering" },
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// decimal — compare as strings
|
|
235
|
-
await db.posts.findMany({
|
|
236
|
-
where: { price: { gte: "9.99", lte: "49.99" } },
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
await db.posts.create({
|
|
48
|
+
// Create with nested relation write
|
|
49
|
+
const user = await db.users.create({
|
|
240
50
|
data: {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
emailAddress: text().notNull(), // SQL: email_address
|
|
302
|
-
}),
|
|
303
|
-
|
|
304
|
-
legacyUsers: table(
|
|
305
|
-
"legacy_users",
|
|
306
|
-
{
|
|
307
|
-
emailAddress: text().notNull(), // SQL: emailAddress
|
|
308
|
-
},
|
|
309
|
-
{ columnNaming: "camelCase" },
|
|
310
|
-
),
|
|
311
|
-
},
|
|
312
|
-
{ columnNaming: "snakeCase" },
|
|
313
|
-
);
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Per-table options can also include `extras`:
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
posts: table(
|
|
320
|
-
"posts",
|
|
321
|
-
{ authorId: fk("users.id", { as: "author", inverse: "posts" }) },
|
|
322
|
-
{
|
|
323
|
-
columnNaming: "camelCase",
|
|
324
|
-
extras: (t) => ({ authorIdx: index().on(t.authorId) }),
|
|
325
|
-
},
|
|
326
|
-
)
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
`columnNaming` affects SQL column names only. TypeScript keys stay exactly as written in the schema, and `.map("exact_name")` still overrides the strategy for individual columns.
|
|
330
|
-
|
|
331
|
-
### PostgreSQL extensions
|
|
332
|
-
|
|
333
|
-
Declare PostgreSQL extensions that migrations should create with `CREATE EXTENSION IF NOT EXISTS`. This is useful for extensions that are not tied to a registered column type plugin, or when you want to ensure an extension is created even if no plugin column is currently used.
|
|
334
|
-
|
|
335
|
-
```ts
|
|
336
|
-
export const schema = defineSchema(
|
|
337
|
-
{
|
|
338
|
-
users: table("users", {
|
|
339
|
-
id: uuid().primary(),
|
|
340
|
-
search: text().notNull(),
|
|
341
|
-
}),
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
extensions: ["uuid-ossp", "pg_trgm"],
|
|
345
|
-
},
|
|
346
|
-
);
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Column type plugins (for example PostGIS or `citext`) still register their required extensions automatically, so you only need to list extensions here that are not covered by a plugin. Hyphenated names are quoted automatically in the generated SQL.
|
|
350
|
-
|
|
351
|
-
### Indexes and composite keys
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
posts: table(
|
|
355
|
-
"posts",
|
|
356
|
-
{ /* columns */ },
|
|
357
|
-
(t) => ({
|
|
358
|
-
authorIdx: index().on(t.authorId),
|
|
359
|
-
slugUnique: unique(t.slug),
|
|
360
|
-
pk: primaryKey(t.orgId, t.localId),
|
|
361
|
-
}),
|
|
362
|
-
)
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### Many-to-many
|
|
366
|
-
|
|
367
|
-
Define the junction table, then register the relation after `defineSchema`:
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
export const schema = defineSchema({
|
|
371
|
-
posts: table("posts", { /* ... */ }),
|
|
372
|
-
tags: table("tags", { /* ... */ }),
|
|
373
|
-
postTags: table("post_tags", {
|
|
374
|
-
postId: fk("posts.id", { as: "post", inverse: "postTags", nullable: false }),
|
|
375
|
-
tagId: fk("tags.id", { as: "tag", inverse: "postTags", nullable: false }),
|
|
376
|
-
}, (t) => ({
|
|
377
|
-
pk: primaryKey(t.postId, t.tagId),
|
|
378
|
-
})),
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
manyToMany(schema.posts, schema.tags, {
|
|
382
|
-
through: schema.postTags,
|
|
383
|
-
left: "post",
|
|
384
|
-
right: "tag",
|
|
385
|
-
as: "tags",
|
|
386
|
-
inverse: "posts",
|
|
387
|
-
});
|
|
388
|
-
```
|
|
51
|
+
email: "alice@example.com",
|
|
52
|
+
profile: { create: { name: "Alice" } },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Fetch with typed includes, pagination, and filters
|
|
57
|
+
const posts = await db.posts.findMany({
|
|
58
|
+
where: { published: true, tags: { some: { slug: "typescript" } } },
|
|
59
|
+
orderBy: { createdAt: "desc" },
|
|
60
|
+
take: 20,
|
|
61
|
+
with: { author: true, _count: { comments: true } },
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
<table>
|
|
70
|
+
<tr>
|
|
71
|
+
<td width="50%"><strong>🧩 Schema DSL</strong><br/>Tables, columns, foreign keys, indexes, enums, composite keys, many-to-many — all in TypeScript with full type inference.</td>
|
|
72
|
+
<td width="50%"><strong>📦 Code generation</strong><br/>`neoorm generate` emits a typed client, models with payload types, include types, a manifest, and migration SQL.</td>
|
|
73
|
+
</tr>
|
|
74
|
+
<tr>
|
|
75
|
+
<td><strong>🔍 Rich queries</strong><br/>`findMany`, `findById`, `findUnique`, `count`, `aggregate`. Where operators for strings, numbers, dates, JSONB, arrays, and nulls. `AND/OR/NOT` combinators.</td>
|
|
76
|
+
<td><strong>📄 Cursor pagination</strong><br/>Keyset-based `paginate` for feeds and infinite scroll. Type-safe cursors, `hasMore` probe, and `encodeCursor`/`decodeCursor` for HTTP APIs.</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td><strong>🔗 Relation writes</strong><br/>Nested `connect`, `create`, `disconnect`, `set`, `delete` on to-one, one-to-many, and many-to-many — all in a single query.</td>
|
|
80
|
+
<td><strong>🔁 Transactions</strong><br/>Interactive callbacks, batch steps, savepoints for nested transactions, isolation levels, read-only mode.</td>
|
|
81
|
+
</tr>
|
|
82
|
+
<tr>
|
|
83
|
+
<td><strong>🧱 Migrations</strong><br/>Schema diff generates DDL automatically. `deploy`, `dev`, `status`, `reset`, and `down` (rollback). Destructive change detection with `--accept-data-loss` opt-in.</td>
|
|
84
|
+
<td><strong>🔌 Plugin system</strong><br/>Column type plugins for PostGIS (geometry, geography, spatial operators), citext, and custom extensions.</td>
|
|
85
|
+
</tr>
|
|
86
|
+
<tr>
|
|
87
|
+
<td><strong>🐘 PostgreSQL powered</strong><br/>JSONB, arrays (`TEXT[]`, `INTEGER[]`), `NUMERIC`, `BYTEA`, native enums, UUID v4/v7, serial identity, `CITEXT`, extensions. More databases on the roadmap.</td>
|
|
88
|
+
<td><strong>🎯 TypeScript end-to-end</strong><br/>Strict types from schema to query results. Discriminated payload types for `with` includes. Compile-time union checking on enums.</td>
|
|
89
|
+
</tr>
|
|
90
|
+
</table>
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
| Topic | |
|
|
97
|
+
|-------|-|
|
|
98
|
+
| [Getting started](docs/getting-started.md) | Setup, manual config, env vars, tenant schemas |
|
|
99
|
+
| [Schema DSL](docs/schema.md) | Tables, columns, types, enums, indexes, many-to-many, naming strategy |
|
|
100
|
+
| [Queries](docs/queries.md) | CRUD, where clauses, pagination, aggregates, distinct |
|
|
101
|
+
| [Relation writes](docs/relations.md) | Nested connect/create/disconnect/set/delete |
|
|
102
|
+
| [Transactions](docs/transactions.md) | Interactive, batch, nested, isolation levels |
|
|
103
|
+
| [Migrations](docs/migrations.md) | Deploy, dev, status, rollback, reset |
|
|
104
|
+
| [CLI reference](docs/cli.md) | All commands and flags |
|
|
105
|
+
| [Configuration](docs/configuration.md) | Config file options reference |
|
|
106
|
+
| [Plugins](docs/plugins.md) | PostGIS, citext, custom plugins |
|
|
107
|
+
|
|
108
|
+
See the [blog example](examples/blog/schema.ts) for a complete schema, and [queries.example.ts](examples/blog/queries.example.ts) for typed queries and mutations.
|
|
109
|
+
|
|
110
|
+
---
|
|
389
111
|
|
|
390
|
-
##
|
|
391
|
-
|
|
392
|
-
`create` and `update` accept nested relation writes alongside scalar fields. Relation-only updates are supported (no scalar `SET` required).
|
|
393
|
-
|
|
394
|
-
| Relation kind | Operations |
|
|
395
|
-
|---------------|------------|
|
|
396
|
-
| **To-one** (outgoing FK) | `connect`, `create`, `disconnect` (nullable FK only) |
|
|
397
|
-
| **One-to-many** (inverse) | `create`, `connect`, `disconnect`, `set`, `delete` |
|
|
398
|
-
| **Many-to-many** | `connect`, `connectOrCreate`, `disconnect`, `set`, `delete` |
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
// To-one on update
|
|
402
|
-
await db.posts.update({
|
|
403
|
-
where: { id: postId },
|
|
404
|
-
data: { author: { connect: { id: userId } } },
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Nested create on one-to-many (full nested relation writes inside create items)
|
|
408
|
-
await db.posts.update({
|
|
409
|
-
where: { id: postId },
|
|
410
|
-
data: {
|
|
411
|
-
comments: {
|
|
412
|
-
create: [{ body: "Hi", author: { connect: { id: userId } } }],
|
|
413
|
-
connect: [{ id: commentId }],
|
|
414
|
-
disconnect: [{ id: oldCommentId }], // or `true` to unlink all
|
|
415
|
-
set: [{ id: commentId }], // replace all links
|
|
416
|
-
delete: [{ id: commentId }], // or `true` to delete all linked children
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// M2M on create or update
|
|
422
|
-
await db.posts.update({
|
|
423
|
-
where: { id: postId },
|
|
424
|
-
data: {
|
|
425
|
-
tags: {
|
|
426
|
-
connect: [{ id: tagId }],
|
|
427
|
-
set: [{ id: tagId }],
|
|
428
|
-
connectOrCreate: [
|
|
429
|
-
{ where: { slug: "orm" }, create: { slug: "orm", name: "ORM" } },
|
|
430
|
-
],
|
|
431
|
-
delete: [{ id: tagId }], // removes junction links, then deletes tag rows
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// updateMany applies scalar SET once, then nested writes per matched parent
|
|
437
|
-
await db.posts.updateMany({
|
|
438
|
-
where: { published: true },
|
|
439
|
-
data: {
|
|
440
|
-
status: "archived",
|
|
441
|
-
tags: { connect: [{ id: tagId }] },
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
Within a single relation field, operations run in order: `delete` → `disconnect` → `set` → `connect` / `connectOrCreate` → `create`. Mixing `set` with `connect` or `create` is discouraged — `set` replaces the full link set.
|
|
447
|
-
|
|
448
|
-
## Cursor pagination
|
|
449
|
-
|
|
450
|
-
For feeds, infinite scroll, and large tables, use `paginate` instead of `limit`/`offset`. It uses **keyset pagination** on your `orderBy` columns plus the table primary key as a stable tiebreaker (for example `(createdAt, id)`).
|
|
451
|
-
|
|
452
|
-
```ts
|
|
453
|
-
let cursor: { createdAt: string; id: string } | null = null;
|
|
454
|
-
|
|
455
|
-
for (;;) {
|
|
456
|
-
const page = await db.posts.paginate({
|
|
457
|
-
where: { published: true },
|
|
458
|
-
orderBy: { createdAt: "desc" },
|
|
459
|
-
take: 20,
|
|
460
|
-
...(cursor ? { after: cursor } : {}),
|
|
461
|
-
with: { author: true },
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
for (const post of page.items) {
|
|
465
|
-
// render post
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (!page.hasMore) break;
|
|
469
|
-
cursor = page.nextCursor;
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
- `orderBy` is required; scalar `id` is appended automatically when omitted.
|
|
474
|
-
- `take` is the page size; `hasMore` uses a `take + 1` probe row.
|
|
475
|
-
- `after` is a typed cursor object (`nextCursor` from the previous page).
|
|
476
|
-
- For HTTP APIs, encode cursors with `encodeCursor` / `decodeCursor` from `neoorm`.
|
|
477
|
-
|
|
478
|
-
On feed tables, add a composite index on the sort columns (for example `index().on(t.createdAt, t.id)`).
|
|
479
|
-
|
|
480
|
-
## Aggregates
|
|
481
|
-
|
|
482
|
-
### Relation counts in `with`
|
|
483
|
-
|
|
484
|
-
Load counts without fetching full related rows:
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
const users = await db.users.findMany({
|
|
488
|
-
with: {
|
|
489
|
-
_count: { posts: true },
|
|
490
|
-
profile: true,
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
// users[0]._count.posts === number
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
Optional per-relation filter: `_count: { posts: { where: { published: true } } }`.
|
|
497
|
-
|
|
498
|
-
### Table-level `aggregate()`
|
|
499
|
-
|
|
500
|
-
```ts
|
|
501
|
-
const stats = await db.posts.aggregate({
|
|
502
|
-
where: { published: true },
|
|
503
|
-
_count: true,
|
|
504
|
-
_avg: { views: true },
|
|
505
|
-
});
|
|
506
|
-
// { _count: number, _avg: { views: number | null } }
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
For grouped dashboards, use `db.sql` or the `sqlBuilder` helper.
|
|
510
|
-
|
|
511
|
-
## Bulk insert
|
|
512
|
-
|
|
513
|
-
`createMany` returns the number of rows inserted. Use `createManyAndReturn` when you need serial IDs, generated UUIDs, or default values in the result:
|
|
514
|
-
|
|
515
|
-
```ts
|
|
516
|
-
const users = await db.users.createManyAndReturn({
|
|
517
|
-
data: [
|
|
518
|
-
{ email: "a@example.com", name: "A" },
|
|
519
|
-
{ email: "b@example.com", name: "B" },
|
|
520
|
-
],
|
|
521
|
-
});
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
Scalar fields only — no nested relation writes (same as `createMany`).
|
|
525
|
-
|
|
526
|
-
## Where clauses
|
|
527
|
-
|
|
528
|
-
`findMany`, `findFirst`, `findUnique`, `count`, `update`, `updateMany`, `delete`, and `deleteMany` all accept a typed `where` argument.
|
|
529
|
-
|
|
530
|
-
### Column filters
|
|
531
|
-
|
|
532
|
-
Equality shorthand and explicit operators:
|
|
533
|
-
|
|
534
|
-
```ts
|
|
535
|
-
// equality
|
|
536
|
-
await db.posts.findMany({ where: { published: true } });
|
|
537
|
-
|
|
538
|
-
// operators
|
|
539
|
-
await db.posts.findFirst({
|
|
540
|
-
where: {
|
|
541
|
-
title: { contains: "ORM" },
|
|
542
|
-
views: { gte: 100 },
|
|
543
|
-
id: { in: ["post_1", "post_2"] },
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
String columns support `equals`, `contains`, `startsWith`, `endsWith`, `in`, and `notIn`. Numeric, boolean, and date columns support `equals`, `gt`, `gte`, `lt`, `lte`, `in`, and `notIn`. JSON columns support `jsonContains`, `hasKey`, `hasAnyKeys`, `hasAllKeys`, and `path`. All nullable columns also support `isNull` and `isNotNull`.
|
|
549
|
-
|
|
550
|
-
### `distinct`
|
|
551
|
-
|
|
552
|
-
PostgreSQL `DISTINCT ON` — `orderBy` must lead with the same columns:
|
|
553
|
-
|
|
554
|
-
```ts
|
|
555
|
-
await db.users.findMany({
|
|
556
|
-
distinct: ["email"],
|
|
557
|
-
orderBy: { email: "asc" },
|
|
558
|
-
});
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### Selective `with` return types
|
|
562
|
-
|
|
563
|
-
Relation `select` narrows the TypeScript return type at compile time:
|
|
564
|
-
|
|
565
|
-
```ts
|
|
566
|
-
const users = await db.users.findMany({
|
|
567
|
-
with: { posts: { select: { title: true } } },
|
|
568
|
-
});
|
|
569
|
-
// users[0].posts[0].title is string; .body is excluded from the type
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
await db.users.findMany({
|
|
574
|
-
where: {
|
|
575
|
-
name: null, // shorthand for IS NULL
|
|
576
|
-
email: { isNotNull: true },
|
|
577
|
-
id: { notIn: ["user_1", "user_2"] },
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
### Logical combinators
|
|
583
|
-
|
|
584
|
-
Top-level `AND`, `OR`, and `NOT` keys combine conditions. Multiple sibling keys still imply `AND`:
|
|
585
|
-
|
|
586
|
-
```ts
|
|
587
|
-
await db.users.findMany({
|
|
588
|
-
where: {
|
|
589
|
-
OR: [
|
|
590
|
-
{ email: { contains: "@example.com" } },
|
|
591
|
-
{ email: { contains: "@test.com" } },
|
|
592
|
-
],
|
|
593
|
-
NOT: { name: { isNull: true } },
|
|
594
|
-
createdAt: { gte: new Date("2025-01-01") },
|
|
595
|
-
},
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
### Relation filters
|
|
600
|
-
|
|
601
|
-
Relation names come from `fk(..., { as: "author", inverse: "posts" })` and `manyToMany(..., { as: "tags" })` — they are not the FK column names on the table.
|
|
602
|
-
|
|
603
|
-
| Pattern | Use for |
|
|
604
|
-
|---------|---------|
|
|
605
|
-
| `authorId: "user_1"` | Filter by FK column value |
|
|
606
|
-
| `author: { email: "a@b.c" }` | Filter by related record (to-one) |
|
|
607
|
-
| `posts: { some: { ... } }` | At least one related record matches (to-many) |
|
|
608
|
-
| `posts: { every: { ... } }` | All related records match (to-many) |
|
|
609
|
-
| `posts: { none: { ... } }` | No related records match (to-many) |
|
|
610
|
-
|
|
611
|
-
```ts
|
|
612
|
-
// users who have at least one published post
|
|
613
|
-
await db.users.findMany({
|
|
614
|
-
where: {
|
|
615
|
-
posts: { some: { published: true } },
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// posts whose author has a verified email
|
|
620
|
-
await db.posts.findMany({
|
|
621
|
-
where: {
|
|
622
|
-
author: { email: { contains: "@" } },
|
|
623
|
-
},
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// posts tagged "orm" (many-to-many via junction table)
|
|
627
|
-
await db.posts.findMany({
|
|
628
|
-
where: {
|
|
629
|
-
tags: { some: { slug: "orm" } },
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
Relation filters compile to SQL `EXISTS` subqueries, so they work with `findMany`, `count`, `updateMany`, and `deleteMany` without duplicate rows.
|
|
635
|
-
|
|
636
|
-
## Transactions
|
|
637
|
-
|
|
638
|
-
```ts
|
|
639
|
-
// Interactive callback
|
|
640
|
-
await db.$transaction(async (tx) => {
|
|
641
|
-
const user = await tx.users.create({ data: { email: "a@b.com" } });
|
|
642
|
-
await tx.posts.create({
|
|
643
|
-
data: {
|
|
644
|
-
title: "Hello",
|
|
645
|
-
authorId: user.id,
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// Batch steps (sequential, one transaction)
|
|
651
|
-
const [user, post] = await db.$transaction([
|
|
652
|
-
(tx) => tx.users.create({ data: { email: "a@b.com" } }),
|
|
653
|
-
(tx) => tx.posts.create({ data: { title: "Hello" } }),
|
|
654
|
-
]);
|
|
655
|
-
|
|
656
|
-
// Options (outermost transaction only)
|
|
657
|
-
await db.$transaction(fn, {
|
|
658
|
-
isolationLevel: "Serializable", // ReadUncommitted | ReadCommitted | RepeatableRead | Serializable
|
|
659
|
-
readOnly: true,
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// Nested $transaction uses PostgreSQL savepoints on the same connection.
|
|
663
|
-
// A nested failure rolls back only that block; the outer transaction can continue.
|
|
664
|
-
await db.$transaction(async (tx) => {
|
|
665
|
-
await tx.users.create({ data: { email: "outer@example.com" } });
|
|
666
|
-
|
|
667
|
-
await tx.$transaction(async (nested) => {
|
|
668
|
-
await nested.posts.create({
|
|
669
|
-
data: { title: "Nested", body: "...", authorId: "user_1" },
|
|
670
|
-
});
|
|
671
|
-
}).catch(() => undefined);
|
|
672
|
-
|
|
673
|
-
// Outer writes are kept even if the nested block failed.
|
|
674
|
-
});
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
Nested `create` calls inside a transaction do not start a separate transaction. Nested `$transaction` calls use savepoints; `readOnly` and `isolationLevel` apply only to the outermost `BEGIN`.
|
|
678
|
-
|
|
679
|
-
## CLI
|
|
680
|
-
|
|
681
|
-
| Command | Description |
|
|
682
|
-
|---------|-------------|
|
|
683
|
-
| `neoorm init` | Scaffold `neoorm.config.ts`, `schema.ts`, `.env.example`, generate client, and first migration |
|
|
684
|
-
| `neoorm generate` | Emit manifest, typed client, models, includes, and migrations |
|
|
685
|
-
| `neoorm migrate dev` | Apply pending migrations, then generate a new one if the schema changed |
|
|
686
|
-
| `neoorm migrate deploy` | Apply pending migrations |
|
|
687
|
-
| `neoorm migrate status` | List applied vs pending migrations |
|
|
688
|
-
| `neoorm migrate down [--steps N]` | Roll back the last N applied migrations (default 1) |
|
|
689
|
-
| `neoorm migrate reset --force` | Drop public schema and re-apply migrations (local dev) |
|
|
690
|
-
| `neoorm db push` | Push the current snapshot schema to the database |
|
|
691
|
-
| `neoorm db pull` | Introspect the database into a schema file |
|
|
692
|
-
|
|
693
|
-
### Generate outcomes
|
|
694
|
-
|
|
695
|
-
`neoorm generate` always refreshes generated TypeScript files (`client.ts`, `manifest.ts`, `models.ts`, etc.). It prints one of four outcomes:
|
|
696
|
-
|
|
697
|
-
| Outcome | Meaning |
|
|
698
|
-
|---------|---------|
|
|
699
|
-
| **Schema unchanged** | Snapshot hash matches — no manifest or migration changes |
|
|
700
|
-
| **Client regenerated** | Manifest changed but no database DDL was needed (e.g. `enumMode`, relation metadata) |
|
|
701
|
-
| **Migration created** | New `migrations/<timestamp>/migration.sql` written |
|
|
702
|
-
| **Migration blocked** | Destructive or manual changes prevented writing SQL |
|
|
703
|
-
|
|
704
|
-
When migration is blocked or skipped, the CLI explains why — for example unsupported type casts (`alter_column_type_manual`), enum value changes, or destructive drops. Re-run with `--accept-data-loss` to include destructive DDL, or write a manual migration for unsupported type changes.
|
|
705
|
-
|
|
706
|
-
```bash
|
|
707
|
-
neoorm generate --accept-data-loss
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
### Migration status and reset
|
|
711
|
-
|
|
712
|
-
```bash
|
|
713
|
-
neoorm migrate status
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
Shows applied migrations (with timestamps), pending folders on disk, and warnings for drift (applied in DB but missing on disk).
|
|
717
|
-
|
|
718
|
-
```bash
|
|
719
|
-
neoorm migrate reset --force
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
Drops the `public` schema and re-applies all migrations from disk. Requires `--force`. Use `--skip-apply` to only drop the schema without re-applying.
|
|
723
|
-
|
|
724
|
-
### Rolling back migrations
|
|
725
|
-
|
|
726
|
-
```bash
|
|
727
|
-
neoorm migrate down
|
|
728
|
-
neoorm migrate down --steps 2
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
Rolls back the most recently applied migration(s) by running each migration folder's `down.sql`, removing the ledger entry from `_neoorm_migrations`, and restoring `snapshot.json` from `snapshot.before.json` (the manifest state before that migration was applied).
|
|
732
|
-
|
|
733
|
-
`down.sql` and `snapshot.before.json` are written automatically when `neoorm generate` or `neoorm migrate dev` creates a migration. The down SQL is the reverse schema diff (`next → prev`), with destructive changes accepted so rollbacks can drop columns or tables added in the forward migration.
|
|
734
|
-
|
|
735
|
-
Legacy migrations without `down.sql` cannot be rolled back — re-generate the migration or add `down.sql` manually.
|
|
736
|
-
|
|
737
|
-
Migration folders are not deleted on rollback (same as Prisma). Re-run `neoorm migrate deploy` to re-apply rolled-back migrations.
|
|
738
|
-
|
|
739
|
-
After rollback, `schema.ts` may still describe a newer schema than the restored snapshot; `neoorm migrate dev` may generate a new forward migration. `db push` and `migrate down` are independent — push ignores the migration ledger.
|
|
740
|
-
|
|
741
|
-
For a full wipe during local development, use `neoorm migrate reset --force` instead.
|
|
742
|
-
|
|
743
|
-
`generate` creates migration SQL only when the schema diff produces DDL changes (new tables/columns, column renames via `.map()`, etc.).
|
|
744
|
-
|
|
745
|
-
## Plugins
|
|
746
|
-
|
|
747
|
-
### PostGIS
|
|
748
|
-
|
|
749
|
-
```ts
|
|
750
|
-
import "neoorm/plugins/postgis";
|
|
751
|
-
import { geometry, point } from "neoorm/plugins/postgis";
|
|
752
|
-
|
|
753
|
-
places: table("places", {
|
|
754
|
-
id: uuid().primary(),
|
|
755
|
-
location: geometry({ subtype: "Point", srid: 4326 }).notNull(),
|
|
756
|
-
boundary: point({ srid: 4326 }),
|
|
757
|
-
})
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
Spatial `where` operators: `intersects`, `within`, `dWithin`.
|
|
761
|
-
|
|
762
|
-
```ts
|
|
763
|
-
await db.places.findMany({
|
|
764
|
-
where: {
|
|
765
|
-
location: {
|
|
766
|
-
dWithin: {
|
|
767
|
-
geometry: { type: "Point", coordinates: [-122.4, 37.8] },
|
|
768
|
-
distance: 1000,
|
|
769
|
-
},
|
|
770
|
-
},
|
|
771
|
-
},
|
|
772
|
-
});
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
PostGIS columns are stored as geometry/geography in PostgreSQL and exposed as GeoJSON in TypeScript.
|
|
776
|
-
|
|
777
|
-
### Citext
|
|
112
|
+
## API surface
|
|
778
113
|
|
|
779
|
-
|
|
114
|
+
| Import | Purpose |
|
|
115
|
+
|--------|---------|
|
|
116
|
+
| `neoorm` | `defineConfig`, `createNeoOrmClient`, `createNeoOrmClientFromPool`, client types |
|
|
117
|
+
| `neoorm/schema` | Schema DSL (`defineSchema`, `table`, column builders, `fk`, `manyToMany`, `index`, `unique`, `primaryKey`) |
|
|
118
|
+
| `neoorm/sql` | Tagged SQL templates (`sql`), SQL fragment builder, fluent query builder |
|
|
119
|
+
| `neoorm/plugins` | Plugin registry, `NeoOrmPlugin`, `ColumnTypePlugin` |
|
|
120
|
+
| `neoorm/plugins/postgis` | PostGIS column types (`geometry`, `geography`, `point`) and spatial operators |
|
|
780
121
|
|
|
781
|
-
|
|
782
|
-
import { citext, table } from "neoorm/schema";
|
|
122
|
+
---
|
|
783
123
|
|
|
784
|
-
|
|
785
|
-
email: citext().notNull().unique(),
|
|
786
|
-
})
|
|
787
|
-
```
|
|
124
|
+
## Philosophy
|
|
788
125
|
|
|
789
|
-
|
|
126
|
+
NeoOrm was built because existing TypeScript ORMs either sacrificed type safety for flexibility, or sacrificed flexibility for type safety. We think you shouldn't have to choose.
|
|
790
127
|
|
|
791
|
-
|
|
128
|
+
- **Schema is the source of truth** — not decorators, not reflection, not a proprietary DSL. Your schema file is plain TypeScript.
|
|
129
|
+
- **Generated code is a compile-time artifact** — no runtime dependency on the schema. Swap the schema, regenerate, everything still compiles.
|
|
130
|
+
- **SQL is not hidden** — the client compiles to parameterized SQL that you can inspect. No magic, no surprises.
|
|
131
|
+
- **PostgreSQL first** — we ship with a Postgres dialect and lean into its features. MySQL, SQLite, and other dialects are on the roadmap and will slot into the same architecture.
|
|
792
132
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
| Import | Purpose |
|
|
796
|
-
|--------|---------|
|
|
797
|
-
| `neoorm` | Config helpers, `createNeoOrmClient`, client types |
|
|
798
|
-
| `neoorm/schema` | Schema DSL (`defineSchema`, `table`, column builders, `fk`, `manyToMany`) |
|
|
799
|
-
| `neoorm/sql` | Tagged SQL templates and query builder |
|
|
800
|
-
| `neoorm/plugins` | Plugin registry |
|
|
801
|
-
| `neoorm/plugins/postgis` | PostGIS column types and spatial operators |
|
|
133
|
+
---
|
|
802
134
|
|
|
803
135
|
## License
|
|
804
136
|
|