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