neoorm 0.6.2 → 0.7.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 +6 -4
- package/dist/bin/neoorm.js +28 -1
- package/dist/bin/neoorm.js.map +1 -1
- package/dist/codegen/generate.d.ts +7 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/codegen/generate.js +18 -2
- package/dist/codegen/generate.js.map +1 -1
- package/dist/codegen/schema-to-manifest.d.ts +15 -1
- package/dist/codegen/schema-to-manifest.d.ts.map +1 -1
- package/dist/codegen/schema-to-manifest.js +87 -25
- package/dist/codegen/schema-to-manifest.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/docs/pages.d.ts +9 -0
- package/dist/docs/pages.d.ts.map +1 -0
- package/dist/docs/pages.js +64 -0
- package/dist/docs/pages.js.map +1 -0
- package/dist/docs/render.d.ts +4 -0
- package/dist/docs/render.d.ts.map +1 -0
- package/dist/docs/render.js +245 -0
- package/dist/docs/render.js.map +1 -0
- package/dist/docs/resolve-docs-dir.d.ts +2 -0
- package/dist/docs/resolve-docs-dir.d.ts.map +1 -0
- package/dist/docs/resolve-docs-dir.js +27 -0
- package/dist/docs/resolve-docs-dir.js.map +1 -0
- package/dist/docs/server.d.ts +11 -0
- package/dist/docs/server.d.ts.map +1 -0
- package/dist/docs/server.js +99 -0
- package/dist/docs/server.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/init/templates.js +2 -2
- package/dist/introspect/pull.d.ts.map +1 -1
- package/dist/introspect/pull.js +16 -2
- package/dist/introspect/pull.js.map +1 -1
- package/dist/migrate/runner.d.ts +4 -0
- package/dist/migrate/runner.d.ts.map +1 -1
- package/dist/migrate/runner.js +4 -0
- package/dist/migrate/runner.js.map +1 -1
- package/dist/plugins/builtin.d.ts +24 -0
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +21 -0
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/postgis/columns.d.ts +7 -3
- package/dist/plugins/postgis/columns.d.ts.map +1 -1
- package/dist/plugins/postgis/columns.js +3 -0
- package/dist/plugins/postgis/columns.js.map +1 -1
- package/dist/plugins/registry.d.ts +1 -0
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +1 -0
- package/dist/plugins/registry.js.map +1 -1
- package/dist/plugins/types.d.ts +2 -0
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/runtime/client.d.ts +27 -1
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +15 -5
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/error-builders.d.ts +14 -0
- package/dist/runtime/error-builders.d.ts.map +1 -0
- package/dist/runtime/error-builders.js +54 -0
- package/dist/runtime/error-builders.js.map +1 -0
- package/dist/runtime/error-hints.d.ts +19 -0
- package/dist/runtime/error-hints.d.ts.map +1 -0
- package/dist/runtime/error-hints.js +181 -0
- package/dist/runtime/error-hints.js.map +1 -0
- package/dist/runtime/errors.d.ts +12 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +24 -2
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/pg-error.d.ts.map +1 -1
- package/dist/runtime/pg-error.js +86 -0
- package/dist/runtime/pg-error.js.map +1 -1
- package/dist/runtime/query/compile.d.ts.map +1 -1
- package/dist/runtime/query/compile.js +5 -12
- package/dist/runtime/query/compile.js.map +1 -1
- package/dist/runtime/query/cursor-codec.d.ts +11 -0
- package/dist/runtime/query/cursor-codec.d.ts.map +1 -1
- package/dist/runtime/query/cursor-codec.js +11 -0
- 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 +2 -5
- package/dist/runtime/query/cursor.js.map +1 -1
- package/dist/runtime/query/execute.d.ts.map +1 -1
- package/dist/runtime/query/execute.js +28 -9
- package/dist/runtime/query/execute.js.map +1 -1
- package/dist/runtime/query/group-by.d.ts.map +1 -1
- package/dist/runtime/query/group-by.js +3 -7
- package/dist/runtime/query/group-by.js.map +1 -1
- package/dist/runtime/query/projection.d.ts.map +1 -1
- package/dist/runtime/query/projection.js +2 -4
- package/dist/runtime/query/projection.js.map +1 -1
- package/dist/runtime/query/relation-planner.d.ts.map +1 -1
- package/dist/runtime/query/relation-planner.js +6 -1
- package/dist/runtime/query/relation-planner.js.map +1 -1
- package/dist/runtime/query/strip.d.ts.map +1 -1
- package/dist/runtime/query/strip.js +2 -4
- package/dist/runtime/query/strip.js.map +1 -1
- package/dist/runtime/query/table-index.d.ts +4 -0
- package/dist/runtime/query/table-index.d.ts.map +1 -1
- package/dist/runtime/query/table-index.js +45 -0
- package/dist/runtime/query/table-index.js.map +1 -1
- package/dist/runtime/schema-error.d.ts +1 -1
- package/dist/runtime/schema-error.d.ts.map +1 -1
- package/dist/runtime/schema-error.js +7 -2
- package/dist/runtime/schema-error.js.map +1 -1
- package/dist/runtime/sqlite-error.d.ts +12 -0
- package/dist/runtime/sqlite-error.d.ts.map +1 -0
- package/dist/runtime/sqlite-error.js +169 -0
- package/dist/runtime/sqlite-error.js.map +1 -0
- package/dist/runtime/types.d.ts +8 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/schema/column.d.ts +32 -5
- package/dist/schema/column.d.ts.map +1 -1
- package/dist/schema/column.js +5 -0
- package/dist/schema/column.js.map +1 -1
- package/dist/schema/define-schema.d.ts +19 -0
- package/dist/schema/define-schema.d.ts.map +1 -1
- package/dist/schema/define-schema.js +15 -0
- package/dist/schema/define-schema.js.map +1 -1
- package/dist/schema/index.d.ts +7 -1
- 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/many-to-many.d.ts +23 -1
- package/dist/schema/many-to-many.d.ts.map +1 -1
- package/dist/schema/many-to-many.js +16 -1
- package/dist/schema/many-to-many.js.map +1 -1
- package/dist/schema/nested-relation-types.d.ts +2 -1
- package/dist/schema/nested-relation-types.d.ts.map +1 -1
- package/dist/schema/relation-types.d.ts +14 -9
- package/dist/schema/relation-types.d.ts.map +1 -1
- package/dist/schema/relation.d.ts +34 -2
- package/dist/schema/relation.d.ts.map +1 -1
- package/dist/schema/relation.js.map +1 -1
- package/dist/schema/table.d.ts +20 -0
- package/dist/schema/table.d.ts.map +1 -1
- package/dist/schema/table.js +3 -0
- package/dist/schema/table.js.map +1 -1
- package/dist/schema/types.d.ts +7 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/sql/builder.d.ts +1 -0
- package/dist/sql/builder.d.ts.map +1 -1
- package/dist/sql/builder.js +1 -0
- package/dist/sql/builder.js.map +1 -1
- package/dist/sql/index.d.ts +5 -1
- package/dist/sql/index.d.ts.map +1 -1
- package/dist/sql/index.js +5 -1
- package/dist/sql/index.js.map +1 -1
- package/dist/sql/template.d.ts +3 -0
- package/dist/sql/template.d.ts.map +1 -1
- package/dist/sql/template.js +3 -0
- package/dist/sql/template.js.map +1 -1
- package/docs/api-docstrings.md +43 -0
- package/docs/cli.md +107 -0
- package/docs/configuration.md +51 -0
- package/docs/errors.md +72 -0
- package/docs/examples.md +498 -0
- package/docs/getting-started.md +122 -0
- package/docs/migrations.md +67 -0
- package/docs/plugins.md +47 -0
- package/docs/queries.md +465 -0
- package/docs/relations.md +75 -0
- package/docs/schema.md +265 -0
- package/docs/sqlite.md +105 -0
- package/docs/transactions.md +54 -0
- package/package.json +3 -1
package/docs/examples.md
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Copy-paste patterns for common NeoOrm tasks. The full runnable blog demo lives in [`examples/blog/`](https://github.com/xmreur/neoorm/tree/main/examples/blog) (`schema.ts` + `queries.example.ts`). PostGIS sample: [`examples/postgis/`](https://github.com/xmreur/neoorm/tree/main/examples/postgis).
|
|
4
|
+
|
|
5
|
+
Run `neoorm docs` to browse all documentation locally.
|
|
6
|
+
|
|
7
|
+
## Blog schema
|
|
8
|
+
|
|
9
|
+
A small CMS-style schema used throughout these examples:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
bool,
|
|
14
|
+
defineSchema,
|
|
15
|
+
enumType,
|
|
16
|
+
fk,
|
|
17
|
+
id,
|
|
18
|
+
int,
|
|
19
|
+
jsonb,
|
|
20
|
+
many,
|
|
21
|
+
table,
|
|
22
|
+
text,
|
|
23
|
+
timestamps,
|
|
24
|
+
uuid,
|
|
25
|
+
} from "neoorm/schema";
|
|
26
|
+
|
|
27
|
+
export const schema = defineSchema({
|
|
28
|
+
users: table({
|
|
29
|
+
id: uuid().primary(),
|
|
30
|
+
email: text().notNull().unique(),
|
|
31
|
+
name: text(),
|
|
32
|
+
password: text().notNull().hidden(),
|
|
33
|
+
...timestamps(),
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
profiles: table({
|
|
37
|
+
id: id(),
|
|
38
|
+
userId: fk("users").notNull().unique().onDelete("cascade"),
|
|
39
|
+
bio: text(),
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
posts: table({
|
|
43
|
+
id: id(),
|
|
44
|
+
authorId: fk("users").notNull().index().onDelete("restrict"),
|
|
45
|
+
title: text().notNull(),
|
|
46
|
+
body: text().notNull(),
|
|
47
|
+
published: bool().notNull().default(false),
|
|
48
|
+
views: int().notNull().default(0),
|
|
49
|
+
status: enumType(["draft", "published", "archived"]).notNull().default("draft"),
|
|
50
|
+
metadata: jsonb<Record<string, unknown>>(),
|
|
51
|
+
...timestamps(),
|
|
52
|
+
tags: many("tags"),
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
comments: table({
|
|
56
|
+
id: id(),
|
|
57
|
+
postId: fk("posts").notNull().onDelete("cascade"),
|
|
58
|
+
authorId: fk("users").notNull(),
|
|
59
|
+
body: text().notNull(),
|
|
60
|
+
createdAt: timestamps().createdAt,
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
tags: table({
|
|
64
|
+
id: id(),
|
|
65
|
+
slug: text().notNull().unique(),
|
|
66
|
+
name: text().notNull(),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Generate the client, then import it:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { db } from "./neoorm/client.js";
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## CRUD basics
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Create
|
|
81
|
+
const user = await db.users.create({
|
|
82
|
+
data: { email: "alice@example.com", name: "Alice" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Create and return full row
|
|
86
|
+
const post = await db.posts.create({
|
|
87
|
+
data: {
|
|
88
|
+
title: "Hello",
|
|
89
|
+
body: "World",
|
|
90
|
+
authorId: user.id,
|
|
91
|
+
},
|
|
92
|
+
returnCreated: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Find by primary key
|
|
96
|
+
const found = await db.users.findById(user.id);
|
|
97
|
+
|
|
98
|
+
// Find first match
|
|
99
|
+
const draft = await db.posts.findFirst({
|
|
100
|
+
where: { published: false },
|
|
101
|
+
orderBy: { createdAt: "desc" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Update
|
|
105
|
+
await db.users.updateById(user.id, {
|
|
106
|
+
data: { name: "Alice Smith" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Update many
|
|
110
|
+
await db.posts.updateMany({
|
|
111
|
+
where: { published: false },
|
|
112
|
+
data: { views: 0 },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Delete
|
|
116
|
+
await db.posts.delete({ where: { id: post.id } });
|
|
117
|
+
await db.users.deleteById(user.id);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Nested reads (`with`)
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const user = await db.users.findById("user_1", {
|
|
124
|
+
with: {
|
|
125
|
+
profile: true,
|
|
126
|
+
posts: {
|
|
127
|
+
where: { published: true },
|
|
128
|
+
orderBy: { createdAt: "desc" },
|
|
129
|
+
take: 10,
|
|
130
|
+
with: {
|
|
131
|
+
comments: {
|
|
132
|
+
orderBy: { createdAt: "desc" },
|
|
133
|
+
take: 5,
|
|
134
|
+
with: { author: { select: { id: true, name: true } } },
|
|
135
|
+
},
|
|
136
|
+
tags: true,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Select only specific columns:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const users = await db.users.findMany({
|
|
147
|
+
select: { id: true, email: true },
|
|
148
|
+
with: {
|
|
149
|
+
posts: { select: { title: true, published: true } },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Omit columns at query time (still fetched from DB):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const user = await db.users.findById("user_1", {
|
|
158
|
+
omit: { createdAt: true, updatedAt: true },
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Where filters
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// Equality and operators
|
|
166
|
+
const posts = await db.posts.findMany({
|
|
167
|
+
where: {
|
|
168
|
+
published: true,
|
|
169
|
+
views: { gte: 100 },
|
|
170
|
+
title: { contains: "ORM" },
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Case-insensitive search
|
|
175
|
+
const hits = await db.posts.findMany({
|
|
176
|
+
where: { title: { contains: "orm", mode: "insensitive" } },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Logical combinators
|
|
180
|
+
const filtered = await db.posts.findMany({
|
|
181
|
+
where: {
|
|
182
|
+
OR: [
|
|
183
|
+
{ status: "published" },
|
|
184
|
+
{ AND: [{ published: true }, { views: { gt: 50 } }] },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Filter through relations
|
|
190
|
+
const authors = await db.users.findMany({
|
|
191
|
+
where: {
|
|
192
|
+
posts: { some: { published: true, tags: { some: { slug: "typescript" } } } },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// JSONB containment
|
|
197
|
+
const featured = await db.posts.findMany({
|
|
198
|
+
where: {
|
|
199
|
+
metadata: {
|
|
200
|
+
jsonContains: { featured: true, category: "engineering" },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Exists / count helpers
|
|
206
|
+
const taken = await db.users.exists({ where: { email: "alice@example.com" } });
|
|
207
|
+
const total = await db.posts.count({ where: { published: true } });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Relation writes
|
|
211
|
+
|
|
212
|
+
Create with nested relations in one call:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const post = await db.posts.create({
|
|
216
|
+
data: {
|
|
217
|
+
title: "NeoORM",
|
|
218
|
+
body: "Typed relations and JSONB.",
|
|
219
|
+
published: true,
|
|
220
|
+
status: "published",
|
|
221
|
+
author: { connect: { id: "user_1" } },
|
|
222
|
+
tags: {
|
|
223
|
+
connectOrCreate: [
|
|
224
|
+
{
|
|
225
|
+
where: { slug: "orm" },
|
|
226
|
+
create: { slug: "orm", name: "ORM" },
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
with: { author: true, tags: true },
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Update relations on an existing row:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
await db.posts.update({
|
|
239
|
+
where: { id: post.id },
|
|
240
|
+
data: {
|
|
241
|
+
views: { increment: 1 },
|
|
242
|
+
comments: {
|
|
243
|
+
create: [
|
|
244
|
+
{
|
|
245
|
+
body: "Great post!",
|
|
246
|
+
author: { connect: { id: "user_1" } },
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
tags: { set: [{ id: "tag_1" }, { id: "tag_2" }] },
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`connect`, `disconnect`, `set`, `create`, and `delete` work on to-one, one-to-many, and many-to-many relations. See [Relation writes](relations.md).
|
|
256
|
+
|
|
257
|
+
## Pagination
|
|
258
|
+
|
|
259
|
+
Offset/limit:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const page = await db.posts.findMany({
|
|
263
|
+
where: { published: true },
|
|
264
|
+
orderBy: { createdAt: "desc" },
|
|
265
|
+
take: 20,
|
|
266
|
+
skip: 40,
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Keyset (cursor) pagination for feeds:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
let cursor: { createdAt: Date; id: string } | null = null;
|
|
274
|
+
|
|
275
|
+
const page = await db.posts.paginate({
|
|
276
|
+
where: { published: true },
|
|
277
|
+
orderBy: { createdAt: "desc" },
|
|
278
|
+
take: 20,
|
|
279
|
+
...(cursor ? { after: cursor } : {}),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
cursor = page.nextCursor;
|
|
283
|
+
// page.items, page.hasMore, page.hasPrevious
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Aggregations
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const byStatus = await db.posts.groupBy({
|
|
290
|
+
by: ["status"],
|
|
291
|
+
_count: true,
|
|
292
|
+
orderBy: { _count: "desc" },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const stats = await db.posts.aggregate({
|
|
296
|
+
_count: true,
|
|
297
|
+
_avg: { views: true },
|
|
298
|
+
where: { published: true },
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Transactions
|
|
303
|
+
|
|
304
|
+
Batch steps (single transaction):
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const [author, post] = await db.$transaction([
|
|
308
|
+
(tx) =>
|
|
309
|
+
tx.users.create({
|
|
310
|
+
data: { email: "author@example.com", name: "Author" },
|
|
311
|
+
}),
|
|
312
|
+
(tx) =>
|
|
313
|
+
tx.posts.create({
|
|
314
|
+
data: {
|
|
315
|
+
title: "Transactional post",
|
|
316
|
+
body: "...",
|
|
317
|
+
author: { connect: { id: "user_1" } },
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
]);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Interactive callback with rollback:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
await db.$transaction(async (tx) => {
|
|
327
|
+
const user = await tx.users.create({
|
|
328
|
+
data: { email: "x@y.z", name: "Temp" },
|
|
329
|
+
});
|
|
330
|
+
await tx.posts.create({
|
|
331
|
+
data: {
|
|
332
|
+
title: "Draft",
|
|
333
|
+
body: "...",
|
|
334
|
+
author: { connect: { id: user.id } },
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// throw to roll back everything
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Nested savepoints:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
await db.$transaction(async (tx) => {
|
|
345
|
+
await tx.users.create({ data: { email: "outer@x.y", name: "Outer" } });
|
|
346
|
+
|
|
347
|
+
await tx.$transaction(async (nested) => {
|
|
348
|
+
await nested.users.create({ data: { email: "inner@x.y", name: "Inner" } });
|
|
349
|
+
throw new Error("roll back inner only");
|
|
350
|
+
}).catch(() => undefined);
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Raw SQL
|
|
355
|
+
|
|
356
|
+
Use `db.sql` when you need full SQL control. Table names in tagged templates use your schema accessors:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const rows = await db.sql`
|
|
360
|
+
SELECT u.id, u.email, count(p.id) AS post_count
|
|
361
|
+
FROM users u
|
|
362
|
+
LEFT JOIN posts p ON p.author_id = u.id
|
|
363
|
+
GROUP BY u.id, u.email
|
|
364
|
+
ORDER BY post_count DESC
|
|
365
|
+
`;
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## API responses (`strip`)
|
|
369
|
+
|
|
370
|
+
Fetch sensitive fields for auth, then strip before sending JSON:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
const user = await db.users.findFirst({
|
|
374
|
+
where: { email: input.email },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (!user || !verifyPassword(input.password, user.password)) {
|
|
378
|
+
throw new Error("Invalid credentials");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return user.strip(); // removes .hidden() columns (e.g. password)
|
|
382
|
+
return user.strip({ email: true }); // hidden + extra fields
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`.strip()` is non-enumerable and returns a plain object. See [Queries → API responses](queries.md#api-responses-strip).
|
|
386
|
+
|
|
387
|
+
## Explicit junction table (many-to-many)
|
|
388
|
+
|
|
389
|
+
Auto-junction works for most cases. For a custom junction table:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
posts: table({
|
|
393
|
+
id: id(),
|
|
394
|
+
tags: many("tags", {
|
|
395
|
+
through: "postTags",
|
|
396
|
+
leftKey: "postId",
|
|
397
|
+
rightKey: "tagId",
|
|
398
|
+
}),
|
|
399
|
+
}),
|
|
400
|
+
postTags: table("post_tags", {
|
|
401
|
+
postId: fk("posts").primary(),
|
|
402
|
+
tagId: fk("tags").primary(),
|
|
403
|
+
}),
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`through` is the **accessor** (`postTags`), not the SQL name (`post_tags`).
|
|
407
|
+
|
|
408
|
+
## SQLite
|
|
409
|
+
|
|
410
|
+
Same schema and client API — point config at a file:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// neoorm.config.ts
|
|
414
|
+
datasource: {
|
|
415
|
+
provider: "sqlite",
|
|
416
|
+
url: "./dev.db",
|
|
417
|
+
},
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { createNeoOrmClient } from "neoorm";
|
|
422
|
+
import { manifest } from "./neoorm/manifest.js";
|
|
423
|
+
|
|
424
|
+
const db = createNeoOrmClient(manifest, {
|
|
425
|
+
provider: "sqlite",
|
|
426
|
+
databasePath: "./dev.db",
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
See [SQLite](sqlite.md) for type mapping and limitations.
|
|
431
|
+
|
|
432
|
+
## PostGIS
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import "neoorm/plugins/postgis";
|
|
436
|
+
import { geometry, point } from "neoorm/plugins/postgis";
|
|
437
|
+
|
|
438
|
+
places: table({
|
|
439
|
+
id: id(),
|
|
440
|
+
name: text().notNull(),
|
|
441
|
+
location: geometry({ subtype: "Point", srid: 4326 }).notNull(),
|
|
442
|
+
}),
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const nearby = await db.places.findMany({
|
|
447
|
+
where: {
|
|
448
|
+
location: {
|
|
449
|
+
dWithin: {
|
|
450
|
+
geometry: { type: "Point", coordinates: [-122.4, 37.8] },
|
|
451
|
+
distance: 1000,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
See [Plugins](plugins.md).
|
|
459
|
+
|
|
460
|
+
## Schema extras
|
|
461
|
+
|
|
462
|
+
Composite unique constraint and partial index:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
import { fk, id, index, table, text, unique } from "neoorm/schema";
|
|
466
|
+
|
|
467
|
+
posts: table(
|
|
468
|
+
{
|
|
469
|
+
id: id(),
|
|
470
|
+
authorId: fk("users").notNull(),
|
|
471
|
+
title: text().notNull(),
|
|
472
|
+
published: bool().notNull().default(false),
|
|
473
|
+
},
|
|
474
|
+
(t) => [
|
|
475
|
+
unique(t.authorId, t.title),
|
|
476
|
+
index(t.title).where({ published: true }),
|
|
477
|
+
],
|
|
478
|
+
),
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
SQL name override when accessor differs from table name:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
postTags: table("post_tags", {
|
|
485
|
+
postId: fk("posts").primary(),
|
|
486
|
+
tagId: fk("tags").primary(),
|
|
487
|
+
}),
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Next steps
|
|
491
|
+
|
|
492
|
+
| Topic | Doc |
|
|
493
|
+
|-------|-----|
|
|
494
|
+
| Schema DSL | [schema.md](schema.md) |
|
|
495
|
+
| Queries | [queries.md](queries.md) |
|
|
496
|
+
| Relation writes | [relations.md](relations.md) |
|
|
497
|
+
| Migrations | [migrations.md](migrations.md) |
|
|
498
|
+
| CLI | [cli.md](cli.md) |
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Scaffold a new project
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bunx neoorm init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This creates `neoorm.config.ts`, `schema.ts`, and `.env.example`. Run `bunx neoorm migrate dev` after setting your database URL.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cp .env.example .env
|
|
13
|
+
# edit DATABASE_URL in .env
|
|
14
|
+
bunx neoorm migrate dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Query:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { db } from "./neoorm/client.js";
|
|
21
|
+
|
|
22
|
+
const user = await db.users.findById(userId, {
|
|
23
|
+
with: { posts: { where: { published: true }, orderBy: { createdAt: "desc" }, take: 10 } },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Manual setup
|
|
28
|
+
|
|
29
|
+
### 1. Define a schema
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// schema.ts
|
|
33
|
+
import {
|
|
34
|
+
defineSchema,
|
|
35
|
+
fk,
|
|
36
|
+
id,
|
|
37
|
+
table,
|
|
38
|
+
text,
|
|
39
|
+
timestamps,
|
|
40
|
+
uuid,
|
|
41
|
+
} from "neoorm/schema";
|
|
42
|
+
|
|
43
|
+
export const schema = defineSchema({
|
|
44
|
+
users: table({
|
|
45
|
+
id: uuid().primary(),
|
|
46
|
+
email: text().notNull().unique(),
|
|
47
|
+
...timestamps(),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
posts: table({
|
|
51
|
+
id: id(),
|
|
52
|
+
authorId: fk("users").notNull().index(),
|
|
53
|
+
title: text().notNull(),
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `table({ ... })` uses the accessor as the SQL table name.
|
|
59
|
+
- `fk("users")` references the **users accessor** (not a SQL string).
|
|
60
|
+
- Relation names are inferred (`authorId` → `author` on posts, `posts` on users).
|
|
61
|
+
|
|
62
|
+
See [Schema DSL](schema.md).
|
|
63
|
+
|
|
64
|
+
Browse [Examples](examples.md) for copy-paste patterns (CRUD, relations, pagination, transactions, `strip`, and more).
|
|
65
|
+
|
|
66
|
+
### 2. Configure NeoOrm
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// neoorm.config.ts
|
|
70
|
+
import { defineConfig } from "neoorm";
|
|
71
|
+
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
schema: "./schema.ts",
|
|
74
|
+
out: "./neoorm",
|
|
75
|
+
datasource: {
|
|
76
|
+
provider: "postgresql",
|
|
77
|
+
url: process.env.DATABASE_URL!,
|
|
78
|
+
schema: "public",
|
|
79
|
+
enum: "check",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Generate the client
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bunx neoorm generate
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## SQLite
|
|
91
|
+
|
|
92
|
+
Set `provider: "sqlite"` and `url` to a file path or `:memory:`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
datasource: {
|
|
96
|
+
provider: "sqlite",
|
|
97
|
+
url: "./dev.db",
|
|
98
|
+
},
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createNeoOrmClient } from "neoorm";
|
|
103
|
+
import { manifest } from "./neoorm/manifest.js";
|
|
104
|
+
|
|
105
|
+
const db = createNeoOrmClient(manifest, {
|
|
106
|
+
provider: "sqlite",
|
|
107
|
+
databasePath: "./dev.db",
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
See [SQLite](sqlite.md).
|
|
112
|
+
|
|
113
|
+
## Tenant-specific schemas
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const tenantDb = createNeoOrmClient(manifest, {
|
|
117
|
+
connectionString: process.env.DATABASE_URL!,
|
|
118
|
+
schema: "tenant_acme",
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Raw `db.sql` / `db.execute` are not rewritten — qualify tenant schema yourself in raw SQL.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
|
|
5
|
+
| Command | Description |
|
|
6
|
+
|---------|-------------|
|
|
7
|
+
| `neoorm init` | Scaffold `neoorm.config.ts`, `schema.ts`, `.env.example` only (no codegen or migrations; run `neoorm migrate dev` after) |
|
|
8
|
+
| `neoorm generate` | Emit manifest, typed client, models, includes, and migrations |
|
|
9
|
+
| `neoorm migrate dev` | Apply pending migrations, then generate a new one if the schema changed |
|
|
10
|
+
| `neoorm migrate deploy` | Apply pending migrations |
|
|
11
|
+
| `neoorm migrate status` | List applied vs pending migrations |
|
|
12
|
+
| `neoorm migrate down [--steps N]` | Roll back the last N applied migrations (default 1) |
|
|
13
|
+
| `neoorm migrate reset --force` | Drop the `public` schema (PostgreSQL) or all tables (SQLite) and re-apply migrations (local dev) |
|
|
14
|
+
| `neoorm db push` | Push the current snapshot schema to the database |
|
|
15
|
+
| `neoorm db pull` | Introspect the database into a schema file |
|
|
16
|
+
|
|
17
|
+
## Generate outcomes
|
|
18
|
+
|
|
19
|
+
`neoorm generate` always refreshes generated TypeScript files. It prints one of four outcomes:
|
|
20
|
+
|
|
21
|
+
| Outcome | Meaning |
|
|
22
|
+
|---------|---------|
|
|
23
|
+
| Schema unchanged | Snapshot hash matches — no manifest or migration changes |
|
|
24
|
+
| Client regenerated | Manifest changed but no database DDL needed |
|
|
25
|
+
| Migration created | New `migrations/<timestamp>/migration.sql` written |
|
|
26
|
+
| Migration blocked | Destructive or manual changes prevented writing SQL |
|
|
27
|
+
|
|
28
|
+
When migration is blocked, 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:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
neoorm generate --accept-data-loss
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Status
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
neoorm migrate status
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Shows applied migrations (with timestamps), pending folders on disk, and warnings for drift (applied in DB but missing on disk).
|
|
41
|
+
|
|
42
|
+
## Reset
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
neoorm migrate reset --force
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Drops the `public` schema (PostgreSQL) or all tables (SQLite) and re-applies all migrations from disk. Requires `--force`. Use `--skip-apply` to only drop without re-applying.
|
|
49
|
+
|
|
50
|
+
## Rollback
|
|
51
|
+
|
|
52
|
+
`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.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
neoorm migrate down
|
|
56
|
+
neoorm migrate down --steps 2
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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`.
|
|
60
|
+
|
|
61
|
+
Legacy migrations without `down.sql` cannot be rolled back — re-generate the migration or add `down.sql` manually.
|
|
62
|
+
|
|
63
|
+
Migration folders are not deleted on rollback (same as Prisma). Re-run `neoorm migrate deploy` to re-apply rolled-back migrations.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
For a full wipe during local development, use `neoorm migrate reset --force` instead.
|