@bairock/lenz 0.0.5 → 0.0.6

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.
@@ -185,11 +185,11 @@ class CodeGenerator {
185
185
  });
186
186
  }
187
187
  // Generate JavaScript file
188
- let result = `// This file was auto-generated by Lenz. Do not edit manually.
189
- // @generated
190
- // This file provides JavaScript-compatible exports for TypeScript types.
191
- // TypeScript projects should use the .d.ts files for full type information.
192
-
188
+ let result = `// This file was auto-generated by Lenz. Do not edit manually.
189
+ // @generated
190
+ // This file provides JavaScript-compatible exports for TypeScript types.
191
+ // TypeScript projects should use the .d.ts files for full type information.
192
+
193
193
  `;
194
194
  // Add imports (convert import type to import and add .js extensions)
195
195
  const importLines = lines.filter(line => line.includes('import '));
@@ -281,203 +281,203 @@ class CodeGenerator {
281
281
  return result;
282
282
  }
283
283
  generateIndex(clientName) {
284
- return `// This file was auto-generated by Lenz. Do not edit manually.
285
- // @generated
286
-
287
- export { ${clientName} } from './client'
288
- export * from './types'
289
- export * from './enums'
290
-
291
- import { ${clientName} } from './client'
292
-
293
- /**
294
- * Default export for the Lenz client
295
- */
296
- const lenz = new ${clientName}()
297
- export default lenz
284
+ return `// This file was auto-generated by Lenz. Do not edit manually.
285
+ // @generated
286
+
287
+ export { ${clientName} } from './client'
288
+ export * from './types'
289
+ export * from './enums'
290
+
291
+ import { ${clientName} } from './client'
292
+
293
+ /**
294
+ * Default export for the Lenz client
295
+ */
296
+ const lenz = new ${clientName}()
297
+ export default lenz
298
298
  `;
299
299
  }
300
300
  generateClient(clientName, models) {
301
- return `// This file was auto-generated by Lenz. Do not edit manually.
302
- // @generated
303
-
304
- import { MongoClient, Db, ObjectId } from 'mongodb'
305
- import type { LenzConfig } from './types'
306
- import { QueryBuilder } from './runtime/query'
307
- import { RelationResolver } from './runtime/relations'
308
-
309
- ${models.map(model => `
310
- import { ${model.name}Delegate } from './models/${model.name}'`).join('\n')}
311
-
312
- export class ${clientName} {
313
- private mongoClient: MongoClient | null = null
314
- private db: Db | null = null
315
- private config: LenzConfig
316
- private supportsTransactions: boolean = false
317
-
318
- // Model delegates
319
- ${models.map(model => ` public ${this.toCamelCase(model.name)}: ${model.name}Delegate`).join('\n')}
320
-
321
- constructor(config: LenzConfig = {}) {
322
- this.config = {
323
- url: config.url || process.env.MONGODB_URI || 'mongodb://localhost:27017',
324
- database: config.database || 'myapp',
325
- autoCreateCollections: config.autoCreateCollections ?? true,
326
- log: config.log || [],
327
- ...config
328
- }
329
-
330
- // Initialize model delegates
331
- ${models.map(model => ` this.${this.toCamelCase(model.name)} = new ${model.name}Delegate(this)`).join('\n')}
332
- }
333
-
334
- async $connect(): Promise<void> {
335
- if (this.mongoClient) {
336
- return
337
- }
338
-
339
- this.mongoClient = new MongoClient(this.config.url, {
340
- maxPoolSize: this.config.maxPoolSize || 10,
341
- connectTimeoutMS: this.config.connectTimeoutMS || 10000,
342
- socketTimeoutMS: this.config.socketTimeoutMS || 45000
343
- })
344
-
345
- await this.mongoClient.connect()
346
- this.db = this.mongoClient.db(this.config.database)
347
-
348
- // Test connection
349
- await this.db.command({ ping: 1 })
350
-
351
- // Check if MongoDB supports transactions (requires replica set)
352
- try {
353
- const serverInfo = await this.db.admin().serverInfo()
354
- this.supportsTransactions = serverInfo.repl?.replSetName !== undefined
355
-
356
- if (!this.supportsTransactions) {
357
- console.warn('⚠️ MongoDB is running in standalone mode. Transactions will not work.')
358
- console.warn(' Consider setting up a replica set for transaction support.')
359
- console.warn(' Example: mongod --replSet rs0 --port 27017')
360
- }
361
- } catch (error) {
362
- console.warn('⚠️ Could not determine MongoDB deployment type:', error.message)
363
- this.supportsTransactions = false
364
- }
365
-
366
- // Initialize collections and indexes
367
- await this.initializeCollections()
368
-
369
- if (this.config.log?.includes('info')) {
370
- console.log('✅ Connected to MongoDB')
371
- }
372
- }
373
-
374
- async $disconnect(): Promise<void> {
375
- if (this.mongoClient) {
376
- await this.mongoClient.close()
377
- this.mongoClient = null
378
- this.db = null
379
-
380
- if (this.config.log?.includes('info')) {
381
- console.log('👋 Disconnected from MongoDB')
382
- }
383
- }
384
- }
385
-
386
- async $transaction<T>(callback: (tx: any) => Promise<T>): Promise<T> {
387
- if (!this.mongoClient) {
388
- throw new Error('Not connected to database')
389
- }
390
-
391
- // Check if transactions are supported
392
- if (!this.supportsTransactions) {
393
- throw new Error(
394
- 'Transactions are not supported in standalone MongoDB. ' +
395
- 'Set up a replica set or use alternative consistency patterns. ' +
396
- 'During development: mongod --replSet rs0 --port 27017'
397
- )
398
- }
399
-
400
- const session = this.mongoClient.startSession()
401
-
402
- try {
403
- session.startTransaction()
404
- const result = await callback(session)
405
- await session.commitTransaction()
406
- return result
407
- } catch (error) {
408
- await session.abortTransaction()
409
- throw error
410
- } finally {
411
- session.endSession()
412
- }
413
- }
414
-
415
- /**
416
- * Check if the connected MongoDB deployment supports transactions
417
- * Returns false if not connected or if running in standalone mode
418
- */
419
- $supportsTransactions(): boolean {
420
- return this.supportsTransactions
421
- }
422
-
423
- private async initializeCollections(): Promise<void> {
424
- if (!this.db || !this.config.autoCreateCollections) return
425
-
426
- const models = ${JSON.stringify(this.getModelsForRuntime(models), null, 2)}
427
-
428
- for (const model of models) {
429
- // Skip embedded models with empty collection names
430
- if (!model.collectionName) {
431
- continue
432
- }
433
-
434
- const collections = await this.db.listCollections({ name: model.collectionName }).toArray()
435
-
436
- if (collections.length === 0) {
437
- await this.db.createCollection(model.collectionName)
438
-
439
- // Create indexes
440
- if (model.indexes.length > 0) {
441
- const indexes = model.indexes.map(index => ({
442
- key: index.fields.reduce((acc, field) => {
443
- acc[field] = 1
444
- return acc
445
- }, {}),
446
- unique: index.unique,
447
- sparse: index.sparse || false
448
- }))
449
-
450
- try {
451
- await this.db.collection(model.collectionName).createIndexes(indexes)
452
- } catch (error) {
453
- console.warn(\`Failed to create indexes for \${model.name}:\`, error)
454
- }
455
- }
456
- }
457
- }
458
- }
459
-
460
- $isConnected(): boolean {
461
- return this.mongoClient !== null
462
- }
463
-
464
- get $db(): Db {
465
- if (!this.db) {
466
- throw new Error('Database not connected. Call $connect() first.')
467
- }
468
- return this.db
469
- }
470
-
471
- get $mongo(): { client: MongoClient; ObjectId: any } {
472
- if (!this.mongoClient) {
473
- throw new Error('Database not connected. Call $connect() first.')
474
- }
475
- return {
476
- client: this.mongoClient,
477
- ObjectId: ObjectId
478
- }
479
- }
480
- }
301
+ return `// This file was auto-generated by Lenz. Do not edit manually.
302
+ // @generated
303
+
304
+ import { MongoClient, Db, ObjectId } from 'mongodb'
305
+ import type { LenzConfig } from './types'
306
+ import { QueryBuilder } from './runtime/query'
307
+ import { RelationResolver } from './runtime/relations'
308
+
309
+ ${models.map(model => `
310
+ import { ${model.name}Delegate } from './models/${model.name}'`).join('\n')}
311
+
312
+ export class ${clientName} {
313
+ private mongoClient: MongoClient | null = null
314
+ private db: Db | null = null
315
+ private config: LenzConfig
316
+ private supportsTransactions: boolean = false
317
+
318
+ // Model delegates
319
+ ${models.map(model => ` public ${this.toCamelCase(model.name)}: ${model.name}Delegate`).join('\n')}
320
+
321
+ constructor(config: LenzConfig = {}) {
322
+ this.config = {
323
+ url: config.url || process.env.MONGODB_URI || 'mongodb://localhost:27017',
324
+ database: config.database || 'myapp',
325
+ autoCreateCollections: config.autoCreateCollections ?? true,
326
+ log: config.log || [],
327
+ ...config
328
+ }
329
+
330
+ // Initialize model delegates
331
+ ${models.map(model => ` this.${this.toCamelCase(model.name)} = new ${model.name}Delegate(this)`).join('\n')}
332
+ }
333
+
334
+ async $connect(): Promise<void> {
335
+ if (this.mongoClient) {
336
+ return
337
+ }
338
+
339
+ this.mongoClient = new MongoClient(this.config.url, {
340
+ maxPoolSize: this.config.maxPoolSize || 10,
341
+ connectTimeoutMS: this.config.connectTimeoutMS || 10000,
342
+ socketTimeoutMS: this.config.socketTimeoutMS || 45000
343
+ })
344
+
345
+ await this.mongoClient.connect()
346
+ this.db = this.mongoClient.db(this.config.database)
347
+
348
+ // Test connection
349
+ await this.db.command({ ping: 1 })
350
+
351
+ // Check if MongoDB supports transactions (requires replica set)
352
+ try {
353
+ const serverInfo = await this.db.admin().serverInfo()
354
+ this.supportsTransactions = serverInfo.repl?.replSetName !== undefined
355
+
356
+ if (!this.supportsTransactions) {
357
+ console.warn('⚠️ MongoDB is running in standalone mode. Transactions will not work.')
358
+ console.warn(' Consider setting up a replica set for transaction support.')
359
+ console.warn(' Example: mongod --replSet rs0 --port 27017')
360
+ }
361
+ } catch (error) {
362
+ console.warn('⚠️ Could not determine MongoDB deployment type:', error.message)
363
+ this.supportsTransactions = false
364
+ }
365
+
366
+ // Initialize collections and indexes
367
+ await this.initializeCollections()
368
+
369
+ if (this.config.log?.includes('info')) {
370
+ console.log('✅ Connected to MongoDB')
371
+ }
372
+ }
373
+
374
+ async $disconnect(): Promise<void> {
375
+ if (this.mongoClient) {
376
+ await this.mongoClient.close()
377
+ this.mongoClient = null
378
+ this.db = null
379
+
380
+ if (this.config.log?.includes('info')) {
381
+ console.log('👋 Disconnected from MongoDB')
382
+ }
383
+ }
384
+ }
385
+
386
+ async $transaction<T>(callback: (tx: any) => Promise<T>): Promise<T> {
387
+ if (!this.mongoClient) {
388
+ throw new Error('Not connected to database')
389
+ }
390
+
391
+ // Check if transactions are supported
392
+ if (!this.supportsTransactions) {
393
+ throw new Error(
394
+ 'Transactions are not supported in standalone MongoDB. ' +
395
+ 'Set up a replica set or use alternative consistency patterns. ' +
396
+ 'During development: mongod --replSet rs0 --port 27017'
397
+ )
398
+ }
399
+
400
+ const session = this.mongoClient.startSession()
401
+
402
+ try {
403
+ session.startTransaction()
404
+ const result = await callback(session)
405
+ await session.commitTransaction()
406
+ return result
407
+ } catch (error) {
408
+ await session.abortTransaction()
409
+ throw error
410
+ } finally {
411
+ session.endSession()
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Check if the connected MongoDB deployment supports transactions
417
+ * Returns false if not connected or if running in standalone mode
418
+ */
419
+ $supportsTransactions(): boolean {
420
+ return this.supportsTransactions
421
+ }
422
+
423
+ private async initializeCollections(): Promise<void> {
424
+ if (!this.db || !this.config.autoCreateCollections) return
425
+
426
+ const models = ${JSON.stringify(this.getModelsForRuntime(models), null, 2)}
427
+
428
+ for (const model of models) {
429
+ // Skip embedded models with empty collection names
430
+ if (!model.collectionName) {
431
+ continue
432
+ }
433
+
434
+ const collections = await this.db.listCollections({ name: model.collectionName }).toArray()
435
+
436
+ if (collections.length === 0) {
437
+ await this.db.createCollection(model.collectionName)
438
+
439
+ // Create indexes
440
+ if (model.indexes.length > 0) {
441
+ const indexes = model.indexes.map(index => ({
442
+ key: index.fields.reduce((acc, field) => {
443
+ acc[field] = 1
444
+ return acc
445
+ }, {}),
446
+ unique: index.unique,
447
+ sparse: index.sparse || false
448
+ }))
449
+
450
+ try {
451
+ await this.db.collection(model.collectionName).createIndexes(indexes)
452
+ } catch (error) {
453
+ console.warn(\`Failed to create indexes for \${model.name}:\`, error)
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ $isConnected(): boolean {
461
+ return this.mongoClient !== null
462
+ }
463
+
464
+ get $db(): Db {
465
+ if (!this.db) {
466
+ throw new Error('Database not connected. Call $connect() first.')
467
+ }
468
+ return this.db
469
+ }
470
+
471
+ get $mongo(): { client: MongoClient; ObjectId: any } {
472
+ if (!this.mongoClient) {
473
+ throw new Error('Database not connected. Call $connect() first.')
474
+ }
475
+ return {
476
+ client: this.mongoClient,
477
+ ObjectId: ObjectId
478
+ }
479
+ }
480
+ }
481
481
  `;
482
482
  }
483
483
  getModelsForRuntime(models) {
@@ -492,193 +492,193 @@ ${models.map(model => ` this.${this.toCamelCase(model.name)} = new ${model.na
492
492
  }));
493
493
  }
494
494
  generateTypes(models, enums) {
495
- return `// This file was auto-generated by Lenz. Do not edit manually.
496
- // @generated
497
-
498
- import { ObjectId } from 'mongodb'
499
-
500
- // Base types
501
- export type ScalarType = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID' | 'DateTime' | 'Date' | 'Json' | 'ObjectId'
502
- export type RelationType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany'
503
-
504
- // Enum types
505
- ${enums.map(e => `
506
- export enum ${e.name} {
507
- ${e.values.map(v => `${v} = '${v}'`).join(',\n ')}
508
- }`).join('\n\n')}
509
-
510
- // Model types
511
- ${models.map(model => this.generateModelType(model)).join('\n\n')}
512
-
513
- // Pagination types
514
- export interface PageInfo {
515
- hasNextPage: boolean
516
- hasPreviousPage: boolean
517
- startCursor?: string
518
- endCursor?: string
519
- }
520
-
521
- export interface Connection<T> {
522
- edges: Array<Edge<T>>
523
- pageInfo: PageInfo
524
- totalCount: number
525
- }
526
-
527
- export interface Edge<T> {
528
- node: T
529
- cursor: string
530
- }
531
-
532
- // Operation types
533
- export interface WhereInput<T = any> {
534
- id?: string | ObjectId
535
- AND?: WhereInput<T>[]
536
- OR?: WhereInput<T>[]
537
- NOT?: WhereInput<T>[]
538
- [key: string]: any
539
- }
540
-
541
- export interface OrderByInput {
542
- [key: string]: 'asc' | 'desc' | 1 | -1
543
- }
544
-
545
- export interface SelectInput {
546
- [key: string]: boolean | SelectInput
547
- }
548
-
549
- export interface IncludeInput {
550
- [key: string]: boolean | IncludeInput
551
- }
552
-
553
- export interface QueryOptions<T = any> {
554
- where?: WhereInput<T>
555
- select?: SelectInput
556
- include?: IncludeInput
557
- skip?: number
558
- take?: number
559
- orderBy?: OrderByInput | OrderByInput[]
560
- distinct?: string | string[]
561
- /** Cursor for pagination */
562
- cursor?: string | ObjectId
563
- }
564
-
565
- export interface CreateInput<T = any> {
566
- data: Partial<T>
567
- select?: SelectInput
568
- include?: IncludeInput
569
- }
570
-
571
- export interface UpdateInput<T = any> {
572
- where: WhereInput<T>
573
- data: Partial<T>
574
- select?: SelectInput
575
- include?: IncludeInput
576
- }
577
-
578
- export interface DeleteInput<T = any> {
579
- where: WhereInput<T>
580
- select?: SelectInput
581
- include?: IncludeInput
582
- }
583
-
584
- export interface UpsertInput<T = any> {
585
- where: WhereInput<T>
586
- create: Partial<T>
587
- update: Partial<T>
588
- select?: SelectInput
589
- include?: IncludeInput
590
- }
591
-
592
- // Pagination specific interfaces
593
- export interface OffsetPaginationArgs<T = any> extends QueryOptions<T> {
594
- page?: number
595
- perPage?: number
596
- }
597
-
598
- export interface CursorPaginationArgs<T = any> extends QueryOptions<T> {
599
- cursor?: string | ObjectId
600
- take?: number
601
- }
602
-
603
- export interface PaginatedResult<T> {
604
- data: T[]
605
- meta: {
606
- total: number
607
- page: number
608
- perPage: number
609
- totalPages: number
610
- hasNextPage: boolean
611
- hasPreviousPage: boolean
612
- }
613
- }
614
-
615
- export interface CursorPaginatedResult<T> {
616
- edges: Array<{
617
- node: T
618
- cursor: string
619
- }>
620
- pageInfo: {
621
- hasNextPage: boolean
622
- hasPreviousPage: boolean
623
- startCursor?: string
624
- endCursor?: string
625
- }
626
- totalCount: number
627
- }
628
-
629
- // Config types
630
- export interface LenzConfig {
631
- url?: string
632
- database?: string
633
- schemaPath?: string
634
- log?: ('query' | 'error' | 'warn' | 'info')[]
635
- autoCreateCollections?: boolean
636
- maxPoolSize?: number
637
- connectTimeoutMS?: number
638
- socketTimeoutMS?: number
639
- }
640
-
641
- // Utility types
642
- export type DeepPartial<T> = {
643
- [P in keyof T]?: T[P] extends Array<infer U>
644
- ? Array<DeepPartial<U>>
645
- : T[P] extends ReadonlyArray<infer U>
646
- ? ReadonlyArray<DeepPartial<U>>
647
- : DeepPartial<T[P]> | T[P]
648
- }
649
-
650
- export type WithId<T> = T & { id: string }
651
- export type OptionalId<T> = Omit<T, 'id'> & { id?: string }
495
+ return `// This file was auto-generated by Lenz. Do not edit manually.
496
+ // @generated
497
+
498
+ import { ObjectId } from 'mongodb'
499
+
500
+ // Base types
501
+ export type ScalarType = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID' | 'DateTime' | 'Date' | 'Json' | 'ObjectId'
502
+ export type RelationType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany'
503
+
504
+ // Enum types
505
+ ${enums.map(e => `
506
+ export enum ${e.name} {
507
+ ${e.values.map(v => `${v} = '${v}'`).join(',\n ')}
508
+ }`).join('\n\n')}
509
+
510
+ // Model types
511
+ ${models.map(model => this.generateModelType(model)).join('\n\n')}
512
+
513
+ // Pagination types
514
+ export interface PageInfo {
515
+ hasNextPage: boolean
516
+ hasPreviousPage: boolean
517
+ startCursor?: string
518
+ endCursor?: string
519
+ }
520
+
521
+ export interface Connection<T> {
522
+ edges: Array<Edge<T>>
523
+ pageInfo: PageInfo
524
+ totalCount: number
525
+ }
526
+
527
+ export interface Edge<T> {
528
+ node: T
529
+ cursor: string
530
+ }
531
+
532
+ // Operation types
533
+ export interface WhereInput<T = any> {
534
+ id?: string | ObjectId
535
+ AND?: WhereInput<T>[]
536
+ OR?: WhereInput<T>[]
537
+ NOT?: WhereInput<T>[]
538
+ [key: string]: any
539
+ }
540
+
541
+ export interface OrderByInput {
542
+ [key: string]: 'asc' | 'desc' | 1 | -1
543
+ }
544
+
545
+ export interface SelectInput {
546
+ [key: string]: boolean | SelectInput
547
+ }
548
+
549
+ export interface IncludeInput {
550
+ [key: string]: boolean | IncludeInput
551
+ }
552
+
553
+ export interface QueryOptions<T = any> {
554
+ where?: WhereInput<T>
555
+ select?: SelectInput
556
+ include?: IncludeInput
557
+ skip?: number
558
+ take?: number
559
+ orderBy?: OrderByInput | OrderByInput[]
560
+ distinct?: string | string[]
561
+ /** Cursor for pagination */
562
+ cursor?: string | ObjectId
563
+ }
564
+
565
+ export interface CreateInput<T = any> {
566
+ data: Partial<T>
567
+ select?: SelectInput
568
+ include?: IncludeInput
569
+ }
570
+
571
+ export interface UpdateInput<T = any> {
572
+ where: WhereInput<T>
573
+ data: Partial<T>
574
+ select?: SelectInput
575
+ include?: IncludeInput
576
+ }
577
+
578
+ export interface DeleteInput<T = any> {
579
+ where: WhereInput<T>
580
+ select?: SelectInput
581
+ include?: IncludeInput
582
+ }
583
+
584
+ export interface UpsertInput<T = any> {
585
+ where: WhereInput<T>
586
+ create: Partial<T>
587
+ update: Partial<T>
588
+ select?: SelectInput
589
+ include?: IncludeInput
590
+ }
591
+
592
+ // Pagination specific interfaces
593
+ export interface OffsetPaginationArgs<T = any> extends QueryOptions<T> {
594
+ page?: number
595
+ perPage?: number
596
+ }
597
+
598
+ export interface CursorPaginationArgs<T = any> extends QueryOptions<T> {
599
+ cursor?: string | ObjectId
600
+ take?: number
601
+ }
602
+
603
+ export interface PaginatedResult<T> {
604
+ data: T[]
605
+ meta: {
606
+ total: number
607
+ page: number
608
+ perPage: number
609
+ totalPages: number
610
+ hasNextPage: boolean
611
+ hasPreviousPage: boolean
612
+ }
613
+ }
614
+
615
+ export interface CursorPaginatedResult<T> {
616
+ edges: Array<{
617
+ node: T
618
+ cursor: string
619
+ }>
620
+ pageInfo: {
621
+ hasNextPage: boolean
622
+ hasPreviousPage: boolean
623
+ startCursor?: string
624
+ endCursor?: string
625
+ }
626
+ totalCount: number
627
+ }
628
+
629
+ // Config types
630
+ export interface LenzConfig {
631
+ url?: string
632
+ database?: string
633
+ schemaPath?: string
634
+ log?: ('query' | 'error' | 'warn' | 'info')[]
635
+ autoCreateCollections?: boolean
636
+ maxPoolSize?: number
637
+ connectTimeoutMS?: number
638
+ socketTimeoutMS?: number
639
+ }
640
+
641
+ // Utility types
642
+ export type DeepPartial<T> = {
643
+ [P in keyof T]?: T[P] extends Array<infer U>
644
+ ? Array<DeepPartial<U>>
645
+ : T[P] extends ReadonlyArray<infer U>
646
+ ? ReadonlyArray<DeepPartial<U>>
647
+ : DeepPartial<T[P]> | T[P]
648
+ }
649
+
650
+ export type WithId<T> = T & { id: string }
651
+ export type OptionalId<T> = Omit<T, 'id'> & { id?: string }
652
652
  `;
653
653
  }
654
654
  generateModelType(model) {
655
- return `export interface ${model.name} {
656
- id: string
655
+ return `export interface ${model.name} {
656
+ id: string
657
657
  ${model.fields.filter(f => !f.isId).map(field => {
658
658
  const tsType = this.mapToTSType(field.type, field.isArray);
659
659
  return ` ${field.name}${field.isRequired ? '' : '?'}: ${tsType}`;
660
- }).join('\n')}
661
- }
662
-
663
- export interface ${model.name}CreateInput {
660
+ }).join('\n')}
661
+ }
662
+
663
+ export interface ${model.name}CreateInput {
664
664
  ${model.fields.filter(f => !f.isId && !f.directives.includes('@generated')).map(field => {
665
665
  const tsType = this.mapToTSType(field.type, field.isArray);
666
666
  return ` ${field.name}${field.isRequired ? '' : '?'}: ${tsType}`;
667
- }).join('\n')}
668
- }
669
-
670
- export interface ${model.name}UpdateInput {
667
+ }).join('\n')}
668
+ }
669
+
670
+ export interface ${model.name}UpdateInput {
671
671
  ${model.fields.filter(f => !f.isId).map(field => {
672
672
  const tsType = this.mapToTSType(field.type, field.isArray);
673
673
  return ` ${field.name}?: ${tsType}`;
674
- }).join('\n')}
675
- }
676
-
677
- export type ${model.name}WhereInput = WhereInput<${model.name}>
678
- export type ${model.name}QueryOptions = QueryOptions<${model.name}>
679
- export type ${model.name}CreateArgs = CreateInput<${model.name}>
680
- export type ${model.name}UpdateArgs = UpdateInput<${model.name}>
681
- export type ${model.name}DeleteArgs = DeleteInput<${model.name}>
674
+ }).join('\n')}
675
+ }
676
+
677
+ export type ${model.name}WhereInput = WhereInput<${model.name}>
678
+ export type ${model.name}QueryOptions = QueryOptions<${model.name}>
679
+ export type ${model.name}CreateArgs = CreateInput<${model.name}>
680
+ export type ${model.name}UpdateArgs = UpdateInput<${model.name}>
681
+ export type ${model.name}DeleteArgs = DeleteInput<${model.name}>
682
682
  export type ${model.name}UpsertArgs = UpsertInput<${model.name}>`;
683
683
  }
684
684
  mapToTSType(type, isArray) {
@@ -692,493 +692,493 @@ export type ${model.name}UpsertArgs = UpsertInput<${model.name}>`;
692
692
  if (enums.length === 0) {
693
693
  return '// No enums defined in schema\n\nexport {}';
694
694
  }
695
- return `// This file was auto-generated by Lenz. Do not edit manually.
696
- // @generated
697
-
698
- ${enums.map(e => `
699
- export const ${e.name} = {
700
- ${e.values.map(v => ` ${v}: '${v}',`).join('\n')}
701
- } as const
702
-
703
- export type ${e.name} = typeof ${e.name}[keyof typeof ${e.name}]
695
+ return `// This file was auto-generated by Lenz. Do not edit manually.
696
+ // @generated
697
+
698
+ ${enums.map(e => `
699
+ export const ${e.name} = {
700
+ ${e.values.map(v => ` ${v}: '${v}',`).join('\n')}
701
+ } as const
702
+
703
+ export type ${e.name} = typeof ${e.name}[keyof typeof ${e.name}]
704
704
  `).join('\n')}`;
705
705
  }
706
706
  generateRuntimePagination() {
707
- return `// This file was auto-generated by Lenz. Do not edit manually.
708
- // @generated
709
-
710
- import { ObjectId } from 'mongodb'
711
-
712
- /**
713
- * Pagination helper for Lenz ORM
714
- * Implements both offset-based and cursor-based pagination
715
- * Similar to Prisma's pagination patterns
716
- */
717
- export class PaginationHelper {
718
- /**
719
- * Create a cursor from a document
720
- * Uses the document's _id by default
721
- */
722
- static createCursor(doc: any): string {
723
- if (!doc) throw new Error('Cannot create cursor from null document')
724
-
725
- // Use _id if available, otherwise id
726
- const id = doc._id || doc.id
727
- if (!id) throw new Error('Document must have an id to create cursor')
728
-
729
- // Base64 encode for cursor
730
- return Buffer.from(id.toString()).toString('base64')
731
- }
732
-
733
- /**
734
- * Parse cursor to get the id
735
- */
736
- static parseCursor(cursor: string): string {
737
- try {
738
- return Buffer.from(cursor, 'base64').toString('utf8')
739
- } catch (error) {
740
- throw new Error('Invalid cursor format')
741
- }
742
- }
743
-
744
- /**
745
- * Build MongoDB filter for cursor-based pagination
746
- * Assumes ordering by _id unless specified otherwise
747
- */
748
- static buildCursorFilter(
749
- cursor: string,
750
- orderBy: any = { _id: 'asc' },
751
- direction: 'forward' | 'backward' = 'forward'
752
- ): any {
753
- const cursorId = this.parseCursor(cursor)
754
-
755
- // For simplicity, we'll handle single field ordering
756
- const orderField = Object.keys(orderBy)[0] || '_id'
757
- const orderDirection = orderBy[orderField] || 'asc'
758
-
759
- const isAscending = orderDirection === 'asc' || orderDirection === 1
760
- const isForward = direction === 'forward'
761
-
762
- // Build comparison operator based on direction and order
763
- let operator: string
764
- if (isForward) {
765
- operator = isAscending ? '$gt' : '$lt'
766
- } else {
767
- operator = isAscending ? '$lt' : '$gt'
768
- }
769
-
770
- return {
771
- [orderField]: { [operator]: new ObjectId(cursorId) }
772
- }
773
- }
774
-
775
- /**
776
- * Calculate skip for offset pagination
777
- */
778
- static calculateSkip(page: number, perPage: number): number {
779
- if (page < 1) throw new Error('Page must be greater than 0')
780
- return (page - 1) * perPage
781
- }
782
-
783
- /**
784
- * Calculate total pages
785
- */
786
- static calculateTotalPages(total: number, perPage: number): number {
787
- return Math.ceil(total / perPage)
788
- }
789
-
790
- /**
791
- * Get pagination metadata
792
- */
793
- static getPaginationMeta(
794
- total: number,
795
- page: number,
796
- perPage: number,
797
- dataLength: number
798
- ): {
799
- total: number
800
- page: number
801
- perPage: number
802
- totalPages: number
803
- hasNextPage: boolean
804
- hasPreviousPage: boolean
805
- } {
806
- const totalPages = this.calculateTotalPages(total, perPage)
807
-
808
- return {
809
- total,
810
- page,
811
- perPage,
812
- totalPages,
813
- hasNextPage: page < totalPages,
814
- hasPreviousPage: page > 1
815
- }
816
- }
817
-
818
- /**
819
- * Format results for GraphQL-style connection
820
- */
821
- static toConnection<T>(
822
- data: T[],
823
- totalCount: number,
824
- hasNextPage: boolean,
825
- hasPreviousPage: boolean,
826
- createCursor: (item: T) => string = (item: any) => this.createCursor(item)
827
- ): {
828
- edges: Array<{ node: T; cursor: string }>
829
- pageInfo: {
830
- hasNextPage: boolean
831
- hasPreviousPage: boolean
832
- startCursor?: string
833
- endCursor?: string
834
- }
835
- totalCount: number
836
- } {
837
- const edges = data.map(item => ({
838
- node: item,
839
- cursor: createCursor(item)
840
- }))
841
-
842
- return {
843
- edges,
844
- pageInfo: {
845
- hasNextPage,
846
- hasPreviousPage,
847
- startCursor: edges[0]?.cursor,
848
- endCursor: edges[edges.length - 1]?.cursor
849
- },
850
- totalCount
851
- }
852
- }
853
-
854
- /**
855
- * Simple offset pagination
856
- */
857
- static paginate<T>(
858
- data: T[],
859
- page: number = 1,
860
- perPage: number = 10
861
- ): {
862
- data: T[]
863
- meta: {
864
- page: number
865
- perPage: number
866
- total: number
867
- totalPages: number
868
- hasNextPage: boolean
869
- hasPreviousPage: boolean
870
- }
871
- } {
872
- const startIndex = (page - 1) * perPage
873
- const endIndex = startIndex + perPage
874
- const paginatedData = data.slice(startIndex, endIndex)
875
- const total = data.length
876
- const totalPages = Math.ceil(total / perPage)
877
-
878
- return {
879
- data: paginatedData,
880
- meta: {
881
- page,
882
- perPage,
883
- total,
884
- totalPages,
885
- hasNextPage: page < totalPages,
886
- hasPreviousPage: page > 1
887
- }
888
- }
889
- }
890
- }
707
+ return `// This file was auto-generated by Lenz. Do not edit manually.
708
+ // @generated
709
+
710
+ import { ObjectId } from 'mongodb'
711
+
712
+ /**
713
+ * Pagination helper for Lenz ORM
714
+ * Implements both offset-based and cursor-based pagination
715
+ * Similar to Prisma's pagination patterns
716
+ */
717
+ export class PaginationHelper {
718
+ /**
719
+ * Create a cursor from a document
720
+ * Uses the document's _id by default
721
+ */
722
+ static createCursor(doc: any): string {
723
+ if (!doc) throw new Error('Cannot create cursor from null document')
724
+
725
+ // Use _id if available, otherwise id
726
+ const id = doc._id || doc.id
727
+ if (!id) throw new Error('Document must have an id to create cursor')
728
+
729
+ // Base64 encode for cursor
730
+ return Buffer.from(id.toString()).toString('base64')
731
+ }
732
+
733
+ /**
734
+ * Parse cursor to get the id
735
+ */
736
+ static parseCursor(cursor: string): string {
737
+ try {
738
+ return Buffer.from(cursor, 'base64').toString('utf8')
739
+ } catch (error) {
740
+ throw new Error('Invalid cursor format')
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Build MongoDB filter for cursor-based pagination
746
+ * Assumes ordering by _id unless specified otherwise
747
+ */
748
+ static buildCursorFilter(
749
+ cursor: string,
750
+ orderBy: any = { _id: 'asc' },
751
+ direction: 'forward' | 'backward' = 'forward'
752
+ ): any {
753
+ const cursorId = this.parseCursor(cursor)
754
+
755
+ // For simplicity, we'll handle single field ordering
756
+ const orderField = Object.keys(orderBy)[0] || '_id'
757
+ const orderDirection = orderBy[orderField] || 'asc'
758
+
759
+ const isAscending = orderDirection === 'asc' || orderDirection === 1
760
+ const isForward = direction === 'forward'
761
+
762
+ // Build comparison operator based on direction and order
763
+ let operator: string
764
+ if (isForward) {
765
+ operator = isAscending ? '$gt' : '$lt'
766
+ } else {
767
+ operator = isAscending ? '$lt' : '$gt'
768
+ }
769
+
770
+ return {
771
+ [orderField]: { [operator]: new ObjectId(cursorId) }
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Calculate skip for offset pagination
777
+ */
778
+ static calculateSkip(page: number, perPage: number): number {
779
+ if (page < 1) throw new Error('Page must be greater than 0')
780
+ return (page - 1) * perPage
781
+ }
782
+
783
+ /**
784
+ * Calculate total pages
785
+ */
786
+ static calculateTotalPages(total: number, perPage: number): number {
787
+ return Math.ceil(total / perPage)
788
+ }
789
+
790
+ /**
791
+ * Get pagination metadata
792
+ */
793
+ static getPaginationMeta(
794
+ total: number,
795
+ page: number,
796
+ perPage: number,
797
+ dataLength: number
798
+ ): {
799
+ total: number
800
+ page: number
801
+ perPage: number
802
+ totalPages: number
803
+ hasNextPage: boolean
804
+ hasPreviousPage: boolean
805
+ } {
806
+ const totalPages = this.calculateTotalPages(total, perPage)
807
+
808
+ return {
809
+ total,
810
+ page,
811
+ perPage,
812
+ totalPages,
813
+ hasNextPage: page < totalPages,
814
+ hasPreviousPage: page > 1
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Format results for GraphQL-style connection
820
+ */
821
+ static toConnection<T>(
822
+ data: T[],
823
+ totalCount: number,
824
+ hasNextPage: boolean,
825
+ hasPreviousPage: boolean,
826
+ createCursor: (item: T) => string = (item: any) => this.createCursor(item)
827
+ ): {
828
+ edges: Array<{ node: T; cursor: string }>
829
+ pageInfo: {
830
+ hasNextPage: boolean
831
+ hasPreviousPage: boolean
832
+ startCursor?: string
833
+ endCursor?: string
834
+ }
835
+ totalCount: number
836
+ } {
837
+ const edges = data.map(item => ({
838
+ node: item,
839
+ cursor: createCursor(item)
840
+ }))
841
+
842
+ return {
843
+ edges,
844
+ pageInfo: {
845
+ hasNextPage,
846
+ hasPreviousPage,
847
+ startCursor: edges[0]?.cursor,
848
+ endCursor: edges[edges.length - 1]?.cursor
849
+ },
850
+ totalCount
851
+ }
852
+ }
853
+
854
+ /**
855
+ * Simple offset pagination
856
+ */
857
+ static paginate<T>(
858
+ data: T[],
859
+ page: number = 1,
860
+ perPage: number = 10
861
+ ): {
862
+ data: T[]
863
+ meta: {
864
+ page: number
865
+ perPage: number
866
+ total: number
867
+ totalPages: number
868
+ hasNextPage: boolean
869
+ hasPreviousPage: boolean
870
+ }
871
+ } {
872
+ const startIndex = (page - 1) * perPage
873
+ const endIndex = startIndex + perPage
874
+ const paginatedData = data.slice(startIndex, endIndex)
875
+ const total = data.length
876
+ const totalPages = Math.ceil(total / perPage)
877
+
878
+ return {
879
+ data: paginatedData,
880
+ meta: {
881
+ page,
882
+ perPage,
883
+ total,
884
+ totalPages,
885
+ hasNextPage: page < totalPages,
886
+ hasPreviousPage: page > 1
887
+ }
888
+ }
889
+ }
890
+ }
891
891
  `;
892
892
  }
893
893
  generateRuntimeIndex() {
894
- return `// This file was auto-generated by Lenz. Do not edit manually.
895
- // @generated
896
-
897
- export { QueryBuilder } from './query'
898
- export { PaginationHelper } from './pagination'
899
- export { RelationResolver } from './relations'
894
+ return `// This file was auto-generated by Lenz. Do not edit manually.
895
+ // @generated
896
+
897
+ export { QueryBuilder } from './query'
898
+ export { PaginationHelper } from './pagination'
899
+ export { RelationResolver } from './relations'
900
900
  `;
901
901
  }
902
902
  generateRuntimeQuery() {
903
- return `// This file was auto-generated by Lenz. Do not edit manually.
904
- // @generated
905
-
906
- import { ObjectId, Filter, UpdateFilter } from 'mongodb'
907
- import type { WhereInput, QueryOptions, SelectInput } from '../types'
908
- import { PaginationHelper } from './pagination'
909
-
910
- export class QueryBuilder {
911
- static buildWhere<T>(where: WhereInput<T>): Filter<any> {
912
- const filter: Filter<any> = {}
913
-
914
- for (const [key, value] of Object.entries(where || {})) {
915
- if (key === 'id') {
916
- if (typeof value === 'object' && value !== null) {
917
- // Для операторов типа { in: [...] } используем _id
918
- this.applyOperators(filter, '_id', value)
919
- } else {
920
- filter._id = this.normalizeId(value)
921
- }
922
- } else if (typeof value === 'object' && value !== null) {
923
- this.applyOperators(filter, key, value)
924
- } else {
925
- // Оставляем значение как есть для внешних ключей
926
- // (внешние ключи хранятся как строки, не преобразуем в ObjectId)
927
- filter[key] = value
928
- }
929
- }
930
-
931
- return filter
932
- }
933
-
934
- /**
935
- * Build cursor condition for pagination
936
- */
937
- static buildCursorCondition(
938
- cursor: string | ObjectId,
939
- orderBy: any = { _id: 'asc' }
940
- ): Filter<any> {
941
- const cursorId = typeof cursor === 'string'
942
- ? PaginationHelper.parseCursor(cursor)
943
- : cursor.toString()
944
-
945
- // For simple _id ordering
946
- if (orderBy._id || (!Object.keys(orderBy).length)) {
947
- return {
948
- _id: { $gt: new ObjectId(cursorId) }
949
- }
950
- }
951
-
952
- // For other field ordering (simplified implementation)
953
- // In real implementation, you'd need to know the value at the cursor
954
- const orderField = Object.keys(orderBy)[0]
955
- const orderDirection = orderBy[orderField]
956
-
957
- // Note: This is a simplified version
958
- // Full implementation requires fetching the cursor document
959
- return {
960
- [orderField]: orderDirection === 'desc'
961
- ? { $lt: cursorId }
962
- : { $gt: cursorId }
963
- }
964
- }
965
-
966
- /**
967
- * Build MongoDB projection object from select input and hidden fields
968
- */
969
- static buildProjection<T>(
970
- select: SelectInput | undefined,
971
- hiddenFields: string[] = []
972
- ): any {
973
- if (!select) {
974
- // По умолчанию: исключаем скрытые поля
975
- if (hiddenFields.length === 0) return undefined;
976
- const projection: any = {};
977
- hiddenFields.forEach(field => {
978
- projection[field] = 0;
979
- });
980
- return projection;
981
- }
982
-
983
- const projection: any = {};
984
- const processSelect = (sel: SelectInput, prefix = '') => {
985
- for (const [key, value] of Object.entries(sel)) {
986
- const fullPath = prefix ? \`\${prefix}.\${key}\` : key;
987
-
988
- if (typeof value === 'boolean') {
989
- // Базовое поле: true - включать, false - исключать
990
- projection[fullPath] = value ? 1 : 0;
991
- } else {
992
- // Вложенный объект (отношение)
993
- processSelect(value, fullPath);
994
- }
995
- }
996
- };
997
-
998
- processSelect(select);
999
-
1000
- // Убедимся, что скрытые поля исключены, если не указано явно
1001
- hiddenFields.forEach(field => {
1002
- if (projection[field] === undefined) {
1003
- projection[field] = 0;
1004
- }
1005
- });
1006
-
1007
- return projection;
1008
- }
1009
-
1010
- static buildOptions<T>(options: QueryOptions<T>, hiddenFields: string[] = []): any {
1011
- const result: any = {}
1012
-
1013
- if (options.skip !== undefined) result.skip = options.skip
1014
- if (options.take !== undefined) result.limit = options.take
1015
-
1016
- if (options.orderBy) {
1017
- result.sort = this.buildSort(options.orderBy)
1018
- }
1019
-
1020
- // Добавить проекцию, если есть select или скрытые поля
1021
- const projection = this.buildProjection(options.select, hiddenFields)
1022
- if (projection) {
1023
- result.projection = projection
1024
- }
1025
-
1026
- return result
1027
- }
1028
-
1029
- private static buildSort(orderBy: any): any {
1030
- if (Array.isArray(orderBy)) {
1031
- return orderBy.reduce((acc, curr) => ({ ...acc, ...this.buildSort(curr) }), {})
1032
- }
1033
-
1034
- const sort: any = {}
1035
- for (const [field, direction] of Object.entries(orderBy)) {
1036
- if (direction === 'asc' || direction === 1) {
1037
- sort[field] = 1
1038
- } else if (direction === 'desc' || direction === -1) {
1039
- sort[field] = -1
1040
- }
1041
- }
1042
- return sort
1043
- }
1044
-
1045
- private static applyOperators(filter: any, field: string, operators: any): void {
1046
- const mongoOperators: Record<string, string> = {
1047
- equals: '$eq',
1048
- not: '$ne',
1049
- in: '$in',
1050
- notIn: '$nin',
1051
- lt: '$lt',
1052
- lte: '$lte',
1053
- gt: '$gt',
1054
- gte: '$gte',
1055
- contains: '$regex',
1056
- startsWith: '$regex',
1057
- endsWith: '$regex'
1058
- }
1059
-
1060
- for (const [op, value] of Object.entries(operators)) {
1061
- const mongoOp = mongoOperators[op]
1062
-
1063
- if (mongoOp) {
1064
- if (op === 'contains') {
1065
- filter[field] = { $regex: value, $options: 'i' }
1066
- } else if (op === 'startsWith') {
1067
- filter[field] = { $regex: \`^\${value}\`, $options: 'i' }
1068
- } else if (op === 'endsWith') {
1069
- filter[field] = { $regex: \`\${value}\$\`, $options: 'i' }
1070
- } else {
1071
- if (!filter[field]) filter[field] = {}
1072
- // Нормализуем ID только для поля _id (внутренний идентификатор)
1073
- // Внешние ключи хранятся как строки, не преобразуем в ObjectId
1074
- const normalizedValue = field === '_id' ? this.normalizeId(value) : value
1075
- filter[field][mongoOp] = normalizedValue
1076
- }
1077
- }
1078
- }
1079
- }
1080
-
1081
- static normalizeId(id: string | ObjectId | (string | ObjectId)[]): ObjectId | string | (ObjectId | string)[] {
1082
- try {
1083
- if (Array.isArray(id)) {
1084
- return id.map(item => this.normalizeId(item) as ObjectId | string)
1085
- }
1086
- if (typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id)) {
1087
- return new ObjectId(id)
1088
- }
1089
- return id
1090
- } catch {
1091
- return id
1092
- }
1093
- }
1094
-
1095
- static buildUpdate(data: any): UpdateFilter<any> {
1096
- const update: UpdateFilter<any> = {}
1097
-
1098
- const setOperations: any = {}
1099
- const otherOperations: any = {}
1100
-
1101
- for (const [key, value] of Object.entries(data)) {
1102
- if (key.startsWith('$')) {
1103
- otherOperations[key] = value
1104
- } else {
1105
- setOperations[key] = value
1106
- }
1107
- }
1108
-
1109
- if (Object.keys(setOperations).length > 0) {
1110
- update.$set = setOperations
1111
- }
1112
-
1113
- Object.assign(update, otherOperations)
1114
-
1115
- return update
1116
- }
1117
- }
903
+ return `// This file was auto-generated by Lenz. Do not edit manually.
904
+ // @generated
905
+
906
+ import { ObjectId, Filter, UpdateFilter } from 'mongodb'
907
+ import type { WhereInput, QueryOptions, SelectInput } from '../types'
908
+ import { PaginationHelper } from './pagination'
909
+
910
+ export class QueryBuilder {
911
+ static buildWhere<T>(where: WhereInput<T>): Filter<any> {
912
+ const filter: Filter<any> = {}
913
+
914
+ for (const [key, value] of Object.entries(where || {})) {
915
+ if (key === 'id') {
916
+ if (typeof value === 'object' && value !== null) {
917
+ // Для операторов типа { in: [...] } используем _id
918
+ this.applyOperators(filter, '_id', value)
919
+ } else {
920
+ filter._id = this.normalizeId(value)
921
+ }
922
+ } else if (typeof value === 'object' && value !== null) {
923
+ this.applyOperators(filter, key, value)
924
+ } else {
925
+ // Оставляем значение как есть для внешних ключей
926
+ // (внешние ключи хранятся как строки, не преобразуем в ObjectId)
927
+ filter[key] = value
928
+ }
929
+ }
930
+
931
+ return filter
932
+ }
933
+
934
+ /**
935
+ * Build cursor condition for pagination
936
+ */
937
+ static buildCursorCondition(
938
+ cursor: string | ObjectId,
939
+ orderBy: any = { _id: 'asc' }
940
+ ): Filter<any> {
941
+ const cursorId = typeof cursor === 'string'
942
+ ? PaginationHelper.parseCursor(cursor)
943
+ : cursor.toString()
944
+
945
+ // For simple _id ordering
946
+ if (orderBy._id || (!Object.keys(orderBy).length)) {
947
+ return {
948
+ _id: { $gt: new ObjectId(cursorId) }
949
+ }
950
+ }
951
+
952
+ // For other field ordering (simplified implementation)
953
+ // In real implementation, you'd need to know the value at the cursor
954
+ const orderField = Object.keys(orderBy)[0]
955
+ const orderDirection = orderBy[orderField]
956
+
957
+ // Note: This is a simplified version
958
+ // Full implementation requires fetching the cursor document
959
+ return {
960
+ [orderField]: orderDirection === 'desc'
961
+ ? { $lt: cursorId }
962
+ : { $gt: cursorId }
963
+ }
964
+ }
965
+
966
+ /**
967
+ * Build MongoDB projection object from select input and hidden fields
968
+ */
969
+ static buildProjection<T>(
970
+ select: SelectInput | undefined,
971
+ hiddenFields: string[] = []
972
+ ): any {
973
+ if (!select) {
974
+ // По умолчанию: исключаем скрытые поля
975
+ if (hiddenFields.length === 0) return undefined;
976
+ const projection: any = {};
977
+ hiddenFields.forEach(field => {
978
+ projection[field] = 0;
979
+ });
980
+ return projection;
981
+ }
982
+
983
+ const projection: any = {};
984
+ const processSelect = (sel: SelectInput, prefix = '') => {
985
+ for (const [key, value] of Object.entries(sel)) {
986
+ const fullPath = prefix ? \`\${prefix}.\${key}\` : key;
987
+
988
+ if (typeof value === 'boolean') {
989
+ // Базовое поле: true - включать, false - исключать
990
+ projection[fullPath] = value ? 1 : 0;
991
+ } else {
992
+ // Вложенный объект (отношение)
993
+ processSelect(value, fullPath);
994
+ }
995
+ }
996
+ };
997
+
998
+ processSelect(select);
999
+
1000
+ // Убедимся, что скрытые поля исключены, если не указано явно
1001
+ hiddenFields.forEach(field => {
1002
+ if (projection[field] === undefined) {
1003
+ projection[field] = 0;
1004
+ }
1005
+ });
1006
+
1007
+ return projection;
1008
+ }
1009
+
1010
+ static buildOptions<T>(options: QueryOptions<T>, hiddenFields: string[] = []): any {
1011
+ const result: any = {}
1012
+
1013
+ if (options.skip !== undefined) result.skip = options.skip
1014
+ if (options.take !== undefined) result.limit = options.take
1015
+
1016
+ if (options.orderBy) {
1017
+ result.sort = this.buildSort(options.orderBy)
1018
+ }
1019
+
1020
+ // Добавить проекцию, если есть select или скрытые поля
1021
+ const projection = this.buildProjection(options.select, hiddenFields)
1022
+ if (projection) {
1023
+ result.projection = projection
1024
+ }
1025
+
1026
+ return result
1027
+ }
1028
+
1029
+ private static buildSort(orderBy: any): any {
1030
+ if (Array.isArray(orderBy)) {
1031
+ return orderBy.reduce((acc, curr) => ({ ...acc, ...this.buildSort(curr) }), {})
1032
+ }
1033
+
1034
+ const sort: any = {}
1035
+ for (const [field, direction] of Object.entries(orderBy)) {
1036
+ if (direction === 'asc' || direction === 1) {
1037
+ sort[field] = 1
1038
+ } else if (direction === 'desc' || direction === -1) {
1039
+ sort[field] = -1
1040
+ }
1041
+ }
1042
+ return sort
1043
+ }
1044
+
1045
+ private static applyOperators(filter: any, field: string, operators: any): void {
1046
+ const mongoOperators: Record<string, string> = {
1047
+ equals: '$eq',
1048
+ not: '$ne',
1049
+ in: '$in',
1050
+ notIn: '$nin',
1051
+ lt: '$lt',
1052
+ lte: '$lte',
1053
+ gt: '$gt',
1054
+ gte: '$gte',
1055
+ contains: '$regex',
1056
+ startsWith: '$regex',
1057
+ endsWith: '$regex'
1058
+ }
1059
+
1060
+ for (const [op, value] of Object.entries(operators)) {
1061
+ const mongoOp = mongoOperators[op]
1062
+
1063
+ if (mongoOp) {
1064
+ if (op === 'contains') {
1065
+ filter[field] = { $regex: value, $options: 'i' }
1066
+ } else if (op === 'startsWith') {
1067
+ filter[field] = { $regex: \`^\${value}\`, $options: 'i' }
1068
+ } else if (op === 'endsWith') {
1069
+ filter[field] = { $regex: \`\${value}\$\`, $options: 'i' }
1070
+ } else {
1071
+ if (!filter[field]) filter[field] = {}
1072
+ // Нормализуем ID только для поля _id (внутренний идентификатор)
1073
+ // Внешние ключи хранятся как строки, не преобразуем в ObjectId
1074
+ const normalizedValue = field === '_id' ? this.normalizeId(value) : value
1075
+ filter[field][mongoOp] = normalizedValue
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ static normalizeId(id: string | ObjectId | (string | ObjectId)[]): ObjectId | string | (ObjectId | string)[] {
1082
+ try {
1083
+ if (Array.isArray(id)) {
1084
+ return id.map(item => this.normalizeId(item) as ObjectId | string)
1085
+ }
1086
+ if (typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id)) {
1087
+ return new ObjectId(id)
1088
+ }
1089
+ return id
1090
+ } catch {
1091
+ return id
1092
+ }
1093
+ }
1094
+
1095
+ static buildUpdate(data: any): UpdateFilter<any> {
1096
+ const update: UpdateFilter<any> = {}
1097
+
1098
+ const setOperations: any = {}
1099
+ const otherOperations: any = {}
1100
+
1101
+ for (const [key, value] of Object.entries(data)) {
1102
+ if (key.startsWith('$')) {
1103
+ otherOperations[key] = value
1104
+ } else {
1105
+ setOperations[key] = value
1106
+ }
1107
+ }
1108
+
1109
+ if (Object.keys(setOperations).length > 0) {
1110
+ update.$set = setOperations
1111
+ }
1112
+
1113
+ Object.assign(update, otherOperations)
1114
+
1115
+ return update
1116
+ }
1117
+ }
1118
1118
  `;
1119
1119
  }
1120
1120
  generateRuntimeRelations() {
1121
- return `// This file was auto-generated by Lenz. Do not edit manually.
1122
- // @generated
1123
-
1124
- import { Db, ObjectId } from 'mongodb'
1125
-
1126
- export class RelationResolver {
1127
- static async resolveOneToOne(
1128
- db: Db,
1129
- sourceCollection: string,
1130
- targetCollection: string,
1131
- sourceId: string,
1132
- foreignKey: string
1133
- ): Promise<any> {
1134
- const collection = db.collection(targetCollection)
1135
- return await collection.findOne({ [foreignKey]: sourceId })
1136
- }
1137
-
1138
- static async resolveOneToMany(
1139
- db: Db,
1140
- sourceCollection: string,
1141
- targetCollection: string,
1142
- sourceId: string,
1143
- foreignKey: string
1144
- ): Promise<any[]> {
1145
- const collection = db.collection(targetCollection)
1146
- return await collection.find({ [foreignKey]: sourceId }).toArray()
1147
- }
1148
-
1149
- static async resolveManyToMany(
1150
- db: Db,
1151
- sourceCollection: string,
1152
- targetCollection: string,
1153
- joinCollection: string,
1154
- sourceId: string
1155
- ): Promise<any[]> {
1156
- const joinCol = db.collection(joinCollection)
1157
- const targetCol = db.collection(targetCollection)
1158
-
1159
- const connections = await joinCol.find({
1160
- [\`\${sourceCollection.toLowerCase()}Id\`]: sourceId
1161
- }).toArray()
1162
-
1163
- const targetIds = connections.map(c => c[\`\${targetCollection.toLowerCase()}Id\`])
1164
-
1165
- return await targetCol.find({
1166
- _id: { $in: targetIds.map(id => new ObjectId(id)) }
1167
- }).toArray()
1168
- }
1169
-
1170
- static formatDocument(doc: any): any {
1171
- if (!doc) return doc
1172
-
1173
- const formatted = { ...doc }
1174
- if (formatted._id) {
1175
- formatted.id = formatted._id.toString()
1176
- delete formatted._id
1177
- }
1178
-
1179
- return formatted
1180
- }
1181
- }
1121
+ return `// This file was auto-generated by Lenz. Do not edit manually.
1122
+ // @generated
1123
+
1124
+ import { Db, ObjectId } from 'mongodb'
1125
+
1126
+ export class RelationResolver {
1127
+ static async resolveOneToOne(
1128
+ db: Db,
1129
+ sourceCollection: string,
1130
+ targetCollection: string,
1131
+ sourceId: string,
1132
+ foreignKey: string
1133
+ ): Promise<any> {
1134
+ const collection = db.collection(targetCollection)
1135
+ return await collection.findOne({ [foreignKey]: sourceId })
1136
+ }
1137
+
1138
+ static async resolveOneToMany(
1139
+ db: Db,
1140
+ sourceCollection: string,
1141
+ targetCollection: string,
1142
+ sourceId: string,
1143
+ foreignKey: string
1144
+ ): Promise<any[]> {
1145
+ const collection = db.collection(targetCollection)
1146
+ return await collection.find({ [foreignKey]: sourceId }).toArray()
1147
+ }
1148
+
1149
+ static async resolveManyToMany(
1150
+ db: Db,
1151
+ sourceCollection: string,
1152
+ targetCollection: string,
1153
+ joinCollection: string,
1154
+ sourceId: string
1155
+ ): Promise<any[]> {
1156
+ const joinCol = db.collection(joinCollection)
1157
+ const targetCol = db.collection(targetCollection)
1158
+
1159
+ const connections = await joinCol.find({
1160
+ [\`\${sourceCollection.toLowerCase()}Id\`]: sourceId
1161
+ }).toArray()
1162
+
1163
+ const targetIds = connections.map(c => c[\`\${targetCollection.toLowerCase()}Id\`])
1164
+
1165
+ return await targetCol.find({
1166
+ _id: { $in: targetIds.map(id => new ObjectId(id)) }
1167
+ }).toArray()
1168
+ }
1169
+
1170
+ static formatDocument(doc: any): any {
1171
+ if (!doc) return doc
1172
+
1173
+ const formatted = { ...doc }
1174
+ if (formatted._id) {
1175
+ formatted.id = formatted._id.toString()
1176
+ delete formatted._id
1177
+ }
1178
+
1179
+ return formatted
1180
+ }
1181
+ }
1182
1182
  `;
1183
1183
  }
1184
1184
  generateModelsIndex(models) {
@@ -1192,431 +1192,431 @@ export class RelationResolver {
1192
1192
  return files;
1193
1193
  }
1194
1194
  generateModelDelegate(model) {
1195
- return `// This file was auto-generated by Lenz. Do not edit manually.
1196
- // @generated
1197
-
1198
- import { Collection, ObjectId, Document } from 'mongodb'
1199
- import type {
1200
- ${model.name},
1201
- ${model.name}CreateInput,
1202
- ${model.name}UpdateInput,
1203
- ${model.name}WhereInput,
1204
- ${model.name}QueryOptions,
1205
- ${model.name}CreateArgs,
1206
- ${model.name}UpdateArgs,
1207
- ${model.name}DeleteArgs,
1208
- ${model.name}UpsertArgs,
1209
- PaginatedResult,
1210
- CursorPaginatedResult,
1211
- OffsetPaginationArgs,
1212
- CursorPaginationArgs
1213
- } from '../types'
1214
- import { QueryBuilder } from '../runtime/query'
1215
- import { PaginationHelper } from '../runtime/pagination'
1216
- import { RelationResolver } from '../runtime/relations'
1217
- import type { LenzClient } from '../client'
1218
-
1219
- export class ${model.name}Delegate {
1220
- constructor(private client: LenzClient) {}
1221
-
1222
- private readonly hiddenFields: string[] = ${JSON.stringify(model.fields.filter(f => f.isHidden).map(f => f.name))};
1223
-
1224
- private get collection(): Collection<Document> {
1225
- return this.client.$db.collection('${model.collectionName}')
1226
- }
1227
-
1228
- async findUnique(args: { where: ${model.name}WhereInput } & ${model.name}QueryOptions): Promise<${model.name} | null> {
1229
- const query = QueryBuilder.buildWhere(args.where)
1230
- const options = QueryBuilder.buildOptions(args, this.hiddenFields)
1231
-
1232
- const doc = await this.collection.findOne(query, options)
1233
- if (!doc) return null
1234
-
1235
- const formatted = RelationResolver.formatDocument(doc)
1236
-
1237
- if (args.include) {
1238
- return await this.includeRelations(formatted, args.include)
1239
- }
1240
-
1241
- return formatted
1242
- }
1243
-
1244
- async findMany(args?: ${model.name}QueryOptions): Promise<${model.name}[]> {
1245
- const { cursor, ...otherArgs } = args || {}
1246
- let where = args?.where || {}
1247
-
1248
- // Handle cursor-based pagination
1249
- if (cursor) {
1250
- const cursorCondition = QueryBuilder.buildCursorCondition(cursor, args?.orderBy)
1251
- where = {
1252
- ...where,
1253
- ...cursorCondition
1254
- }
1255
- }
1256
-
1257
- const query = QueryBuilder.buildWhere(where)
1258
- const options = QueryBuilder.buildOptions(otherArgs || {}, this.hiddenFields)
1259
-
1260
- const mongoCursor = this.collection.find(query, options)
1261
- const docs = await mongoCursor.toArray()
1262
- const formatted = docs.map(RelationResolver.formatDocument)
1263
-
1264
- if (args?.include) {
1265
- return await Promise.all(
1266
- formatted.map(doc => this.includeRelations(doc, args.include!))
1267
- )
1268
- }
1269
-
1270
- return formatted
1271
- }
1272
-
1273
- async findFirst(args?: ${model.name}QueryOptions): Promise<${model.name} | null> {
1274
- const results = await this.findMany({ ...args, take: 1 })
1275
- return results[0] || null
1276
- }
1277
-
1278
- async create(args: ${model.name}CreateArgs): Promise<${model.name}> {
1279
- const now = new Date()
1280
- const document = {
1281
- ...args.data,
1282
- _id: new ObjectId(),
1283
- createdAt: now,
1284
- updatedAt: now
1285
- }
1286
-
1287
- const result = await this.collection.insertOne(document)
1288
- const createdDoc = await this.collection.findOne({ _id: result.insertedId })
1289
-
1290
- if (!createdDoc) {
1291
- throw new Error('Failed to create document')
1292
- }
1293
-
1294
- const formatted = RelationResolver.formatDocument(createdDoc)
1295
-
1296
- if (args.include) {
1297
- return await this.includeRelations(formatted, args.include)
1298
- }
1299
-
1300
- return formatted
1301
- }
1302
-
1303
- async createMany(args: { data: ${model.name}CreateInput[] }): Promise<{ count: number }> {
1304
- const now = new Date()
1305
- const documents = args.data.map(data => ({
1306
- ...data,
1307
- _id: new ObjectId(),
1308
- createdAt: now,
1309
- updatedAt: now
1310
- }))
1311
-
1312
- const result = await this.collection.insertMany(documents)
1313
- return { count: result.insertedCount }
1314
- }
1315
-
1316
- async update(args: ${model.name}UpdateArgs): Promise<${model.name}> {
1317
- const query = QueryBuilder.buildWhere(args.where)
1318
- const updateData = {
1319
- ...args.data,
1320
- updatedAt: new Date()
1321
- }
1322
-
1323
- const update = QueryBuilder.buildUpdate(updateData)
1324
- const result = await this.collection.findOneAndUpdate(
1325
- query,
1326
- update,
1327
- { returnDocument: 'after' }
1328
- )
1329
-
1330
- const updatedDoc = result.value || result
1331
- if (!updatedDoc) {
1332
- throw new Error('Document not found')
1333
- }
1334
-
1335
- const formatted = RelationResolver.formatDocument(updatedDoc)
1336
-
1337
- if (args.include) {
1338
- return await this.includeRelations(formatted, args.include)
1339
- }
1340
-
1341
- return formatted
1342
- }
1343
-
1344
- async updateMany(args: { where?: ${model.name}WhereInput; data: ${model.name}UpdateInput }): Promise<{ count: number }> {
1345
- const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1346
- const updateData = {
1347
- ...args.data,
1348
- updatedAt: new Date()
1349
- }
1350
-
1351
- const update = QueryBuilder.buildUpdate(updateData)
1352
- const result = await this.collection.updateMany(query, update)
1353
- return { count: result.modifiedCount }
1354
- }
1355
-
1356
- async upsert(args: ${model.name}UpsertArgs): Promise<${model.name}> {
1357
- const query = QueryBuilder.buildWhere(args.where)
1358
- const existing = await this.collection.findOne(query)
1359
-
1360
- if (existing) {
1361
- return this.update({
1362
- where: args.where,
1363
- data: args.update,
1364
- select: args.select,
1365
- include: args.include
1366
- })
1367
- } else {
1368
- return this.create({
1369
- data: args.create,
1370
- select: args.select,
1371
- include: args.include
1372
- })
1373
- }
1374
- }
1375
-
1376
- async delete(args: ${model.name}DeleteArgs): Promise<${model.name} | null> {
1377
- const query = QueryBuilder.buildWhere(args.where)
1378
- const doc = await this.collection.findOne(query)
1379
-
1380
- if (!doc) return null
1381
-
1382
- await this.collection.deleteOne(query)
1383
- const formatted = RelationResolver.formatDocument(doc)
1384
-
1385
- if (args.include) {
1386
- return await this.includeRelations(formatted, args.include)
1387
- }
1388
-
1389
- return formatted
1390
- }
1391
-
1392
- async deleteMany(args: { where?: ${model.name}WhereInput }): Promise<{ count: number }> {
1393
- const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1394
- const result = await this.collection.deleteMany(query)
1395
- return { count: result.deletedCount }
1396
- }
1397
-
1398
- async count(args?: { where?: ${model.name}WhereInput }): Promise<number> {
1399
- const query = args?.where ? QueryBuilder.buildWhere(args.where) : {}
1400
- return await this.collection.countDocuments(query)
1401
- }
1402
-
1403
- async aggregate<T = any>(pipeline: any[]): Promise<T[]> {
1404
- return await this.collection.aggregate(pipeline).toArray() as T[]
1405
- }
1406
-
1407
- /**
1408
- * Offset-based pagination (page-based)
1409
- * Similar to Prisma's skip/take pagination
1410
- */
1411
- async findManyPaginated(args: OffsetPaginationArgs<${model.name}>): Promise<PaginatedResult<${model.name}>> {
1412
- const page = args.page || 1
1413
- const perPage = args.take || args.perPage || 10
1414
- const skip = (page - 1) * perPage
1415
-
1416
- // Get total count
1417
- const where = args.where ? QueryBuilder.buildWhere(args.where) : {}
1418
- const total = await this.collection.countDocuments(where)
1419
-
1420
- // Get paginated data
1421
- const query = QueryBuilder.buildWhere(args.where || {})
1422
- const options = {
1423
- skip,
1424
- limit: perPage,
1425
- ...QueryBuilder.buildOptions(args, this.hiddenFields)
1426
- }
1427
-
1428
- const mongoCursor = this.collection.find(query, options)
1429
- const docs = await mongoCursor.toArray()
1430
- const data = docs.map(RelationResolver.formatDocument)
1431
-
1432
- // Handle includes
1433
- let resultData = data
1434
- if (args.include) {
1435
- resultData = await Promise.all(
1436
- data.map(doc => this.includeRelations(doc, args.include!))
1437
- )
1438
- }
1439
-
1440
- const totalPages = Math.ceil(total / perPage)
1441
-
1442
- return {
1443
- data: resultData,
1444
- meta: {
1445
- total,
1446
- page,
1447
- perPage,
1448
- totalPages,
1449
- hasNextPage: page < totalPages,
1450
- hasPreviousPage: page > 1
1451
- }
1452
- }
1453
- }
1454
-
1455
- /**
1456
- * Cursor-based pagination
1457
- * More efficient for large datasets, similar to Relay/GraphQL cursor pagination
1458
- */
1459
- async findManyWithCursor(args: CursorPaginationArgs<${model.name}>): Promise<CursorPaginatedResult<${model.name}>> {
1460
- const take = args.take || 20
1461
- let where = args.where || {}
1462
-
1463
- // Apply cursor if provided
1464
- if (args.cursor) {
1465
- const cursorCondition = QueryBuilder.buildCursorCondition(args.cursor, args.orderBy)
1466
- where = {
1467
- ...where,
1468
- ...cursorCondition
1469
- }
1470
- }
1471
-
1472
- // Get total count (optional, for pageInfo)
1473
- const query = QueryBuilder.buildWhere(args.where || {})
1474
- const totalCount = await this.collection.countDocuments(query)
1475
-
1476
- // Get data with one extra to check if there's more
1477
- const options = {
1478
- limit: take + 1,
1479
- ...QueryBuilder.buildOptions(args, this.hiddenFields)
1480
- }
1481
-
1482
- const mongoCursor = this.collection.find(where, options)
1483
- const docs = await mongoCursor.toArray()
1484
- const hasNextPage = docs.length > take
1485
-
1486
- // Remove extra element if exists
1487
- const resultDocs = hasNextPage ? docs.slice(0, take) : docs
1488
- const data = resultDocs.map(RelationResolver.formatDocument)
1489
-
1490
- // Handle includes
1491
- let resultData = data
1492
- if (args.include) {
1493
- resultData = await Promise.all(
1494
- data.map(doc => this.includeRelations(doc, args.include!))
1495
- )
1496
- }
1497
-
1498
- // Create edges with cursors
1499
- const edges = resultData.map(doc => ({
1500
- node: doc,
1501
- cursor: PaginationHelper.createCursor(doc)
1502
- }))
1503
-
1504
- return {
1505
- edges,
1506
- pageInfo: {
1507
- hasNextPage,
1508
- hasPreviousPage: !!args.cursor,
1509
- startCursor: edges[0]?.cursor,
1510
- endCursor: edges[edges.length - 1]?.cursor
1511
- },
1512
- totalCount
1513
- }
1514
- }
1515
-
1516
- /**
1517
- * Find with advanced pagination options
1518
- * Supports both offset and cursor pagination
1519
- */
1520
- async findWithPagination(
1521
- args: ${model.name}QueryOptions & {
1522
- paginationType?: 'offset' | 'cursor'
1523
- page?: number
1524
- perPage?: number
1525
- cursor?: string | ObjectId
1526
- }
1527
- ): Promise<any> {
1528
- const paginationType = args.paginationType || 'offset'
1529
-
1530
- if (paginationType === 'cursor') {
1531
- return this.findManyWithCursor({
1532
- where: args.where,
1533
- select: args.select,
1534
- include: args.include,
1535
- orderBy: args.orderBy,
1536
- cursor: args.cursor,
1537
- take: args.take || args.perPage
1538
- })
1539
- } else {
1540
- return this.findManyPaginated({
1541
- where: args.where,
1542
- select: args.select,
1543
- include: args.include,
1544
- orderBy: args.orderBy,
1545
- skip: args.skip,
1546
- take: args.take,
1547
- page: args.page,
1548
- perPage: args.perPage
1549
- })
1550
- }
1551
- }
1552
-
1553
- /**
1554
- * Count with pagination info
1555
- */
1556
- async countWithPagination(args?: { where?: ${model.name}WhereInput }): Promise<{
1557
- total: number
1558
- filtered?: number
1559
- }> {
1560
- const where = args?.where ? QueryBuilder.buildWhere(args.where) : {}
1561
- const total = await this.collection.estimatedDocumentCount()
1562
- const filtered = await this.collection.countDocuments(where)
1563
-
1564
- return {
1565
- total,
1566
- filtered: total !== filtered ? filtered : undefined
1567
- }
1568
- }
1569
-
1570
- private applySelect(document: any, select: any): any {
1571
- if (!select) {
1572
- // If no select, exclude hidden fields by default
1573
- if (this.hiddenFields.length === 0) return document;
1574
- const result = { ...document };
1575
- this.hiddenFields.forEach(field => {
1576
- delete result[field];
1577
- });
1578
- return result;
1579
- }
1580
-
1581
- // Build projection using QueryBuilder
1582
- const projection = QueryBuilder.buildProjection(select, this.hiddenFields);
1583
- if (!projection) return document;
1584
-
1585
- const result = { ...document };
1586
- // Apply projection (simplified - only top-level fields)
1587
- for (const [field, value] of Object.entries(projection)) {
1588
- if (value === 0 && field in result) {
1589
- delete result[field];
1590
- }
1591
- // If value === 1, keep the field (already present)
1592
- }
1593
- return result;
1594
- }
1595
-
1596
- private async includeRelations(document: any, include: any): Promise<any> {
1597
- const result = { ...document }
1598
- if (!include || typeof include !== 'object') {
1599
- return result
1600
- }
1601
-
1602
- ${this.generateRelationInclusionCode(model)}
1603
-
1604
- return result
1605
- }
1606
-
1607
- // Raw access
1608
- get $raw() {
1609
- return {
1610
- collection: this.collection,
1611
- find: async (filter: any) => await this.collection.find(filter).toArray(),
1612
- findOne: async (filter: any) => await this.collection.findOne(filter),
1613
- insertOne: async (doc: any) => await this.collection.insertOne(doc),
1614
- updateOne: async (filter: any, update: any) => await this.collection.updateOne(filter, update),
1615
- deleteOne: async (filter: any) => await this.collection.deleteOne(filter),
1616
- aggregate: async (pipeline: any[]) => await this.collection.aggregate(pipeline).toArray()
1617
- }
1618
- }
1619
- }
1195
+ return `// This file was auto-generated by Lenz. Do not edit manually.
1196
+ // @generated
1197
+
1198
+ import { Collection, ObjectId, Document } from 'mongodb'
1199
+ import type {
1200
+ ${model.name},
1201
+ ${model.name}CreateInput,
1202
+ ${model.name}UpdateInput,
1203
+ ${model.name}WhereInput,
1204
+ ${model.name}QueryOptions,
1205
+ ${model.name}CreateArgs,
1206
+ ${model.name}UpdateArgs,
1207
+ ${model.name}DeleteArgs,
1208
+ ${model.name}UpsertArgs,
1209
+ PaginatedResult,
1210
+ CursorPaginatedResult,
1211
+ OffsetPaginationArgs,
1212
+ CursorPaginationArgs
1213
+ } from '../types'
1214
+ import { QueryBuilder } from '../runtime/query'
1215
+ import { PaginationHelper } from '../runtime/pagination'
1216
+ import { RelationResolver } from '../runtime/relations'
1217
+ import type { LenzClient } from '../client'
1218
+
1219
+ export class ${model.name}Delegate {
1220
+ constructor(private client: LenzClient) {}
1221
+
1222
+ private readonly hiddenFields: string[] = ${JSON.stringify(model.fields.filter(f => f.isHidden).map(f => f.name))};
1223
+
1224
+ private get collection(): Collection<Document> {
1225
+ return this.client.$db.collection('${model.collectionName}')
1226
+ }
1227
+
1228
+ async findUnique(args: { where: ${model.name}WhereInput } & ${model.name}QueryOptions): Promise<${model.name} | null> {
1229
+ const query = QueryBuilder.buildWhere(args.where)
1230
+ const options = QueryBuilder.buildOptions(args, this.hiddenFields)
1231
+
1232
+ const doc = await this.collection.findOne(query, options)
1233
+ if (!doc) return null
1234
+
1235
+ const formatted = RelationResolver.formatDocument(doc)
1236
+
1237
+ if (args.include) {
1238
+ return await this.includeRelations(formatted, args.include)
1239
+ }
1240
+
1241
+ return formatted
1242
+ }
1243
+
1244
+ async findMany(args?: ${model.name}QueryOptions): Promise<${model.name}[]> {
1245
+ const { cursor, ...otherArgs } = args || {}
1246
+ let where = args?.where || {}
1247
+
1248
+ // Handle cursor-based pagination
1249
+ if (cursor) {
1250
+ const cursorCondition = QueryBuilder.buildCursorCondition(cursor, args?.orderBy)
1251
+ where = {
1252
+ ...where,
1253
+ ...cursorCondition
1254
+ }
1255
+ }
1256
+
1257
+ const query = QueryBuilder.buildWhere(where)
1258
+ const options = QueryBuilder.buildOptions(otherArgs || {}, this.hiddenFields)
1259
+
1260
+ const mongoCursor = this.collection.find(query, options)
1261
+ const docs = await mongoCursor.toArray()
1262
+ const formatted = docs.map(RelationResolver.formatDocument)
1263
+
1264
+ if (args?.include) {
1265
+ return await Promise.all(
1266
+ formatted.map(doc => this.includeRelations(doc, args.include!))
1267
+ )
1268
+ }
1269
+
1270
+ return formatted
1271
+ }
1272
+
1273
+ async findFirst(args?: ${model.name}QueryOptions): Promise<${model.name} | null> {
1274
+ const results = await this.findMany({ ...args, take: 1 })
1275
+ return results[0] || null
1276
+ }
1277
+
1278
+ async create(args: ${model.name}CreateArgs): Promise<${model.name}> {
1279
+ const now = new Date()
1280
+ const document = {
1281
+ ...args.data,
1282
+ _id: new ObjectId(),
1283
+ createdAt: now,
1284
+ updatedAt: now
1285
+ }
1286
+
1287
+ const result = await this.collection.insertOne(document)
1288
+ const createdDoc = await this.collection.findOne({ _id: result.insertedId })
1289
+
1290
+ if (!createdDoc) {
1291
+ throw new Error('Failed to create document')
1292
+ }
1293
+
1294
+ const formatted = RelationResolver.formatDocument(createdDoc)
1295
+
1296
+ if (args.include) {
1297
+ return await this.includeRelations(formatted, args.include)
1298
+ }
1299
+
1300
+ return formatted
1301
+ }
1302
+
1303
+ async createMany(args: { data: ${model.name}CreateInput[] }): Promise<{ count: number }> {
1304
+ const now = new Date()
1305
+ const documents = args.data.map(data => ({
1306
+ ...data,
1307
+ _id: new ObjectId(),
1308
+ createdAt: now,
1309
+ updatedAt: now
1310
+ }))
1311
+
1312
+ const result = await this.collection.insertMany(documents)
1313
+ return { count: result.insertedCount }
1314
+ }
1315
+
1316
+ async update(args: ${model.name}UpdateArgs): Promise<${model.name}> {
1317
+ const query = QueryBuilder.buildWhere(args.where)
1318
+ const updateData = {
1319
+ ...args.data,
1320
+ updatedAt: new Date()
1321
+ }
1322
+
1323
+ const update = QueryBuilder.buildUpdate(updateData)
1324
+ const result = await this.collection.findOneAndUpdate(
1325
+ query,
1326
+ update,
1327
+ { returnDocument: 'after' }
1328
+ )
1329
+
1330
+ const updatedDoc = result.value || result
1331
+ if (!updatedDoc) {
1332
+ throw new Error('Document not found')
1333
+ }
1334
+
1335
+ const formatted = RelationResolver.formatDocument(updatedDoc)
1336
+
1337
+ if (args.include) {
1338
+ return await this.includeRelations(formatted, args.include)
1339
+ }
1340
+
1341
+ return formatted
1342
+ }
1343
+
1344
+ async updateMany(args: { where?: ${model.name}WhereInput; data: ${model.name}UpdateInput }): Promise<{ count: number }> {
1345
+ const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1346
+ const updateData = {
1347
+ ...args.data,
1348
+ updatedAt: new Date()
1349
+ }
1350
+
1351
+ const update = QueryBuilder.buildUpdate(updateData)
1352
+ const result = await this.collection.updateMany(query, update)
1353
+ return { count: result.modifiedCount }
1354
+ }
1355
+
1356
+ async upsert(args: ${model.name}UpsertArgs): Promise<${model.name}> {
1357
+ const query = QueryBuilder.buildWhere(args.where)
1358
+ const existing = await this.collection.findOne(query)
1359
+
1360
+ if (existing) {
1361
+ return this.update({
1362
+ where: args.where,
1363
+ data: args.update,
1364
+ select: args.select,
1365
+ include: args.include
1366
+ })
1367
+ } else {
1368
+ return this.create({
1369
+ data: args.create,
1370
+ select: args.select,
1371
+ include: args.include
1372
+ })
1373
+ }
1374
+ }
1375
+
1376
+ async delete(args: ${model.name}DeleteArgs): Promise<${model.name} | null> {
1377
+ const query = QueryBuilder.buildWhere(args.where)
1378
+ const doc = await this.collection.findOne(query)
1379
+
1380
+ if (!doc) return null
1381
+
1382
+ await this.collection.deleteOne(query)
1383
+ const formatted = RelationResolver.formatDocument(doc)
1384
+
1385
+ if (args.include) {
1386
+ return await this.includeRelations(formatted, args.include)
1387
+ }
1388
+
1389
+ return formatted
1390
+ }
1391
+
1392
+ async deleteMany(args: { where?: ${model.name}WhereInput }): Promise<{ count: number }> {
1393
+ const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1394
+ const result = await this.collection.deleteMany(query)
1395
+ return { count: result.deletedCount }
1396
+ }
1397
+
1398
+ async count(args?: { where?: ${model.name}WhereInput }): Promise<number> {
1399
+ const query = args?.where ? QueryBuilder.buildWhere(args.where) : {}
1400
+ return await this.collection.countDocuments(query)
1401
+ }
1402
+
1403
+ async aggregate<T = any>(pipeline: any[]): Promise<T[]> {
1404
+ return await this.collection.aggregate(pipeline).toArray() as T[]
1405
+ }
1406
+
1407
+ /**
1408
+ * Offset-based pagination (page-based)
1409
+ * Similar to Prisma's skip/take pagination
1410
+ */
1411
+ async findManyPaginated(args: OffsetPaginationArgs<${model.name}>): Promise<PaginatedResult<${model.name}>> {
1412
+ const page = args.page || 1
1413
+ const perPage = args.take || args.perPage || 10
1414
+ const skip = (page - 1) * perPage
1415
+
1416
+ // Get total count
1417
+ const where = args.where ? QueryBuilder.buildWhere(args.where) : {}
1418
+ const total = await this.collection.countDocuments(where)
1419
+
1420
+ // Get paginated data
1421
+ const query = QueryBuilder.buildWhere(args.where || {})
1422
+ const options = {
1423
+ skip,
1424
+ limit: perPage,
1425
+ ...QueryBuilder.buildOptions(args, this.hiddenFields)
1426
+ }
1427
+
1428
+ const mongoCursor = this.collection.find(query, options)
1429
+ const docs = await mongoCursor.toArray()
1430
+ const data = docs.map(RelationResolver.formatDocument)
1431
+
1432
+ // Handle includes
1433
+ let resultData = data
1434
+ if (args.include) {
1435
+ resultData = await Promise.all(
1436
+ data.map(doc => this.includeRelations(doc, args.include!))
1437
+ )
1438
+ }
1439
+
1440
+ const totalPages = Math.ceil(total / perPage)
1441
+
1442
+ return {
1443
+ data: resultData,
1444
+ meta: {
1445
+ total,
1446
+ page,
1447
+ perPage,
1448
+ totalPages,
1449
+ hasNextPage: page < totalPages,
1450
+ hasPreviousPage: page > 1
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ /**
1456
+ * Cursor-based pagination
1457
+ * More efficient for large datasets, similar to Relay/GraphQL cursor pagination
1458
+ */
1459
+ async findManyWithCursor(args: CursorPaginationArgs<${model.name}>): Promise<CursorPaginatedResult<${model.name}>> {
1460
+ const take = args.take || 20
1461
+ let where = args.where || {}
1462
+
1463
+ // Apply cursor if provided
1464
+ if (args.cursor) {
1465
+ const cursorCondition = QueryBuilder.buildCursorCondition(args.cursor, args.orderBy)
1466
+ where = {
1467
+ ...where,
1468
+ ...cursorCondition
1469
+ }
1470
+ }
1471
+
1472
+ // Get total count (optional, for pageInfo)
1473
+ const query = QueryBuilder.buildWhere(args.where || {})
1474
+ const totalCount = await this.collection.countDocuments(query)
1475
+
1476
+ // Get data with one extra to check if there's more
1477
+ const options = {
1478
+ limit: take + 1,
1479
+ ...QueryBuilder.buildOptions(args, this.hiddenFields)
1480
+ }
1481
+
1482
+ const mongoCursor = this.collection.find(where, options)
1483
+ const docs = await mongoCursor.toArray()
1484
+ const hasNextPage = docs.length > take
1485
+
1486
+ // Remove extra element if exists
1487
+ const resultDocs = hasNextPage ? docs.slice(0, take) : docs
1488
+ const data = resultDocs.map(RelationResolver.formatDocument)
1489
+
1490
+ // Handle includes
1491
+ let resultData = data
1492
+ if (args.include) {
1493
+ resultData = await Promise.all(
1494
+ data.map(doc => this.includeRelations(doc, args.include!))
1495
+ )
1496
+ }
1497
+
1498
+ // Create edges with cursors
1499
+ const edges = resultData.map(doc => ({
1500
+ node: doc,
1501
+ cursor: PaginationHelper.createCursor(doc)
1502
+ }))
1503
+
1504
+ return {
1505
+ edges,
1506
+ pageInfo: {
1507
+ hasNextPage,
1508
+ hasPreviousPage: !!args.cursor,
1509
+ startCursor: edges[0]?.cursor,
1510
+ endCursor: edges[edges.length - 1]?.cursor
1511
+ },
1512
+ totalCount
1513
+ }
1514
+ }
1515
+
1516
+ /**
1517
+ * Find with advanced pagination options
1518
+ * Supports both offset and cursor pagination
1519
+ */
1520
+ async findWithPagination(
1521
+ args: ${model.name}QueryOptions & {
1522
+ paginationType?: 'offset' | 'cursor'
1523
+ page?: number
1524
+ perPage?: number
1525
+ cursor?: string | ObjectId
1526
+ }
1527
+ ): Promise<any> {
1528
+ const paginationType = args.paginationType || 'offset'
1529
+
1530
+ if (paginationType === 'cursor') {
1531
+ return this.findManyWithCursor({
1532
+ where: args.where,
1533
+ select: args.select,
1534
+ include: args.include,
1535
+ orderBy: args.orderBy,
1536
+ cursor: args.cursor,
1537
+ take: args.take || args.perPage
1538
+ })
1539
+ } else {
1540
+ return this.findManyPaginated({
1541
+ where: args.where,
1542
+ select: args.select,
1543
+ include: args.include,
1544
+ orderBy: args.orderBy,
1545
+ skip: args.skip,
1546
+ take: args.take,
1547
+ page: args.page,
1548
+ perPage: args.perPage
1549
+ })
1550
+ }
1551
+ }
1552
+
1553
+ /**
1554
+ * Count with pagination info
1555
+ */
1556
+ async countWithPagination(args?: { where?: ${model.name}WhereInput }): Promise<{
1557
+ total: number
1558
+ filtered?: number
1559
+ }> {
1560
+ const where = args?.where ? QueryBuilder.buildWhere(args.where) : {}
1561
+ const total = await this.collection.estimatedDocumentCount()
1562
+ const filtered = await this.collection.countDocuments(where)
1563
+
1564
+ return {
1565
+ total,
1566
+ filtered: total !== filtered ? filtered : undefined
1567
+ }
1568
+ }
1569
+
1570
+ private applySelect(document: any, select: any): any {
1571
+ if (!select) {
1572
+ // If no select, exclude hidden fields by default
1573
+ if (this.hiddenFields.length === 0) return document;
1574
+ const result = { ...document };
1575
+ this.hiddenFields.forEach(field => {
1576
+ delete result[field];
1577
+ });
1578
+ return result;
1579
+ }
1580
+
1581
+ // Build projection using QueryBuilder
1582
+ const projection = QueryBuilder.buildProjection(select, this.hiddenFields);
1583
+ if (!projection) return document;
1584
+
1585
+ const result = { ...document };
1586
+ // Apply projection (simplified - only top-level fields)
1587
+ for (const [field, value] of Object.entries(projection)) {
1588
+ if (value === 0 && field in result) {
1589
+ delete result[field];
1590
+ }
1591
+ // If value === 1, keep the field (already present)
1592
+ }
1593
+ return result;
1594
+ }
1595
+
1596
+ private async includeRelations(document: any, include: any): Promise<any> {
1597
+ const result = { ...document }
1598
+ if (!include || typeof include !== 'object') {
1599
+ return result
1600
+ }
1601
+
1602
+ ${this.generateRelationInclusionCode(model)}
1603
+
1604
+ return result
1605
+ }
1606
+
1607
+ // Raw access
1608
+ get $raw() {
1609
+ return {
1610
+ collection: this.collection,
1611
+ find: async (filter: any) => await this.collection.find(filter).toArray(),
1612
+ findOne: async (filter: any) => await this.collection.findOne(filter),
1613
+ insertOne: async (doc: any) => await this.collection.insertOne(doc),
1614
+ updateOne: async (filter: any, update: any) => await this.collection.updateOne(filter, update),
1615
+ deleteOne: async (filter: any) => await this.collection.deleteOne(filter),
1616
+ aggregate: async (pipeline: any[]) => await this.collection.aggregate(pipeline).toArray()
1617
+ }
1618
+ }
1619
+ }
1620
1620
  `;
1621
1621
  }
1622
1622
  generateRelationInclusionCode(model) {
@@ -1626,10 +1626,21 @@ export class ${model.name}Delegate {
1626
1626
  case 'oneToMany':
1627
1627
  lines.push(` // Relation: ${relation.field} (oneToMany)`);
1628
1628
  lines.push(` if (include.${relation.field} !== undefined) {`);
1629
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1630
- lines.push(` where: { ${relation.foreignKey}: document.id },`);
1631
- lines.push(` include: typeof include.${relation.field} === 'object' ? include.${relation.field} : undefined`);
1632
- lines.push(` })`);
1629
+ // Check foreign key location: if in source, document has array of IDs; if in target, target has foreign key
1630
+ if (relation.foreignKeyLocation === 'source') {
1631
+ // Foreign key is array of IDs in source document
1632
+ lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1633
+ lines.push(` where: { id: { in: document.${relation.foreignKey} } },`);
1634
+ lines.push(` include: typeof include.${relation.field} === 'object' ? include.${relation.field} : undefined`);
1635
+ lines.push(` })`);
1636
+ }
1637
+ else {
1638
+ // Foreign key is single ID in target documents (default)
1639
+ lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1640
+ lines.push(` where: { ${relation.foreignKey}: document.id },`);
1641
+ lines.push(` include: typeof include.${relation.field} === 'object' ? include.${relation.field} : undefined`);
1642
+ lines.push(` })`);
1643
+ }
1633
1644
  lines.push(` result.${relation.field} = ${relation.field}`);
1634
1645
  lines.push(` }`);
1635
1646
  break;
@@ -1645,9 +1656,9 @@ export class ${model.name}Delegate {
1645
1656
  break;
1646
1657
  case 'oneToOne':
1647
1658
  lines.push(` // Relation: ${relation.field} (oneToOne)`);
1648
- lines.push(` if (include.${relation.field} !== undefined) {`);
1649
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findFirst({`);
1650
- lines.push(` where: { ${relation.foreignKey}: document.id },`);
1659
+ lines.push(` if (include.${relation.field} !== undefined && document.${relation.foreignKey}) {`);
1660
+ lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findUnique({`);
1661
+ lines.push(` where: { id: document.${relation.foreignKey} },`);
1651
1662
  lines.push(` include: typeof include.${relation.field} === 'object' ? include.${relation.field} : undefined`);
1652
1663
  lines.push(` })`);
1653
1664
  lines.push(` result.${relation.field} = ${relation.field}`);