@bairock/lenz 0.0.18 → 0.0.19
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 +133 -1081
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,618 +5,154 @@ GraphQL SDL → MongoDB ORM — TypeScript-клиент и Apollo Server мод
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
### Schema & Models
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- ✅ **Embedded documents** — Встраиваемые документы через `@embedded`
|
|
13
|
-
- ✅ **Relations** — One-to-One, One-to-Many, Many-to-Many, Many-to-One
|
|
8
|
+
- GraphQL SDL — модели через `type X @model`
|
|
9
|
+
- 20 директив: `@id`, `@unique`, `@index`, `@default`, `@relation`, `@embedded`, `@createdAt`, `@updatedAt`, `@hide`, `@map`, `@ignore`, `@email`, `@url`, `@regex`, `@modelMap`, `@compoundUnique`, `@compoundIndex`, `@compoundId`, `@fulltext`
|
|
10
|
+
- 11 типов: `String`, `Int`, `Float`, `Boolean`, `ID`, `DateTime`, `Date`, `Json`, `ObjectId`, `Bytes`, `BigInt`
|
|
11
|
+
- Enums, embedded documents, relations (1:1, 1:m, m:n, m:1)
|
|
14
12
|
|
|
15
13
|
### Generated ORM Client
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- ✅ **SDL inputTypes** — Все Prisma-style input-типы в формате `gql`
|
|
41
|
-
|
|
42
|
-
### Query Builder (runtime)
|
|
43
|
-
- ✅ **MongoDB query translation** — Конвертация типизированных запросов в MongoDB фильтры
|
|
44
|
-
- ✅ **Автоматическое ObjectId** — `id` → `_id` с авто-конвертацией 24-char hex
|
|
45
|
-
- ✅ **BSON форматирование** — ObjectId → string, Long → bigint, Binary → Buffer
|
|
46
|
-
|
|
47
|
-
### Cluster & Connection
|
|
48
|
-
- ✅ **Connection lifecycle** — `$connect()` / `$disconnect()` с pool management
|
|
49
|
-
- ✅ **Raw access** — `$mongo` (MongoClient), `$db` (Db), `$raw` (collection)
|
|
50
|
-
- ✅ **Логирование** — Настраиваемые уровни: `query`, `info`, `warn`, `error`
|
|
51
|
-
- ✅ **Error handling** — Типизированные ошибки: `NotFoundError`, `UniqueConstraintError`, `ValidationError`, `ConnectionError`, `TransactionError`
|
|
52
|
-
- ✅ **Автоиндексация** — Индексы на FK-полях, unique/text/compound индексы из схемы
|
|
14
|
+
- Полная типизация + `.d.ts`
|
|
15
|
+
- CRUD делегаты: `findUnique`, `findFirst`, `findMany`, `create`, `update`, `upsert`, `delete`, `count`, `aggregate`, `groupBy`
|
|
16
|
+
- Nested operations (create/connect/connectOrCreate/disconnect/set/update/delete/upsert)
|
|
17
|
+
- Все Prisma-фильтры: `equals`, `not`, `in`, `lt`/`lte`/`gt`/`gte`, `contains`, `startsWith`, `endsWith`, `mode: insensitive`
|
|
18
|
+
- `AND`/`OR`/`NOT`, массивы (`has`/`hasEvery`/`hasSome`), full-text (`search`), geo-spatial
|
|
19
|
+
- Offset + cursor пагинация (Relay Connection)
|
|
20
|
+
- Атомарные обновления: `$push`/`$pull`/`$addToSet`/`$pop`/`$pullAll` с `$each`/`$position`, `increment`/`decrement`/`multiply`/`divide`
|
|
21
|
+
- Агрегации: `_count`, `_sum`, `_avg`, `_min`, `_max`, `groupBy`
|
|
22
|
+
- Транзакции (ACID), `$extends` (query interception, computed fields), default-генераторы (`uuid`/`now`/`cuid`/`cuid2`/`ulid`)
|
|
23
|
+
- Валидация (`@email`/`@url`/`@regex` с ReDoS), cascade (`Cascade`/`SetNull`/`Restrict`)
|
|
24
|
+
- Две стратегии загрузки: `populate` (eager) и `lookup` (`$lookup`)
|
|
25
|
+
|
|
26
|
+
### Apollo Server CRUD (`lenz generate crud`)
|
|
27
|
+
- typeDefs + resolvers на каждую модель
|
|
28
|
+
- Резолверы через `{ lenz }` из контекста
|
|
29
|
+
- Баррель `index.ts` — `import { typeDefs, resolvers } from './src'`
|
|
30
|
+
- SDL inputTypes (`*CreateInput`, `*UpdateInput`, все фильтры) в `inputTypes.ts`
|
|
31
|
+
|
|
32
|
+
### Runtime
|
|
33
|
+
- QueryBuilder — конвертация типизированных запросов в MongoDB
|
|
34
|
+
- Авто-ObjectId (`id` → `_id` с 24-char hex)
|
|
35
|
+
- BSON: ObjectId → string, Long → bigint, Binary → Buffer
|
|
36
|
+
- Logger с уровнями `query`/`info`/`warn`/`error`
|
|
37
|
+
- Типизированные ошибки: `NotFoundError`, `UniqueConstraintError`, `ValidationError`, `ConnectionError`, `TransactionError`
|
|
53
38
|
|
|
54
39
|
### CLI
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- ✅ **TypeScript/JavaScript** — Автоопределение языка по конфигу
|
|
59
|
-
|
|
60
|
-
## Сравнение с Prisma
|
|
61
|
-
|
|
62
|
-
Lenz вдохновлён Prisma, но имеет важные отличия, связанные с MongoDB. Ниже — подробное сравнение.
|
|
63
|
-
|
|
64
|
-
### Таблица директив
|
|
65
|
-
|
|
66
|
-
| Prisma атрибут | Lenz директива | Статус |
|
|
67
|
-
|---------------|----------------|--------|
|
|
68
|
-
| `@id` | `@id` | ✅ Полная поддержка (ObjectId) |
|
|
69
|
-
| `@unique` | `@unique` | ✅ Полная поддержка |
|
|
70
|
-
| `@index` | `@index` | ✅ Полная поддержка |
|
|
71
|
-
| `@default(value)` | `@default(value: "...")` | ✅ Полная поддержка |
|
|
72
|
-
| `@default(uuid())` | `@default(generator: "uuid")` | ✅ Эквивалент |
|
|
73
|
-
| `@default(now())` | `@default(generator: "now")` | ✅ Эквивалент |
|
|
74
|
-
| `@default(cuid())` | `@default(generator: "cuid")` | ✅ Базовый CUID |
|
|
75
|
-
| `@default(cuid2())` | `@default(generator: "cuid2")` | ✅ CUID2-совместимый |
|
|
76
|
-
| `@default(autoincrement())` | — | ❌ Не применимо к MongoDB |
|
|
77
|
-
| `@default(dbgenerated())` | — | ❌ Не применимо |
|
|
78
|
-
| `@relation(fields, references)` | `@relation(field)` | ⚠️ Упрощённый синтаксис (одно поле) |
|
|
79
|
-
| `@updatedAt` | `@updatedAt` | ✅ Полная поддержка |
|
|
80
|
-
| `@map` | `@map(name)` | ✅ Полная поддержка |
|
|
81
|
-
| `@@map` | `@modelMap(name)` | ✅ Полная поддержка |
|
|
82
|
-
| `@ignore` | `@ignore` | ✅ Полная поддержка |
|
|
83
|
-
| `@hidden` | `@hide` | ✅ Аналог |
|
|
84
|
-
| `@@unique([a, b])` | `@compoundUnique(fields: [...])` | ✅ Полная поддержка |
|
|
85
|
-
| `@@index([a, b])` | `@compoundIndex(fields: [...])` | ✅ Полная поддержка |
|
|
86
|
-
| `@@id([a, b])` | `@compoundId(fields: [...])` | ✅ Полная поддержка |
|
|
87
|
-
| `@@index([...], type: "text")` | `@fulltext(fields: [...])` | ✅ Эквивалент (MongoDB text index) |
|
|
88
|
-
| `@email` | `@email` | ✅ Полная поддержка |
|
|
89
|
-
| `@url` | `@url` | ✅ Полная поддержка |
|
|
90
|
-
| `@@index(map: "...")` | `@compoundIndex(fields: [...], name: "...")` | ✅ name аргумент |
|
|
91
|
-
| `@id(map: "...")` | `@id(map: "...")` | ✅ Полная поддержка |
|
|
92
|
-
| `@unique(map: "...")` | `@unique(map: "...")` | ✅ Полная поддержка |
|
|
93
|
-
| `@@schema` | — | ❌ Не применимо (multi-schema) |
|
|
94
|
-
| `@@view` | — | ❌ Не реализован |
|
|
95
|
-
| `@relation(onDelete: Restrict)` | `@relation(onDelete: "Restrict")` | ✅ Полная поддержка |
|
|
96
|
-
| `@embedded` (Prisma MongoDB) | `@embedded` | ✅ Полная поддержка |
|
|
97
|
-
| — | `@regex(pattern)` | ✅ Только в Lenz |
|
|
98
|
-
| — | `@@fulltext` | ✅ Только в Lenz (MongoDB text index) |
|
|
99
|
-
|
|
100
|
-
### Возможности
|
|
101
|
-
|
|
102
|
-
| Возможность | Prisma | Lenz |
|
|
103
|
-
|------------|--------|------|
|
|
104
|
-
| **Поддерживаемые БД** | PostgreSQL, MySQL, SQLite, MongoDB, SQL Server, CockroachDB | **MongoDB** |
|
|
105
|
-
| **CLI генерация** | `prisma generate` | `lenz generate` |
|
|
106
|
-
| **Schema DSL** | Prisma Schema Language (.prisma) | GraphQL SDL (.graphql) |
|
|
107
|
-
| **Типизация** | Полная (генерация типов) | Полная (генерация типов) |
|
|
108
|
-
| **CRUD** | ✅ | ✅ |
|
|
109
|
-
| **Relations (1:1, 1:m, m:n)** | ✅ | ✅ |
|
|
110
|
-
| **Pagination (offset + cursor)** | ✅ | ✅ |
|
|
111
|
-
| **Transactions** | ✅ | ✅ (требуется replica set) |
|
|
112
|
-
| **Aggregation** | `groupBy`, `aggregate` | Пайплайн MongoDB + `aggregate` |
|
|
113
|
-
| **Raw queries** | `$queryRaw`, `$executeRaw` | `$raw`, `aggregateRaw` |
|
|
114
|
-
| **Client extensions** | `$extends` | `$extends` |
|
|
115
|
-
| **Middleware** | `$use` (удалён в Prisma 6.14) | ❌ (используйте `$extends`) |
|
|
116
|
-
| **Миграции** | Prisma Migrate | ❌ (ленивое создание коллекций) |
|
|
117
|
-
| **Интроспекция** | `prisma db pull` | ❌ |
|
|
118
|
-
| **Seed** | `prisma db seed` | ❌ |
|
|
119
|
-
| **Studio** | Prisma Studio | ❌ |
|
|
120
|
-
| **Драйверы** | Встроенные + driver adapters | MongoDB Native Driver |
|
|
121
|
-
| **Views** | `@@view` (v5+) | ❌ |
|
|
122
|
-
| **Multi-schema** | `@@schema` (PostgreSQL) | ❌ |
|
|
123
|
-
|
|
124
|
-
### Чего нет в Lenz (но есть в Prisma)
|
|
125
|
-
|
|
126
|
-
1. **Другие БД** — Lenz поддерживает только MongoDB. Prisma работает с PostgreSQL, MySQL, SQLite, SQL Server, CockroachDB и MongoDB.
|
|
127
|
-
2. **Миграции** — нет аналога Prisma Migrate. Коллекции и индексы создаются лениво при первом подключении.
|
|
128
|
-
3. **Интроспекция** — нет `prisma db pull` для импорта схемы из существующей БД.
|
|
129
|
-
4. **Типы данных** — `Decimal`, `Unsupported("...")` не реализованы. `Bytes` и `BigInt` ✅ реализованы.
|
|
130
|
-
5. **Views** — нет поддержки представлений (`@@view`).
|
|
131
|
-
6. **Seed** — нет встроенного фреймворка для наполнения тестовыми данными.
|
|
132
|
-
7. **Studio** — нет графического редактора данных (аналога Prisma Studio).
|
|
133
|
-
8. **Составные внешние ключи** — `@relation` поддерживает только одно поле, не `fields: [a, b], references: [c, d]`.
|
|
134
|
-
|
|
135
|
-
### Что есть в Lenz, чего нет в Prisma
|
|
136
|
-
|
|
137
|
-
1. **`@embedded`** — встроенные документы (MongoDB-специфичная возможность).
|
|
138
|
-
2. **`@hide`** — исключение полей из результатов по умолчанию.
|
|
139
|
-
3. **`@regex(pattern)`** — кастомная валидация через регулярные выражения.
|
|
140
|
-
4. **Стратегии загрузки** — автоматический выбор между `populate` (отдельные запросы) и `lookup` (`$lookup` aggregation) в зависимости от типа связи.
|
|
141
|
-
5. **Гео-пространственные фильтры** — `near`, `nearSphere`, `geoWithin`, `geoIntersects`.
|
|
142
|
-
6. **Атомарные операции с массивами** — `push`, `pull`, `addToSet`, `pop`, `pullAll`, `pushAll`.
|
|
143
|
-
7. **`@fulltext`** — декларативное создание MongoDB text index для полнотекстового поиска.
|
|
144
|
-
8. **Автоматическая инициализация массивов** — обязательные поля-массивы автоматически инициализируются пустым массивом.
|
|
145
|
-
9. **Bytes** — тип `Buffer` для бинарных данных (MongoDB BinData).
|
|
146
|
-
10. **BigInt** — тип `bigint` для 64-битных целых чисел (MongoDB Long).
|
|
147
|
-
|
|
148
|
-
## CRUD Operations
|
|
149
|
-
|
|
150
|
-
Lenz provides a comprehensive set of CRUD operations similar to Prisma, with full TypeScript support and MongoDB-native performance.
|
|
151
|
-
|
|
152
|
-
### Create
|
|
153
|
-
|
|
154
|
-
**Create a single record:**
|
|
155
|
-
```ts
|
|
156
|
-
const user = await lenz.user.create({
|
|
157
|
-
data: {
|
|
158
|
-
email: "elsa@prisma.io",
|
|
159
|
-
name: "Elsa Prisma",
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Create multiple records:**
|
|
165
|
-
```ts
|
|
166
|
-
const createMany = await lenz.user.createMany({
|
|
167
|
-
data: [
|
|
168
|
-
{ name: "Bob", email: "bob@prisma.io" },
|
|
169
|
-
{ name: "Yewande", email: "yewande@prisma.io" },
|
|
170
|
-
],
|
|
171
|
-
});
|
|
172
|
-
// Returns: { count: 2 }
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Read
|
|
176
|
-
|
|
177
|
-
**Get record by ID or unique field:**
|
|
178
|
-
```ts
|
|
179
|
-
// By unique field
|
|
180
|
-
const user = await lenz.user.findUnique({
|
|
181
|
-
where: { email: "elsa@prisma.io" },
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// By ID
|
|
185
|
-
const user = await lenz.user.findUnique({
|
|
186
|
-
where: { id: "99" },
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Get all records:**
|
|
191
|
-
```ts
|
|
192
|
-
const users = await lenz.user.findMany();
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Get first matching record:**
|
|
196
|
-
```ts
|
|
197
|
-
const user = await lenz.user.findFirst({
|
|
198
|
-
where: { posts: { some: { likes: { gt: 100 } } } },
|
|
199
|
-
orderBy: { id: "desc" },
|
|
200
|
-
});
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Filter records:**
|
|
204
|
-
```ts
|
|
205
|
-
// Single field filter
|
|
206
|
-
const users = await lenz.user.findMany({
|
|
207
|
-
where: { email: { endsWith: "prisma.io" } },
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Multiple conditions with OR/AND
|
|
211
|
-
const users = await lenz.user.findMany({
|
|
212
|
-
where: {
|
|
213
|
-
OR: [{ name: { startsWith: "E" } }, { AND: { profileViews: { gt: 0 }, role: "ADMIN" } }],
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Filter by related records
|
|
218
|
-
const users = await lenz.user.findMany({
|
|
219
|
-
where: {
|
|
220
|
-
email: { endsWith: "prisma.io" },
|
|
221
|
-
posts: { some: { published: false } },
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Select fields:**
|
|
227
|
-
```ts
|
|
228
|
-
const user = await lenz.user.findUnique({
|
|
229
|
-
where: { email: "emma@prisma.io" },
|
|
230
|
-
select: { email: true, name: true },
|
|
231
|
-
});
|
|
232
|
-
// Returns: { email: 'emma@prisma.io', name: "Emma" }
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Include related records:**
|
|
236
|
-
```ts
|
|
237
|
-
const users = await lenz.user.findMany({
|
|
238
|
-
where: { role: "ADMIN" },
|
|
239
|
-
include: { posts: true },
|
|
240
|
-
});
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Update
|
|
244
|
-
|
|
245
|
-
**Update a single record:**
|
|
246
|
-
```ts
|
|
247
|
-
const updateUser = await lenz.user.update({
|
|
248
|
-
where: { email: "viola@prisma.io" },
|
|
249
|
-
data: { name: "Viola the Magnificent" },
|
|
250
|
-
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
**Update multiple records:**
|
|
254
|
-
```ts
|
|
255
|
-
const updateUsers = await lenz.user.updateMany({
|
|
256
|
-
where: { email: { contains: "prisma.io" } },
|
|
257
|
-
data: { role: "ADMIN" },
|
|
258
|
-
});
|
|
259
|
-
// Returns: { count: 19 }
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**Upsert (update or create):**
|
|
263
|
-
```ts
|
|
264
|
-
const upsertUser = await lenz.user.upsert({
|
|
265
|
-
where: { email: "viola@prisma.io" },
|
|
266
|
-
update: { name: "Viola the Magnificent" },
|
|
267
|
-
create: { email: "viola@prisma.io", name: "Viola the Magnificent" },
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
**Atomic number operations:**
|
|
272
|
-
```ts
|
|
273
|
-
await lenz.post.updateMany({
|
|
274
|
-
data: {
|
|
275
|
-
views: { increment: 1 },
|
|
276
|
-
likes: { increment: 1 },
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Delete
|
|
282
|
-
|
|
283
|
-
**Delete a single record:**
|
|
284
|
-
```ts
|
|
285
|
-
const deleteUser = await lenz.user.delete({
|
|
286
|
-
where: {
|
|
287
|
-
email: "bert@prisma.io",
|
|
288
|
-
},
|
|
289
|
-
});
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Delete multiple records:**
|
|
293
|
-
```ts
|
|
294
|
-
const deleteUsers = await lenz.user.deleteMany({
|
|
295
|
-
where: {
|
|
296
|
-
email: {
|
|
297
|
-
contains: "prisma.io",
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**Delete all records:**
|
|
304
|
-
```ts
|
|
305
|
-
const deleteUsers = await lenz.user.deleteMany({});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
**Cascading deletes:** Lenz does not automatically cascade deletes. You must manually delete related records or use transactions:
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
const transaction = await lenz.$transaction([
|
|
312
|
-
lenz.post.deleteMany({ where: { authorId: "7" } }),
|
|
313
|
-
lenz.user.delete({ where: { id: "7" } }),
|
|
314
|
-
]);
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Pagination
|
|
318
|
-
|
|
319
|
-
Lenz supports both offset-based and cursor-based pagination directly in the `findMany` method.
|
|
320
|
-
|
|
321
|
-
**Offset-based pagination (skip/take):**
|
|
322
|
-
```ts
|
|
323
|
-
// Get records 41-50 (page 5 with 10 per page)
|
|
324
|
-
const users = await lenz.user.findMany({
|
|
325
|
-
skip: 40,
|
|
326
|
-
take: 10,
|
|
327
|
-
where: { /* your filters */ },
|
|
328
|
-
orderBy: { createdAt: 'desc' }
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Cursor-based pagination (more efficient for large datasets):**
|
|
333
|
-
```ts
|
|
334
|
-
// Get first page (first 10 records)
|
|
335
|
-
const firstPage = await lenz.post.findMany({
|
|
336
|
-
take: 10,
|
|
337
|
-
where: { published: true },
|
|
338
|
-
orderBy: { id: 'asc' },
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const lastPost = firstPage[9]; // Last item on page
|
|
342
|
-
const cursor = lastPost?.id; // Use ID as cursor (string)
|
|
343
|
-
|
|
344
|
-
// Get next page (next 10 records AFTER cursor)
|
|
345
|
-
const nextPage = await lenz.post.findMany({
|
|
346
|
-
take: 10,
|
|
347
|
-
skip: 1, // Skip the cursor item itself to avoid duplication
|
|
348
|
-
cursor: cursor, // Pass cursor as string
|
|
349
|
-
where: { published: true },
|
|
350
|
-
orderBy: { id: 'asc' },
|
|
351
|
-
});
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**Note:** For cursor-based pagination, the cursor should be the value of the field you're ordering by (typically `id`). The cursor is passed as a string or ObjectId. Ensure the field is unique and sequential.
|
|
355
|
-
|
|
356
|
-
### Aggregation
|
|
357
|
-
|
|
358
|
-
Lenz provides direct access to MongoDB aggregation pipeline for complex data analysis, as well as count operations.
|
|
359
|
-
|
|
360
|
-
**Basic aggregation with MongoDB pipeline:**
|
|
361
|
-
```ts
|
|
362
|
-
const aggregations = await lenz.user.aggregate([
|
|
363
|
-
{ $match: { role: "ADMIN" } },
|
|
364
|
-
{ $group: { _id: "$country", totalViews: { $sum: "$profileViews" } } },
|
|
365
|
-
{ $sort: { totalViews: -1 } }
|
|
366
|
-
]);
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**Count operations:**
|
|
370
|
-
```ts
|
|
371
|
-
// Count all users
|
|
372
|
-
const userCount = await lenz.user.count();
|
|
373
|
-
|
|
374
|
-
// Count with filtering
|
|
375
|
-
const activeUsers = await lenz.user.count({
|
|
376
|
-
where: { profileViews: { gte: 100 } },
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Count relations (using _count in select)
|
|
380
|
-
const usersWithPostCount = await lenz.user.findMany({
|
|
381
|
-
select: {
|
|
382
|
-
_count: {
|
|
383
|
-
select: { posts: true },
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
**Aggregation with grouping and filtering:**
|
|
390
|
-
```ts
|
|
391
|
-
// Group posts by category and calculate average likes
|
|
392
|
-
const stats = await lenz.post.aggregate([
|
|
393
|
-
{ $match: { published: true } },
|
|
394
|
-
{ $group: {
|
|
395
|
-
_id: "$categoryId",
|
|
396
|
-
totalPosts: { $sum: 1 },
|
|
397
|
-
avgLikes: { $avg: "$likes" },
|
|
398
|
-
maxLikes: { $max: "$likes" }
|
|
399
|
-
}},
|
|
400
|
-
{ $sort: { avgLikes: -1 } }
|
|
401
|
-
]);
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
**Distinct values:**
|
|
405
|
-
```ts
|
|
406
|
-
// Get distinct roles using aggregation
|
|
407
|
-
const distinctRoles = await lenz.user.aggregate([
|
|
408
|
-
{ $group: { _id: "$role" } },
|
|
409
|
-
{ $project: { role: "$_id" } }
|
|
410
|
-
]);
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
**Note:** The `aggregate()` method accepts raw MongoDB aggregation pipeline stages. For type-safe aggregations, you can define TypeScript interfaces for the aggregation result.
|
|
40
|
+
- `lenz init` — создать проект
|
|
41
|
+
- `lenz generate orm` — сгенерировать ORM-клиент
|
|
42
|
+
- `lenz generate crud` — сгенерировать Apollo Server модули
|
|
414
43
|
|
|
415
44
|
## Quick Start
|
|
416
45
|
|
|
417
|
-
### 1. Install Lenz
|
|
418
|
-
|
|
419
46
|
```bash
|
|
420
|
-
|
|
421
|
-
|
|
47
|
+
# Установка
|
|
48
|
+
npm install @bairock/lenz
|
|
422
49
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
```bash
|
|
50
|
+
# Инициализация проекта
|
|
426
51
|
npx lenz init
|
|
427
|
-
```
|
|
428
52
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
- `lenz/lenz.config.ts` - Configuration file
|
|
433
|
-
- `.env.example` - Environment variables template
|
|
53
|
+
# Настрой схему в lenz/schema.graphql
|
|
54
|
+
# Сгенерируй ORM-клиент
|
|
55
|
+
npx lenz generate orm
|
|
434
56
|
|
|
435
|
-
|
|
57
|
+
# (опционально) Сгенерируй CRUD для Apollo Server
|
|
58
|
+
npx lenz generate crud
|
|
59
|
+
```
|
|
436
60
|
|
|
437
|
-
|
|
61
|
+
Пример схемы (`lenz/schema.graphql`):
|
|
438
62
|
|
|
439
63
|
```graphql
|
|
440
64
|
type User @model {
|
|
441
|
-
id:
|
|
65
|
+
id: String @id
|
|
442
66
|
email: String! @unique
|
|
443
67
|
name: String!
|
|
444
|
-
|
|
445
|
-
# One-to-many relationship (auto-selects lookup strategy)
|
|
446
68
|
posts: [Post!]! @relation(field: "postIds")
|
|
447
|
-
postIds: [ID!]!
|
|
448
|
-
|
|
449
|
-
# One-to-one relationship (auto-selects populate strategy)
|
|
450
|
-
profile: Profile @relation(field: "profileId")
|
|
451
|
-
profileId: ID # Sparse index automatically created
|
|
452
|
-
|
|
69
|
+
postIds: [ID!]!
|
|
453
70
|
createdAt: DateTime! @createdAt
|
|
454
71
|
updatedAt: DateTime! @updatedAt
|
|
455
72
|
}
|
|
456
73
|
|
|
457
74
|
type Post @model {
|
|
458
|
-
id:
|
|
75
|
+
id: String @id
|
|
459
76
|
title: String!
|
|
460
77
|
content: String
|
|
461
|
-
|
|
462
|
-
# Many-to-one relationship (auto-selects populate strategy)
|
|
78
|
+
published: Boolean! @default(value: "false")
|
|
463
79
|
author: User! @relation(field: "authorId")
|
|
464
|
-
authorId: ID!
|
|
465
|
-
|
|
466
|
-
# Many-to-many relationship with ID arrays (auto-selects lookup strategy)
|
|
467
|
-
categories: [Category!]! @relation(field: "categoryIds")
|
|
468
|
-
categoryIds: [ID!]! # Multikey index automatically created
|
|
80
|
+
authorId: ID!
|
|
81
|
+
tags: [String!]
|
|
469
82
|
}
|
|
470
|
-
|
|
471
|
-
type Profile @model {
|
|
472
|
-
id: ID! @id
|
|
473
|
-
bio: String
|
|
474
|
-
user: User @relation(field: "userId", strategy: "populate")
|
|
475
|
-
userId: ID
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
type Category @model {
|
|
479
|
-
id: ID! @id
|
|
480
|
-
name: String! @unique
|
|
481
|
-
posts: [Post!]! @relation(field: "postIds", strategy: "lookup", index: false)
|
|
482
|
-
postIds: [ID!]! # No auto-index (index: false)
|
|
483
|
-
}
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### 4. Generate the client
|
|
487
|
-
|
|
488
|
-
```bash
|
|
489
|
-
npx lenz generate
|
|
490
83
|
```
|
|
491
84
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
### 5. Use in your code
|
|
85
|
+
Использование:
|
|
495
86
|
|
|
496
87
|
```typescript
|
|
497
|
-
import { LenzClient } from '../generated/lenz/client'
|
|
88
|
+
import { LenzClient } from '../generated/lenz/client/index.js'
|
|
498
89
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
url: process.env.MONGODB_URI,
|
|
502
|
-
database: 'myapp',
|
|
503
|
-
log: ['query', 'info'] // Enable query logging
|
|
504
|
-
});
|
|
90
|
+
const lenz = new LenzClient({ url: 'mongodb://localhost:27017/myapp' })
|
|
91
|
+
await lenz.$connect()
|
|
505
92
|
|
|
506
|
-
|
|
93
|
+
// Create
|
|
94
|
+
const user = await lenz.user.create({
|
|
95
|
+
data: { email: 'test@test.com', name: 'Test' },
|
|
96
|
+
include: { posts: true }
|
|
97
|
+
})
|
|
507
98
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
create: [
|
|
515
|
-
{ title: 'Hello World', content: 'My first post' }
|
|
516
|
-
]
|
|
517
|
-
}
|
|
518
|
-
},
|
|
519
|
-
include: {
|
|
520
|
-
posts: true, // Uses populate strategy (separate queries)
|
|
521
|
-
profile: true // Uses populate strategy (one-to-one)
|
|
522
|
-
}
|
|
523
|
-
});
|
|
99
|
+
// Read
|
|
100
|
+
const users = await lenz.user.findMany({
|
|
101
|
+
where: { email: { contains: 'test' } },
|
|
102
|
+
orderBy: { createdAt: 'desc' },
|
|
103
|
+
take: 10
|
|
104
|
+
})
|
|
524
105
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
});
|
|
106
|
+
// Update
|
|
107
|
+
await lenz.user.update({
|
|
108
|
+
where: { id: user.id },
|
|
109
|
+
data: { name: 'Updated' }
|
|
110
|
+
})
|
|
532
111
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const newBook = await lenz.book.create({
|
|
536
|
-
data: {
|
|
537
|
-
title: 'New Book',
|
|
538
|
-
authorId: 'author-id'
|
|
539
|
-
}
|
|
540
|
-
});
|
|
112
|
+
// Delete
|
|
113
|
+
await lenz.user.delete({ where: { id: user.id } })
|
|
541
114
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
});
|
|
115
|
+
// Transaction
|
|
116
|
+
await lenz.$transaction(async (tx) => {
|
|
117
|
+
await lenz.user.update({ where: { id: '1' }, data: { name: 'x' } })
|
|
118
|
+
await lenz.post.create({ data: { title: 'New', authorId: '1' } })
|
|
119
|
+
})
|
|
120
|
+
```
|
|
551
121
|
|
|
552
|
-
|
|
553
|
-
const posts = await lenz.post.findMany({
|
|
554
|
-
where: {
|
|
555
|
-
title: { contains: 'tutorial' },
|
|
556
|
-
categories: {
|
|
557
|
-
some: { name: { equals: 'Programming' } }
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
orderBy: { createdAt: 'desc' },
|
|
561
|
-
take: 10,
|
|
562
|
-
include: {
|
|
563
|
-
author: true, // populate strategy
|
|
564
|
-
categories: true // many-to-many lookup strategy (server-side join with $lookup)
|
|
565
|
-
}
|
|
566
|
-
});
|
|
122
|
+
## Apollo Server Integration
|
|
567
123
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
});
|
|
124
|
+
```typescript
|
|
125
|
+
import { ApolloServer } from '@apollo/server'
|
|
126
|
+
import { startStandaloneServer } from '@apollo/server/standalone'
|
|
127
|
+
import { LenzClient } from '../generated/lenz/client/index.js'
|
|
128
|
+
import { typeDefs, resolvers } from './src/index.js'
|
|
574
129
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
title: 'Transaction Post',
|
|
578
|
-
authorId: user.id
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
130
|
+
const lenz = new LenzClient({ url: process.env.MONGO_URL })
|
|
131
|
+
await lenz.$connect()
|
|
582
132
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
some: {
|
|
589
|
-
name: { equals: 'Technology' }
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
include: {
|
|
594
|
-
categories: {
|
|
595
|
-
where: {
|
|
596
|
-
name: { equals: 'Technology' }
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
orderBy: { createdAt: 'desc' },
|
|
601
|
-
take: 5
|
|
602
|
-
});
|
|
133
|
+
const server = new ApolloServer({ typeDefs, resolvers })
|
|
134
|
+
const { url } = await startStandaloneServer(server, {
|
|
135
|
+
context: async () => ({ lenz }),
|
|
136
|
+
})
|
|
137
|
+
```
|
|
603
138
|
|
|
604
|
-
|
|
605
|
-
// When creating relationships, update both arrays:
|
|
606
|
-
// await lenz.post.update({ where: { id: postId }, data: { categoryIds: { push: categoryId } } });
|
|
607
|
-
// await lenz.category.update({ where: { id: categoryId }, data: { postIds: { push: postId } } });
|
|
139
|
+
## CLI
|
|
608
140
|
|
|
609
|
-
|
|
610
|
-
|
|
141
|
+
```bash
|
|
142
|
+
npx lenz init # Создать проект
|
|
143
|
+
npx lenz generate orm # Сгенерировать ORM-клиент
|
|
144
|
+
npx lenz generate orm --config lenz/lenz.config.ts
|
|
145
|
+
npx lenz generate crud # Сгенерировать CRUD для Apollo
|
|
146
|
+
npx lenz generate crud --schema ./schema.graphql --output ./src
|
|
147
|
+
npx lenz generate # Справка по генерации
|
|
611
148
|
```
|
|
612
149
|
|
|
613
|
-
##
|
|
614
|
-
|
|
615
|
-
### `lenz/lenz.config.ts`
|
|
150
|
+
## Config
|
|
616
151
|
|
|
617
152
|
```typescript
|
|
153
|
+
// lenz/lenz.config.ts
|
|
618
154
|
import 'dotenv/config'
|
|
619
|
-
import { defineConfig } from 'lenz/config'
|
|
155
|
+
import { defineConfig } from '@bairock/lenz/config'
|
|
620
156
|
|
|
621
157
|
export default defineConfig({
|
|
622
158
|
schema: 'schema.graphql',
|
|
@@ -628,544 +164,60 @@ export default defineConfig({
|
|
|
628
164
|
client: {
|
|
629
165
|
output: '../generated/lenz/client',
|
|
630
166
|
},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
})
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### `lenz/lenz.config.js` (for JavaScript projects)
|
|
637
|
-
|
|
638
|
-
For JavaScript projects, use `lenz.config.js` as an ESM module:
|
|
639
|
-
|
|
640
|
-
```javascript
|
|
641
|
-
// ESM module - lenz.config.js
|
|
642
|
-
import 'dotenv/config';
|
|
643
|
-
|
|
644
|
-
export default {
|
|
645
|
-
schema: 'schema.graphql',
|
|
646
|
-
datasource: {
|
|
647
|
-
url: process.env.MONGODB_URI || 'mongodb://localhost:27017',
|
|
648
|
-
database: process.env.MONGODB_DATABASE || 'myapp',
|
|
649
|
-
},
|
|
650
|
-
generate: {
|
|
651
|
-
client: {
|
|
652
|
-
output: '../generated/lenz/client',
|
|
167
|
+
crud: {
|
|
168
|
+
output: './src',
|
|
653
169
|
},
|
|
654
170
|
},
|
|
655
|
-
|
|
656
|
-
};
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
> **Note**: Lenz supports only ESM modules for JavaScript projects. The `lenz init` command automatically detects your project type (TypeScript or JavaScript) and creates the appropriate config file (`lenz.config.ts` for TypeScript, `lenz.config.js` for JavaScript). JavaScript config files must use ESM syntax (`export default`).
|
|
660
|
-
|
|
661
|
-
## GraphQL Directives
|
|
662
|
-
|
|
663
|
-
Lenz extends GraphQL with custom directives:
|
|
664
|
-
|
|
665
|
-
- `@model` - Marks a type as a database model
|
|
666
|
-
- `@id` - Marks a field as primary key (auto-generated ObjectId)
|
|
667
|
-
- `@unique` - Creates a unique index
|
|
668
|
-
- `@index` - Creates a regular index
|
|
669
|
-
- `@default(value: "...")` - Sets default value
|
|
670
|
-
- `@relation(field: "...", strategy: "populate|lookup", index: true|false, onDelete: "NoAction|Cascade|SetNull")` - Defines relation with loading strategy, index control, and cascade delete behavior (default: `NoAction`)
|
|
671
|
-
- `@createdAt` - Auto-sets creation timestamp
|
|
672
|
-
- `@updatedAt` - Auto-updates timestamp
|
|
673
|
-
- `@embedded` - Marks a type as embedded document
|
|
674
|
-
- `@hide` - Excludes field from query results by default
|
|
675
|
-
|
|
676
|
-
## Relations
|
|
677
|
-
|
|
678
|
-
Lenz implements MongoDB-native relations with explicit foreign key fields and intelligent loading strategies. Each relation type has an optimal default strategy for loading related data (see [Relation Loading Strategies](#relation-loading-strategies) for details).
|
|
679
|
-
|
|
680
|
-
**Key principles:**
|
|
681
|
-
- Foreign keys must be in the **source model** (the model containing `@relation`)
|
|
682
|
-
- Arrays are automatically initialized for required array fields
|
|
683
|
-
- Indexes are automatically created for foreign key fields (configurable)
|
|
684
|
-
- Loading strategy is automatically selected based on relation type
|
|
685
|
-
|
|
686
|
-
### Automatic Array Initialization
|
|
687
|
-
|
|
688
|
-
Required array fields (like `bookIds: [ID!]!`) are automatically initialized with empty arrays `[]` when creating documents. This prevents MongoDB `$in needs an array` errors when querying relations.
|
|
689
|
-
|
|
690
|
-
```typescript
|
|
691
|
-
// When creating a document, required arrays are automatically set to []
|
|
692
|
-
const author = await lenz.author.create({
|
|
693
|
-
data: {
|
|
694
|
-
name: 'John Doe',
|
|
695
|
-
// bookIds is automatically set to [] even if not provided
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
### One-to-Many / Many-to-One
|
|
701
|
-
A bidirectional relationship where one side has an array and the other has a single reference:
|
|
702
|
-
|
|
703
|
-
```graphql
|
|
704
|
-
type Author @model {
|
|
705
|
-
id: ID! @id
|
|
706
|
-
books: [Book!]! @relation(field: "bookIds") # one-to-many side, auto: lookup strategy
|
|
707
|
-
bookIds: [ID!]! # array automatically indexed (multikey index)
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
type Book @model {
|
|
711
|
-
id: ID! @id
|
|
712
|
-
author: Author! @relation(field: "authorId") # many-to-one side, auto: populate strategy
|
|
713
|
-
authorId: ID! # single ID automatically indexed (sparse index)
|
|
714
|
-
}
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
- **Author → Book (one-to-many):** Uses `lookup` strategy by default (server-side join). Requires manual synchronization of `bookIds` array.
|
|
718
|
-
- **Book → Author (many-to-one):** Uses `populate` strategy by default (separate query). Foreign key `authorId` has automatic sparse index.
|
|
719
|
-
- **Indexes:** Multikey index on `bookIds`, sparse index on `authorId` (created automatically).
|
|
720
|
-
|
|
721
|
-
### One-to-One (foreign key single ID in source model)
|
|
722
|
-
|
|
723
|
-
```graphql
|
|
724
|
-
type User @model {
|
|
725
|
-
id: ID! @id
|
|
726
|
-
profile: Profile @relation(field: "profileId") # auto: populate strategy
|
|
727
|
-
profileId: ID # optional, sparse index
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
type Profile @model {
|
|
731
|
-
id: ID! @id
|
|
732
|
-
user: User @relation(field: "userId", strategy: "populate") # explicit populate
|
|
733
|
-
userId: ID # optional, sparse index
|
|
734
|
-
}
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
- **Strategy:** Uses `populate` by default (separate queries for each side)
|
|
738
|
-
- **Indexes:** Sparse indexes on `profileId` and `userId` (optional fields)
|
|
739
|
-
- **Optional:** Both sides can be optional (nullable IDs)
|
|
740
|
-
- **Bidirectional:** Each side maintains its own foreign key
|
|
741
|
-
|
|
742
|
-
### Many-to-Many (ID arrays on both sides)
|
|
743
|
-
|
|
744
|
-
```graphql
|
|
745
|
-
type Post @model {
|
|
746
|
-
id: ID! @id
|
|
747
|
-
categories: [Category!]! @relation(field: "categoryIds") # auto: lookup strategy (default for arrays)
|
|
748
|
-
categoryIds: [ID!]! # multikey index
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
type Category @model {
|
|
752
|
-
id: ID! @id
|
|
753
|
-
posts: [Post!]! @relation(field: "postIds", strategy: "lookup", index: false)
|
|
754
|
-
postIds: [ID!]! # no auto-index (index: false)
|
|
755
|
-
}
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
- **Strategy:** Uses `lookup` by default when foreign key arrays are specified (server-side joins with MongoDB's `$lookup` aggregation), `populate` for join collections
|
|
759
|
-
- **Indexes:** Multikey indexes on both array fields (configurable with `index: false`)
|
|
760
|
-
- **Bidirectional:** Both sides maintain arrays of IDs
|
|
761
|
-
- **Manual sync:** You must manually synchronize both arrays when creating/updating relations (for `lookup` strategy)
|
|
762
|
-
|
|
763
|
-
**Example - Manual synchronization for many-to-many lookup strategy:**
|
|
764
|
-
|
|
765
|
-
```typescript
|
|
766
|
-
// Create a post and category
|
|
767
|
-
const post = await lenz.post.create({
|
|
768
|
-
data: {
|
|
769
|
-
title: 'My Post',
|
|
770
|
-
categoryIds: [] // Initialize empty array
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
const category = await lenz.category.create({
|
|
775
|
-
data: {
|
|
776
|
-
name: 'Technology',
|
|
777
|
-
postIds: [] // Initialize empty array
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
// Add category to post
|
|
782
|
-
await lenz.post.update({
|
|
783
|
-
where: { id: post.id },
|
|
784
|
-
data: {
|
|
785
|
-
categoryIds: {
|
|
786
|
-
push: category.id // Add category ID to post's array
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// Add post to category (bidirectional sync)
|
|
792
|
-
await lenz.category.update({
|
|
793
|
-
where: { id: category.id },
|
|
794
|
-
data: {
|
|
795
|
-
postIds: {
|
|
796
|
-
push: post.id // Add post ID to category's array
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
**Important:** Foreign keys must always be in the **source model** (the model containing the `@relation` directive). Classic one-to-many patterns with foreign keys in target models will cause validation errors.
|
|
803
|
-
|
|
804
|
-
## Relation Loading Strategies
|
|
805
|
-
|
|
806
|
-
Lenz automatically chooses the optimal strategy for loading related data, balancing performance and simplicity. You can also explicitly override the strategy.
|
|
807
|
-
|
|
808
|
-
### Populate Strategy (Default for oneToOne, manyToOne)
|
|
809
|
-
|
|
810
|
-
Uses separate queries - first fetches the main document, then queries related collections. Best for simple relationships and sharded environments.
|
|
811
|
-
|
|
812
|
-
```graphql
|
|
813
|
-
type User @model {
|
|
814
|
-
profile: Profile @relation(field: "profileId", strategy: "populate")
|
|
815
|
-
profileId: ID
|
|
816
|
-
}
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
### Lookup Strategy (Default for oneToMany)
|
|
820
|
-
|
|
821
|
-
Uses MongoDB's `$lookup` aggregation operator for server-side joins. Best for high-read scenarios with bidirectional relationships.
|
|
822
|
-
|
|
823
|
-
```graphql
|
|
824
|
-
type Author @model {
|
|
825
|
-
books: [Book!]! @relation(field: "bookIds", strategy: "lookup")
|
|
826
|
-
bookIds: [ID!]!
|
|
827
|
-
}
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
**Note:** When using `lookup` strategy, you must manually synchronize ID arrays (e.g., `bookIds`) when creating/updating/deleting related documents.
|
|
831
|
-
|
|
832
|
-
**Include options support:** Lookup strategy now supports `where`, `orderBy`, `take`, and `skip` options for filtering, sorting, and paginating related documents.
|
|
833
|
-
|
|
834
|
-
Example:
|
|
835
|
-
```typescript
|
|
836
|
-
const author = await lenz.author.findUnique({
|
|
837
|
-
where: { id: 'author-id' },
|
|
838
|
-
include: {
|
|
839
|
-
books: {
|
|
840
|
-
where: { published: true },
|
|
841
|
-
orderBy: { createdAt: 'desc' },
|
|
842
|
-
take: 5
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
### Automatic Strategy Selection
|
|
849
|
-
|
|
850
|
-
| Relation Type | Default Strategy | Reason |
|
|
851
|
-
|--------------|------------------|--------|
|
|
852
|
-
| `oneToOne` | `populate` | Simple relationships, no arrays |
|
|
853
|
-
| `manyToOne` | `populate` | Single reference, no arrays |
|
|
854
|
-
| `oneToMany` | `lookup` | Array of IDs in source document (e.g., Author.bookIds) |
|
|
855
|
-
| `manyToMany` | `lookup` (if foreign key array specified) or `populate` (if join collection) | Foreign key arrays use server-side joins, join collections use separate queries |
|
|
856
|
-
|
|
857
|
-
**Note:** The lookup strategy now supports include options (where, orderBy, take, skip) for both array foreign keys and single foreign key relations. However, nested includes are not yet supported for lookup strategy but are fully supported for populate strategy.
|
|
858
|
-
|
|
859
|
-
**Many-to-Many Lookup Strategy:**
|
|
860
|
-
|
|
861
|
-
When a many-to-many relationship uses foreign key arrays (either single-sided or both sides), Lenz automatically uses the `lookup` strategy with MongoDB's `$lookup` aggregation operator. This performs server-side joins for optimal read performance. For example, `post.categoryIds` array referencing categories.
|
|
862
|
-
|
|
863
|
-
```graphql
|
|
864
|
-
type Post @model {
|
|
865
|
-
categories: [Category!]! @relation(field: "categoryIds", strategy: "lookup")
|
|
866
|
-
categoryIds: [ID!]! # multikey index automatically created
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
type Category @model {
|
|
870
|
-
posts: [Post!]! @relation(field: "postIds", strategy: "lookup", index: false)
|
|
871
|
-
postIds: [ID!]! # no auto-index (index: false)
|
|
872
|
-
}
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
**Many-to-Many Populate Strategy:**
|
|
876
|
-
|
|
877
|
-
When a many-to-many relationship uses a join collection (no foreign key arrays), Lenz uses the `populate` strategy with separate queries.
|
|
878
|
-
|
|
879
|
-
```graphql
|
|
880
|
-
type Post @model {
|
|
881
|
-
categories: [Category!]! @relation # no field parameter → join collection
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
type Category @model {
|
|
885
|
-
posts: [Post!]! @relation
|
|
886
|
-
}
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### Cascade Delete Behavior (`onDelete`)
|
|
890
|
-
|
|
891
|
-
Lenz supports cascade delete operations on relations. By default, **no cascade is performed** (`onDelete: "NoAction"`) for performance and safety. You must explicitly opt in.
|
|
892
|
-
|
|
893
|
-
| Value | Behavior |
|
|
894
|
-
|-------|----------|
|
|
895
|
-
| `NoAction` (default) | No cascade. Deleted document's related references become orphaned. |
|
|
896
|
-
| `Cascade` | When a document is deleted, all related documents are also deleted. |
|
|
897
|
-
| `SetNull` | When a document is deleted, foreign key fields in related documents are set to `null` (only for nullable FK fields). |
|
|
898
|
-
|
|
899
|
-
**Important:** Cascade operations negatively impact write performance because each delete triggers additional queries. Use only when necessary.
|
|
900
|
-
|
|
901
|
-
Examples:
|
|
902
|
-
|
|
903
|
-
```graphql
|
|
904
|
-
type Author @model {
|
|
905
|
-
id: ID! @id
|
|
906
|
-
posts: [Post!]! @relation(field: "postIds", onDelete: "Cascade") # Deletes all posts when author is deleted
|
|
907
|
-
postIds: [ID!]!
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
type Profile @model {
|
|
911
|
-
id: ID! @id
|
|
912
|
-
user: User @relation(field: "userId", onDelete: "SetNull") # Sets userId to null when user is deleted
|
|
913
|
-
userId: ID
|
|
914
|
-
}
|
|
171
|
+
})
|
|
915
172
|
```
|
|
916
173
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
### Automatic Indexing
|
|
920
|
-
|
|
921
|
-
Lenz automatically creates indexes for foreign key fields when `index: true` (default). You can disable this:
|
|
174
|
+
## Generated Structure
|
|
922
175
|
|
|
923
|
-
|
|
924
|
-
type Author @model {
|
|
925
|
-
books: [Book!]! @relation(field: "bookIds", index: false)
|
|
926
|
-
bookIds: [ID!]!
|
|
927
|
-
}
|
|
176
|
+
### ORM-клиент (`lenz generate orm`)
|
|
928
177
|
```
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
## Best Practices with MongoDB
|
|
946
|
-
|
|
947
|
-
### 1. Schema Design
|
|
948
|
-
- **Embed documents** when data is accessed together frequently (use `@embedded` directive)
|
|
949
|
-
- **Reference documents** when data is large or accessed independently
|
|
950
|
-
- **Use arrays judiciously** - large arrays can impact performance
|
|
951
|
-
- **Denormalize carefully** - duplicate data for read performance, but maintain consistency
|
|
952
|
-
|
|
953
|
-
### 2. Index Strategy
|
|
954
|
-
- **Auto-index foreign keys** - Lenz does this by default with `index: true`
|
|
955
|
-
- **Add compound indexes** for frequently queried field combinations
|
|
956
|
-
- **Use sparse indexes** for optional fields (auto-created for optional relations)
|
|
957
|
-
- **Monitor index usage** with `$indexStats`
|
|
958
|
-
|
|
959
|
-
### 3. Performance Optimization
|
|
960
|
-
- **Use `lookup` strategy** for high-read bidirectional relationships
|
|
961
|
-
- **Use `populate` strategy** for simple relationships and sharded clusters
|
|
962
|
-
- **Batch operations** with `createMany`, `updateMany` instead of individual calls
|
|
963
|
-
- **Project only needed fields** with `select` option
|
|
964
|
-
|
|
965
|
-
### 4. Query Patterns
|
|
966
|
-
- **Filter early** - push filters to database with `where` clauses
|
|
967
|
-
- **Avoid large skip/limit** - use cursor-based pagination (`cursor` option)
|
|
968
|
-
- **Use transactions** for multi-document consistency (requires replica set)
|
|
969
|
-
- **Monitor slow queries** with MongoDB profiler
|
|
970
|
-
|
|
971
|
-
### 5. Data Consistency
|
|
972
|
-
- **Manual array sync** - when using `lookup` strategy, maintain ID arrays
|
|
973
|
-
- **Use default values** for required fields to avoid validation errors
|
|
974
|
-
- **Handle race conditions** with optimistic concurrency or transactions
|
|
975
|
-
- **Implement soft deletes** with `deletedAt` field instead of hard deletes
|
|
976
|
-
|
|
977
|
-
### 6. Many-to-Many Performance Optimization
|
|
978
|
-
- **Array size limits:** Keep array sizes reasonable (<1000 elements). Large arrays increase `$lookup` complexity and memory usage.
|
|
979
|
-
- **Batch synchronization:** When updating multiple relationships, batch array operations to reduce database round trips.
|
|
980
|
-
- **Index management:** Regularly monitor and optimize multikey indexes for array fields.
|
|
981
|
-
- **Alternative approaches:** For extremely large many-to-many relationships, consider:
|
|
982
|
-
- **Denormalization:** Embed frequently accessed data
|
|
983
|
-
- **Hybrid approach:** Use `lookup` for recent/active relationships, `populate` for historical
|
|
984
|
-
- **Materialized views:** Pre-compute relationships for read-heavy scenarios
|
|
985
|
-
|
|
986
|
-
### 7. When to Use Each Strategy
|
|
987
|
-
|
|
988
|
-
#### Use **Populate** when:
|
|
989
|
-
- Simple one-to-one or many-to-one relationships
|
|
990
|
-
- Working with sharded clusters (`$lookup` doesn't work across shards)
|
|
991
|
-
- Relationships are rarely accessed
|
|
992
|
-
- You prefer automatic data consistency
|
|
993
|
-
- Many-to-many relationships with join collections (no foreign key arrays)
|
|
994
|
-
- Small datasets where separate queries are acceptable
|
|
995
|
-
|
|
996
|
-
#### Use **Lookup** when:
|
|
997
|
-
- High-read scenarios with one-to-many or many-to-many relationships
|
|
998
|
-
- Need server-side joins for complex filtering/sorting
|
|
999
|
-
- Willing to manually maintain ID arrays
|
|
1000
|
-
- Maximum read performance is critical
|
|
1001
|
-
- Many-to-many relationships with foreign key arrays (both sides)
|
|
1002
|
-
- Medium to large datasets where join performance matters
|
|
1003
|
-
|
|
1004
|
-
#### Special Considerations for Many-to-Many Lookup:
|
|
1005
|
-
- **Array size:** Large arrays (>1000 IDs) can impact `$lookup` performance. Consider denormalization or hybrid approaches.
|
|
1006
|
-
- **Indexing:** Multikey indexes are essential for array fields. Monitor index size and performance.
|
|
1007
|
-
- **Consistency:** Manual array synchronization requires careful application logic to maintain data integrity.
|
|
1008
|
-
- **Sharding:** `$lookup` doesn't work across shards. For sharded clusters, use `populate` strategy or ensure related documents are on the same shard.
|
|
1009
|
-
|
|
1010
|
-
### 8. Production Readiness
|
|
1011
|
-
- **Enable replica set** for transaction support
|
|
1012
|
-
- **Set up proper connection pooling** in `lenz.config.ts`
|
|
1013
|
-
- **Implement retry logic** for transient failures
|
|
1014
|
-
- **Use environment-specific configurations**
|
|
1015
|
-
- **Monitor connection health** with regular `ping` commands
|
|
1016
|
-
|
|
1017
|
-
## CLI Commands
|
|
1018
|
-
|
|
1019
|
-
```bash
|
|
1020
|
-
# Initialize project
|
|
1021
|
-
npx lenz init
|
|
1022
|
-
|
|
1023
|
-
# Generate ORM client
|
|
1024
|
-
npx lenz generate orm
|
|
1025
|
-
|
|
1026
|
-
# Generate ORM client with custom config
|
|
1027
|
-
npx lenz generate orm --config lenz/lenz.config.ts
|
|
1028
|
-
|
|
1029
|
-
# Generate Apollo Server CRUD modules
|
|
1030
|
-
npx lenz generate crud
|
|
1031
|
-
|
|
1032
|
-
# Generate CRUD modules with custom paths
|
|
1033
|
-
npx lenz generate crud --schema lenz/schema.graphql --output ./src
|
|
1034
|
-
|
|
1035
|
-
# Show help
|
|
1036
|
-
npx lenz --help
|
|
1037
|
-
|
|
1038
|
-
# Show generate subcommands
|
|
1039
|
-
npx lenz generate
|
|
178
|
+
generated/lenz/client/
|
|
179
|
+
├── index.ts # Экспорт LenzClient
|
|
180
|
+
├── client.ts # Класс LenzClient
|
|
181
|
+
├── types.ts # TypeScript типы
|
|
182
|
+
├── enums.ts # Enum-константы
|
|
183
|
+
├── inputTypes.ts # SDL input-типы (gql)
|
|
184
|
+
├── models/
|
|
185
|
+
│ ├── index.ts
|
|
186
|
+
│ ├── User.ts # UserDelegate (CRUD)
|
|
187
|
+
│ └── Post.ts
|
|
188
|
+
└── runtime/
|
|
189
|
+
├── query.ts # QueryBuilder
|
|
190
|
+
├── pagination.ts # PaginationHelper
|
|
191
|
+
├── relations.ts # RelationResolver
|
|
192
|
+
├── errors.ts # Runtime ошибки
|
|
193
|
+
└── logger.ts # Logger
|
|
1040
194
|
```
|
|
1041
195
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
```
|
|
1045
|
-
my-app/
|
|
1046
|
-
├── lenz/
|
|
1047
|
-
│ ├── schema.graphql # Your GraphQL schema
|
|
1048
|
-
│ └── lenz.config.ts|js # Configuration (TypeScript or JavaScript ESM)
|
|
1049
|
-
├── generated/
|
|
1050
|
-
│ └── lenz/
|
|
1051
|
-
│ └── client/ # Generated client
|
|
1052
|
-
│ ├── index.ts # Main export
|
|
1053
|
-
│ ├── client.ts # LenzClient class
|
|
1054
|
-
│ ├── types.ts # TypeScript types
|
|
1055
|
-
│ ├── enums.ts # Enum definitions
|
|
1056
|
-
│ ├── inputTypes.ts # GraphQL input types (filters, CreateInput, UpdateInput)
|
|
1057
|
-
│ ├── runtime/ # Runtime utilities
|
|
1058
|
-
│ └── models/ # Model delegates
|
|
1059
|
-
└── .env # Environment variables
|
|
1060
|
-
```
|
|
1061
|
-
|
|
1062
|
-
### `lenz generate crud` — Apollo Server CRUD Modules
|
|
1063
|
-
|
|
1064
|
-
Generates per-model Apollo Server modules (typeDefs + resolvers) from `lenz/schema.graphql`.
|
|
1065
|
-
|
|
1066
|
-
**Опции:**
|
|
1067
|
-
- `-c, --config <path>` — путь к конфиг-файлу (по умолчанию: `lenz/lenz.config.ts`, с автоопределением `lenz/lenz.config.js`)
|
|
1068
|
-
- `-s, --schema <path>` — путь к схеме (по умолчанию: из конфига или `schema.graphql`)
|
|
1069
|
-
- `-o, --output <path>` — выходная директория (по умолчанию: из конфига `generate.crud.output` или `./src`)
|
|
1070
|
-
|
|
1071
|
-
**Определение языка:** генератор определяет TypeScript или JavaScript по расширению конфиг-файла:
|
|
1072
|
-
- `lenz/lenz.config.ts` → генерирует `.ts` файлы
|
|
1073
|
-
- `lenz/lenz.config.js` → генерирует `.js` файлы
|
|
1074
|
-
|
|
1075
|
-
Если конфиг не найден, по умолчанию используются `.ts` файлы.
|
|
1076
|
-
|
|
1077
|
-
**Структура выхода:**
|
|
196
|
+
### CRUD модули (`lenz generate crud`)
|
|
1078
197
|
```
|
|
1079
198
|
src/
|
|
1080
|
-
├── index.ts
|
|
1081
|
-
├──
|
|
199
|
+
├── index.ts # Баррель (typeDefs + resolvers)
|
|
200
|
+
├── User/
|
|
1082
201
|
│ ├── typeDefs/
|
|
1083
|
-
│ │ ├── mutations.ts
|
|
1084
|
-
│ │ └── queries.ts
|
|
202
|
+
│ │ ├── mutations.ts # createUser, updateUser, deleteUser
|
|
203
|
+
│ │ └── queries.ts # findUnique, findFirst, findMany, findManyCount
|
|
1085
204
|
│ ├── resolvers/
|
|
1086
|
-
│ │ ├── mutations.ts
|
|
205
|
+
│ │ ├── mutations.ts # { lenz } из контекста
|
|
1087
206
|
│ │ └── queries.ts
|
|
1088
|
-
│ └── index.ts # Aggregated typeDefs and resolvers
|
|
1089
|
-
├── User/
|
|
1090
|
-
│ ├── ...
|
|
1091
207
|
│ └── index.ts
|
|
1092
|
-
└──
|
|
208
|
+
└── Post/...
|
|
1093
209
|
```
|
|
1094
210
|
|
|
1095
|
-
Баррель `src/index.ts` автоматически импортирует `inputTypes` из ORM-клиента и собирает все модули:
|
|
1096
|
-
|
|
1097
|
-
```typescript
|
|
1098
|
-
// src/index.ts (сгенерировано)
|
|
1099
|
-
import { mergeTypeDefs, mergeResolvers } from '@graphql-tools/merge';
|
|
1100
|
-
import { inputTypes } from '../generated/lenz/client/inputTypes.js';
|
|
1101
|
-
import { categoryTypeDefs, categoryResolvers } from './Category/index.js';
|
|
1102
|
-
import { userTypeDefs, userResolvers } from './User/index.js';
|
|
1103
|
-
|
|
1104
|
-
export const typeDefs = mergeTypeDefs([inputTypes, ...categoryTypeDefs, ...userTypeDefs]);
|
|
1105
|
-
export const resolvers = mergeResolvers([categoryResolvers, userResolvers]);
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
**Пример конфига (опционально):**
|
|
1109
|
-
```js
|
|
1110
|
-
// lenz/lenz.config.js
|
|
1111
|
-
export default {
|
|
1112
|
-
schema: 'lenz/schema.graphql',
|
|
1113
|
-
generate: {
|
|
1114
|
-
crud: {
|
|
1115
|
-
output: './src',
|
|
1116
|
-
},
|
|
1117
|
-
},
|
|
1118
|
-
};
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
**Использование с Apollo Server:**
|
|
1122
|
-
|
|
1123
|
-
Резолверы получают экземпляр `LenzClient` через поле `lenz` GraphQL контекста — без импорта сервисов. Баррель `src/index.ts` уже собирает все typeDefs и resolvers:
|
|
1124
|
-
|
|
1125
|
-
```typescript
|
|
1126
|
-
import { ApolloServer } from '@apollo/server';
|
|
1127
|
-
import { startStandaloneServer } from '@apollo/server/standalone';
|
|
1128
|
-
import { LenzClient } from '../generated/lenz/client/index.js';
|
|
1129
|
-
import { typeDefs, resolvers } from './src/index.js';
|
|
1130
|
-
|
|
1131
|
-
const lenz = new LenzClient({ url: process.env.MONGO_URL });
|
|
1132
|
-
await lenz.$connect();
|
|
1133
|
-
|
|
1134
|
-
const server = new ApolloServer({ typeDefs, resolvers });
|
|
1135
|
-
|
|
1136
|
-
const { url } = await startStandaloneServer(server, {
|
|
1137
|
-
context: async () => ({ lenz }),
|
|
1138
|
-
});
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
> **Важно:** `src/index.ts` автоматически импортирует `inputTypes` из ORM-клиента и объединяет их со всеми CRUD-модулями через `mergeTypeDefs` / `mergeResolvers`. Путь к ORM-клиенту берётся из конфига (`generate.client.output`) или используется `../generated/lenz/client` по умолчанию.
|
|
1142
|
-
|
|
1143
211
|
## Development
|
|
1144
212
|
|
|
1145
|
-
### Build
|
|
1146
|
-
|
|
1147
|
-
```bash
|
|
1148
|
-
npm run build
|
|
1149
|
-
```
|
|
1150
|
-
|
|
1151
|
-
### Watch mode
|
|
1152
|
-
|
|
1153
|
-
```bash
|
|
1154
|
-
npm run dev
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
### Lint
|
|
1158
|
-
|
|
1159
|
-
```bash
|
|
1160
|
-
npm run lint
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### Format
|
|
1164
|
-
|
|
1165
213
|
```bash
|
|
1166
|
-
npm run
|
|
214
|
+
npm run build # tsc
|
|
215
|
+
npm run dev # tsc --watch
|
|
216
|
+
npm test # vitest
|
|
217
|
+
npm run lint # eslint
|
|
218
|
+
npm run format # prettier
|
|
1167
219
|
```
|
|
1168
220
|
|
|
1169
221
|
## License
|
|
1170
222
|
|
|
1171
|
-
MIT
|
|
223
|
+
MIT
|