@bairock/lenz 0.0.15 → 0.0.17

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.
Files changed (134) hide show
  1. package/README.md +195 -19
  2. package/dist/cli/commands/generate/crud.d.ts +3 -0
  3. package/dist/cli/commands/generate/crud.d.ts.map +1 -0
  4. package/dist/cli/commands/generate/crud.js +123 -0
  5. package/dist/cli/commands/generate/crud.js.map +1 -0
  6. package/dist/cli/commands/generate/index.d.ts +3 -0
  7. package/dist/cli/commands/generate/index.d.ts.map +1 -0
  8. package/dist/cli/commands/generate/index.js +8 -0
  9. package/dist/cli/commands/generate/index.js.map +1 -0
  10. package/dist/cli/commands/generate/orm.d.ts +3 -0
  11. package/dist/cli/commands/generate/orm.d.ts.map +1 -0
  12. package/dist/cli/commands/generate/orm.js +107 -0
  13. package/dist/cli/commands/generate/orm.js.map +1 -0
  14. package/dist/cli/commands/generate.d.ts.map +1 -1
  15. package/dist/cli/commands/generate.js +34 -8
  16. package/dist/cli/commands/generate.js.map +1 -1
  17. package/dist/cli/commands/init.d.ts.map +1 -1
  18. package/dist/cli/commands/init.js +0 -2
  19. package/dist/cli/commands/init.js.map +1 -1
  20. package/dist/cli/index.js +1 -3
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/config/index.d.ts +4 -6
  23. package/dist/config/index.d.ts.map +1 -1
  24. package/dist/config/index.js +24 -3
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/engine/CodeGenerator.d.ts +8 -28
  27. package/dist/engine/CodeGenerator.d.ts.map +1 -1
  28. package/dist/engine/CodeGenerator.js +28 -1969
  29. package/dist/engine/CodeGenerator.js.map +1 -1
  30. package/dist/engine/GraphQLParseHelpers.d.ts +25 -0
  31. package/dist/engine/GraphQLParseHelpers.d.ts.map +1 -0
  32. package/dist/engine/GraphQLParseHelpers.js +128 -0
  33. package/dist/engine/GraphQLParseHelpers.js.map +1 -0
  34. package/dist/engine/GraphQLParser.d.ts +23 -10
  35. package/dist/engine/GraphQLParser.d.ts.map +1 -1
  36. package/dist/engine/GraphQLParser.js +154 -240
  37. package/dist/engine/GraphQLParser.js.map +1 -1
  38. package/dist/engine/GraphQLRelationAnalyzer.d.ts +10 -0
  39. package/dist/engine/GraphQLRelationAnalyzer.d.ts.map +1 -0
  40. package/dist/engine/GraphQLRelationAnalyzer.js +117 -0
  41. package/dist/engine/GraphQLRelationAnalyzer.js.map +1 -0
  42. package/dist/engine/LenzEngine.d.ts +1 -1
  43. package/dist/engine/LenzEngine.d.ts.map +1 -1
  44. package/dist/engine/LenzEngine.js +33 -13
  45. package/dist/engine/LenzEngine.js.map +1 -1
  46. package/dist/engine/SchemaRelationValidator.d.ts +15 -0
  47. package/dist/engine/SchemaRelationValidator.d.ts.map +1 -0
  48. package/dist/engine/SchemaRelationValidator.js +133 -0
  49. package/dist/engine/SchemaRelationValidator.js.map +1 -0
  50. package/dist/engine/SchemaValidator.d.ts +11 -10
  51. package/dist/engine/SchemaValidator.d.ts.map +1 -1
  52. package/dist/engine/SchemaValidator.js +151 -169
  53. package/dist/engine/SchemaValidator.js.map +1 -1
  54. package/dist/engine/directives.d.ts +10 -0
  55. package/dist/engine/directives.d.ts.map +1 -1
  56. package/dist/engine/directives.js +152 -6
  57. package/dist/engine/directives.js.map +1 -1
  58. package/dist/engine/generators/ClientGenerator.d.ts +7 -0
  59. package/dist/engine/generators/ClientGenerator.d.ts.map +1 -0
  60. package/dist/engine/generators/ClientGenerator.js +386 -0
  61. package/dist/engine/generators/ClientGenerator.js.map +1 -0
  62. package/dist/engine/generators/CrudModuleGenerator.d.ts +10 -0
  63. package/dist/engine/generators/CrudModuleGenerator.d.ts.map +1 -0
  64. package/dist/engine/generators/CrudModuleGenerator.js +141 -0
  65. package/dist/engine/generators/CrudModuleGenerator.js.map +1 -0
  66. package/dist/engine/generators/DelegateGenerator.d.ts +9 -0
  67. package/dist/engine/generators/DelegateGenerator.d.ts.map +1 -0
  68. package/dist/engine/generators/DelegateGenerator.js +453 -0
  69. package/dist/engine/generators/DelegateGenerator.js.map +1 -0
  70. package/dist/engine/generators/DelegateHelpers.d.ts +7 -0
  71. package/dist/engine/generators/DelegateHelpers.d.ts.map +1 -0
  72. package/dist/engine/generators/DelegateHelpers.js +144 -0
  73. package/dist/engine/generators/DelegateHelpers.js.map +1 -0
  74. package/dist/engine/generators/DelegateRelations.d.ts +11 -0
  75. package/dist/engine/generators/DelegateRelations.d.ts.map +1 -0
  76. package/dist/engine/generators/DelegateRelations.js +794 -0
  77. package/dist/engine/generators/DelegateRelations.js.map +1 -0
  78. package/dist/engine/generators/DelegateTemplateBody.d.ts +8 -0
  79. package/dist/engine/generators/DelegateTemplateBody.d.ts.map +1 -0
  80. package/dist/engine/generators/DelegateTemplateBody.js +776 -0
  81. package/dist/engine/generators/DelegateTemplateBody.js.map +1 -0
  82. package/dist/engine/generators/GenerateRuntimeErrors.d.ts +2 -0
  83. package/dist/engine/generators/GenerateRuntimeErrors.d.ts.map +1 -0
  84. package/dist/engine/generators/GenerateRuntimeErrors.js +140 -0
  85. package/dist/engine/generators/GenerateRuntimeErrors.js.map +1 -0
  86. package/dist/engine/generators/GenerateRuntimeIndex.d.ts +2 -0
  87. package/dist/engine/generators/GenerateRuntimeIndex.d.ts.map +1 -0
  88. package/dist/engine/generators/GenerateRuntimeIndex.js +21 -0
  89. package/dist/engine/generators/GenerateRuntimeIndex.js.map +1 -0
  90. package/dist/engine/generators/GenerateRuntimeLogger.d.ts +2 -0
  91. package/dist/engine/generators/GenerateRuntimeLogger.d.ts.map +1 -0
  92. package/dist/engine/generators/GenerateRuntimeLogger.js +125 -0
  93. package/dist/engine/generators/GenerateRuntimeLogger.js.map +1 -0
  94. package/dist/engine/generators/GenerateRuntimePagination.d.ts +2 -0
  95. package/dist/engine/generators/GenerateRuntimePagination.d.ts.map +1 -0
  96. package/dist/engine/generators/GenerateRuntimePagination.js +159 -0
  97. package/dist/engine/generators/GenerateRuntimePagination.js.map +1 -0
  98. package/dist/engine/generators/GenerateRuntimeQuery.d.ts +2 -0
  99. package/dist/engine/generators/GenerateRuntimeQuery.d.ts.map +1 -0
  100. package/dist/engine/generators/GenerateRuntimeQuery.js +427 -0
  101. package/dist/engine/generators/GenerateRuntimeQuery.js.map +1 -0
  102. package/dist/engine/generators/GenerateRuntimeRelations.d.ts +2 -0
  103. package/dist/engine/generators/GenerateRuntimeRelations.d.ts.map +1 -0
  104. package/dist/engine/generators/GenerateRuntimeRelations.js +130 -0
  105. package/dist/engine/generators/GenerateRuntimeRelations.js.map +1 -0
  106. package/dist/engine/generators/RuntimeGenerator.d.ts +16 -0
  107. package/dist/engine/generators/RuntimeGenerator.d.ts.map +1 -0
  108. package/dist/engine/generators/RuntimeGenerator.js +16 -0
  109. package/dist/engine/generators/RuntimeGenerator.js.map +1 -0
  110. package/dist/engine/generators/SDLInputTypesGenerator.d.ts +6 -0
  111. package/dist/engine/generators/SDLInputTypesGenerator.d.ts.map +1 -0
  112. package/dist/engine/generators/SDLInputTypesGenerator.js +763 -0
  113. package/dist/engine/generators/SDLInputTypesGenerator.js.map +1 -0
  114. package/dist/engine/generators/TypeFilterTypes.d.ts +2 -0
  115. package/dist/engine/generators/TypeFilterTypes.d.ts.map +1 -0
  116. package/dist/engine/generators/TypeFilterTypes.js +220 -0
  117. package/dist/engine/generators/TypeFilterTypes.js.map +1 -0
  118. package/dist/engine/generators/TypeGenerator.d.ts +16 -0
  119. package/dist/engine/generators/TypeGenerator.d.ts.map +1 -0
  120. package/dist/engine/generators/TypeGenerator.js +493 -0
  121. package/dist/engine/generators/TypeGenerator.js.map +1 -0
  122. package/dist/engine/generators/helpers.d.ts +13 -0
  123. package/dist/engine/generators/helpers.d.ts.map +1 -0
  124. package/dist/engine/generators/helpers.js +316 -0
  125. package/dist/engine/generators/helpers.js.map +1 -0
  126. package/dist/errors/index.d.ts +3 -0
  127. package/dist/errors/index.d.ts.map +1 -1
  128. package/dist/errors/index.js +11 -1
  129. package/dist/errors/index.js.map +1 -1
  130. package/dist/index.d.ts +0 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +2 -4
  133. package/dist/index.js.map +1 -1
  134. package/package.json +10 -4
@@ -1,1989 +1,48 @@
1
- import * as ts from 'typescript';
1
+ import { compileTypeScriptToJavaScript, convertTypesToJavaScript, convertToDeclaration } from './generators/helpers.js';
2
+ import { TypeGenerator } from './generators/TypeGenerator.js';
3
+ import { ClientGenerator } from './generators/ClientGenerator.js';
4
+ import { DelegateGenerator } from './generators/DelegateGenerator.js';
5
+ import { RuntimeGenerator } from './generators/RuntimeGenerator.js';
6
+ import { SDLInputTypesGenerator } from './generators/SDLInputTypesGenerator.js';
2
7
  export class CodeGenerator {
3
8
  constructor() {
4
- this.typeMap = {
5
- 'String': 'string',
6
- 'Int': 'number',
7
- 'Float': 'number',
8
- 'Boolean': 'boolean',
9
- 'ID': 'string',
10
- 'DateTime': 'Date',
11
- 'Date': 'Date',
12
- 'Json': 'any',
13
- 'ObjectId': 'string'
14
- };
15
- }
16
- compileTypeScriptToJavaScript(content) {
17
- try {
18
- // Use TypeScript compiler
19
- const result = ts.transpileModule(content, {
20
- compilerOptions: {
21
- target: ts.ScriptTarget.ES2020,
22
- module: ts.ModuleKind.ESNext,
23
- removeComments: false,
24
- preserveConstEnums: true,
25
- sourceMap: false,
26
- declaration: false,
27
- strict: false,
28
- esModuleInterop: true,
29
- skipLibCheck: true
30
- }
31
- });
32
- return this.addJsExtensionsToImports(result.outputText);
33
- }
34
- catch (error) {
35
- console.error('TypeScript compilation failed:', error);
36
- // Fallback to enhanced conversion method
37
- return this.convertToJavaScript(content);
38
- }
39
- }
40
- addJsExtensionsToImports(content) {
41
- let result = content;
42
- // 1. Remove import type (already present)
43
- result = result.replace(/import type/g, 'import');
44
- // 2. Add .js extensions to relative imports (already present)
45
- // Handle both single and double quotes, with optional whitespace
46
- // Match from './path' or from '../path' or from '../../path' etc.
47
- result = result.replace(/from\s+['"](\.\.?\/[^'"]*)['"]/g, (match, p1) => {
48
- if (p1.endsWith('.js') || p1.endsWith('.ts'))
49
- return match;
50
- // Keep the original quote type
51
- const quoteChar = match.includes('"') ? '"' : "'";
52
- return `from ${quoteChar}${p1}.js${quoteChar}`;
53
- });
54
- // Also handle import './path' (without from)
55
- result = result.replace(/import\s+['"](\.\.?\/[^'"]*)['"]/g, (match, p1) => {
56
- if (p1.endsWith('.js') || p1.endsWith('.ts'))
57
- return match;
58
- const quoteChar = match.includes('"') ? '"' : "'";
59
- return `import ${quoteChar}${p1}.js${quoteChar}`;
60
- });
61
- return result;
62
- }
63
- removeTypeScriptSyntax(content) {
64
- let result = content;
65
- // 1. Remove variable type annotations
66
- result = result.replace(/(\w+)\s*:\s*[^=;,\n]+(?=\s*(=|;|,|\n))/g, '$1');
67
- // 2. Remove generic parameters from functions and classes
68
- result = result.replace(/(\w+)<[^>]+>(?=\s*[\s\(])/g, '$1');
69
- // 3. Remove return type annotations
70
- result = result.replace(/\s*:\s*[^{]+(?=\s*{)/g, '');
71
- // 4. Remove type assertions
72
- result = result.replace(/\s+as\s+[^,\n;]+/g, '');
73
- // 5. Remove export type/interface lines
74
- result = result.replace(/export\s+(type|interface)\s+\w+.*\n/g, '');
75
- // 6. Remove 'as const'
76
- result = result.replace(/ as const/g, '');
77
- // 7. Remove private/protected/public modifiers
78
- result = result.replace(/\b(private|protected|public)\s+/g, '');
79
- // 8. Clean up empty lines
80
- result = result.replace(/\n\s*\n\s*\n/g, '\n\n');
81
- return result;
82
- }
83
- convertToJavaScript(content) {
84
- let result = this.addJsExtensionsToImports(content);
85
- result = this.removeTypeScriptSyntax(result);
86
- return result;
87
- }
88
- convertTypesToJavaScript(content) {
89
- // For types.ts and enums.ts files: create JavaScript-compatible exports
90
- // For enums: keep the actual enum objects, remove 'as const' and type exports
91
- // For types: export undefined stubs
92
- // First, extract all constant exports (export const ... = ...)
93
- const constExports = [];
94
- const lines = content.split('\n');
95
- for (let i = 0; i < lines.length; i++) {
96
- const line = lines[i];
97
- // Match export const Name = { ... } or export const Name = ...
98
- const constMatch = line.match(/export const (\w+)\s*=\s*(.+)/);
99
- if (constMatch) {
100
- const name = constMatch[1];
101
- let value = constMatch[2];
102
- // Check if value continues on next lines (for multi-line objects)
103
- let braceCount = (value.match(/{/g) || []).length - (value.match(/}/g) || []).length;
104
- let j = i;
105
- while (braceCount > 0 && j + 1 < lines.length) {
106
- j++;
107
- const nextLine = lines[j];
108
- value += '\n' + nextLine;
109
- braceCount += (nextLine.match(/{/g) || []).length - (nextLine.match(/}/g) || []).length;
110
- }
111
- // Remove 'as const' if present
112
- value = value.replace(/ as const/g, '');
113
- constExports.push({ name, value });
114
- }
115
- }
116
- // Extract other export names (interfaces, types) for stub exports
117
- const stubExportNames = new Set();
118
- // Find export interface Name (but skip if we already have a const export with same name)
119
- const interfaceMatches = content.match(/export interface (\w+)/g);
120
- if (interfaceMatches) {
121
- interfaceMatches.forEach(match => {
122
- const name = match.replace('export interface ', '').trim();
123
- if (!constExports.some(exp => exp.name === name)) {
124
- stubExportNames.add(name);
125
- }
126
- });
127
- }
128
- // Find export type Name
129
- const typeMatches = content.match(/export type (\w+)/g);
130
- if (typeMatches) {
131
- typeMatches.forEach(match => {
132
- const name = match.replace('export type ', '').split('<')[0].trim();
133
- if (!constExports.some(exp => exp.name === name)) {
134
- stubExportNames.add(name);
135
- }
136
- });
137
- }
138
- // Find named exports: export { Name1, Name2 }
139
- const namedExportMatches = content.match(/export \{([^}]+)\}/g);
140
- if (namedExportMatches) {
141
- namedExportMatches.forEach(match => {
142
- const namesStr = match.replace('export {', '').replace('}', '').trim();
143
- const names = namesStr.split(',').map(n => n.trim()).filter(n => n.length > 0);
144
- names.forEach(name => {
145
- if (!constExports.some(exp => exp.name === name)) {
146
- stubExportNames.add(name);
147
- }
148
- });
149
- });
150
- }
151
- // Generate JavaScript file
152
- let result = `// This file was auto-generated by Lenz. Do not edit manually.
153
- // @generated
154
- // This file provides JavaScript-compatible exports for TypeScript types.
155
- // TypeScript projects should use the .d.ts files for full type information.
156
-
157
- `;
158
- // Add imports (convert import type to import and add .js extensions)
159
- const importLines = lines.filter(line => line.includes('import '));
160
- const processedImports = new Set();
161
- for (const line of importLines) {
162
- let processed = line.replace(/import type/g, 'import');
163
- processed = processed.replace(/from '\.\/([^']+)'/g, (match, p1) => {
164
- if (p1.endsWith('.js') || p1.endsWith('.ts')) {
165
- return match;
166
- }
167
- return `from './${p1}.js'`;
168
- });
169
- if (!processedImports.has(processed)) {
170
- processedImports.add(processed);
171
- result += processed + '\n';
172
- }
173
- }
174
- if (processedImports.size > 0) {
175
- result += '\n';
176
- }
177
- // Add constant exports first (enums)
178
- constExports.forEach(exp => {
179
- result += `export const ${exp.name} = ${exp.value};\n`;
180
- });
181
- if (constExports.length > 0) {
182
- result += '\n';
183
- }
184
- // Add stub exports for types and interfaces
185
- const stubExportArray = Array.from(stubExportNames);
186
- if (stubExportArray.length > 0) {
187
- stubExportArray.forEach(name => {
188
- result += `export const ${name} = undefined;\n`;
189
- });
190
- // Also export all as a default object for convenience
191
- result += `\nexport default {\n`;
192
- [...constExports.map(exp => exp.name), ...stubExportArray].forEach((name, index, arr) => {
193
- result += ` ${name}: ${name}${index < arr.length - 1 ? ',' : ''}\n`;
194
- });
195
- result += `};\n`;
196
- }
197
- else if (constExports.length > 0) {
198
- // Only constant exports, add default export
199
- result += `\nexport default {\n`;
200
- constExports.forEach((exp, index) => {
201
- result += ` ${exp.name}: ${exp.name}${index < constExports.length - 1 ? ',' : ''}\n`;
202
- });
203
- result += `};\n`;
204
- }
205
- else {
206
- // No exports found, export empty object
207
- result += `export {};\n`;
208
- }
209
- return result;
210
- }
211
- convertToDeclaration(content) {
212
- // For now, return the TypeScript content as-is for declarations
213
- // This will be refined later to remove implementations
214
- return content;
9
+ this.typeGenerator = new TypeGenerator();
10
+ this.clientGenerator = new ClientGenerator();
11
+ this.delegateGenerator = new DelegateGenerator();
12
+ this.runtimeGenerator = new RuntimeGenerator();
13
+ this.sdlInputTypesGenerator = new SDLInputTypesGenerator();
215
14
  }
216
15
  generate(options) {
217
16
  const { models, enums, clientName = 'LenzClient' } = options;
17
+ const nonEmbeddedModels = models.filter(m => !m.isEmbedded);
218
18
  const files = {
219
- 'index.ts': this.generateIndex(clientName),
220
- 'client.ts': this.generateClient(clientName, models),
221
- 'types.ts': this.generateTypes(models, enums),
222
- 'enums.ts': this.generateEnums(enums),
223
- 'runtime/index.ts': this.generateRuntimeIndex(),
224
- 'runtime/query.ts': this.generateRuntimeQuery(),
225
- 'runtime/pagination.ts': this.generateRuntimePagination(),
226
- 'runtime/relations.ts': this.generateRuntimeRelations(),
227
- 'models/index.ts': this.generateModelsIndex(models),
228
- ...this.generateModelFiles(models)
19
+ 'index.ts': this.clientGenerator.generateIndex(clientName),
20
+ 'client.ts': this.clientGenerator.generateClient(clientName, nonEmbeddedModels),
21
+ 'types.ts': this.typeGenerator.generateTypes(models, enums),
22
+ 'enums.ts': this.typeGenerator.generateEnums(enums),
23
+ 'runtime/index.ts': this.runtimeGenerator.generateRuntimeIndex(),
24
+ 'runtime/query.ts': this.runtimeGenerator.generateRuntimeQuery(),
25
+ 'runtime/pagination.ts': this.runtimeGenerator.generateRuntimePagination(),
26
+ 'runtime/relations.ts': this.runtimeGenerator.generateRuntimeRelations(),
27
+ 'runtime/errors.ts': this.runtimeGenerator.generateRuntimeErrors(),
28
+ 'runtime/logger.ts': this.runtimeGenerator.generateRuntimeLogger(),
29
+ 'inputTypes.ts': this.sdlInputTypesGenerator.generateSDLOutput(models, enums),
30
+ 'models/index.ts': this.delegateGenerator.generateModelsIndex(nonEmbeddedModels),
31
+ ...this.delegateGenerator.generateModelFiles(nonEmbeddedModels)
229
32
  };
230
33
  const result = {};
231
34
  for (const [filePath, content] of Object.entries(files)) {
232
- // Generate JavaScript file (.js)
233
35
  const jsPath = filePath.replace(/\.ts$/, '.js');
234
- // Special handling for types and enums files
235
36
  if (filePath === 'types.ts' || filePath === 'enums.ts') {
236
- result[jsPath] = this.convertTypesToJavaScript(content);
37
+ result[jsPath] = convertTypesToJavaScript(content);
237
38
  }
238
39
  else {
239
- result[jsPath] = this.compileTypeScriptToJavaScript(content);
40
+ result[jsPath] = compileTypeScriptToJavaScript(content);
240
41
  }
241
- // Generate TypeScript declaration file (.d.ts)
242
42
  const dtsPath = filePath.replace(/\.ts$/, '.d.ts');
243
- result[dtsPath] = this.convertToDeclaration(content);
43
+ result[dtsPath] = convertToDeclaration(content);
244
44
  }
245
45
  return result;
246
46
  }
247
- generateIndex(clientName) {
248
- return `// This file was auto-generated by Lenz. Do not edit manually.
249
- // @generated
250
-
251
- export { ${clientName} } from './client'
252
- export * from './types'
253
- export * from './enums'
254
-
255
- import { ${clientName} } from './client'
256
-
257
- /**
258
- * Default export for the Lenz client
259
- */
260
- const lenz = new ${clientName}()
261
- export default lenz
262
- `;
263
- }
264
- generateClient(clientName, models) {
265
- return `// This file was auto-generated by Lenz. Do not edit manually.
266
- // @generated
267
-
268
- import { MongoClient, Db, ObjectId } from 'mongodb'
269
- import type { LenzConfig } from './types'
270
- import { QueryBuilder } from './runtime/query'
271
- import { RelationResolver } from './runtime/relations'
272
-
273
- ${models.map(model => `
274
- import { ${model.name}Delegate } from './models/${model.name}'`).join('\n')}
275
-
276
- export class ${clientName} {
277
- private mongoClient: MongoClient | null = null
278
- private db: Db | null = null
279
- private config: LenzConfig
280
- private database: string
281
- private supportsTransactions: boolean = false
282
-
283
- // Model delegates
284
- ${models.map(model => ` public ${this.toCamelCase(model.name)}: ${model.name}Delegate`).join('\n')}
285
-
286
- private extractDatabaseFromUrl(url: string): string {
287
- // Extract database name from MongoDB URL
288
- // Format: mongodb://host:port/database?options or mongodb+srv://host/database?options
289
- try {
290
- const parts = url.split('://');
291
- if (parts.length < 2) return 'myapp';
292
- const afterProtocol = parts[1];
293
- const slashIndex = afterProtocol.indexOf('/');
294
- if (slashIndex === -1) return 'myapp';
295
- const afterSlash = afterProtocol.substring(slashIndex + 1);
296
- const questionIndex = afterSlash.indexOf('?');
297
- const database = questionIndex === -1 ? afterSlash : afterSlash.substring(0, questionIndex);
298
- return database || 'myapp';
299
- } catch {
300
- return 'myapp';
301
- }
302
- }
303
-
304
- constructor(config: LenzConfig = {}) {
305
- const url = config.url || process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp';
306
- this.config = {
307
- url,
308
- autoCreateCollections: config.autoCreateCollections ?? true,
309
- log: config.log || [],
310
- ...config
311
- };
312
- this.database = config.database || this.extractDatabaseFromUrl(url);
313
-
314
- // Initialize model delegates
315
- ${models.map(model => ` this.${this.toCamelCase(model.name)} = new ${model.name}Delegate(this)`).join('\n')}
316
- }
317
-
318
- async $connect(): Promise<void> {
319
- if (this.mongoClient) {
320
- return
321
- }
322
-
323
- this.mongoClient = new MongoClient(this.config.url, {
324
- maxPoolSize: this.config.maxPoolSize || 10,
325
- connectTimeoutMS: this.config.connectTimeoutMS || 10000,
326
- socketTimeoutMS: this.config.socketTimeoutMS || 45000
327
- })
328
-
329
- await this.mongoClient.connect()
330
- this.db = this.mongoClient.db(this.database)
331
-
332
- // Test connection
333
- await this.db.command({ ping: 1 })
334
-
335
- // Check if MongoDB supports transactions (requires replica set)
336
- try {
337
- const serverInfo = await this.db.admin().serverInfo()
338
- this.supportsTransactions = serverInfo.repl?.replSetName !== undefined
339
-
340
- if (!this.supportsTransactions) {
341
- console.warn('⚠️ MongoDB is running in standalone mode. Transactions will not work.')
342
- console.warn(' Consider setting up a replica set for transaction support.')
343
- console.warn(' Example: mongod --replSet rs0 --port 27017')
344
- }
345
- } catch (error) {
346
- console.warn('⚠️ Could not determine MongoDB deployment type:', error.message)
347
- this.supportsTransactions = false
348
- }
349
-
350
- // Initialize collections and indexes
351
- await this.initializeCollections()
352
-
353
- if (this.config.log?.includes('info')) {
354
- console.log('✅ Connected to MongoDB')
355
- }
356
- }
357
-
358
- async $disconnect(): Promise<void> {
359
- if (this.mongoClient) {
360
- await this.mongoClient.close()
361
- this.mongoClient = null
362
- this.db = null
363
-
364
- if (this.config.log?.includes('info')) {
365
- console.log('👋 Disconnected from MongoDB')
366
- }
367
- }
368
- }
369
-
370
- async $transaction<T>(callback: (tx: any) => Promise<T>): Promise<T> {
371
- if (!this.mongoClient) {
372
- throw new Error('Not connected to database')
373
- }
374
-
375
- // Check if transactions are supported
376
- if (!this.supportsTransactions) {
377
- throw new Error(
378
- 'Transactions are not supported in standalone MongoDB. ' +
379
- 'Set up a replica set or use alternative consistency patterns. ' +
380
- 'During development: mongod --replSet rs0 --port 27017'
381
- )
382
- }
383
-
384
- const session = this.mongoClient.startSession()
385
-
386
- try {
387
- session.startTransaction()
388
- const result = await callback(session)
389
- await session.commitTransaction()
390
- return result
391
- } catch (error) {
392
- await session.abortTransaction()
393
- throw error
394
- } finally {
395
- session.endSession()
396
- }
397
- }
398
-
399
- /**
400
- * Check if the connected MongoDB deployment supports transactions
401
- * Returns false if not connected or if running in standalone mode
402
- */
403
- $supportsTransactions(): boolean {
404
- return this.supportsTransactions
405
- }
406
-
407
- private async initializeCollections(): Promise<void> {
408
- if (!this.db || !this.config.autoCreateCollections) return
409
-
410
- const models = ${JSON.stringify(this.getModelsForRuntime(models), null, 2)}
411
-
412
- for (const model of models) {
413
- // Skip embedded models with empty collection names
414
- if (!model.collectionName) {
415
- continue
416
- }
417
-
418
- const collections = await this.db.listCollections({ name: model.collectionName }).toArray()
419
-
420
- if (collections.length === 0) {
421
- await this.db.createCollection(model.collectionName)
422
-
423
- // Create indexes
424
- if (model.indexes.length > 0) {
425
- const indexes = model.indexes.map(index => ({
426
- key: index.fields.reduce((acc, field) => {
427
- acc[field] = 1
428
- return acc
429
- }, {}),
430
- unique: index.unique,
431
- sparse: index.sparse || false
432
- }))
433
-
434
- try {
435
- await this.db.collection(model.collectionName).createIndexes(indexes)
436
- } catch (error) {
437
- console.warn(\`Failed to create indexes for \${model.name}:\`, error)
438
- }
439
- }
440
- }
441
- }
442
- }
443
-
444
- $isConnected(): boolean {
445
- return this.mongoClient !== null
446
- }
447
-
448
- get $db(): Db {
449
- if (!this.db) {
450
- throw new Error('Database not connected. Call $connect() first.')
451
- }
452
- return this.db
453
- }
454
-
455
- get $mongo(): { client: MongoClient; ObjectId: any } {
456
- if (!this.mongoClient) {
457
- throw new Error('Database not connected. Call $connect() first.')
458
- }
459
- return {
460
- client: this.mongoClient,
461
- ObjectId: ObjectId
462
- }
463
- }
464
- }
465
- `;
466
- }
467
- getModelsForRuntime(models) {
468
- return models.map(model => ({
469
- name: model.name,
470
- collectionName: model.collectionName,
471
- indexes: model.indexes.map(index => ({
472
- fields: index.fields,
473
- unique: index.unique,
474
- sparse: index.sparse ?? false
475
- }))
476
- }));
477
- }
478
- generateTypes(models, enums) {
479
- return `// This file was auto-generated by Lenz. Do not edit manually.
480
- // @generated
481
-
482
- import { ObjectId } from 'mongodb'
483
-
484
- // Base types
485
- export type ScalarType = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID' | 'DateTime' | 'Date' | 'Json' | 'ObjectId'
486
- export type RelationType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany'
487
-
488
- // Enum types
489
- ${enums.map(e => `
490
- export enum ${e.name} {
491
- ${e.values.map(v => `${v} = '${v}'`).join(',\n ')}
492
- }`).join('\n\n')}
493
-
494
- // Model types
495
- ${models.map(model => this.generateModelType(model)).join('\n\n')}
496
-
497
- // Pagination types
498
- export interface PageInfo {
499
- hasNextPage: boolean
500
- hasPreviousPage: boolean
501
- startCursor?: string
502
- endCursor?: string
503
- }
504
-
505
- export interface Connection<T> {
506
- edges: Array<Edge<T>>
507
- pageInfo: PageInfo
508
- totalCount: number
509
- }
510
-
511
- export interface Edge<T> {
512
- node: T
513
- cursor: string
514
- }
515
-
516
- // Operation types
517
- export interface WhereInput<T = any> {
518
- id?: string | ObjectId
519
- AND?: WhereInput<T>[]
520
- OR?: WhereInput<T>[]
521
- NOT?: WhereInput<T>[]
522
- [key: string]: any
523
- }
524
-
525
- export interface OrderByInput {
526
- [key: string]: 'asc' | 'desc' | 1 | -1
527
- }
528
-
529
- export interface SelectInput {
530
- [key: string]: boolean | SelectInput
531
- }
532
-
533
- export interface IncludeInput {
534
- [key: string]: boolean | IncludeInput
535
- }
536
-
537
- export interface QueryOptions<T = any> {
538
- where?: WhereInput<T>
539
- select?: SelectInput
540
- include?: IncludeInput
541
- skip?: number
542
- take?: number
543
- orderBy?: OrderByInput | OrderByInput[]
544
- distinct?: string | string[]
545
- /** Cursor for pagination */
546
- cursor?: string | ObjectId
547
- }
548
-
549
- export interface CreateInput<T = any> {
550
- data: Partial<T>
551
- select?: SelectInput
552
- include?: IncludeInput
553
- }
554
-
555
- export interface UpdateInput<T = any> {
556
- where: WhereInput<T>
557
- data: Partial<T>
558
- select?: SelectInput
559
- include?: IncludeInput
560
- }
561
-
562
- export interface DeleteInput<T = any> {
563
- where: WhereInput<T>
564
- select?: SelectInput
565
- include?: IncludeInput
566
- }
567
-
568
- export interface UpsertInput<T = any> {
569
- where: WhereInput<T>
570
- create: Partial<T>
571
- update: Partial<T>
572
- select?: SelectInput
573
- include?: IncludeInput
574
- }
575
-
576
- // Pagination specific interfaces
577
- export interface OffsetPaginationArgs<T = any> extends QueryOptions<T> {
578
- page?: number
579
- perPage?: number
580
- }
581
-
582
- export interface CursorPaginationArgs<T = any> extends QueryOptions<T> {
583
- cursor?: string | ObjectId
584
- take?: number
585
- }
586
-
587
- export interface PaginatedResult<T> {
588
- data: T[]
589
- meta: {
590
- total: number
591
- page: number
592
- perPage: number
593
- totalPages: number
594
- hasNextPage: boolean
595
- hasPreviousPage: boolean
596
- }
597
- }
598
-
599
- export interface CursorPaginatedResult<T> {
600
- edges: Array<{
601
- node: T
602
- cursor: string
603
- }>
604
- pageInfo: {
605
- hasNextPage: boolean
606
- hasPreviousPage: boolean
607
- startCursor?: string
608
- endCursor?: string
609
- }
610
- totalCount: number
611
- }
612
-
613
- // Config types
614
- export interface LenzConfig {
615
- url?: string
616
- schemaPath?: string
617
- log?: ('query' | 'error' | 'warn' | 'info')[]
618
- autoCreateCollections?: boolean
619
- maxPoolSize?: number
620
- connectTimeoutMS?: number
621
- socketTimeoutMS?: number
622
- }
623
-
624
- // Utility types
625
- export type DeepPartial<T> = {
626
- [P in keyof T]?: T[P] extends Array<infer U>
627
- ? Array<DeepPartial<U>>
628
- : T[P] extends ReadonlyArray<infer U>
629
- ? ReadonlyArray<DeepPartial<U>>
630
- : DeepPartial<T[P]> | T[P]
631
- }
632
-
633
- export type WithId<T> = T & { id: string }
634
- export type OptionalId<T> = Omit<T, 'id'> & { id?: string }
635
- `;
636
- }
637
- generateModelType(model) {
638
- return `export interface ${model.name} {
639
- id: string
640
- ${model.fields.filter(f => !f.isId).map(field => {
641
- const tsType = this.mapToTSType(field.type, field.isArray);
642
- return ` ${field.name}${field.isRequired ? '' : '?'}: ${tsType}`;
643
- }).join('\n')}
644
- }
645
-
646
- export interface ${model.name}CreateInput {
647
- ${model.fields.filter(f => !f.isId && !f.directives.includes('@generated')).map(field => {
648
- const tsType = this.mapToTSType(field.type, field.isArray);
649
- return ` ${field.name}${field.isRequired ? '' : '?'}: ${tsType}`;
650
- }).join('\n')}
651
- }
652
-
653
- export interface ${model.name}UpdateInput {
654
- ${model.fields.filter(f => !f.isId).map(field => {
655
- const tsType = this.mapToTSType(field.type, field.isArray);
656
- return ` ${field.name}?: ${tsType}`;
657
- }).join('\n')}
658
- }
659
-
660
- export type ${model.name}WhereInput = WhereInput<${model.name}>
661
- export type ${model.name}QueryOptions = QueryOptions<${model.name}>
662
- export type ${model.name}CreateArgs = CreateInput<${model.name}>
663
- export type ${model.name}UpdateArgs = UpdateInput<${model.name}>
664
- export type ${model.name}DeleteArgs = DeleteInput<${model.name}>
665
- export type ${model.name}UpsertArgs = UpsertInput<${model.name}>`;
666
- }
667
- mapToTSType(type, isArray) {
668
- let baseType = this.typeMap[type] || type;
669
- if (isArray) {
670
- return `${baseType}[]`;
671
- }
672
- return baseType;
673
- }
674
- generateEnums(enums) {
675
- if (enums.length === 0) {
676
- return '// No enums defined in schema\n\nexport {}';
677
- }
678
- return `// This file was auto-generated by Lenz. Do not edit manually.
679
- // @generated
680
-
681
- ${enums.map(e => `
682
- export const ${e.name} = {
683
- ${e.values.map(v => ` ${v}: '${v}',`).join('\n')}
684
- } as const
685
-
686
- export type ${e.name} = typeof ${e.name}[keyof typeof ${e.name}]
687
- `).join('\n')}`;
688
- }
689
- generateRuntimePagination() {
690
- return `// This file was auto-generated by Lenz. Do not edit manually.
691
- // @generated
692
-
693
- import { ObjectId } from 'mongodb'
694
-
695
- /**
696
- * Pagination helper for Lenz ORM
697
- * Implements both offset-based and cursor-based pagination
698
- * Similar to Prisma's pagination patterns
699
- */
700
- export class PaginationHelper {
701
- /**
702
- * Create a cursor from a document
703
- * Uses the document's _id by default
704
- */
705
- static createCursor(doc: any): string {
706
- if (!doc) throw new Error('Cannot create cursor from null document')
707
-
708
- // Use _id if available, otherwise id
709
- const id = doc._id || doc.id
710
- if (!id) throw new Error('Document must have an id to create cursor')
711
-
712
- // Base64 encode for cursor
713
- return Buffer.from(id.toString()).toString('base64')
714
- }
715
-
716
- /**
717
- * Parse cursor to get the id
718
- */
719
- static parseCursor(cursor: string): string {
720
- try {
721
- return Buffer.from(cursor, 'base64').toString('utf8')
722
- } catch (error) {
723
- throw new Error('Invalid cursor format')
724
- }
725
- }
726
-
727
- /**
728
- * Build MongoDB filter for cursor-based pagination
729
- * Assumes ordering by _id unless specified otherwise
730
- */
731
- static buildCursorFilter(
732
- cursor: string,
733
- orderBy: any = { _id: 'asc' },
734
- direction: 'forward' | 'backward' = 'forward'
735
- ): any {
736
- const cursorId = this.parseCursor(cursor)
737
-
738
- // For simplicity, we'll handle single field ordering
739
- const orderField = Object.keys(orderBy)[0] || '_id'
740
- const orderDirection = orderBy[orderField] || 'asc'
741
-
742
- const isAscending = orderDirection === 'asc' || orderDirection === 1
743
- const isForward = direction === 'forward'
744
-
745
- // Build comparison operator based on direction and order
746
- let operator: string
747
- if (isForward) {
748
- operator = isAscending ? '$gt' : '$lt'
749
- } else {
750
- operator = isAscending ? '$lt' : '$gt'
751
- }
752
-
753
- return {
754
- [orderField]: { [operator]: new ObjectId(cursorId) }
755
- }
756
- }
757
-
758
- /**
759
- * Calculate skip for offset pagination
760
- */
761
- static calculateSkip(page: number, perPage: number): number {
762
- if (page < 1) throw new Error('Page must be greater than 0')
763
- return (page - 1) * perPage
764
- }
765
-
766
- /**
767
- * Calculate total pages
768
- */
769
- static calculateTotalPages(total: number, perPage: number): number {
770
- return Math.ceil(total / perPage)
771
- }
772
-
773
- /**
774
- * Get pagination metadata
775
- */
776
- static getPaginationMeta(
777
- total: number,
778
- page: number,
779
- perPage: number,
780
- dataLength: number
781
- ): {
782
- total: number
783
- page: number
784
- perPage: number
785
- totalPages: number
786
- hasNextPage: boolean
787
- hasPreviousPage: boolean
788
- } {
789
- const totalPages = this.calculateTotalPages(total, perPage)
790
-
791
- return {
792
- total,
793
- page,
794
- perPage,
795
- totalPages,
796
- hasNextPage: page < totalPages,
797
- hasPreviousPage: page > 1
798
- }
799
- }
800
-
801
- /**
802
- * Format results for GraphQL-style connection
803
- */
804
- static toConnection<T>(
805
- data: T[],
806
- totalCount: number,
807
- hasNextPage: boolean,
808
- hasPreviousPage: boolean,
809
- createCursor: (item: T) => string = (item: any) => this.createCursor(item)
810
- ): {
811
- edges: Array<{ node: T; cursor: string }>
812
- pageInfo: {
813
- hasNextPage: boolean
814
- hasPreviousPage: boolean
815
- startCursor?: string
816
- endCursor?: string
817
- }
818
- totalCount: number
819
- } {
820
- const edges = data.map(item => ({
821
- node: item,
822
- cursor: createCursor(item)
823
- }))
824
-
825
- return {
826
- edges,
827
- pageInfo: {
828
- hasNextPage,
829
- hasPreviousPage,
830
- startCursor: edges[0]?.cursor,
831
- endCursor: edges[edges.length - 1]?.cursor
832
- },
833
- totalCount
834
- }
835
- }
836
-
837
- /**
838
- * Simple offset pagination
839
- */
840
- static paginate<T>(
841
- data: T[],
842
- page: number = 1,
843
- perPage: number = 10
844
- ): {
845
- data: T[]
846
- meta: {
847
- page: number
848
- perPage: number
849
- total: number
850
- totalPages: number
851
- hasNextPage: boolean
852
- hasPreviousPage: boolean
853
- }
854
- } {
855
- const startIndex = (page - 1) * perPage
856
- const endIndex = startIndex + perPage
857
- const paginatedData = data.slice(startIndex, endIndex)
858
- const total = data.length
859
- const totalPages = Math.ceil(total / perPage)
860
-
861
- return {
862
- data: paginatedData,
863
- meta: {
864
- page,
865
- perPage,
866
- total,
867
- totalPages,
868
- hasNextPage: page < totalPages,
869
- hasPreviousPage: page > 1
870
- }
871
- }
872
- }
873
- }
874
- `;
875
- }
876
- generateRuntimeIndex() {
877
- return `// This file was auto-generated by Lenz. Do not edit manually.
878
- // @generated
879
-
880
- export { QueryBuilder } from './query'
881
- export { PaginationHelper } from './pagination'
882
- export { RelationResolver } from './relations'
883
- `;
884
- }
885
- generateRuntimeQuery() {
886
- return `// This file was auto-generated by Lenz. Do not edit manually.
887
- // @generated
888
-
889
- import { ObjectId, Filter, UpdateFilter } from 'mongodb'
890
- import type { WhereInput, QueryOptions, SelectInput } from '../types'
891
- import { PaginationHelper } from './pagination'
892
-
893
- export class QueryBuilder {
894
- static buildWhere<T>(where: WhereInput<T>): Filter<any> {
895
- const filter: Filter<any> = {}
896
-
897
- for (const [key, value] of Object.entries(where || {})) {
898
- if (key === 'id') {
899
- if (typeof value === 'object' && value !== null) {
900
- // Для операторов типа { in: [...] } используем _id
901
- this.applyOperators(filter, '_id', value)
902
- } else {
903
- filter._id = this.normalizeId(value)
904
- }
905
- } else if (key === 'AND') {
906
- // Логический оператор AND
907
- if (value !== null && typeof value === 'object') {
908
- const conditions = Array.isArray(value) ? value : [value]
909
- filter.$and = conditions.map(cond => this.buildWhere(cond))
910
- }
911
- } else if (key === 'OR') {
912
- // Логический оператор OR
913
- if (value !== null && typeof value === 'object') {
914
- const conditions = Array.isArray(value) ? value : [value]
915
- filter.$or = conditions.map(cond => this.buildWhere(cond))
916
- }
917
- } else if (key === 'NOT') {
918
- // Логический оператор NOT
919
- if (value !== null && typeof value === 'object') {
920
- // NOT может быть объектом условий (массив не поддерживается)
921
- filter.$not = this.buildWhere(value)
922
- }
923
- } else if (typeof value === 'object' && value !== null) {
924
- this.applyOperators(filter, key, value)
925
- } else {
926
- // Оставляем значение как есть для внешних ключей
927
- // (внешние ключи хранятся как строки, не преобразуем в ObjectId)
928
- filter[key] = value
929
- }
930
- }
931
-
932
- return filter
933
- }
934
-
935
- /**
936
- * Build cursor condition for pagination
937
- */
938
- static buildCursorCondition(
939
- cursor: string | ObjectId,
940
- orderBy: any = { _id: 'asc' }
941
- ): Filter<any> {
942
- const cursorId = typeof cursor === 'string'
943
- ? PaginationHelper.parseCursor(cursor)
944
- : cursor.toString()
945
-
946
- // For simple _id ordering
947
- if (orderBy._id || (!Object.keys(orderBy).length)) {
948
- return {
949
- _id: { $gt: new ObjectId(cursorId) }
950
- }
951
- }
952
-
953
- // For other field ordering (simplified implementation)
954
- // In real implementation, you'd need to know the value at the cursor
955
- const orderField = Object.keys(orderBy)[0]
956
- const orderDirection = orderBy[orderField]
957
-
958
- // Note: This is a simplified version
959
- // Full implementation requires fetching the cursor document
960
- return {
961
- [orderField]: orderDirection === 'desc'
962
- ? { $lt: cursorId }
963
- : { $gt: cursorId }
964
- }
965
- }
966
-
967
- /**
968
- * Build MongoDB projection object from select input and hidden fields
969
- */
970
- static buildProjection<T>(
971
- select: SelectInput | undefined,
972
- hiddenFields: string[] = []
973
- ): any {
974
- if (!select) {
975
- // По умолчанию: исключаем скрытые поля
976
- if (hiddenFields.length === 0) return undefined;
977
- const projection: any = {};
978
- hiddenFields.forEach(field => {
979
- projection[field] = 0;
980
- });
981
- return projection;
982
- }
983
-
984
- const projection: any = {};
985
- const processSelect = (sel: SelectInput, prefix = '') => {
986
- for (const [key, value] of Object.entries(sel)) {
987
- const fullPath = prefix ? \`\${prefix}.\${key}\` : key;
988
-
989
- if (typeof value === 'boolean') {
990
- // Базовое поле: true - включать, false - исключать
991
- projection[fullPath] = value ? 1 : 0;
992
- } else {
993
- // Вложенный объект (отношение)
994
- processSelect(value, fullPath);
995
- }
996
- }
997
- };
998
-
999
- processSelect(select);
1000
-
1001
- // Убедимся, что скрытые поля исключены, если не указано явно
1002
- hiddenFields.forEach(field => {
1003
- if (projection[field] === undefined) {
1004
- projection[field] = 0;
1005
- }
1006
- });
1007
-
1008
- return projection;
1009
- }
1010
-
1011
- static buildOptions<T>(options: QueryOptions<T>, hiddenFields: string[] = []): any {
1012
- const result: any = {}
1013
-
1014
- if (options.skip !== undefined) result.skip = options.skip
1015
- if (options.take !== undefined) result.limit = options.take
1016
-
1017
- if (options.orderBy) {
1018
- result.sort = this.buildSort(options.orderBy)
1019
- }
1020
-
1021
- // Добавить проекцию, если есть select или скрытые поля
1022
- const projection = this.buildProjection(options.select, hiddenFields)
1023
- if (projection) {
1024
- result.projection = projection
1025
- }
1026
-
1027
- return result
1028
- }
1029
-
1030
- static buildSort(orderBy: any): any {
1031
- if (Array.isArray(orderBy)) {
1032
- return orderBy.reduce((acc, curr) => ({ ...acc, ...this.buildSort(curr) }), {})
1033
- }
1034
-
1035
- const sort: any = {}
1036
- for (const [field, direction] of Object.entries(orderBy)) {
1037
- if (direction === 'asc' || direction === 1) {
1038
- sort[field] = 1
1039
- } else if (direction === 'desc' || direction === -1) {
1040
- sort[field] = -1
1041
- }
1042
- }
1043
- return sort
1044
- }
1045
-
1046
- private static applyOperators(filter: any, field: string, operators: any): void {
1047
- const mongoOperators: Record<string, string> = {
1048
- equals: '$eq',
1049
- not: '$ne',
1050
- in: '$in',
1051
- notIn: '$nin',
1052
- lt: '$lt',
1053
- lte: '$lte',
1054
- gt: '$gt',
1055
- gte: '$gte',
1056
- contains: '$regex',
1057
- startsWith: '$regex',
1058
- endsWith: '$regex'
1059
- }
1060
-
1061
- for (const [op, value] of Object.entries(operators)) {
1062
- const mongoOp = mongoOperators[op]
1063
-
1064
- if (mongoOp) {
1065
- if (op === 'contains') {
1066
- filter[field] = { $regex: value, $options: 'i' }
1067
- } else if (op === 'startsWith') {
1068
- filter[field] = { $regex: \`^\${value}\`, $options: 'i' }
1069
- } else if (op === 'endsWith') {
1070
- filter[field] = { $regex: \`\${value}\$\`, $options: 'i' }
1071
- } else {
1072
- if (!filter[field]) filter[field] = {}
1073
- // Нормализуем ID только для поля _id (внутренний идентификатор)
1074
- // Внешние ключи хранятся как строки, не преобразуем в ObjectId
1075
- const normalizedValue = field === '_id' ? this.normalizeId(value) : value
1076
- filter[field][mongoOp] = normalizedValue
1077
- }
1078
- }
1079
- }
1080
- }
1081
-
1082
- static normalizeId(id: string | ObjectId | (string | ObjectId)[]): ObjectId | string | (ObjectId | string)[] {
1083
- try {
1084
- if (Array.isArray(id)) {
1085
- return id.map(item => this.normalizeId(item) as ObjectId | string)
1086
- }
1087
- if (typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id)) {
1088
- return new ObjectId(id)
1089
- }
1090
- return id
1091
- } catch {
1092
- return id
1093
- }
1094
- }
1095
-
1096
- static buildUpdate(data: any): UpdateFilter<any> {
1097
- const update: UpdateFilter<any> = {}
1098
-
1099
- const setOperations: any = {}
1100
- const mongoOperators: any = {}
1101
-
1102
- const arrayOperators = ['push', 'pull', 'addToSet', 'pop', 'pullAll', 'pushAll'];
1103
- const operatorMap: Record<string, string> = {
1104
- push: '$push',
1105
- pull: '$pull',
1106
- addToSet: '$addToSet',
1107
- pop: '$pop',
1108
- pullAll: '$pullAll',
1109
- pushAll: '$pushAll'
1110
- };
1111
-
1112
- for (const [key, value] of Object.entries(data)) {
1113
- if (key.startsWith('$')) {
1114
- // Already a MongoDB operator
1115
- mongoOperators[key] = value
1116
- } else {
1117
- // Check if value is an array operator object
1118
- if (typeof value === 'object' && value !== null) {
1119
- const keys = Object.keys(value)
1120
-
1121
- // Handle { push: value } or { addToSet: { each: [...] } }
1122
- if (keys.length === 1 && arrayOperators.includes(keys[0])) {
1123
- const op = keys[0]
1124
- const opValue = value[op]
1125
-
1126
- // Check if opValue is an object with 'each' key (for $each support)
1127
- if (op === 'push' || op === 'addToSet') {
1128
- if (typeof opValue === 'object' && opValue !== null && Object.keys(opValue).length === 1 && opValue.each !== undefined) {
1129
- // Convert { push: { each: [...] } } to { $push: { $each: [...] } }
1130
- const mongoOp = operatorMap[op]
1131
- if (!mongoOperators[mongoOp]) {
1132
- mongoOperators[mongoOp] = {}
1133
- }
1134
- mongoOperators[mongoOp][key] = { $each: opValue.each }
1135
- continue
1136
- }
1137
- }
1138
-
1139
- // Regular array operator
1140
- const mongoOp = operatorMap[op]
1141
- if (!mongoOperators[mongoOp]) {
1142
- mongoOperators[mongoOp] = {}
1143
- }
1144
- mongoOperators[mongoOp][key] = opValue
1145
- continue
1146
- }
1147
-
1148
- // Handle { push: { each: [...] } } where value is { each: [...] } (already handled above)
1149
- // Also check for nested objects with dot notation? Not supported.
1150
- }
1151
-
1152
- // Otherwise treat as $set
1153
- setOperations[key] = value
1154
- }
1155
- }
1156
-
1157
- if (Object.keys(setOperations).length > 0) {
1158
- update.$set = setOperations
1159
- }
1160
-
1161
- Object.assign(update, mongoOperators)
1162
-
1163
- return update
1164
- }
1165
- }
1166
- `;
1167
- }
1168
- generateRuntimeRelations() {
1169
- return `// This file was auto-generated by Lenz. Do not edit manually.
1170
- // @generated
1171
-
1172
- import { Db, ObjectId } from 'mongodb'
1173
- import { QueryBuilder } from './query'
1174
-
1175
- export class RelationResolver {
1176
- static async resolveOneToOne(
1177
- db: Db,
1178
- sourceCollection: string,
1179
- targetCollection: string,
1180
- sourceId: string,
1181
- foreignKey: string
1182
- ): Promise<any> {
1183
- const collection = db.collection(targetCollection)
1184
- return await collection.findOne({ [foreignKey]: sourceId })
1185
- }
1186
-
1187
- static async resolveOneToMany(
1188
- db: Db,
1189
- sourceCollection: string,
1190
- targetCollection: string,
1191
- sourceId: string,
1192
- foreignKey: string
1193
- ): Promise<any[]> {
1194
- const collection = db.collection(targetCollection)
1195
- return await collection.find({ [foreignKey]: sourceId }).toArray()
1196
- }
1197
-
1198
- static async resolveManyToMany(
1199
- db: Db,
1200
- sourceCollection: string,
1201
- targetCollection: string,
1202
- joinCollection: string,
1203
- sourceId: string,
1204
- where?: any,
1205
- orderBy?: any,
1206
- take?: number,
1207
- skip?: number,
1208
- select?: any
1209
- ): Promise<any[]> {
1210
- const joinCol = db.collection(joinCollection)
1211
- const targetCol = db.collection(targetCollection)
1212
-
1213
- const connections = await joinCol.find({
1214
- [\`\${sourceCollection.toLowerCase()}Id\`]: sourceId
1215
- }).toArray()
1216
-
1217
- const targetIds = connections.map(c => c[\`\${targetCollection.toLowerCase()}Id\`])
1218
-
1219
- if (targetIds.length === 0) {
1220
- return []
1221
- }
1222
-
1223
- let query: any = { _id: { $in: targetIds.map(id => new ObjectId(id)) } }
1224
-
1225
- // Apply where filter if provided
1226
- if (where && Object.keys(where).length > 0) {
1227
- // Merge where with the existing $in condition
1228
- query = { $and: [query, where] }
1229
- }
1230
-
1231
- let cursor = targetCol.find(query)
1232
-
1233
- // Apply sorting
1234
- if (orderBy) {
1235
- cursor = cursor.sort(orderBy)
1236
- }
1237
-
1238
- // Apply skip
1239
- if (skip !== undefined && skip !== null) {
1240
- cursor = cursor.skip(skip)
1241
- }
1242
-
1243
- // Apply limit
1244
- if (take !== undefined && take !== null) {
1245
- cursor = cursor.limit(take)
1246
- }
1247
-
1248
- // Apply projection if select provided
1249
- if (select) {
1250
- const projection = QueryBuilder.buildProjection(select, [])
1251
- if (projection) {
1252
- cursor = cursor.project(projection)
1253
- }
1254
- }
1255
-
1256
- return await cursor.toArray()
1257
- }
1258
-
1259
- static formatDocument(doc: any): any {
1260
- if (!doc) return doc
1261
-
1262
- const formatted = { ...doc }
1263
- if (formatted._id) {
1264
- formatted.id = formatted._id.toString()
1265
- delete formatted._id
1266
- }
1267
-
1268
- return formatted
1269
- }
1270
- }
1271
- `;
1272
- }
1273
- generateModelsIndex(models) {
1274
- return models.map(model => `export { ${model.name}Delegate } from './${model.name}'`).join('\n');
1275
- }
1276
- generateModelFiles(models) {
1277
- const files = {};
1278
- for (const model of models) {
1279
- files[`models/${model.name}.ts`] = this.generateModelDelegate(model);
1280
- }
1281
- return files;
1282
- }
1283
- generateCascadeMethod(model) {
1284
- const cascadeRelations = model.relations.filter(r => r.onDelete !== 'NoAction');
1285
- if (cascadeRelations.length === 0)
1286
- return '';
1287
- const lines = [];
1288
- lines.push(` private async handleCascadeDelete(doc: any): Promise<void> {`);
1289
- for (const rel of cascadeRelations) {
1290
- lines.push(` // Relation: ${rel.field} (${rel.type}) - onDelete: ${rel.onDelete}`);
1291
- if (rel.onDelete === 'Cascade') {
1292
- if (rel.isForeignKeyArray) {
1293
- // FK is an array of IDs in source doc
1294
- lines.push(` if (doc.${rel.foreignKey} && Array.isArray(doc.${rel.foreignKey}) && doc.${rel.foreignKey}.length > 0) {`);
1295
- lines.push(` await this.client.${this.toCamelCase(rel.target)}.deleteMany({ where: { id: { in: doc.${rel.foreignKey} } } });`);
1296
- lines.push(` }`);
1297
- }
1298
- else if (rel.foreignKeyLocation === 'source' || !rel.foreignKeyLocation) {
1299
- // FK is a single ID in source doc (manyToOne, oneToOne)
1300
- const isSingleResult = rel.type === 'manyToOne' || rel.type === 'oneToOne';
1301
- lines.push(` if (doc.${rel.foreignKey}) {`);
1302
- if (isSingleResult) {
1303
- lines.push(` await this.client.${this.toCamelCase(rel.target)}.delete({ where: { id: doc.${rel.foreignKey} } });`);
1304
- }
1305
- else {
1306
- lines.push(` await this.client.${this.toCamelCase(rel.target)}.deleteMany({ where: { id: doc.${rel.foreignKey} } });`);
1307
- }
1308
- lines.push(` }`);
1309
- }
1310
- else {
1311
- // FK is in target docs (oneToMany with FK in target)
1312
- lines.push(` await this.client.${this.toCamelCase(rel.target)}.deleteMany({ where: { ${rel.foreignKey}: doc.id } });`);
1313
- }
1314
- }
1315
- else if (rel.onDelete === 'SetNull') {
1316
- if (rel.foreignKeyLocation === 'source' || rel.isForeignKeyArray) {
1317
- // FK is in source that's being deleted — no-op for SetNull
1318
- lines.push(` // FK '${rel.foreignKey}' is in the source document being deleted — no action needed`);
1319
- }
1320
- else {
1321
- // FK is in target docs — nullify it
1322
- if (rel.isForeignKeyArray) {
1323
- // Pull doc.id from target's FK array
1324
- lines.push(` await this.client.$db.collection('${this.toCollectionName(rel.target)}').updateMany(`);
1325
- lines.push(` { ${rel.foreignKey}: doc.id },`);
1326
- lines.push(` { $pull: { ${rel.foreignKey}: doc.id } }`);
1327
- lines.push(` );`);
1328
- }
1329
- else {
1330
- // Set single FK to null
1331
- lines.push(` await this.client.${this.toCamelCase(rel.target)}.updateMany({`);
1332
- lines.push(` where: { ${rel.foreignKey}: doc.id },`);
1333
- lines.push(` data: { ${rel.foreignKey}: null }`);
1334
- lines.push(` });`);
1335
- }
1336
- }
1337
- }
1338
- }
1339
- lines.push(` }`);
1340
- return lines.join('\n');
1341
- }
1342
- generateModelDelegate(model) {
1343
- const hasCascade = model.relations.some(r => r.onDelete !== 'NoAction');
1344
- const cascadeMethod = this.generateCascadeMethod(model);
1345
- const cascadeDeleteCall = hasCascade ? `\n await this.handleCascadeDelete(doc)` : '';
1346
- const cascadeDeleteManyBody = hasCascade
1347
- ? ` const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1348
- const docs = await this.collection.find(query).toArray()
1349
- for (const doc of docs) {
1350
- await this.handleCascadeDelete(doc)
1351
- }
1352
- const result = await this.collection.deleteMany(query)
1353
- return { count: result.deletedCount }`
1354
- : ` const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1355
- const result = await this.collection.deleteMany(query)
1356
- return { count: result.deletedCount }`;
1357
- return `// This file was auto-generated by Lenz. Do not edit manually.
1358
- // @generated
1359
-
1360
- import { Collection, ObjectId, Document } from 'mongodb'
1361
- import type {
1362
- ${model.name},
1363
- ${model.name}CreateInput,
1364
- ${model.name}UpdateInput,
1365
- ${model.name}WhereInput,
1366
- ${model.name}QueryOptions,
1367
- ${model.name}CreateArgs,
1368
- ${model.name}UpdateArgs,
1369
- ${model.name}DeleteArgs,
1370
- ${model.name}UpsertArgs
1371
- } from '../types'
1372
- import { QueryBuilder } from '../runtime/query'
1373
- import { PaginationHelper } from '../runtime/pagination'
1374
- import { RelationResolver } from '../runtime/relations'
1375
- import type { LenzClient } from '../client'
1376
-
1377
- export class ${model.name}Delegate {
1378
- constructor(private client: LenzClient) {}
1379
-
1380
- private readonly hiddenFields: string[] = ${JSON.stringify(model.fields.filter(f => f.isHidden).map(f => f.name))};
1381
-
1382
- private readonly defaultValues = {
1383
- ${model.fields.filter(f => f.defaultValue !== undefined || (f.isRequired && f.isArray && !f.isId)).map(f => `${f.name}: ${JSON.stringify(f.defaultValue !== undefined ? f.defaultValue : [])}`).join(',\n ')}
1384
- };
1385
-
1386
-
1387
- private get collection(): Collection<Document> {
1388
- return this.client.$db.collection('${model.collectionName}')
1389
- }
1390
-
1391
- async findUnique(args: { where: ${model.name}WhereInput } & ${model.name}QueryOptions): Promise<${model.name} | null> {
1392
- const query = QueryBuilder.buildWhere(args.where)
1393
- const options = QueryBuilder.buildOptions(args, this.hiddenFields)
1394
-
1395
- const doc = await this.collection.findOne(query, options)
1396
- if (!doc) return null
1397
-
1398
- const formatted = RelationResolver.formatDocument(doc)
1399
-
1400
- if (args.include) {
1401
- return await this.includeRelations(formatted, args.include)
1402
- }
1403
-
1404
- return formatted
1405
- }
1406
-
1407
- async findMany(args?: ${model.name}QueryOptions): Promise<${model.name}[]> {
1408
- const { cursor, ...otherArgs } = args || {}
1409
- let where = args?.where || {}
1410
-
1411
- // Handle cursor-based pagination
1412
- if (cursor) {
1413
- const cursorCondition = QueryBuilder.buildCursorCondition(cursor, args?.orderBy)
1414
- where = {
1415
- ...where,
1416
- ...cursorCondition
1417
- }
1418
- }
1419
-
1420
- const query = QueryBuilder.buildWhere(where)
1421
- const options = QueryBuilder.buildOptions(otherArgs || {}, this.hiddenFields)
1422
-
1423
- const mongoCursor = this.collection.find(query, options)
1424
- const docs = await mongoCursor.toArray()
1425
- const formatted = docs.map(RelationResolver.formatDocument)
1426
-
1427
- if (args?.include) {
1428
- return await Promise.all(
1429
- formatted.map(doc => this.includeRelations(doc, args.include!))
1430
- )
1431
- }
1432
-
1433
- return formatted
1434
- }
1435
-
1436
- async findFirst(args?: ${model.name}QueryOptions): Promise<${model.name} | null> {
1437
- const results = await this.findMany({ ...args, take: 1 })
1438
- return results[0] || null
1439
- }
1440
-
1441
- async create(args: ${model.name}CreateArgs): Promise<${model.name}> {
1442
- const now = new Date()
1443
- const document = {
1444
- ...this.defaultValues,
1445
- ...args.data,
1446
- _id: new ObjectId(),
1447
- createdAt: now,
1448
- updatedAt: now
1449
- }
1450
-
1451
- const result = await this.collection.insertOne(document)
1452
- const createdDoc = await this.collection.findOne({ _id: result.insertedId })
1453
-
1454
- if (!createdDoc) {
1455
- throw new Error('Failed to create document')
1456
- }
1457
-
1458
- const formatted = RelationResolver.formatDocument(createdDoc)
1459
-
1460
- if (args.include) {
1461
- return await this.includeRelations(formatted, args.include)
1462
- }
1463
-
1464
- return formatted
1465
- }
1466
-
1467
- async createMany(args: { data: ${model.name}CreateInput[] }): Promise<{ count: number }> {
1468
- const now = new Date()
1469
- const documents = args.data.map(data => ({
1470
- ...this.defaultValues,
1471
- ...data,
1472
- _id: new ObjectId(),
1473
- createdAt: now,
1474
- updatedAt: now
1475
- }))
1476
-
1477
- const result = await this.collection.insertMany(documents)
1478
- return { count: result.insertedCount }
1479
- }
1480
-
1481
- async update(args: ${model.name}UpdateArgs): Promise<${model.name}> {
1482
- const query = QueryBuilder.buildWhere(args.where)
1483
-
1484
- const updateData = {
1485
- ...args.data,
1486
- updatedAt: new Date()
1487
- }
1488
-
1489
- const update = QueryBuilder.buildUpdate(updateData)
1490
- const result = await this.collection.findOneAndUpdate(
1491
- query,
1492
- update,
1493
- { returnDocument: 'after' }
1494
- )
1495
-
1496
- const updatedDoc = result.value || result
1497
- if (!updatedDoc) {
1498
- throw new Error('Document not found')
1499
- }
1500
-
1501
- const formatted = RelationResolver.formatDocument(updatedDoc)
1502
-
1503
- if (args.include) {
1504
- return await this.includeRelations(formatted, args.include)
1505
- }
1506
-
1507
- return formatted
1508
- }
1509
-
1510
- async updateMany(args: { where?: ${model.name}WhereInput; data: ${model.name}UpdateInput }): Promise<{ count: number }> {
1511
- const query = args.where ? QueryBuilder.buildWhere(args.where) : {}
1512
- const updateData = {
1513
- ...args.data,
1514
- updatedAt: new Date()
1515
- }
1516
-
1517
- const update = QueryBuilder.buildUpdate(updateData)
1518
- const result = await this.collection.updateMany(query, update)
1519
- return { count: result.modifiedCount }
1520
- }
1521
-
1522
- async upsert(args: ${model.name}UpsertArgs): Promise<${model.name}> {
1523
- const query = QueryBuilder.buildWhere(args.where)
1524
- const existing = await this.collection.findOne(query)
1525
-
1526
- if (existing) {
1527
- return this.update({
1528
- where: args.where,
1529
- data: args.update,
1530
- select: args.select,
1531
- include: args.include
1532
- })
1533
- } else {
1534
- return this.create({
1535
- data: args.create,
1536
- select: args.select,
1537
- include: args.include
1538
- })
1539
- }
1540
- }
1541
-
1542
- async delete(args: ${model.name}DeleteArgs): Promise<${model.name} | null> {
1543
- const query = QueryBuilder.buildWhere(args.where)
1544
- const doc = await this.collection.findOne(query)
1545
-
1546
- if (!doc) return null
1547
- ${cascadeDeleteCall}
1548
- await this.collection.deleteOne(query)
1549
- const formatted = RelationResolver.formatDocument(doc)
1550
-
1551
- if (args.include) {
1552
- return await this.includeRelations(formatted, args.include)
1553
- }
1554
-
1555
- return formatted
1556
- }
1557
-
1558
- async deleteMany(args: { where?: ${model.name}WhereInput }): Promise<{ count: number }> {
1559
- ${cascadeDeleteManyBody}
1560
- }
1561
-
1562
- async count(args?: { where?: ${model.name}WhereInput }): Promise<number> {
1563
- const query = args?.where ? QueryBuilder.buildWhere(args.where) : {}
1564
- return await this.collection.countDocuments(query)
1565
- }
1566
-
1567
- async aggregate<T = any>(pipeline: any[]): Promise<T[]> {
1568
- return await this.collection.aggregate(pipeline).toArray() as T[]
1569
- }
1570
-
1571
-
1572
-
1573
-
1574
-
1575
- private applySelect(document: any, select: any): any {
1576
- if (!select) {
1577
- // If no select, exclude hidden fields by default
1578
- if (this.hiddenFields.length === 0) return document;
1579
- const result = { ...document };
1580
- this.hiddenFields.forEach(field => {
1581
- delete result[field];
1582
- });
1583
- return result;
1584
- }
1585
-
1586
- // Build projection using QueryBuilder
1587
- const projection = QueryBuilder.buildProjection(select, this.hiddenFields);
1588
- if (!projection) return document;
1589
-
1590
- const result = { ...document };
1591
- // Apply projection (simplified - only top-level fields)
1592
- for (const [field, value] of Object.entries(projection)) {
1593
- if (value === 0 && field in result) {
1594
- delete result[field];
1595
- }
1596
- // If value === 1, keep the field (already present)
1597
- }
1598
- return result;
1599
- }
1600
-
1601
- public async includeRelations(document: any, include: any): Promise<any> {
1602
- const result = { ...document }
1603
- if (!include || typeof include !== 'object') {
1604
- return result
1605
- }
1606
-
1607
- ${this.generateRelationInclusionCode(model)}
1608
-
1609
- return result
1610
- }
1611
-
1612
- // Raw access
1613
- get $raw() {
1614
- return {
1615
- collection: this.collection,
1616
- find: async (filter: any) => await this.collection.find(filter).toArray(),
1617
- findOne: async (filter: any) => await this.collection.findOne(filter),
1618
- insertOne: async (doc: any) => await this.collection.insertOne(doc),
1619
- updateOne: async (filter: any, update: any) => await this.collection.updateOne(filter, update),
1620
- deleteOne: async (filter: any) => await this.collection.deleteOne(filter),
1621
- aggregate: async (pipeline: any[]) => await this.collection.aggregate(pipeline).toArray()
1622
- }
1623
- }
1624
- }
1625
- ${cascadeMethod}
1626
- `;
1627
- }
1628
- generateRelationInclusionCode(model) {
1629
- const lines = [];
1630
- for (const relation of model.relations) {
1631
- if (relation.strategy === 'lookup') {
1632
- // Generate lookup (aggregation) based code
1633
- lines.push(` // Relation: ${relation.field} (${relation.type}) - lookup strategy`);
1634
- lines.push(` if (include.${relation.field} !== undefined) {`);
1635
- lines.push(` const includeOpts = include.${relation.field};`);
1636
- lines.push(` let whereFilter = {};`);
1637
- lines.push(` let sort = null;`);
1638
- lines.push(` let skip = null;`);
1639
- lines.push(` let limit = null;`);
1640
- lines.push(` let select = undefined;`);
1641
- lines.push(` let nestedInclude = undefined;`);
1642
- lines.push(` if (typeof includeOpts === 'object' && includeOpts !== null) {`);
1643
- lines.push(` // Process include options for lookup strategy`);
1644
- lines.push(` whereFilter = includeOpts.where ? QueryBuilder.buildWhere(includeOpts.where) : {};`);
1645
- lines.push(` sort = includeOpts.orderBy ? QueryBuilder.buildSort(includeOpts.orderBy) : null;`);
1646
- lines.push(` skip = includeOpts.skip !== undefined ? includeOpts.skip : null;`);
1647
- lines.push(` limit = includeOpts.take !== undefined ? includeOpts.take : null;`);
1648
- lines.push(` select = includeOpts.select;`);
1649
- lines.push(` nestedInclude = includeOpts.include;`);
1650
- lines.push(` }`);
1651
- // Determine local field (foreign key in source document)
1652
- const localField = relation.foreignKey;
1653
- if (!localField) {
1654
- // manyToMany with join collection (no foreign key array)
1655
- if (relation.type === 'manyToMany' && relation.joinCollection) {
1656
- // Use RelationResolver for join collection lookup
1657
- lines.push(` let ${relation.field}Result = await RelationResolver.resolveManyToMany(`);
1658
- lines.push(` this.client.$db,`);
1659
- lines.push(` '${model.collectionName}',`);
1660
- lines.push(` '${this.toCollectionName(relation.target)}',`);
1661
- lines.push(` '${relation.joinCollection}',`);
1662
- lines.push(` document.id,`);
1663
- lines.push(` whereFilter,`);
1664
- lines.push(` sort,`);
1665
- lines.push(` limit,`);
1666
- lines.push(` skip,`);
1667
- lines.push(` select`);
1668
- lines.push(` )`);
1669
- lines.push(` // Handle nested include`);
1670
- lines.push(` if (nestedInclude && ${relation.field}Result && Array.isArray(${relation.field}Result)) {`);
1671
- lines.push(` ${relation.field}Result = await Promise.all(${relation.field}Result.map(doc =>`);
1672
- lines.push(` this.client.${this.toCamelCase(relation.target)}.includeRelations(doc, nestedInclude)`);
1673
- lines.push(` ))`);
1674
- lines.push(` }`);
1675
- lines.push(` result.${relation.field} = ${relation.field}Result`);
1676
- }
1677
- else {
1678
- lines.push(` console.warn('lookup strategy not implemented for relation ${relation.field} without foreign key')`);
1679
- lines.push(` result.${relation.field} = []`);
1680
- }
1681
- }
1682
- else if (relation.isForeignKeyArray) {
1683
- // Foreign key is an array of IDs (could be oneToMany or manyToMany with array in source)
1684
- // Use $lookup with pipeline and $in to match IDs
1685
- // Build inner pipeline stages for array foreign key lookup
1686
- lines.push(` const innerPipeline = [`);
1687
- lines.push(` {`);
1688
- lines.push(` $match: {`);
1689
- lines.push(` $expr: {`);
1690
- lines.push(` $in: [`);
1691
- lines.push(` '$_id',`);
1692
- lines.push(` {`);
1693
- lines.push(` $map: {`);
1694
- lines.push(` input: { $ifNull: ['$$ids', []] },`);
1695
- lines.push(` as: 'id',`);
1696
- lines.push(` in: { $toObjectId: '$$id' }`);
1697
- lines.push(` }`);
1698
- lines.push(` }`);
1699
- lines.push(` ]`);
1700
- lines.push(` }`);
1701
- lines.push(` }`);
1702
- lines.push(` }`);
1703
- lines.push(` ];`);
1704
- lines.push(` // Add additional stages if include options provided`);
1705
- lines.push(` if (whereFilter && Object.keys(whereFilter).length > 0) {`);
1706
- lines.push(` innerPipeline.push({ $match: whereFilter });`);
1707
- lines.push(` }`);
1708
- lines.push(` if (sort) {`);
1709
- lines.push(` innerPipeline.push({ $sort: sort });`);
1710
- lines.push(` }`);
1711
- lines.push(` if (skip !== null) {`);
1712
- lines.push(` innerPipeline.push({ $skip: skip });`);
1713
- lines.push(` }`);
1714
- lines.push(` if (limit !== null) {`);
1715
- lines.push(` innerPipeline.push({ $limit: limit });`);
1716
- lines.push(` }`);
1717
- lines.push(` const pipeline = [`);
1718
- lines.push(` { $match: { _id: document._id } },`);
1719
- lines.push(` { $lookup: {`);
1720
- lines.push(` from: '${this.toCollectionName(relation.target)}',`);
1721
- lines.push(` let: { ids: '$${localField}' },`);
1722
- lines.push(` pipeline: innerPipeline,`);
1723
- lines.push(` as: '${relation.field}_lookup'`);
1724
- lines.push(` } }`);
1725
- lines.push(` ];`);
1726
- lines.push(` const aggResult = await this.collection.aggregate(pipeline).toArray()`);
1727
- lines.push(` if (aggResult.length > 0) {`);
1728
- lines.push(` let ${relation.field}Result = aggResult[0].${relation.field}_lookup`);
1729
- lines.push(` // Handle nested include`);
1730
- lines.push(` if (nestedInclude && ${relation.field}Result) {`);
1731
- lines.push(` if (Array.isArray(${relation.field}Result)) {`);
1732
- lines.push(` ${relation.field}Result = await Promise.all(${relation.field}Result.map(doc =>`);
1733
- lines.push(` this.client.${this.toCamelCase(relation.target)}.includeRelations(doc, nestedInclude)`);
1734
- lines.push(` ))`);
1735
- lines.push(` } else {`);
1736
- lines.push(` ${relation.field}Result = await this.client.${this.toCamelCase(relation.target)}.includeRelations(${relation.field}Result, nestedInclude)`);
1737
- lines.push(` }`);
1738
- lines.push(` }`);
1739
- lines.push(` result.${relation.field} = ${relation.field}Result`);
1740
- lines.push(` } else {`);
1741
- lines.push(` result.${relation.field} = []`);
1742
- lines.push(` }`);
1743
- }
1744
- else {
1745
- // Foreign key is a single ID (oneToOne, manyToOne, oneToMany with foreign key in target)
1746
- // Use standard $lookup with localField/foreignField
1747
- // Build inner pipeline for single foreign key lookup
1748
- lines.push(` const innerPipeline = [`);
1749
- lines.push(` {`);
1750
- lines.push(` $match: {`);
1751
- lines.push(` $expr: {`);
1752
- lines.push(` $eq: [`);
1753
- lines.push(` '$_id',`);
1754
- lines.push(` { $toObjectId: '$$localId' }`);
1755
- lines.push(` ]`);
1756
- lines.push(` }`);
1757
- lines.push(` }`);
1758
- lines.push(` }`);
1759
- lines.push(` ];`);
1760
- lines.push(` // Add additional stages if include options provided`);
1761
- lines.push(` if (whereFilter && Object.keys(whereFilter).length > 0) {`);
1762
- lines.push(` innerPipeline.push({ $match: whereFilter });`);
1763
- lines.push(` }`);
1764
- lines.push(` if (sort) {`);
1765
- lines.push(` innerPipeline.push({ $sort: sort });`);
1766
- lines.push(` }`);
1767
- lines.push(` if (skip !== null) {`);
1768
- lines.push(` innerPipeline.push({ $skip: skip });`);
1769
- lines.push(` }`);
1770
- lines.push(` if (limit !== null) {`);
1771
- lines.push(` innerPipeline.push({ $limit: limit });`);
1772
- lines.push(` }`);
1773
- lines.push(` const pipeline = [`);
1774
- lines.push(` { $match: { _id: document._id } },`);
1775
- lines.push(` { $lookup: {`);
1776
- lines.push(` from: '${this.toCollectionName(relation.target)}',`);
1777
- lines.push(` let: { localId: '$${localField}' },`);
1778
- lines.push(` pipeline: innerPipeline,`);
1779
- lines.push(` as: '${relation.field}_lookup'`);
1780
- lines.push(` } }`);
1781
- lines.push(` ];`);
1782
- // Add unwind for single relations (oneToOne, manyToOne)
1783
- const needUnwind = relation.type === 'oneToOne' || relation.type === 'manyToOne';
1784
- if (needUnwind) {
1785
- lines.push(` pipeline.push({ $unwind: { path: '$${relation.field}_lookup', preserveNullAndEmptyArrays: true } });`);
1786
- }
1787
- lines.push(` const aggResult = await this.collection.aggregate(pipeline).toArray()`);
1788
- lines.push(` if (aggResult.length > 0) {`);
1789
- lines.push(` let ${relation.field}Result = aggResult[0].${relation.field}_lookup`);
1790
- lines.push(` // Handle nested include`);
1791
- lines.push(` if (nestedInclude && ${relation.field}Result) {`);
1792
- lines.push(` if (Array.isArray(${relation.field}Result)) {`);
1793
- lines.push(` ${relation.field}Result = await Promise.all(${relation.field}Result.map(doc =>`);
1794
- lines.push(` this.client.${this.toCamelCase(relation.target)}.includeRelations(doc, nestedInclude)`);
1795
- lines.push(` ))`);
1796
- lines.push(` } else {`);
1797
- lines.push(` ${relation.field}Result = await this.client.${this.toCamelCase(relation.target)}.includeRelations(${relation.field}Result, nestedInclude)`);
1798
- lines.push(` }`);
1799
- lines.push(` }`);
1800
- lines.push(` result.${relation.field} = ${relation.field}Result`);
1801
- lines.push(` } else {`);
1802
- lines.push(` result.${relation.field} = ${needUnwind ? 'null' : '[]'}`);
1803
- lines.push(` }`);
1804
- }
1805
- lines.push(` }`);
1806
- }
1807
- else {
1808
- // Populate strategy (separate queries)
1809
- switch (relation.type) {
1810
- case 'oneToMany':
1811
- lines.push(` // Relation: ${relation.field} (oneToMany) - populate strategy`);
1812
- lines.push(` if (include.${relation.field} !== undefined) {`);
1813
- lines.push(` const includeOpts = include.${relation.field};`);
1814
- lines.push(` let whereFilter = {};`);
1815
- lines.push(` let orderBy = null;`);
1816
- lines.push(` let take = null;`);
1817
- lines.push(` let skip = null;`);
1818
- lines.push(` let select = undefined;`);
1819
- lines.push(` let nestedInclude = undefined;`);
1820
- lines.push(` if (typeof includeOpts === 'object' && includeOpts !== null) {`);
1821
- lines.push(` whereFilter = includeOpts.where || {};`);
1822
- lines.push(` orderBy = includeOpts.orderBy || null;`);
1823
- lines.push(` take = includeOpts.take !== undefined ? includeOpts.take : null;`);
1824
- lines.push(` skip = includeOpts.skip !== undefined ? includeOpts.skip : null;`);
1825
- lines.push(` select = includeOpts.select;`);
1826
- lines.push(` nestedInclude = includeOpts.include;`);
1827
- lines.push(` }`);
1828
- // Check foreign key location: if in source, document has array of IDs; if in target, target has foreign key
1829
- if (relation.foreignKeyLocation === 'source') {
1830
- // Foreign key is array of IDs in source document
1831
- lines.push(` if (document.${relation.foreignKey} && Array.isArray(document.${relation.foreignKey})) {`);
1832
- lines.push(` const baseWhere = { id: { in: document.${relation.foreignKey} } };`);
1833
- lines.push(` const finalWhere = Object.keys(whereFilter).length > 0 ? { AND: [baseWhere, whereFilter] } : baseWhere;`);
1834
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1835
- lines.push(` where: finalWhere,`);
1836
- lines.push(` orderBy: orderBy,`);
1837
- lines.push(` take: take,`);
1838
- lines.push(` skip: skip,`);
1839
- lines.push(` select: select,`);
1840
- lines.push(` include: nestedInclude`);
1841
- lines.push(` })`);
1842
- lines.push(` result.${relation.field} = ${relation.field}`);
1843
- lines.push(` } else {`);
1844
- lines.push(` result.${relation.field} = []`);
1845
- lines.push(` }`);
1846
- }
1847
- else {
1848
- // Foreign key is single ID in target documents (default)
1849
- lines.push(` const baseWhere = { ${relation.foreignKey}: document.id };`);
1850
- lines.push(` const finalWhere = Object.keys(whereFilter).length > 0 ? { AND: [baseWhere, whereFilter] } : baseWhere;`);
1851
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1852
- lines.push(` where: finalWhere,`);
1853
- lines.push(` orderBy: orderBy,`);
1854
- lines.push(` take: take,`);
1855
- lines.push(` skip: skip,`);
1856
- lines.push(` include: nestedInclude`);
1857
- lines.push(` })`);
1858
- lines.push(` result.${relation.field} = ${relation.field}`);
1859
- }
1860
- lines.push(` }`);
1861
- break;
1862
- case 'manyToOne':
1863
- lines.push(` // Relation: ${relation.field} (manyToOne) - populate strategy`);
1864
- lines.push(` if (include.${relation.field} !== undefined && document.${relation.foreignKey}) {`);
1865
- lines.push(` const includeOpts = include.${relation.field};`);
1866
- lines.push(` let whereFilter = {};`);
1867
- lines.push(` let select = undefined;`);
1868
- lines.push(` let nestedInclude = undefined;`);
1869
- lines.push(` if (typeof includeOpts === 'object' && includeOpts !== null) {`);
1870
- lines.push(` whereFilter = includeOpts.where || {};`);
1871
- lines.push(` select = includeOpts.select;`);
1872
- lines.push(` nestedInclude = includeOpts.include;`);
1873
- lines.push(` }`);
1874
- lines.push(` const baseWhere = { id: document.${relation.foreignKey} };`);
1875
- lines.push(` const finalWhere = Object.keys(whereFilter).length > 0 ? { AND: [baseWhere, whereFilter] } : baseWhere;`);
1876
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findUnique({`);
1877
- lines.push(` where: finalWhere,`);
1878
- lines.push(` select: select,`);
1879
- lines.push(` include: nestedInclude`);
1880
- lines.push(` })`);
1881
- lines.push(` result.${relation.field} = ${relation.field}`);
1882
- lines.push(` }`);
1883
- break;
1884
- case 'oneToOne':
1885
- lines.push(` // Relation: ${relation.field} (oneToOne) - populate strategy`);
1886
- lines.push(` if (include.${relation.field} !== undefined && document.${relation.foreignKey}) {`);
1887
- lines.push(` const includeOpts = include.${relation.field};`);
1888
- lines.push(` let whereFilter = {};`);
1889
- lines.push(` let select = undefined;`);
1890
- lines.push(` let nestedInclude = undefined;`);
1891
- lines.push(` if (typeof includeOpts === 'object' && includeOpts !== null) {`);
1892
- lines.push(` whereFilter = includeOpts.where || {};`);
1893
- lines.push(` select = includeOpts.select;`);
1894
- lines.push(` nestedInclude = includeOpts.include;`);
1895
- lines.push(` }`);
1896
- lines.push(` const baseWhere = { id: document.${relation.foreignKey} };`);
1897
- lines.push(` const finalWhere = Object.keys(whereFilter).length > 0 ? { AND: [baseWhere, whereFilter] } : baseWhere;`);
1898
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findUnique({`);
1899
- lines.push(` where: finalWhere,`);
1900
- lines.push(` select: select,`);
1901
- lines.push(` include: nestedInclude`);
1902
- lines.push(` })`);
1903
- lines.push(` result.${relation.field} = ${relation.field}`);
1904
- lines.push(` }`);
1905
- break;
1906
- case 'manyToMany':
1907
- lines.push(` // Relation: ${relation.field} (manyToMany) - populate strategy`);
1908
- lines.push(` if (include.${relation.field} !== undefined) {`);
1909
- lines.push(` const includeOpts = include.${relation.field};`);
1910
- lines.push(` let whereFilter = {};`);
1911
- lines.push(` let orderBy = null;`);
1912
- lines.push(` let take = null;`);
1913
- lines.push(` let skip = null;`);
1914
- lines.push(` let select = undefined;`);
1915
- lines.push(` let nestedInclude = undefined;`);
1916
- lines.push(` if (typeof includeOpts === 'object' && includeOpts !== null) {`);
1917
- lines.push(` whereFilter = includeOpts.where || {};`);
1918
- lines.push(` orderBy = includeOpts.orderBy || null;`);
1919
- lines.push(` take = includeOpts.take !== undefined ? includeOpts.take : null;`);
1920
- lines.push(` skip = includeOpts.skip !== undefined ? includeOpts.skip : null;`);
1921
- lines.push(` select = includeOpts.select;`);
1922
- lines.push(` nestedInclude = includeOpts.include;`);
1923
- lines.push(` }`);
1924
- if (relation.foreignKey) {
1925
- // manyToMany with foreign key array (IDs array) - similar to oneToMany with source foreign key
1926
- lines.push(` if (document.${relation.foreignKey} && Array.isArray(document.${relation.foreignKey})) {`);
1927
- lines.push(` const baseWhere = { id: { in: document.${relation.foreignKey} } };`);
1928
- lines.push(` const finalWhere = Object.keys(whereFilter).length > 0 ? { AND: [baseWhere, whereFilter] } : baseWhere;`);
1929
- lines.push(` const ${relation.field} = await this.client.${this.toCamelCase(relation.target)}.findMany({`);
1930
- lines.push(` where: finalWhere,`);
1931
- lines.push(` orderBy: orderBy,`);
1932
- lines.push(` take: take,`);
1933
- lines.push(` skip: skip,`);
1934
- lines.push(` select: select,`);
1935
- lines.push(` include: nestedInclude`);
1936
- lines.push(` })`);
1937
- lines.push(` result.${relation.field} = ${relation.field}`);
1938
- lines.push(` } else {`);
1939
- lines.push(` result.${relation.field} = []`);
1940
- lines.push(` }`);
1941
- }
1942
- else if (relation.joinCollection) {
1943
- // manyToMany with join collection - use RelationResolver
1944
- // Convert where and orderBy using QueryBuilder
1945
- lines.push(` let mongoWhere = {};`);
1946
- lines.push(` let mongoSort = null;`);
1947
- lines.push(` if (Object.keys(whereFilter).length > 0) {`);
1948
- lines.push(` mongoWhere = QueryBuilder.buildWhere(whereFilter);`);
1949
- lines.push(` }`);
1950
- lines.push(` if (orderBy) {`);
1951
- lines.push(` mongoSort = QueryBuilder.buildSort(orderBy);`);
1952
- lines.push(` }`);
1953
- lines.push(` const ${relation.field} = await RelationResolver.resolveManyToMany(`);
1954
- lines.push(` this.client.$db,`);
1955
- lines.push(` '${model.collectionName}',`);
1956
- lines.push(` '${this.toCollectionName(relation.target)}',`);
1957
- lines.push(` '${relation.joinCollection}',`);
1958
- lines.push(` document.id,`);
1959
- lines.push(` mongoWhere,`);
1960
- lines.push(` mongoSort,`);
1961
- lines.push(` take,`);
1962
- lines.push(` skip,`);
1963
- lines.push(` select`);
1964
- lines.push(` )`);
1965
- lines.push(` result.${relation.field} = ${relation.field}`);
1966
- // Note: nestedInclude not supported for join collection case yet
1967
- }
1968
- else {
1969
- lines.push(` console.warn('manyToMany relation ${relation.field} has no foreign key or join collection')`);
1970
- lines.push(` result.${relation.field} = []`);
1971
- }
1972
- lines.push(` }`);
1973
- break;
1974
- }
1975
- }
1976
- }
1977
- if (lines.length === 0) {
1978
- return '';
1979
- }
1980
- return lines.join('\n');
1981
- }
1982
- toCamelCase(str) {
1983
- return str.charAt(0).toLowerCase() + str.slice(1);
1984
- }
1985
- toCollectionName(modelName) {
1986
- return modelName.toLowerCase() + 's';
1987
- }
1988
47
  }
1989
48
  //# sourceMappingURL=CodeGenerator.js.map