@veloxts/cli 0.6.96 → 0.6.98
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/CHANGELOG.md +24 -0
- package/dist/generators/generators/namespace.js +26 -2
- package/dist/generators/generators/resource.js +23 -3
- package/dist/generators/templates/namespace.d.ts +11 -0
- package/dist/generators/templates/namespace.js +19 -3
- package/dist/generators/templates/resource.d.ts +10 -1
- package/dist/generators/templates/resource.js +25 -9
- package/dist/generators/utils/prisma-schema.d.ts +43 -0
- package/dist/generators/utils/prisma-schema.js +100 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @veloxts/cli
|
|
2
2
|
|
|
3
|
+
## 0.6.98
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(router): nested resource schema relations with .hasOne() / .hasMany()
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @veloxts/auth@0.6.98
|
|
10
|
+
- @veloxts/core@0.6.98
|
|
11
|
+
- @veloxts/orm@0.6.98
|
|
12
|
+
- @veloxts/router@0.6.98
|
|
13
|
+
- @veloxts/validation@0.6.98
|
|
14
|
+
|
|
15
|
+
## 0.6.97
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- sync zod dependency version
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @veloxts/auth@0.6.97
|
|
22
|
+
- @veloxts/core@0.6.97
|
|
23
|
+
- @veloxts/orm@0.6.97
|
|
24
|
+
- @veloxts/router@0.6.97
|
|
25
|
+
- @veloxts/validation@0.6.97
|
|
26
|
+
|
|
3
27
|
## 0.6.96
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { BaseGenerator } from '../base.js';
|
|
15
15
|
import { getNamespaceInstructions, getNamespaceProcedurePath, getNamespaceSchemaPath, getNamespaceTestPath, namespaceSchemaTemplate, namespaceTemplate, namespaceTestTemplate, } from '../templates/namespace.js';
|
|
16
|
+
import { deriveEntityNames } from '../utils/naming.js';
|
|
17
|
+
import { analyzePrismaSchema, findPrismaSchema, getModelRelations, hasModel, } from '../utils/prisma-schema.js';
|
|
16
18
|
import { detectRouterPattern, isProcedureRegistered, registerProcedures, } from '../utils/router-integration.js';
|
|
17
19
|
// ============================================================================
|
|
18
20
|
// Generator Implementation
|
|
@@ -96,9 +98,31 @@ Examples:
|
|
|
96
98
|
* Generate namespace files
|
|
97
99
|
*/
|
|
98
100
|
async generate(config) {
|
|
99
|
-
|
|
101
|
+
// Detect relations from existing Prisma model before creating context
|
|
102
|
+
const enrichedOptions = { ...config.options };
|
|
103
|
+
const schemaPath = findPrismaSchema(config.cwd);
|
|
104
|
+
if (schemaPath) {
|
|
105
|
+
try {
|
|
106
|
+
const entity = deriveEntityNames(config.entityName);
|
|
107
|
+
const schemaAnalysis = analyzePrismaSchema(schemaPath);
|
|
108
|
+
if (hasModel(schemaAnalysis, entity.pascal)) {
|
|
109
|
+
const modelRelations = getModelRelations(schemaAnalysis, entity.pascal);
|
|
110
|
+
if (modelRelations.hasOne.length > 0 || modelRelations.hasMany.length > 0) {
|
|
111
|
+
enrichedOptions.relations = {
|
|
112
|
+
hasOne: [...modelRelations.hasOne],
|
|
113
|
+
hasMany: [...modelRelations.hasMany],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Schema parsing failed — proceed without relations
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const enrichedConfig = { ...config, options: enrichedOptions };
|
|
123
|
+
const context = this.createContext(enrichedConfig);
|
|
100
124
|
const { entity } = context;
|
|
101
|
-
const { options } =
|
|
125
|
+
const { options } = enrichedConfig;
|
|
102
126
|
// Collect files to generate
|
|
103
127
|
const files = [];
|
|
104
128
|
// Generate schema file
|
|
@@ -19,7 +19,7 @@ import { generateInjectablePrismaContent, generateResourceFiles, } from '../temp
|
|
|
19
19
|
import { findAllSimilarFiles, formatSimilarFilesWarning } from '../utils/filesystem.js';
|
|
20
20
|
import { deriveEntityNames } from '../utils/naming.js';
|
|
21
21
|
import { promptAndRunMigration } from '../utils/prisma-migration.js';
|
|
22
|
-
import { analyzePrismaSchema, findPrismaSchema, hasModel, injectIntoSchema, } from '../utils/prisma-schema.js';
|
|
22
|
+
import { analyzePrismaSchema, findPrismaSchema, getModelRelations, hasModel, injectIntoSchema, } from '../utils/prisma-schema.js';
|
|
23
23
|
import { detectRouterPattern, isProcedureRegistered, registerProcedures, } from '../utils/router-integration.js';
|
|
24
24
|
import { createSnapshot, rollback, saveOriginal, trackCreated, } from '../utils/snapshot.js';
|
|
25
25
|
// ============================================================================
|
|
@@ -257,6 +257,26 @@ export class ResourceGenerator extends BaseGenerator {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
// Detect relations from existing Prisma schema (if model exists)
|
|
261
|
+
let relations;
|
|
262
|
+
const schemaPath = findPrismaSchema(config.cwd);
|
|
263
|
+
if (schemaPath) {
|
|
264
|
+
try {
|
|
265
|
+
const schemaAnalysis = analyzePrismaSchema(schemaPath);
|
|
266
|
+
if (hasModel(schemaAnalysis, ctx.entity.pascal)) {
|
|
267
|
+
const modelRelations = getModelRelations(schemaAnalysis, ctx.entity.pascal);
|
|
268
|
+
if (modelRelations.hasOne.length > 0 || modelRelations.hasMany.length > 0) {
|
|
269
|
+
relations = {
|
|
270
|
+
hasOne: [...modelRelations.hasOne],
|
|
271
|
+
hasMany: [...modelRelations.hasMany],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Schema parsing failed — proceed without relations
|
|
278
|
+
}
|
|
279
|
+
}
|
|
260
280
|
// Show spinner during actual file generation
|
|
261
281
|
// Only show if we're in interactive mode (we collected fields or user opted to skip)
|
|
262
282
|
const showSpinner = interactive && !skipFields && !config.dryRun;
|
|
@@ -265,7 +285,7 @@ export class ResourceGenerator extends BaseGenerator {
|
|
|
265
285
|
const s = p.spinner();
|
|
266
286
|
s.start('Scaffolding resource...');
|
|
267
287
|
try {
|
|
268
|
-
generatedFiles = generateResourceFiles(ctx);
|
|
288
|
+
generatedFiles = generateResourceFiles(ctx, relations);
|
|
269
289
|
s.stop(`Scaffolded ${generatedFiles.length} file(s)`);
|
|
270
290
|
}
|
|
271
291
|
catch (err) {
|
|
@@ -274,7 +294,7 @@ export class ResourceGenerator extends BaseGenerator {
|
|
|
274
294
|
}
|
|
275
295
|
}
|
|
276
296
|
else {
|
|
277
|
-
generatedFiles = generateResourceFiles(ctx);
|
|
297
|
+
generatedFiles = generateResourceFiles(ctx, relations);
|
|
278
298
|
}
|
|
279
299
|
// Auto-registration (skip in dry-run mode)
|
|
280
300
|
let autoResult = {
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* custom procedures rather than pre-defined CRUD operations.
|
|
7
7
|
*/
|
|
8
8
|
import type { TemplateContext, TemplateFunction } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Relation info for code generation (shared with resource template)
|
|
11
|
+
*/
|
|
12
|
+
export interface RelationInfo {
|
|
13
|
+
/** Single-object relation field names */
|
|
14
|
+
hasOne: string[];
|
|
15
|
+
/** Array relation field names */
|
|
16
|
+
hasMany: string[];
|
|
17
|
+
}
|
|
9
18
|
export interface NamespaceOptions {
|
|
10
19
|
/** Skip auto-registering in router.ts */
|
|
11
20
|
skipRegistration: boolean;
|
|
@@ -13,6 +22,8 @@ export interface NamespaceOptions {
|
|
|
13
22
|
withExample: boolean;
|
|
14
23
|
/** Generate test file */
|
|
15
24
|
withTests: boolean;
|
|
25
|
+
/** Detected relations from Prisma schema */
|
|
26
|
+
relations?: RelationInfo;
|
|
16
27
|
}
|
|
17
28
|
/**
|
|
18
29
|
* Generate namespace procedure file
|
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
* custom procedures rather than pre-defined CRUD operations.
|
|
7
7
|
*/
|
|
8
8
|
// ============================================================================
|
|
9
|
+
// Relation Helpers
|
|
10
|
+
// ============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Build an `include: { ... }` clause string for Prisma queries
|
|
13
|
+
*/
|
|
14
|
+
function buildIncludeClause(relations) {
|
|
15
|
+
if (!relations)
|
|
16
|
+
return '';
|
|
17
|
+
const allRelations = [...relations.hasOne, ...relations.hasMany];
|
|
18
|
+
if (allRelations.length === 0)
|
|
19
|
+
return '';
|
|
20
|
+
const entries = allRelations.map((name) => `${name}: true`).join(', ');
|
|
21
|
+
return `\n include: { ${entries} },`;
|
|
22
|
+
}
|
|
23
|
+
// ============================================================================
|
|
9
24
|
// Template Functions
|
|
10
25
|
// ============================================================================
|
|
11
26
|
/**
|
|
@@ -64,7 +79,8 @@ export const ${entity.camel}Procedures = procedures('${entity.plural}', {
|
|
|
64
79
|
* Generate namespace with example procedure
|
|
65
80
|
*/
|
|
66
81
|
function generateWithExample(ctx) {
|
|
67
|
-
const { entity } = ctx;
|
|
82
|
+
const { entity, options } = ctx;
|
|
83
|
+
const includeClause = buildIncludeClause(options.relations);
|
|
68
84
|
return `/**
|
|
69
85
|
* ${entity.pascal} Procedures
|
|
70
86
|
*
|
|
@@ -92,7 +108,7 @@ export const ${entity.camel}Procedures = procedures('${entity.plural}', {
|
|
|
92
108
|
.output(${entity.pascal}Schema.nullable())
|
|
93
109
|
.query(async ({ input, ctx }) => {
|
|
94
110
|
return ctx.db.${entity.camel}.findUnique({
|
|
95
|
-
where: { id: input.id }
|
|
111
|
+
where: { id: input.id },${includeClause}
|
|
96
112
|
});
|
|
97
113
|
}),
|
|
98
114
|
|
|
@@ -103,7 +119,7 @@ export const ${entity.camel}Procedures = procedures('${entity.plural}', {
|
|
|
103
119
|
list${entity.pascalPlural}: procedure()
|
|
104
120
|
.output(z.array(${entity.pascal}Schema))
|
|
105
121
|
.query(async ({ ctx }) => {
|
|
106
|
-
return ctx.db.${entity.camel}.findMany();
|
|
122
|
+
return ctx.db.${entity.camel}.findMany(${includeClause ? `{${includeClause}\n }` : ''});
|
|
107
123
|
}),
|
|
108
124
|
|
|
109
125
|
/**
|
|
@@ -88,10 +88,19 @@ export declare function generateInjectablePrismaContent(entity: {
|
|
|
88
88
|
pascal: string;
|
|
89
89
|
camel: string;
|
|
90
90
|
}, options: ResourceOptions, database?: DatabaseType): InjectablePrismaContent;
|
|
91
|
+
/**
|
|
92
|
+
* Relation info for code generation
|
|
93
|
+
*/
|
|
94
|
+
export interface RelationInfo {
|
|
95
|
+
/** Single-object relation field names */
|
|
96
|
+
hasOne: string[];
|
|
97
|
+
/** Array relation field names */
|
|
98
|
+
hasMany: string[];
|
|
99
|
+
}
|
|
91
100
|
/**
|
|
92
101
|
* Generate all files for a resource
|
|
93
102
|
*/
|
|
94
|
-
export declare function generateResourceFiles(ctx: TemplateContext<ResourceOptions
|
|
103
|
+
export declare function generateResourceFiles(ctx: TemplateContext<ResourceOptions>, relations?: RelationInfo): GeneratedFile[];
|
|
95
104
|
/**
|
|
96
105
|
* Generate post-generation instructions
|
|
97
106
|
*/
|
|
@@ -123,6 +123,23 @@ export function generatePrismaEnums(fields) {
|
|
|
123
123
|
});
|
|
124
124
|
return `\n${enums.join('\n\n')}\n`;
|
|
125
125
|
}
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Relation Helpers
|
|
128
|
+
// ============================================================================
|
|
129
|
+
/**
|
|
130
|
+
* Build an `include: { ... }` clause string for Prisma queries
|
|
131
|
+
*
|
|
132
|
+
* Returns empty string if no relations, or a formatted include block
|
|
133
|
+
*/
|
|
134
|
+
function buildIncludeClause(relations) {
|
|
135
|
+
if (!relations)
|
|
136
|
+
return '';
|
|
137
|
+
const allRelations = [...relations.hasOne, ...relations.hasMany];
|
|
138
|
+
if (allRelations.length === 0)
|
|
139
|
+
return '';
|
|
140
|
+
const entries = allRelations.map((name) => `${name}: true`).join(', ');
|
|
141
|
+
return `\n include: { ${entries} },`;
|
|
142
|
+
}
|
|
126
143
|
/**
|
|
127
144
|
* Generate injectable Prisma content (model + enums without comments)
|
|
128
145
|
*
|
|
@@ -334,12 +351,11 @@ export type ${pascal} = z.infer<typeof ${camel}Schema>;
|
|
|
334
351
|
${crudSchemas}
|
|
335
352
|
`;
|
|
336
353
|
}
|
|
337
|
-
|
|
338
|
-
// Procedure Template
|
|
339
|
-
// ============================================================================
|
|
340
|
-
function generateProcedure(entity, options) {
|
|
354
|
+
function generateProcedure(entity, options, relations) {
|
|
341
355
|
const { pascal, camel, kebab } = entity;
|
|
342
356
|
const { crud, paginated, softDelete } = options;
|
|
357
|
+
// Build include clause if relations exist
|
|
358
|
+
const includeClause = buildIncludeClause(relations);
|
|
343
359
|
if (!crud) {
|
|
344
360
|
// Simple procedure without CRUD
|
|
345
361
|
return `/**
|
|
@@ -361,7 +377,7 @@ export const ${camel}Procedures = procedures('${kebab}s', {
|
|
|
361
377
|
.output(${camel}Schema.nullable())
|
|
362
378
|
.query(async ({ input, ctx }) => {
|
|
363
379
|
return ctx.db.${camel}.findUnique({
|
|
364
|
-
where: { id: input.id }
|
|
380
|
+
where: { id: input.id },${includeClause}
|
|
365
381
|
});
|
|
366
382
|
}),
|
|
367
383
|
});
|
|
@@ -421,7 +437,7 @@ export const ${camel}Procedures = procedures('${kebab}s', {
|
|
|
421
437
|
.output(${camel}Schema.nullable())
|
|
422
438
|
.query(async ({ input, ctx }) => {
|
|
423
439
|
return ctx.db.${camel}.findUnique({
|
|
424
|
-
where: { id: input.id${softDeleteWhere} }
|
|
440
|
+
where: { id: input.id${softDeleteWhere} },${includeClause}
|
|
425
441
|
});
|
|
426
442
|
}),
|
|
427
443
|
|
|
@@ -439,7 +455,7 @@ export const ${camel}Procedures = procedures('${kebab}s', {
|
|
|
439
455
|
take: input.limit,
|
|
440
456
|
orderBy: input.sortBy
|
|
441
457
|
? { [input.sortBy]: input.sortOrder }
|
|
442
|
-
: { createdAt: 'desc' }
|
|
458
|
+
: { createdAt: 'desc' },${includeClause}
|
|
443
459
|
});
|
|
444
460
|
${paginationCode}
|
|
445
461
|
}),
|
|
@@ -626,7 +642,7 @@ describe('${pascal} Procedures', () => {
|
|
|
626
642
|
/**
|
|
627
643
|
* Generate all files for a resource
|
|
628
644
|
*/
|
|
629
|
-
export function generateResourceFiles(ctx) {
|
|
645
|
+
export function generateResourceFiles(ctx, relations) {
|
|
630
646
|
const files = [];
|
|
631
647
|
const { entity, options, project } = ctx;
|
|
632
648
|
// Prisma model (added to models folder for reference)
|
|
@@ -647,7 +663,7 @@ export function generateResourceFiles(ctx) {
|
|
|
647
663
|
if (!options.skipProcedure) {
|
|
648
664
|
files.push({
|
|
649
665
|
path: `src/procedures/${entity.kebab}.ts`,
|
|
650
|
-
content: generateProcedure(entity, options),
|
|
666
|
+
content: generateProcedure(entity, options, relations),
|
|
651
667
|
});
|
|
652
668
|
}
|
|
653
669
|
// Tests
|
|
@@ -4,6 +4,41 @@
|
|
|
4
4
|
* Parses Prisma schema files to find models/enums and enables safe injection
|
|
5
5
|
* of new definitions without breaking existing code.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parsed information about a single field in a Prisma model
|
|
9
|
+
*/
|
|
10
|
+
export interface PrismaFieldInfo {
|
|
11
|
+
/** Field name (e.g., 'author', 'posts') */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** Base type without modifiers (e.g., 'User', 'Post') */
|
|
14
|
+
readonly type: string;
|
|
15
|
+
/** Whether this field is a relation to another model */
|
|
16
|
+
readonly isRelation: boolean;
|
|
17
|
+
/** Whether this is an array relation (e.g., Post[]) */
|
|
18
|
+
readonly isArray: boolean;
|
|
19
|
+
/** The related model name, if this is a relation field */
|
|
20
|
+
readonly relatedModel: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detailed information about a Prisma model including its fields
|
|
24
|
+
*/
|
|
25
|
+
export interface PrismaModelInfo {
|
|
26
|
+
/** Model name (PascalCase) */
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** All parsed fields */
|
|
29
|
+
readonly fields: readonly PrismaFieldInfo[];
|
|
30
|
+
/** Only the relation fields (convenience accessor) */
|
|
31
|
+
readonly relationFields: readonly PrismaFieldInfo[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Relation info categorized for code generation
|
|
35
|
+
*/
|
|
36
|
+
export interface ModelRelations {
|
|
37
|
+
/** Single-object relations (e.g., author User) */
|
|
38
|
+
readonly hasOne: readonly string[];
|
|
39
|
+
/** Array relations (e.g., posts Post[]) */
|
|
40
|
+
readonly hasMany: readonly string[];
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Represents a parsed Prisma schema
|
|
9
44
|
*/
|
|
@@ -22,6 +57,8 @@ export interface PrismaSchemaAnalysis {
|
|
|
22
57
|
readonly lastEnumEnd: number;
|
|
23
58
|
/** Position where models start (after datasource/generator blocks) */
|
|
24
59
|
readonly modelSectionStart: number;
|
|
60
|
+
/** Detailed model info with field-level data */
|
|
61
|
+
readonly modelDetails: Map<string, PrismaModelInfo>;
|
|
25
62
|
}
|
|
26
63
|
/**
|
|
27
64
|
* Model definition to inject
|
|
@@ -64,6 +101,12 @@ export declare function findPrismaSchema(projectRoot: string): string | null;
|
|
|
64
101
|
* Parse a Prisma schema file
|
|
65
102
|
*/
|
|
66
103
|
export declare function analyzePrismaSchema(filePath: string): PrismaSchemaAnalysis;
|
|
104
|
+
/**
|
|
105
|
+
* Get categorized relations for a model
|
|
106
|
+
*
|
|
107
|
+
* Returns hasOne (single-object) and hasMany (array) relation field names
|
|
108
|
+
*/
|
|
109
|
+
export declare function getModelRelations(analysis: PrismaSchemaAnalysis, modelName: string): ModelRelations;
|
|
67
110
|
/**
|
|
68
111
|
* Check if a model exists in the schema
|
|
69
112
|
*/
|
|
@@ -65,6 +65,8 @@ export function analyzePrismaSchema(filePath) {
|
|
|
65
65
|
if (modelSectionStart === 0) {
|
|
66
66
|
modelSectionStart = findEndOfConfigBlocks(content);
|
|
67
67
|
}
|
|
68
|
+
// Parse field-level details for each model
|
|
69
|
+
const modelDetails = parseModelDetails(content, models);
|
|
68
70
|
return {
|
|
69
71
|
content,
|
|
70
72
|
filePath,
|
|
@@ -73,6 +75,7 @@ export function analyzePrismaSchema(filePath) {
|
|
|
73
75
|
lastModelEnd,
|
|
74
76
|
lastEnumEnd,
|
|
75
77
|
modelSectionStart,
|
|
78
|
+
modelDetails,
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
@@ -115,6 +118,103 @@ function findEndOfConfigBlocks(content) {
|
|
|
115
118
|
return lastEnd;
|
|
116
119
|
}
|
|
117
120
|
// ============================================================================
|
|
121
|
+
// Field-Level Parsing
|
|
122
|
+
// ============================================================================
|
|
123
|
+
/**
|
|
124
|
+
* Parse detailed field information for all models in the schema
|
|
125
|
+
*/
|
|
126
|
+
function parseModelDetails(content, modelNames) {
|
|
127
|
+
const details = new Map();
|
|
128
|
+
const modelRegex = /^model\s+(\w+)\s*\{/gm;
|
|
129
|
+
for (const match of content.matchAll(modelRegex)) {
|
|
130
|
+
const modelName = match[1];
|
|
131
|
+
const blockStart = content.indexOf('{', match.index ?? 0);
|
|
132
|
+
const blockEnd = findBlockEnd(content, match.index ?? 0);
|
|
133
|
+
const body = content.slice(blockStart + 1, blockEnd - 1);
|
|
134
|
+
const fields = parseModelFields(body, modelNames);
|
|
135
|
+
details.set(modelName, {
|
|
136
|
+
name: modelName,
|
|
137
|
+
fields,
|
|
138
|
+
relationFields: fields.filter((f) => f.isRelation),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return details;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Parse individual fields from a model body string
|
|
145
|
+
*
|
|
146
|
+
* Detects relation fields by checking if the field type matches a known model name.
|
|
147
|
+
* Skips back-reference fields (those with @relation(...) that are the inverse side).
|
|
148
|
+
*/
|
|
149
|
+
function parseModelFields(body, modelNames) {
|
|
150
|
+
const fields = [];
|
|
151
|
+
const lines = body.split('\n');
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
// Skip empty lines, comments, and directives (@@map, @@unique, etc.)
|
|
155
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Parse field: name Type[?][] [@annotations...]
|
|
159
|
+
// Match: fieldName TypeName optional modifiers
|
|
160
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\??/);
|
|
161
|
+
if (!fieldMatch)
|
|
162
|
+
continue;
|
|
163
|
+
const name = fieldMatch[1];
|
|
164
|
+
const baseType = fieldMatch[2];
|
|
165
|
+
const isArray = fieldMatch[3] === '[]';
|
|
166
|
+
// Check if this is a relation field (type matches a known model)
|
|
167
|
+
const isRelation = modelNames.has(baseType);
|
|
168
|
+
if (!isRelation) {
|
|
169
|
+
fields.push({
|
|
170
|
+
name,
|
|
171
|
+
type: baseType,
|
|
172
|
+
isRelation: false,
|
|
173
|
+
isArray: false,
|
|
174
|
+
relatedModel: undefined,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// For relation fields, skip back-references (inverse side with @relation)
|
|
179
|
+
// Back-references typically have @relation and reference back to the parent
|
|
180
|
+
// We detect them by the presence of `fields:` and `references:` in @relation
|
|
181
|
+
const hasRelationWithForeignKey = /@relation\([^)]*fields:\s*\[/.test(trimmed);
|
|
182
|
+
if (hasRelationWithForeignKey) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
fields.push({
|
|
186
|
+
name,
|
|
187
|
+
type: baseType,
|
|
188
|
+
isRelation: true,
|
|
189
|
+
isArray,
|
|
190
|
+
relatedModel: baseType,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return fields;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get categorized relations for a model
|
|
197
|
+
*
|
|
198
|
+
* Returns hasOne (single-object) and hasMany (array) relation field names
|
|
199
|
+
*/
|
|
200
|
+
export function getModelRelations(analysis, modelName) {
|
|
201
|
+
const modelInfo = analysis.modelDetails.get(modelName);
|
|
202
|
+
if (!modelInfo) {
|
|
203
|
+
return { hasOne: [], hasMany: [] };
|
|
204
|
+
}
|
|
205
|
+
const hasOne = [];
|
|
206
|
+
const hasMany = [];
|
|
207
|
+
for (const field of modelInfo.relationFields) {
|
|
208
|
+
if (field.isArray) {
|
|
209
|
+
hasMany.push(field.name);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
hasOne.push(field.name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { hasOne, hasMany };
|
|
216
|
+
}
|
|
217
|
+
// ============================================================================
|
|
118
218
|
// Schema Modification
|
|
119
219
|
// ============================================================================
|
|
120
220
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.98",
|
|
4
4
|
"description": "Developer tooling and CLI commands for VeloxTS framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"pluralize": "8.0.0",
|
|
42
42
|
"tsx": "4.21.0",
|
|
43
43
|
"yaml": "2.8.2",
|
|
44
|
-
"@veloxts/
|
|
45
|
-
"@veloxts/
|
|
46
|
-
"@veloxts/
|
|
47
|
-
"@veloxts/
|
|
48
|
-
"@veloxts/validation": "0.6.
|
|
44
|
+
"@veloxts/auth": "0.6.98",
|
|
45
|
+
"@veloxts/orm": "0.6.98",
|
|
46
|
+
"@veloxts/router": "0.6.98",
|
|
47
|
+
"@veloxts/core": "0.6.98",
|
|
48
|
+
"@veloxts/validation": "0.6.98"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@prisma/client": ">=7.0.0"
|