@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.
- package/README.md +59 -2
- package/dist/engine/CodeGenerator.d.ts.map +1 -1
- package/dist/engine/CodeGenerator.js +1292 -1281
- package/dist/engine/CodeGenerator.js.map +1 -1
- package/dist/engine/GraphQLParser.d.ts +2 -0
- package/dist/engine/GraphQLParser.d.ts.map +1 -1
- package/dist/engine/GraphQLParser.js +48 -9
- package/dist/engine/GraphQLParser.js.map +1 -1
- package/dist/engine/SchemaValidator.d.ts.map +1 -1
- package/dist/engine/SchemaValidator.js +68 -5
- package/dist/engine/SchemaValidator.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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)}.
|
|
1650
|
-
lines.push(` where: {
|
|
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}`);
|