@bairock/lenz 0.0.14 → 0.0.16

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