neoorm 0.1.4 → 0.2.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/README.md +423 -27
- package/dist/bin/neoorm.js +117 -30
- package/dist/bin/neoorm.js.map +1 -1
- package/dist/codegen/diff-manifest.d.ts +7 -1
- package/dist/codegen/diff-manifest.d.ts.map +1 -1
- package/dist/codegen/diff-manifest.js +100 -4
- 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 +7 -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 +28 -6
- package/dist/codegen/emit-models.js.map +1 -1
- package/dist/codegen/generate-summary.d.ts +21 -0
- package/dist/codegen/generate-summary.d.ts.map +1 -0
- package/dist/codegen/generate-summary.js +101 -0
- package/dist/codegen/generate-summary.js.map +1 -0
- package/dist/codegen/generate.d.ts +12 -3
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/codegen/generate.js +36 -13
- package/dist/codegen/generate.js.map +1 -1
- package/dist/codegen/manifest-relations.d.ts.map +1 -1
- package/dist/codegen/manifest-relations.js +9 -2
- package/dist/codegen/manifest-relations.js.map +1 -1
- package/dist/codegen/schema-to-manifest.d.ts +4 -1
- package/dist/codegen/schema-to-manifest.d.ts.map +1 -1
- package/dist/codegen/schema-to-manifest.js +69 -12
- 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.map +1 -1
- package/dist/dialect/postgres.d.ts +3 -0
- package/dist/dialect/postgres.d.ts.map +1 -1
- package/dist/dialect/postgres.js +51 -1
- package/dist/dialect/postgres.js.map +1 -1
- package/dist/dialect/types.d.ts +13 -1
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/init/scaffold.d.ts +19 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +92 -0
- package/dist/init/scaffold.js.map +1 -0
- package/dist/init/templates.d.ts +4 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +44 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/introspect/queries.d.ts +5 -0
- package/dist/introspect/queries.d.ts.map +1 -1
- package/dist/introspect/queries.js +17 -0
- package/dist/introspect/queries.js.map +1 -1
- package/dist/introspect/to-manifest.d.ts.map +1 -1
- package/dist/introspect/to-manifest.js +58 -14
- package/dist/introspect/to-manifest.js.map +1 -1
- package/dist/migrate/runner.d.ts +26 -0
- package/dist/migrate/runner.d.ts.map +1 -1
- package/dist/migrate/runner.js +166 -7
- package/dist/migrate/runner.js.map +1 -1
- package/dist/plugins/builtin.d.ts +35 -2
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +318 -1
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/json/operators.d.ts +3 -0
- package/dist/plugins/json/operators.d.ts.map +1 -0
- package/dist/plugins/json/operators.js +65 -0
- package/dist/plugins/json/operators.js.map +1 -0
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +3 -1
- package/dist/plugins/registry.js.map +1 -1
- package/dist/plugins/types.d.ts +1 -0
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/runtime/client.d.ts +33 -3
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +54 -36
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/errors.d.ts +20 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +68 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/executor.d.ts +1 -0
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +27 -4
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/pg-error.d.ts +19 -0
- package/dist/runtime/pg-error.d.ts.map +1 -0
- package/dist/runtime/pg-error.js +110 -0
- package/dist/runtime/pg-error.js.map +1 -0
- package/dist/runtime/query/aggregate.d.ts +11 -0
- package/dist/runtime/query/aggregate.d.ts.map +1 -0
- package/dist/runtime/query/aggregate.js +44 -0
- package/dist/runtime/query/aggregate.js.map +1 -0
- package/dist/runtime/query/compile.d.ts +21 -6
- package/dist/runtime/query/compile.d.ts.map +1 -1
- package/dist/runtime/query/compile.js +110 -19
- package/dist/runtime/query/compile.js.map +1 -1
- package/dist/runtime/query/count.d.ts +3 -3
- package/dist/runtime/query/count.d.ts.map +1 -1
- package/dist/runtime/query/count.js +7 -4
- package/dist/runtime/query/count.js.map +1 -1
- package/dist/runtime/query/create.d.ts +12 -2
- package/dist/runtime/query/create.d.ts.map +1 -1
- package/dist/runtime/query/create.js +74 -111
- package/dist/runtime/query/create.js.map +1 -1
- package/dist/runtime/query/cursor-codec.d.ts +3 -0
- package/dist/runtime/query/cursor-codec.d.ts.map +1 -0
- package/dist/runtime/query/cursor-codec.js +27 -0
- package/dist/runtime/query/cursor-codec.js.map +1 -0
- package/dist/runtime/query/cursor.d.ts +22 -0
- package/dist/runtime/query/cursor.d.ts.map +1 -0
- package/dist/runtime/query/cursor.js +84 -0
- package/dist/runtime/query/cursor.js.map +1 -0
- package/dist/runtime/query/delete.d.ts +4 -4
- package/dist/runtime/query/delete.d.ts.map +1 -1
- package/dist/runtime/query/delete.js +18 -8
- package/dist/runtime/query/delete.js.map +1 -1
- package/dist/runtime/query/execute.d.ts +16 -0
- package/dist/runtime/query/execute.d.ts.map +1 -0
- package/dist/runtime/query/execute.js +74 -0
- package/dist/runtime/query/execute.js.map +1 -0
- package/dist/runtime/query/find.d.ts +7 -5
- package/dist/runtime/query/find.d.ts.map +1 -1
- package/dist/runtime/query/find.js +148 -29
- package/dist/runtime/query/find.js.map +1 -1
- package/dist/runtime/query/paginate.d.ts +17 -0
- package/dist/runtime/query/paginate.d.ts.map +1 -0
- package/dist/runtime/query/paginate.js +35 -0
- package/dist/runtime/query/paginate.js.map +1 -0
- package/dist/runtime/query/primary-key.d.ts +11 -1
- package/dist/runtime/query/primary-key.d.ts.map +1 -1
- package/dist/runtime/query/primary-key.js +55 -0
- package/dist/runtime/query/primary-key.js.map +1 -1
- package/dist/runtime/query/relation-writes.d.ts +25 -0
- package/dist/runtime/query/relation-writes.d.ts.map +1 -0
- package/dist/runtime/query/relation-writes.js +404 -0
- package/dist/runtime/query/relation-writes.js.map +1 -0
- package/dist/runtime/query/unique.d.ts.map +1 -1
- package/dist/runtime/query/unique.js +1 -5
- package/dist/runtime/query/unique.js.map +1 -1
- package/dist/runtime/query/update.d.ts +4 -4
- package/dist/runtime/query/update.d.ts.map +1 -1
- package/dist/runtime/query/update.js +97 -38
- package/dist/runtime/query/update.js.map +1 -1
- package/dist/runtime/query/updated-at.d.ts +6 -0
- package/dist/runtime/query/updated-at.d.ts.map +1 -0
- package/dist/runtime/query/updated-at.js +21 -0
- package/dist/runtime/query/updated-at.js.map +1 -0
- package/dist/runtime/query/upsert.d.ts +2 -2
- package/dist/runtime/query/upsert.d.ts.map +1 -1
- package/dist/runtime/query/upsert.js +12 -8
- package/dist/runtime/query/upsert.js.map +1 -1
- package/dist/runtime/types.d.ts +21 -11
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/schema/column-where.d.ts +30 -14
- package/dist/schema/column-where.d.ts.map +1 -1
- package/dist/schema/column.d.ts +6 -2
- package/dist/schema/column.d.ts.map +1 -1
- package/dist/schema/column.js +7 -1
- package/dist/schema/column.js.map +1 -1
- package/dist/schema/index.d.ts +4 -4
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/nested-relation-types.d.ts +61 -0
- package/dist/schema/nested-relation-types.d.ts.map +1 -0
- package/dist/schema/nested-relation-types.js +2 -0
- package/dist/schema/nested-relation-types.js.map +1 -0
- package/dist/schema/relation-types.d.ts +164 -22
- package/dist/schema/relation-types.d.ts.map +1 -1
- package/dist/schema/types.d.ts +83 -37
- package/dist/schema/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,44 @@ Requires Node.js 20+ and PostgreSQL.
|
|
|
12
12
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
15
|
-
**1.
|
|
15
|
+
**1. Scaffold a new project**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx neoorm init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This creates `neoorm.config.ts`, `schema.ts`, `.env.example`, generates `neoorm/client.ts` (and related files), and writes the first migration under `neoorm/migrations/`.
|
|
22
|
+
|
|
23
|
+
**2. Set your database URL**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cp .env.example .env
|
|
27
|
+
# edit DATABASE_URL in .env
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**3. Apply migrations**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx neoorm migrate deploy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**4. Query**
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { db } from "./neoorm/client.js";
|
|
40
|
+
|
|
41
|
+
const user = await db.users.findById(userId, {
|
|
42
|
+
with: {
|
|
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`):
|
|
16
53
|
|
|
17
54
|
```ts
|
|
18
55
|
import { defineSchema, table, uuid, text, timestamp, fk } from "neoorm/schema";
|
|
@@ -43,6 +80,7 @@ export default defineConfig({
|
|
|
43
80
|
datasource: {
|
|
44
81
|
provider: "postgresql",
|
|
45
82
|
url: process.env.DATABASE_URL!,
|
|
83
|
+
enum: "check", // "check" (default) | "union" | "native"
|
|
46
84
|
},
|
|
47
85
|
});
|
|
48
86
|
```
|
|
@@ -55,18 +93,6 @@ npx neoorm generate
|
|
|
55
93
|
|
|
56
94
|
This writes `client.ts`, `manifest.ts`, `models.ts`, `includes.ts`, and migration SQL when the schema changed.
|
|
57
95
|
|
|
58
|
-
**4. Query**
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { db } from "./neoorm/client.js";
|
|
62
|
-
|
|
63
|
-
const user = await db.users.findById(userId, {
|
|
64
|
-
with: {
|
|
65
|
-
posts: { orderBy: { createdAt: "desc" }, limit: 10 },
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
96
|
## Schema DSL
|
|
71
97
|
|
|
72
98
|
### Tables and accessors
|
|
@@ -81,20 +107,148 @@ Column field names use camelCase in TypeScript. By default, SQL column names are
|
|
|
81
107
|
|
|
82
108
|
### Column types
|
|
83
109
|
|
|
84
|
-
| Builder | SQL type | Notes |
|
|
85
|
-
|
|
86
|
-
| `id.primary()` | `TEXT` | App-generated string IDs (e.g. `user_a1b2c3d4`) |
|
|
87
|
-
| `uuid()` | `UUID` | Defaults to UUID v7; pass `{ version: 4 }` for v4 |
|
|
88
|
-
| `uuid().primary()` | `UUID` | Primary key; auto-generated on create if omitted |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
110
|
+
| Builder | SQL type | TypeScript | Notes |
|
|
111
|
+
|---------|----------|------------|-------|
|
|
112
|
+
| `id.primary()` | `TEXT` | `string` | App-generated string IDs (e.g. `user_a1b2c3d4`) |
|
|
113
|
+
| `uuid()` | `UUID` | `string` | Defaults to UUID v7; pass `{ version: 4 }` for v4 |
|
|
114
|
+
| `uuid().primary()` | `UUID` | `string` | Primary key; auto-generated on create if omitted |
|
|
115
|
+
| `serial()` | `INTEGER GENERATED BY DEFAULT AS IDENTITY` | `number` | Auto-increment; omit on insert, DB assigns via `RETURNING` |
|
|
116
|
+
| `serial().primary()` | same | `number` | Integer PK with DB-generated values |
|
|
117
|
+
| `text()` | `TEXT` | `string \| null` | |
|
|
118
|
+
| `bool()` | `BOOLEAN` | `boolean \| null` | |
|
|
119
|
+
| `int()` | `INTEGER` | `number \| null` | |
|
|
120
|
+
| `timestamp()` | `TIMESTAMPTZ` | `Date \| null` | Use `.defaultNow()` for `DEFAULT NOW()`; use `.updatedAt()` for auto-update on ORM writes |
|
|
93
121
|
|
|
94
|
-
|
|
122
|
+
Pair audit timestamps on mutable tables:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
createdAt: timestamp().notNull().defaultNow(),
|
|
126
|
+
updatedAt: timestamp().notNull().defaultNow().updatedAt(),
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`.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.
|
|
130
|
+
| `json()` | `JSON` | `unknown \| null` | Generic: `json<MyType>()` |
|
|
131
|
+
| `jsonb()` | `JSONB` | `unknown \| null` | Generic: `jsonb<MyType>()` |
|
|
132
|
+
| `decimal()` / `numeric()` | `NUMERIC` or `NUMERIC(p,s)` | `string \| null` | Use strings to avoid float precision loss |
|
|
133
|
+
| `enumType(["a", "b"])` | mode-dependent | union literals | See [Enum columns](#enum-columns) |
|
|
134
|
+
| `bytea()` | `BYTEA` | `Buffer \| null` | Binary data |
|
|
135
|
+
| `textArray()` | `TEXT[]` | `string[] \| null` | |
|
|
136
|
+
| `intArray()` | `INTEGER[]` | `number[] \| null` | |
|
|
137
|
+
| `citext()` | `CITEXT` | `string \| null` | Case-insensitive text; requires `citext` extension |
|
|
138
|
+
|
|
139
|
+
All column builders support `.notNull()`, `.unique()`, `.default(value)`, `.defaultNow()`, `.updatedAt()` (timestamp only), `.primary()`, and `.map(name)`.
|
|
95
140
|
|
|
96
141
|
Foreign keys use `fk("target_table.target_column", { as, inverse, nullable?, onDelete? })`.
|
|
97
142
|
|
|
143
|
+
### Enum columns
|
|
144
|
+
|
|
145
|
+
Define allowed values in the schema:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { enumType, table } from "neoorm/schema";
|
|
149
|
+
|
|
150
|
+
posts: table("posts", {
|
|
151
|
+
status: enumType(["draft", "published", "archived"] as const)
|
|
152
|
+
.notNull()
|
|
153
|
+
.default("draft"),
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Control how enums are stored via `datasource.enum` in `neoorm.config.ts`:
|
|
158
|
+
|
|
159
|
+
| Mode | SQL | DB enforcement |
|
|
160
|
+
|------|-----|----------------|
|
|
161
|
+
| `check` (default) | `TEXT` + `CHECK (...)` | yes |
|
|
162
|
+
| `union` | `TEXT` | no (TypeScript union only) |
|
|
163
|
+
| `native` | Postgres `CREATE TYPE ... AS ENUM` | yes |
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
export default defineConfig({
|
|
167
|
+
schema: "./schema.ts",
|
|
168
|
+
out: "./neoorm",
|
|
169
|
+
datasource: {
|
|
170
|
+
provider: "postgresql",
|
|
171
|
+
url: process.env.DATABASE_URL!,
|
|
172
|
+
enum: "check", // or "union" | "native"
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Optional custom SQL type name for native mode: `enumType(["draft", "published"], { name: "post_status" })`.
|
|
178
|
+
|
|
179
|
+
Query and mutate enum columns like strings with compile-time union checking:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await db.posts.findMany({ where: { status: "published" } });
|
|
183
|
+
await db.posts.create({ data: { title: "Hello", status: "draft" } });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### JSON and decimal columns
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
posts: table("posts", {
|
|
190
|
+
metadata: jsonb<{ tags: string[]; featured?: boolean }>(),
|
|
191
|
+
price: decimal({ precision: 10, scale: 2 }).default("0.00"),
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// jsonb — exact equality on the full object (all keys must match)
|
|
197
|
+
await db.posts.findMany({
|
|
198
|
+
where: { metadata: { featured: true, category: "engineering" } },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// jsonb — partial / subset match (@> containment)
|
|
202
|
+
await db.posts.findMany({
|
|
203
|
+
where: { metadata: { jsonContains: { featured: true } } },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// jsonb — key existence and path filters
|
|
207
|
+
await db.posts.findMany({
|
|
208
|
+
where: {
|
|
209
|
+
metadata: {
|
|
210
|
+
hasKey: "featured",
|
|
211
|
+
hasAnyKeys: ["category", "tags"],
|
|
212
|
+
path: { segments: ["category"], equals: "engineering" },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// decimal — compare as strings
|
|
218
|
+
await db.posts.findMany({
|
|
219
|
+
where: { price: { gte: "9.99", lte: "49.99" } },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await db.posts.create({
|
|
223
|
+
data: {
|
|
224
|
+
title: "Premium",
|
|
225
|
+
price: "19.99",
|
|
226
|
+
metadata: { featured: true, tags: ["orm"] },
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Serial primary keys
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
items: table("items", {
|
|
235
|
+
id: serial().primary(),
|
|
236
|
+
name: text().notNull(),
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Omit `id` on insert — the database assigns it and `RETURNING` populates the result:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const item = await db.items.create({ data: { name: "Widget" } });
|
|
244
|
+
// item.id is the DB-generated integer
|
|
245
|
+
|
|
246
|
+
// Bulk insert with returned rows (serial IDs, UUIDs, defaults materialized)
|
|
247
|
+
const items = await db.items.createManyAndReturn({
|
|
248
|
+
data: [{ name: "A" }, { name: "B" }],
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
98
252
|
### UUID columns
|
|
99
253
|
|
|
100
254
|
```ts
|
|
@@ -158,6 +312,142 @@ manyToMany(schema.posts, schema.tags, {
|
|
|
158
312
|
});
|
|
159
313
|
```
|
|
160
314
|
|
|
315
|
+
## Relation writes
|
|
316
|
+
|
|
317
|
+
`create` and `update` accept nested relation writes alongside scalar fields. Relation-only updates are supported (no scalar `SET` required).
|
|
318
|
+
|
|
319
|
+
| Relation kind | Operations |
|
|
320
|
+
|---------------|------------|
|
|
321
|
+
| **To-one** (outgoing FK) | `connect`, `create`, `disconnect` (nullable FK only) |
|
|
322
|
+
| **One-to-many** (inverse) | `create`, `connect`, `disconnect`, `set`, `delete` |
|
|
323
|
+
| **Many-to-many** | `connect`, `connectOrCreate`, `disconnect`, `set`, `delete` |
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
// To-one on update
|
|
327
|
+
await db.posts.update({
|
|
328
|
+
where: { id: postId },
|
|
329
|
+
data: { author: { connect: { id: userId } } },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Nested create on one-to-many (full nested relation writes inside create items)
|
|
333
|
+
await db.posts.update({
|
|
334
|
+
where: { id: postId },
|
|
335
|
+
data: {
|
|
336
|
+
comments: {
|
|
337
|
+
create: [{ body: "Hi", author: { connect: { id: userId } } }],
|
|
338
|
+
connect: [{ id: commentId }],
|
|
339
|
+
disconnect: [{ id: oldCommentId }], // or `true` to unlink all
|
|
340
|
+
set: [{ id: commentId }], // replace all links
|
|
341
|
+
delete: [{ id: commentId }], // or `true` to delete all linked children
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// M2M on create or update
|
|
347
|
+
await db.posts.update({
|
|
348
|
+
where: { id: postId },
|
|
349
|
+
data: {
|
|
350
|
+
tags: {
|
|
351
|
+
connect: [{ id: tagId }],
|
|
352
|
+
set: [{ id: tagId }],
|
|
353
|
+
connectOrCreate: [
|
|
354
|
+
{ where: { slug: "orm" }, create: { slug: "orm", name: "ORM" } },
|
|
355
|
+
],
|
|
356
|
+
delete: [{ id: tagId }], // removes junction links, then deletes tag rows
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// updateMany applies scalar SET once, then nested writes per matched parent
|
|
362
|
+
await db.posts.updateMany({
|
|
363
|
+
where: { published: true },
|
|
364
|
+
data: {
|
|
365
|
+
status: "archived",
|
|
366
|
+
tags: { connect: [{ id: tagId }] },
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
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.
|
|
372
|
+
|
|
373
|
+
## Cursor pagination
|
|
374
|
+
|
|
375
|
+
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)`).
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
let cursor: { createdAt: string; id: string } | null = null;
|
|
379
|
+
|
|
380
|
+
for (;;) {
|
|
381
|
+
const page = await db.posts.paginate({
|
|
382
|
+
where: { published: true },
|
|
383
|
+
orderBy: { createdAt: "desc" },
|
|
384
|
+
take: 20,
|
|
385
|
+
...(cursor ? { after: cursor } : {}),
|
|
386
|
+
with: { author: true },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
for (const post of page.items) {
|
|
390
|
+
// render post
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!page.hasMore) break;
|
|
394
|
+
cursor = page.nextCursor;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- `orderBy` is required; scalar `id` is appended automatically when omitted.
|
|
399
|
+
- `take` is the page size; `hasMore` uses a `take + 1` probe row.
|
|
400
|
+
- `after` is a typed cursor object (`nextCursor` from the previous page).
|
|
401
|
+
- For HTTP APIs, encode cursors with `encodeCursor` / `decodeCursor` from `neoorm`.
|
|
402
|
+
|
|
403
|
+
On feed tables, add a composite index on the sort columns (for example `index().on(t.createdAt, t.id)`).
|
|
404
|
+
|
|
405
|
+
## Aggregates
|
|
406
|
+
|
|
407
|
+
### Relation counts in `with`
|
|
408
|
+
|
|
409
|
+
Load counts without fetching full related rows:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const users = await db.users.findMany({
|
|
413
|
+
with: {
|
|
414
|
+
_count: { posts: true },
|
|
415
|
+
profile: true,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
// users[0]._count.posts === number
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Optional per-relation filter: `_count: { posts: { where: { published: true } } }`.
|
|
422
|
+
|
|
423
|
+
### Table-level `aggregate()`
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
const stats = await db.posts.aggregate({
|
|
427
|
+
where: { published: true },
|
|
428
|
+
_count: true,
|
|
429
|
+
_avg: { views: true },
|
|
430
|
+
});
|
|
431
|
+
// { _count: number, _avg: { views: number | null } }
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
For grouped dashboards, use `db.sql` or the `sqlBuilder` helper.
|
|
435
|
+
|
|
436
|
+
## Bulk insert
|
|
437
|
+
|
|
438
|
+
`createMany` returns the number of rows inserted. Use `createManyAndReturn` when you need serial IDs, generated UUIDs, or default values in the result:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const users = await db.users.createManyAndReturn({
|
|
442
|
+
data: [
|
|
443
|
+
{ email: "a@example.com", name: "A" },
|
|
444
|
+
{ email: "b@example.com", name: "B" },
|
|
445
|
+
],
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Scalar fields only — no nested relation writes (same as `createMany`).
|
|
450
|
+
|
|
161
451
|
## Where clauses
|
|
162
452
|
|
|
163
453
|
`findMany`, `findFirst`, `findUnique`, `count`, `update`, `updateMany`, `delete`, and `deleteMany` all accept a typed `where` argument.
|
|
@@ -180,7 +470,29 @@ await db.posts.findFirst({
|
|
|
180
470
|
});
|
|
181
471
|
```
|
|
182
472
|
|
|
183
|
-
String columns support `equals`, `contains`, `startsWith`, `endsWith`, `in`, and `notIn`. Numeric, boolean, and date columns support `equals`, `gt`, `gte`, `lt`, `lte`, `in`, and `notIn`. All nullable columns also support `isNull` and `isNotNull`.
|
|
473
|
+
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`.
|
|
474
|
+
|
|
475
|
+
### `distinct`
|
|
476
|
+
|
|
477
|
+
PostgreSQL `DISTINCT ON` — `orderBy` must lead with the same columns:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
await db.users.findMany({
|
|
481
|
+
distinct: ["email"],
|
|
482
|
+
orderBy: { email: "asc" },
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Selective `with` return types
|
|
487
|
+
|
|
488
|
+
Relation `select` narrows the TypeScript return type at compile time:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
const users = await db.users.findMany({
|
|
492
|
+
with: { posts: { select: { title: true } } },
|
|
493
|
+
});
|
|
494
|
+
// users[0].posts[0].title is string; .body is excluded from the type
|
|
495
|
+
```
|
|
184
496
|
|
|
185
497
|
```ts
|
|
186
498
|
await db.users.findMany({
|
|
@@ -266,26 +578,94 @@ const [user, post] = await db.$transaction([
|
|
|
266
578
|
(tx) => tx.posts.create({ data: { title: "Hello" } }),
|
|
267
579
|
]);
|
|
268
580
|
|
|
269
|
-
// Options
|
|
581
|
+
// Options (outermost transaction only)
|
|
270
582
|
await db.$transaction(fn, {
|
|
271
583
|
isolationLevel: "Serializable", // ReadUncommitted | ReadCommitted | RepeatableRead | Serializable
|
|
272
584
|
readOnly: true,
|
|
273
585
|
});
|
|
586
|
+
|
|
587
|
+
// Nested $transaction uses PostgreSQL savepoints on the same connection.
|
|
588
|
+
// A nested failure rolls back only that block; the outer transaction can continue.
|
|
589
|
+
await db.$transaction(async (tx) => {
|
|
590
|
+
await tx.users.create({ data: { email: "outer@example.com" } });
|
|
591
|
+
|
|
592
|
+
await tx.$transaction(async (nested) => {
|
|
593
|
+
await nested.posts.create({
|
|
594
|
+
data: { title: "Nested", body: "...", authorId: "user_1" },
|
|
595
|
+
});
|
|
596
|
+
}).catch(() => undefined);
|
|
597
|
+
|
|
598
|
+
// Outer writes are kept even if the nested block failed.
|
|
599
|
+
});
|
|
274
600
|
```
|
|
275
601
|
|
|
276
|
-
Nested `create` calls inside a transaction do not start a separate transaction.
|
|
602
|
+
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`.
|
|
277
603
|
|
|
278
604
|
## CLI
|
|
279
605
|
|
|
280
606
|
| Command | Description |
|
|
281
607
|
|---------|-------------|
|
|
608
|
+
| `neoorm init` | Scaffold `neoorm.config.ts`, `schema.ts`, `.env.example`, generate client, and first migration |
|
|
282
609
|
| `neoorm generate` | Emit manifest, typed client, models, includes, and migrations |
|
|
283
610
|
| `neoorm migrate dev` | Apply pending migrations, then generate a new one if the schema changed |
|
|
284
611
|
| `neoorm migrate deploy` | Apply pending migrations |
|
|
612
|
+
| `neoorm migrate status` | List applied vs pending migrations |
|
|
613
|
+
| `neoorm migrate down [--steps N]` | Roll back the last N applied migrations (default 1) |
|
|
614
|
+
| `neoorm migrate reset --force` | Drop public schema and re-apply migrations (local dev) |
|
|
285
615
|
| `neoorm db push` | Push the current snapshot schema to the database |
|
|
286
616
|
| `neoorm db pull` | Introspect the database into a schema file |
|
|
287
617
|
|
|
288
|
-
|
|
618
|
+
### Generate outcomes
|
|
619
|
+
|
|
620
|
+
`neoorm generate` always refreshes generated TypeScript files (`client.ts`, `manifest.ts`, `models.ts`, etc.). It prints one of four outcomes:
|
|
621
|
+
|
|
622
|
+
| Outcome | Meaning |
|
|
623
|
+
|---------|---------|
|
|
624
|
+
| **Schema unchanged** | Snapshot hash matches — no manifest or migration changes |
|
|
625
|
+
| **Client regenerated** | Manifest changed but no database DDL was needed (e.g. `enumMode`, relation metadata) |
|
|
626
|
+
| **Migration created** | New `migrations/<timestamp>/migration.sql` written |
|
|
627
|
+
| **Migration blocked** | Destructive or manual changes prevented writing SQL |
|
|
628
|
+
|
|
629
|
+
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.
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
neoorm generate --accept-data-loss
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Migration status and reset
|
|
636
|
+
|
|
637
|
+
```bash
|
|
638
|
+
neoorm migrate status
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Shows applied migrations (with timestamps), pending folders on disk, and warnings for drift (applied in DB but missing on disk).
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
neoorm migrate reset --force
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Drops the `public` schema and re-applies all migrations from disk. Requires `--force`. Use `--skip-apply` to only drop the schema without re-applying.
|
|
648
|
+
|
|
649
|
+
### Rolling back migrations
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
neoorm migrate down
|
|
653
|
+
neoorm migrate down --steps 2
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
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).
|
|
657
|
+
|
|
658
|
+
`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.
|
|
659
|
+
|
|
660
|
+
Legacy migrations without `down.sql` cannot be rolled back — re-generate the migration or add `down.sql` manually.
|
|
661
|
+
|
|
662
|
+
Migration folders are not deleted on rollback (same as Prisma). Re-run `neoorm migrate deploy` to re-apply rolled-back migrations.
|
|
663
|
+
|
|
664
|
+
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.
|
|
665
|
+
|
|
666
|
+
For a full wipe during local development, use `neoorm migrate reset --force` instead.
|
|
667
|
+
|
|
668
|
+
`generate` creates migration SQL only when the schema diff produces DDL changes (new tables/columns, column renames via `.map()`, etc.).
|
|
289
669
|
|
|
290
670
|
## Plugins
|
|
291
671
|
|
|
@@ -319,6 +699,22 @@ await db.places.findMany({
|
|
|
319
699
|
|
|
320
700
|
PostGIS columns are stored as geometry/geography in PostgreSQL and exposed as GeoJSON in TypeScript.
|
|
321
701
|
|
|
702
|
+
### Citext
|
|
703
|
+
|
|
704
|
+
`citext()` is registered as a separate plugin that enables the `citext` extension when used in a schema:
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
import { citext, table } from "neoorm/schema";
|
|
708
|
+
|
|
709
|
+
users: table("users", {
|
|
710
|
+
email: citext().notNull().unique(),
|
|
711
|
+
})
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Examples
|
|
715
|
+
|
|
716
|
+
The [blog example](examples/blog/schema.ts) demonstrates relations, many-to-many, `jsonb`, `decimal`, and `enumType` columns. See [queries.example.ts](examples/blog/queries.example.ts) for typed queries and mutations using those types.
|
|
717
|
+
|
|
322
718
|
## API surface
|
|
323
719
|
|
|
324
720
|
| Import | Purpose |
|