@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.
- package/README.md +195 -19
- package/dist/cli/commands/generate/crud.d.ts +3 -0
- package/dist/cli/commands/generate/crud.d.ts.map +1 -0
- package/dist/cli/commands/generate/crud.js +123 -0
- package/dist/cli/commands/generate/crud.js.map +1 -0
- package/dist/cli/commands/generate/index.d.ts +3 -0
- package/dist/cli/commands/generate/index.d.ts.map +1 -0
- package/dist/cli/commands/generate/index.js +8 -0
- package/dist/cli/commands/generate/index.js.map +1 -0
- package/dist/cli/commands/generate/orm.d.ts +3 -0
- package/dist/cli/commands/generate/orm.d.ts.map +1 -0
- package/dist/cli/commands/generate/orm.js +107 -0
- package/dist/cli/commands/generate/orm.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +34 -8
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +0 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +1 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +4 -6
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +24 -3
- package/dist/config/index.js.map +1 -1
- package/dist/engine/CodeGenerator.d.ts +8 -28
- package/dist/engine/CodeGenerator.d.ts.map +1 -1
- package/dist/engine/CodeGenerator.js +28 -1969
- package/dist/engine/CodeGenerator.js.map +1 -1
- package/dist/engine/GraphQLParseHelpers.d.ts +25 -0
- package/dist/engine/GraphQLParseHelpers.d.ts.map +1 -0
- package/dist/engine/GraphQLParseHelpers.js +128 -0
- package/dist/engine/GraphQLParseHelpers.js.map +1 -0
- package/dist/engine/GraphQLParser.d.ts +23 -10
- package/dist/engine/GraphQLParser.d.ts.map +1 -1
- package/dist/engine/GraphQLParser.js +154 -240
- package/dist/engine/GraphQLParser.js.map +1 -1
- package/dist/engine/GraphQLRelationAnalyzer.d.ts +10 -0
- package/dist/engine/GraphQLRelationAnalyzer.d.ts.map +1 -0
- package/dist/engine/GraphQLRelationAnalyzer.js +117 -0
- package/dist/engine/GraphQLRelationAnalyzer.js.map +1 -0
- package/dist/engine/LenzEngine.d.ts +1 -1
- package/dist/engine/LenzEngine.d.ts.map +1 -1
- package/dist/engine/LenzEngine.js +33 -13
- package/dist/engine/LenzEngine.js.map +1 -1
- package/dist/engine/SchemaRelationValidator.d.ts +15 -0
- package/dist/engine/SchemaRelationValidator.d.ts.map +1 -0
- package/dist/engine/SchemaRelationValidator.js +133 -0
- package/dist/engine/SchemaRelationValidator.js.map +1 -0
- package/dist/engine/SchemaValidator.d.ts +11 -10
- package/dist/engine/SchemaValidator.d.ts.map +1 -1
- package/dist/engine/SchemaValidator.js +151 -169
- package/dist/engine/SchemaValidator.js.map +1 -1
- package/dist/engine/directives.d.ts +10 -0
- package/dist/engine/directives.d.ts.map +1 -1
- package/dist/engine/directives.js +152 -6
- package/dist/engine/directives.js.map +1 -1
- package/dist/engine/generators/ClientGenerator.d.ts +7 -0
- package/dist/engine/generators/ClientGenerator.d.ts.map +1 -0
- package/dist/engine/generators/ClientGenerator.js +386 -0
- package/dist/engine/generators/ClientGenerator.js.map +1 -0
- package/dist/engine/generators/CrudModuleGenerator.d.ts +10 -0
- package/dist/engine/generators/CrudModuleGenerator.d.ts.map +1 -0
- package/dist/engine/generators/CrudModuleGenerator.js +141 -0
- package/dist/engine/generators/CrudModuleGenerator.js.map +1 -0
- package/dist/engine/generators/DelegateGenerator.d.ts +9 -0
- package/dist/engine/generators/DelegateGenerator.d.ts.map +1 -0
- package/dist/engine/generators/DelegateGenerator.js +453 -0
- package/dist/engine/generators/DelegateGenerator.js.map +1 -0
- package/dist/engine/generators/DelegateHelpers.d.ts +7 -0
- package/dist/engine/generators/DelegateHelpers.d.ts.map +1 -0
- package/dist/engine/generators/DelegateHelpers.js +144 -0
- package/dist/engine/generators/DelegateHelpers.js.map +1 -0
- package/dist/engine/generators/DelegateRelations.d.ts +11 -0
- package/dist/engine/generators/DelegateRelations.d.ts.map +1 -0
- package/dist/engine/generators/DelegateRelations.js +794 -0
- package/dist/engine/generators/DelegateRelations.js.map +1 -0
- package/dist/engine/generators/DelegateTemplateBody.d.ts +8 -0
- package/dist/engine/generators/DelegateTemplateBody.d.ts.map +1 -0
- package/dist/engine/generators/DelegateTemplateBody.js +776 -0
- package/dist/engine/generators/DelegateTemplateBody.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimeErrors.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimeErrors.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimeErrors.js +140 -0
- package/dist/engine/generators/GenerateRuntimeErrors.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimeIndex.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimeIndex.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimeIndex.js +21 -0
- package/dist/engine/generators/GenerateRuntimeIndex.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimeLogger.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimeLogger.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimeLogger.js +125 -0
- package/dist/engine/generators/GenerateRuntimeLogger.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimePagination.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimePagination.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimePagination.js +159 -0
- package/dist/engine/generators/GenerateRuntimePagination.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimeQuery.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimeQuery.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimeQuery.js +427 -0
- package/dist/engine/generators/GenerateRuntimeQuery.js.map +1 -0
- package/dist/engine/generators/GenerateRuntimeRelations.d.ts +2 -0
- package/dist/engine/generators/GenerateRuntimeRelations.d.ts.map +1 -0
- package/dist/engine/generators/GenerateRuntimeRelations.js +130 -0
- package/dist/engine/generators/GenerateRuntimeRelations.js.map +1 -0
- package/dist/engine/generators/RuntimeGenerator.d.ts +16 -0
- package/dist/engine/generators/RuntimeGenerator.d.ts.map +1 -0
- package/dist/engine/generators/RuntimeGenerator.js +16 -0
- package/dist/engine/generators/RuntimeGenerator.js.map +1 -0
- package/dist/engine/generators/SDLInputTypesGenerator.d.ts +6 -0
- package/dist/engine/generators/SDLInputTypesGenerator.d.ts.map +1 -0
- package/dist/engine/generators/SDLInputTypesGenerator.js +763 -0
- package/dist/engine/generators/SDLInputTypesGenerator.js.map +1 -0
- package/dist/engine/generators/TypeFilterTypes.d.ts +2 -0
- package/dist/engine/generators/TypeFilterTypes.d.ts.map +1 -0
- package/dist/engine/generators/TypeFilterTypes.js +220 -0
- package/dist/engine/generators/TypeFilterTypes.js.map +1 -0
- package/dist/engine/generators/TypeGenerator.d.ts +16 -0
- package/dist/engine/generators/TypeGenerator.d.ts.map +1 -0
- package/dist/engine/generators/TypeGenerator.js +493 -0
- package/dist/engine/generators/TypeGenerator.js.map +1 -0
- package/dist/engine/generators/helpers.d.ts +13 -0
- package/dist/engine/generators/helpers.d.ts.map +1 -0
- package/dist/engine/generators/helpers.js +316 -0
- package/dist/engine/generators/helpers.js.map +1 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +11 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
|
@@ -1,1989 +1,48 @@
|
|
|
1
|
-
import
|
|
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.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
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
|
-
'
|
|
228
|
-
|
|
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] =
|
|
37
|
+
result[jsPath] = convertTypesToJavaScript(content);
|
|
237
38
|
}
|
|
238
39
|
else {
|
|
239
|
-
result[jsPath] =
|
|
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] =
|
|
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
|