@claudetools/tools 0.3.9 → 0.5.0
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 +60 -4
- package/dist/cli.js +0 -0
- package/dist/codedna/generators/base.d.ts +41 -0
- package/dist/codedna/generators/base.js +102 -0
- package/dist/codedna/generators/express-api.d.ts +12 -0
- package/dist/codedna/generators/express-api.js +61 -0
- package/dist/codedna/index.d.ts +4 -0
- package/dist/codedna/index.js +7 -0
- package/dist/codedna/parser.d.ts +117 -0
- package/dist/codedna/parser.js +233 -0
- package/dist/codedna/registry.d.ts +60 -0
- package/dist/codedna/registry.js +217 -0
- package/dist/codedna/template-engine.d.ts +17 -0
- package/dist/codedna/template-engine.js +183 -0
- package/dist/codedna/types.d.ts +64 -0
- package/dist/codedna/types.js +4 -0
- package/dist/handlers/codedna-handlers.d.ts +122 -0
- package/dist/handlers/codedna-handlers.js +194 -0
- package/dist/handlers/tool-handlers.js +593 -14
- package/dist/helpers/api-client.d.ts +37 -0
- package/dist/helpers/api-client.js +70 -0
- package/dist/helpers/codedna-monitoring.d.ts +34 -0
- package/dist/helpers/codedna-monitoring.js +159 -0
- package/dist/helpers/error-tracking.d.ts +73 -0
- package/dist/helpers/error-tracking.js +164 -0
- package/dist/helpers/library-detection.d.ts +26 -0
- package/dist/helpers/library-detection.js +145 -0
- package/dist/helpers/tasks-retry.d.ts +49 -0
- package/dist/helpers/tasks-retry.js +168 -0
- package/dist/helpers/tasks.d.ts +24 -1
- package/dist/helpers/tasks.js +146 -50
- package/dist/helpers/usage-analytics.d.ts +91 -0
- package/dist/helpers/usage-analytics.js +256 -0
- package/dist/helpers/workers.d.ts +25 -0
- package/dist/helpers/workers.js +80 -0
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +16 -5
- package/dist/tools.js +314 -0
- package/docs/AUTO-REGISTRATION.md +353 -0
- package/docs/CLAUDE4_PROMPT_ANALYSIS.md +589 -0
- package/docs/ENTITY_DSL_REFERENCE.md +685 -0
- package/docs/MODERN_STACK_COMPLETE_GUIDE.md +706 -0
- package/docs/PROMPT_STANDARDIZATION_RESULTS.md +324 -0
- package/docs/PROMPT_TIER_TEMPLATES.md +787 -0
- package/docs/RESEARCH_METHODOLOGY_EXTRACTION.md +336 -0
- package/package.json +14 -3
- package/scripts/verify-prompt-compliance.sh +197 -0
package/README.md
CHANGED
|
@@ -67,19 +67,75 @@ CLAUDETOOLS_API_URL=https://api.claudetools.dev
|
|
|
67
67
|
- **Impact analysis** for changes
|
|
68
68
|
- **Pattern detection** (security, performance)
|
|
69
69
|
|
|
70
|
+
### CodeDNA (AI Code Generation)
|
|
71
|
+
- **95-99% token savings** by generating production code from compact Entity DSL specs
|
|
72
|
+
- **Production-ready APIs** with models, controllers, routes, validation, tests
|
|
73
|
+
- **Express.js generator** available (FastAPI, NestJS, React coming soon)
|
|
74
|
+
- **Template registry** hosted on ClaudeTools API with global CDN
|
|
75
|
+
|
|
76
|
+
### 10/10 Prompt Engineering Framework
|
|
77
|
+
- **Production-proven architecture** from Claude 4 analysis (~60K char prompt)
|
|
78
|
+
- **4 complexity tiers** (Minimal/Standard/Professional/Enterprise) with progressive disclosure
|
|
79
|
+
- **7 semantic layers** with XML boundaries for machine-parseability
|
|
80
|
+
- **Token-optimized templates** (500t → 10000t based on complexity)
|
|
81
|
+
- **Compliance verification** with automated validation script
|
|
82
|
+
|
|
83
|
+
#### Quick Example
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Instead of AI writing 1000 lines of code (30,000 tokens)...
|
|
87
|
+
// Use a compact Entity DSL spec (150 tokens):
|
|
88
|
+
|
|
89
|
+
codedna_generate_api({
|
|
90
|
+
spec: "User(email:string:unique, password:string:hashed, age:integer:min(18))",
|
|
91
|
+
framework: "express",
|
|
92
|
+
options: {
|
|
93
|
+
auth: true,
|
|
94
|
+
validation: true,
|
|
95
|
+
tests: true,
|
|
96
|
+
database: "postgresql"
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Returns 6 complete files:
|
|
101
|
+
// - src/models/user.model.ts
|
|
102
|
+
// - src/controllers/user.controller.ts
|
|
103
|
+
// - src/routes/user.routes.ts
|
|
104
|
+
// - src/validators/user.validator.ts
|
|
105
|
+
// - src/middleware/auth.middleware.ts
|
|
106
|
+
// - tests/user.test.ts
|
|
107
|
+
|
|
108
|
+
// Token savings: 11,100 tokens (98.7%)
|
|
109
|
+
// Cost savings: $0.11 per generation
|
|
110
|
+
// Time savings: 2-3 minutes → 10 seconds
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Available Tools
|
|
114
|
+
|
|
115
|
+
- `codedna_generate_api(spec, framework, options)` - Generate complete REST API
|
|
116
|
+
- `codedna_validate_spec(spec)` - Validate Entity DSL syntax
|
|
117
|
+
- `codedna_list_generators()` - List available code generators
|
|
118
|
+
|
|
119
|
+
See [CODEDNA_README.md](./CODEDNA_README.md) for full documentation.
|
|
120
|
+
|
|
70
121
|
## CLI
|
|
71
122
|
|
|
72
123
|
```bash
|
|
73
|
-
claudetools --setup
|
|
74
|
-
claudetools --version
|
|
75
|
-
claudetools --help
|
|
76
|
-
claudetools
|
|
124
|
+
claudetools --setup # Interactive configuration
|
|
125
|
+
claudetools --version # Show version
|
|
126
|
+
claudetools --help # Show help
|
|
127
|
+
claudetools # Start MCP server
|
|
128
|
+
npm run prompt:verify # Verify prompt compliance with 10/10 framework
|
|
77
129
|
```
|
|
78
130
|
|
|
79
131
|
## Documentation
|
|
80
132
|
|
|
81
133
|
- [GitHub](https://github.com/claudetools/memory)
|
|
82
134
|
- [Configuration Guide](./CONFIG.md)
|
|
135
|
+
- [CodeDNA Guide](./CODEDNA_README.md) - AI code generation with 95-99% token savings
|
|
136
|
+
- [10/10 Prompt Framework](./docs/PROMPT_TIER_TEMPLATES.md) - Production-grade prompt engineering
|
|
137
|
+
- [Claude 4 Analysis](./docs/CLAUDE4_PROMPT_ANALYSIS.md) - Insights from ~60K char production prompt
|
|
138
|
+
- [Research Methodology](./docs/RESEARCH_METHODOLOGY_EXTRACTION.md) - Claude.ai Desktop research patterns
|
|
83
139
|
|
|
84
140
|
## License
|
|
85
141
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EntitySpec } from '../parser.js';
|
|
2
|
+
import { TemplateEngine } from '../template-engine.js';
|
|
3
|
+
import { TemplateRegistry } from '../registry.js';
|
|
4
|
+
import { GenerationResult, GenerationMetadata } from '../types.js';
|
|
5
|
+
export declare abstract class BaseGenerator {
|
|
6
|
+
protected engine: TemplateEngine;
|
|
7
|
+
protected registry: TemplateRegistry;
|
|
8
|
+
constructor(registry: TemplateRegistry);
|
|
9
|
+
/**
|
|
10
|
+
* Generate code from entity specification
|
|
11
|
+
*/
|
|
12
|
+
generate(entity: EntitySpec, options?: any): Promise<GenerationResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Get the generator ID (e.g., "express-api")
|
|
15
|
+
*/
|
|
16
|
+
protected abstract getGeneratorId(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Get list of required template files based on options
|
|
19
|
+
*/
|
|
20
|
+
protected abstract getRequiredTemplates(options: any): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Get mapping of output file paths to template files
|
|
23
|
+
*/
|
|
24
|
+
protected abstract getFileMapping(entity: EntitySpec, options: any): Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Calculate generation metadata
|
|
27
|
+
*/
|
|
28
|
+
protected calculateMetadata(generatorId: string, framework: string, entity: EntitySpec, files: Record<string, string>): GenerationMetadata;
|
|
29
|
+
/**
|
|
30
|
+
* Validate options against generator capabilities
|
|
31
|
+
*/
|
|
32
|
+
protected validateOptions(options: any): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Helper to get output path for entity file
|
|
35
|
+
*/
|
|
36
|
+
protected getEntityPath(entity: EntitySpec, directory: string, extension: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Helper to check if option is enabled
|
|
39
|
+
*/
|
|
40
|
+
protected isEnabled(options: any, key: string, defaultValue?: boolean): boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Base Generator Class
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Abstract base class for all code generators with common file generation
|
|
6
|
+
// pipeline, metadata tracking, and utilities.
|
|
7
|
+
//
|
|
8
|
+
import { TemplateEngine, buildContext } from '../template-engine.js';
|
|
9
|
+
export class BaseGenerator {
|
|
10
|
+
engine;
|
|
11
|
+
registry;
|
|
12
|
+
constructor(registry) {
|
|
13
|
+
this.engine = new TemplateEngine();
|
|
14
|
+
this.registry = registry;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate code from entity specification
|
|
18
|
+
*/
|
|
19
|
+
async generate(entity, options = {}) {
|
|
20
|
+
// Get generator metadata
|
|
21
|
+
const generatorId = this.getGeneratorId();
|
|
22
|
+
const metadata = await this.registry.getGeneratorMetadata(generatorId);
|
|
23
|
+
// Get required templates
|
|
24
|
+
const templateFiles = this.getRequiredTemplates(options);
|
|
25
|
+
const templates = await this.registry.getTemplates(generatorId, templateFiles);
|
|
26
|
+
// Build template context
|
|
27
|
+
const context = buildContext(entity, options);
|
|
28
|
+
// Generate files
|
|
29
|
+
const files = {};
|
|
30
|
+
const fileMapping = this.getFileMapping(entity, options);
|
|
31
|
+
for (const [outputPath, templateFile] of Object.entries(fileMapping)) {
|
|
32
|
+
const template = templates[templateFile];
|
|
33
|
+
if (!template) {
|
|
34
|
+
throw new Error(`Template not found: ${templateFile}`);
|
|
35
|
+
}
|
|
36
|
+
const content = this.engine.render(template, context);
|
|
37
|
+
files[outputPath] = content;
|
|
38
|
+
}
|
|
39
|
+
// Calculate metadata
|
|
40
|
+
const generationMetadata = this.calculateMetadata(generatorId, metadata.framework, entity, files);
|
|
41
|
+
return {
|
|
42
|
+
files,
|
|
43
|
+
metadata: generationMetadata,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Calculate generation metadata
|
|
48
|
+
*/
|
|
49
|
+
calculateMetadata(generatorId, framework, entity, files) {
|
|
50
|
+
const filesGenerated = Object.keys(files).length;
|
|
51
|
+
const linesOfCode = Object.values(files).reduce((sum, content) => sum + content.split('\n').length, 0);
|
|
52
|
+
// Estimate token savings
|
|
53
|
+
// Average: 1 line of code ≈ 25 tokens
|
|
54
|
+
// AI would generate all lines from scratch
|
|
55
|
+
// With CodeDNA: ~150 tokens for the API call
|
|
56
|
+
const estimatedTokensSaved = Math.max(0, linesOfCode * 25 - 150);
|
|
57
|
+
return {
|
|
58
|
+
generator: generatorId,
|
|
59
|
+
framework,
|
|
60
|
+
entities: [entity.name],
|
|
61
|
+
filesGenerated,
|
|
62
|
+
linesOfCode,
|
|
63
|
+
estimatedTokensSaved,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate options against generator capabilities
|
|
68
|
+
*/
|
|
69
|
+
async validateOptions(options) {
|
|
70
|
+
const generatorId = this.getGeneratorId();
|
|
71
|
+
const metadata = await this.registry.getGeneratorMetadata(generatorId);
|
|
72
|
+
// Validate database option
|
|
73
|
+
if (options.database && metadata.databases) {
|
|
74
|
+
if (!metadata.databases.includes(options.database)) {
|
|
75
|
+
throw new Error(`Unsupported database: ${options.database}. Supported: ${metadata.databases.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Validate features
|
|
79
|
+
if (options.auth && !metadata.features.includes('auth')) {
|
|
80
|
+
throw new Error('This generator does not support authentication');
|
|
81
|
+
}
|
|
82
|
+
if (options.validation && !metadata.features.includes('validation')) {
|
|
83
|
+
throw new Error('This generator does not support validation');
|
|
84
|
+
}
|
|
85
|
+
if (options.tests && !metadata.features.includes('tests')) {
|
|
86
|
+
throw new Error('This generator does not support test generation');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Helper to get output path for entity file
|
|
91
|
+
*/
|
|
92
|
+
getEntityPath(entity, directory, extension) {
|
|
93
|
+
const filename = entity.name.toLowerCase();
|
|
94
|
+
return `${directory}/${filename}.${extension}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Helper to check if option is enabled
|
|
98
|
+
*/
|
|
99
|
+
isEnabled(options, key, defaultValue = false) {
|
|
100
|
+
return options[key] ?? defaultValue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseGenerator } from './base.js';
|
|
2
|
+
import { EntitySpec } from '../parser.js';
|
|
3
|
+
import { GenerateApiOptions } from '../types.js';
|
|
4
|
+
export declare class ExpressApiGenerator extends BaseGenerator {
|
|
5
|
+
protected getGeneratorId(): string;
|
|
6
|
+
protected getRequiredTemplates(options: GenerateApiOptions): string[];
|
|
7
|
+
protected getFileMapping(entity: EntitySpec, options: GenerateApiOptions): Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Generate Express API from entity specification
|
|
10
|
+
*/
|
|
11
|
+
generate(entity: EntitySpec, options?: GenerateApiOptions): Promise<import('../types.js').GenerationResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Express API Generator
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Generate TypeScript Express REST API with CRUD operations, models,
|
|
6
|
+
// controllers, routes, middleware, and validators.
|
|
7
|
+
//
|
|
8
|
+
import { BaseGenerator } from './base.js';
|
|
9
|
+
export class ExpressApiGenerator extends BaseGenerator {
|
|
10
|
+
getGeneratorId() {
|
|
11
|
+
return 'express-api';
|
|
12
|
+
}
|
|
13
|
+
getRequiredTemplates(options) {
|
|
14
|
+
const templates = [
|
|
15
|
+
'model.ts.j2',
|
|
16
|
+
'controller.ts.j2',
|
|
17
|
+
'route.ts.j2',
|
|
18
|
+
'validator.ts.j2',
|
|
19
|
+
];
|
|
20
|
+
if (this.isEnabled(options, 'auth')) {
|
|
21
|
+
templates.push('middleware.ts.j2');
|
|
22
|
+
}
|
|
23
|
+
if (this.isEnabled(options, 'tests')) {
|
|
24
|
+
templates.push('test.ts.j2');
|
|
25
|
+
}
|
|
26
|
+
return templates;
|
|
27
|
+
}
|
|
28
|
+
getFileMapping(entity, options) {
|
|
29
|
+
const entityLower = entity.name.toLowerCase();
|
|
30
|
+
const mapping = {
|
|
31
|
+
[`src/models/${entityLower}.model.ts`]: 'model.ts.j2',
|
|
32
|
+
[`src/controllers/${entityLower}.controller.ts`]: 'controller.ts.j2',
|
|
33
|
+
[`src/routes/${entityLower}.routes.ts`]: 'route.ts.j2',
|
|
34
|
+
[`src/validators/${entityLower}.validator.ts`]: 'validator.ts.j2',
|
|
35
|
+
};
|
|
36
|
+
if (this.isEnabled(options, 'auth')) {
|
|
37
|
+
mapping['src/middleware/auth.middleware.ts'] = 'middleware.ts.j2';
|
|
38
|
+
}
|
|
39
|
+
if (this.isEnabled(options, 'tests')) {
|
|
40
|
+
mapping[`src/__tests__/${entityLower}.test.ts`] = 'test.ts.j2';
|
|
41
|
+
}
|
|
42
|
+
return mapping;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Generate Express API from entity specification
|
|
46
|
+
*/
|
|
47
|
+
async generate(entity, options = {}) {
|
|
48
|
+
// Validate options
|
|
49
|
+
await this.validateOptions(options);
|
|
50
|
+
// Set defaults
|
|
51
|
+
const opts = {
|
|
52
|
+
auth: false,
|
|
53
|
+
validation: true,
|
|
54
|
+
tests: false,
|
|
55
|
+
database: 'postgresql',
|
|
56
|
+
...options,
|
|
57
|
+
};
|
|
58
|
+
// Generate files
|
|
59
|
+
return super.generate(entity, opts);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// CodeDNA Module Exports
|
|
3
|
+
// =============================================================================
|
|
4
|
+
export * from './parser.js';
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export * from './template-engine.js';
|
|
7
|
+
export * from './registry.js';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export interface EntitySpec {
|
|
2
|
+
name: string;
|
|
3
|
+
fields: Field[];
|
|
4
|
+
hooks?: LifecycleHooks;
|
|
5
|
+
permissions?: Permission[];
|
|
6
|
+
}
|
|
7
|
+
export interface LifecycleHooks {
|
|
8
|
+
beforeCreate?: string[];
|
|
9
|
+
afterCreate?: string[];
|
|
10
|
+
beforeUpdate?: string[];
|
|
11
|
+
afterUpdate?: string[];
|
|
12
|
+
beforeDelete?: string[];
|
|
13
|
+
afterDelete?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface Permission {
|
|
16
|
+
action: 'create' | 'read' | 'update' | 'delete';
|
|
17
|
+
roles?: string[];
|
|
18
|
+
condition?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Field {
|
|
21
|
+
name: string;
|
|
22
|
+
type: FieldType;
|
|
23
|
+
constraints: Constraint[];
|
|
24
|
+
}
|
|
25
|
+
export type FieldType = {
|
|
26
|
+
kind: 'primitive';
|
|
27
|
+
value: 'string' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'email' | 'url' | 'json';
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'array';
|
|
30
|
+
itemType: 'string' | 'integer' | 'decimal' | 'boolean';
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'reference';
|
|
33
|
+
entity: string;
|
|
34
|
+
relation?: 'oneToMany' | 'manyToMany';
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'enum';
|
|
37
|
+
values: string[];
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'computed';
|
|
40
|
+
expression: string;
|
|
41
|
+
};
|
|
42
|
+
export type Constraint = {
|
|
43
|
+
kind: 'unique';
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'required';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'hashed';
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'index';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'min';
|
|
52
|
+
value: number;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'max';
|
|
55
|
+
value: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'default';
|
|
58
|
+
value: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'length';
|
|
61
|
+
min?: number;
|
|
62
|
+
max?: number;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'pattern';
|
|
65
|
+
regex: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'email';
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'url';
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'nullable';
|
|
72
|
+
} | {
|
|
73
|
+
kind: 'immutable';
|
|
74
|
+
};
|
|
75
|
+
export declare class EntityParser {
|
|
76
|
+
/**
|
|
77
|
+
* Parse Entity DSL specification into structured EntitySpec
|
|
78
|
+
*/
|
|
79
|
+
parse(spec: string): EntitySpec;
|
|
80
|
+
/**
|
|
81
|
+
* Parse comma-separated field definitions
|
|
82
|
+
*/
|
|
83
|
+
private parseFields;
|
|
84
|
+
/**
|
|
85
|
+
* Split fields by commas, respecting parentheses nesting
|
|
86
|
+
*/
|
|
87
|
+
private splitFields;
|
|
88
|
+
/**
|
|
89
|
+
* Parse single field definition
|
|
90
|
+
* Format: name:type:constraint1:constraint2
|
|
91
|
+
*/
|
|
92
|
+
private parseField;
|
|
93
|
+
/**
|
|
94
|
+
* Parse field type (primitive, reference, enum, array, computed)
|
|
95
|
+
*/
|
|
96
|
+
private parseType;
|
|
97
|
+
/**
|
|
98
|
+
* Parse field constraint (unique, required, hashed, min, max, default, length, pattern, email, url, nullable, immutable)
|
|
99
|
+
*/
|
|
100
|
+
private parseConstraint;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate Entity DSL specification
|
|
104
|
+
*/
|
|
105
|
+
export declare function validateSpec(spec: string): {
|
|
106
|
+
valid: boolean;
|
|
107
|
+
errors?: string[];
|
|
108
|
+
parsed?: EntitySpec;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Helper to check if field has specific constraint
|
|
112
|
+
*/
|
|
113
|
+
export declare function hasConstraint(field: Field, kind: Constraint['kind']): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Helper to get constraint value
|
|
116
|
+
*/
|
|
117
|
+
export declare function getConstraintValue<T extends Constraint>(field: Field, kind: T['kind']): T | undefined;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Entity DSL Parser
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Parses compact Entity DSL specifications into structured data for code generation.
|
|
6
|
+
//
|
|
7
|
+
// Syntax: EntityName(field:type:constraint, field:type:constraint)
|
|
8
|
+
//
|
|
9
|
+
// Example: User(email:string:unique:required, password:string:hashed, age:integer:min(18))
|
|
10
|
+
//
|
|
11
|
+
export class EntityParser {
|
|
12
|
+
/**
|
|
13
|
+
* Parse Entity DSL specification into structured EntitySpec
|
|
14
|
+
*/
|
|
15
|
+
parse(spec) {
|
|
16
|
+
// Remove whitespace
|
|
17
|
+
spec = spec.trim();
|
|
18
|
+
// Extract entity name
|
|
19
|
+
const nameMatch = spec.match(/^([A-Z][a-zA-Z0-9]*)\(/);
|
|
20
|
+
if (!nameMatch) {
|
|
21
|
+
throw new Error('Invalid entity spec: must start with EntityName(');
|
|
22
|
+
}
|
|
23
|
+
const name = nameMatch[1];
|
|
24
|
+
// Extract fields section
|
|
25
|
+
const fieldsMatch = spec.match(/\((.*)\)$/);
|
|
26
|
+
if (!fieldsMatch) {
|
|
27
|
+
throw new Error('Invalid entity spec: missing closing parenthesis');
|
|
28
|
+
}
|
|
29
|
+
const fieldsStr = fieldsMatch[1];
|
|
30
|
+
const fields = fieldsStr.trim() ? this.parseFields(fieldsStr) : [];
|
|
31
|
+
return { name, fields };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse comma-separated field definitions
|
|
35
|
+
*/
|
|
36
|
+
parseFields(fieldsStr) {
|
|
37
|
+
// Split by commas (but not inside parentheses)
|
|
38
|
+
const fieldStrs = this.splitFields(fieldsStr);
|
|
39
|
+
return fieldStrs.map(fieldStr => this.parseField(fieldStr.trim()));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Split fields by commas, respecting parentheses nesting
|
|
43
|
+
*/
|
|
44
|
+
splitFields(str) {
|
|
45
|
+
const fields = [];
|
|
46
|
+
let current = '';
|
|
47
|
+
let depth = 0;
|
|
48
|
+
for (let i = 0; i < str.length; i++) {
|
|
49
|
+
const char = str[i];
|
|
50
|
+
if (char === '(')
|
|
51
|
+
depth++;
|
|
52
|
+
if (char === ')')
|
|
53
|
+
depth--;
|
|
54
|
+
if (char === ',' && depth === 0) {
|
|
55
|
+
fields.push(current);
|
|
56
|
+
current = '';
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
current += char;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (current)
|
|
63
|
+
fields.push(current);
|
|
64
|
+
return fields;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse single field definition
|
|
68
|
+
* Format: name:type:constraint1:constraint2
|
|
69
|
+
*/
|
|
70
|
+
parseField(fieldStr) {
|
|
71
|
+
const parts = fieldStr.split(':');
|
|
72
|
+
if (parts.length < 2) {
|
|
73
|
+
throw new Error(`Invalid field spec: ${fieldStr}`);
|
|
74
|
+
}
|
|
75
|
+
const name = parts[0].trim();
|
|
76
|
+
const typeStr = parts[1].trim();
|
|
77
|
+
const constraintStrs = parts.slice(2).map(s => s.trim());
|
|
78
|
+
// Validate field name
|
|
79
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
80
|
+
throw new Error(`Invalid field name: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
const type = this.parseType(typeStr);
|
|
83
|
+
const constraints = constraintStrs.map(c => this.parseConstraint(c));
|
|
84
|
+
return { name, type, constraints };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse field type (primitive, reference, enum, array, computed)
|
|
88
|
+
*/
|
|
89
|
+
parseType(typeStr) {
|
|
90
|
+
// Check for reference with relation: refMany(EntityName) or refOne(EntityName)
|
|
91
|
+
// oneToMany = entity has many of this type (e.g., User has many Posts)
|
|
92
|
+
// manyToMany = entities have many-to-many relationship
|
|
93
|
+
const refManyMatch = typeStr.match(/^refMany\(([A-Z][a-zA-Z0-9]*)\)$/);
|
|
94
|
+
if (refManyMatch) {
|
|
95
|
+
return { kind: 'reference', entity: refManyMatch[1], relation: 'manyToMany' };
|
|
96
|
+
}
|
|
97
|
+
const refOneMatch = typeStr.match(/^refOne\(([A-Z][a-zA-Z0-9]*)\)$/);
|
|
98
|
+
if (refOneMatch) {
|
|
99
|
+
return { kind: 'reference', entity: refOneMatch[1], relation: 'oneToMany' };
|
|
100
|
+
}
|
|
101
|
+
// Check for reference: ref(EntityName) - default simple reference
|
|
102
|
+
const refMatch = typeStr.match(/^ref\(([A-Z][a-zA-Z0-9]*)\)$/);
|
|
103
|
+
if (refMatch) {
|
|
104
|
+
return { kind: 'reference', entity: refMatch[1] };
|
|
105
|
+
}
|
|
106
|
+
// Check for array: array(string), array(integer), etc.
|
|
107
|
+
const arrayMatch = typeStr.match(/^array\((string|integer|decimal|boolean)\)$/);
|
|
108
|
+
if (arrayMatch) {
|
|
109
|
+
const itemType = arrayMatch[1];
|
|
110
|
+
return { kind: 'array', itemType };
|
|
111
|
+
}
|
|
112
|
+
// Check for computed: computed(expression)
|
|
113
|
+
const computedMatch = typeStr.match(/^computed\((.+)\)$/);
|
|
114
|
+
if (computedMatch) {
|
|
115
|
+
return { kind: 'computed', expression: computedMatch[1] };
|
|
116
|
+
}
|
|
117
|
+
// Check for enum: enum(val1,val2,val3)
|
|
118
|
+
const enumMatch = typeStr.match(/^enum\((.+)\)$/);
|
|
119
|
+
if (enumMatch) {
|
|
120
|
+
const values = enumMatch[1].split(',').map(v => v.trim());
|
|
121
|
+
if (values.length === 0) {
|
|
122
|
+
throw new Error('Enum must have at least one value');
|
|
123
|
+
}
|
|
124
|
+
return { kind: 'enum', values };
|
|
125
|
+
}
|
|
126
|
+
// Primitive types (including new email, url, json)
|
|
127
|
+
const primitives = ['string', 'integer', 'decimal', 'boolean', 'datetime', 'email', 'url', 'json'];
|
|
128
|
+
if (primitives.includes(typeStr)) {
|
|
129
|
+
return { kind: 'primitive', value: typeStr };
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Unknown field type: ${typeStr}`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse field constraint (unique, required, hashed, min, max, default, length, pattern, email, url, nullable, immutable)
|
|
135
|
+
*/
|
|
136
|
+
parseConstraint(constraintStr) {
|
|
137
|
+
// Simple constraints
|
|
138
|
+
if (constraintStr === 'unique')
|
|
139
|
+
return { kind: 'unique' };
|
|
140
|
+
if (constraintStr === 'required')
|
|
141
|
+
return { kind: 'required' };
|
|
142
|
+
if (constraintStr === 'hashed')
|
|
143
|
+
return { kind: 'hashed' };
|
|
144
|
+
if (constraintStr === 'index')
|
|
145
|
+
return { kind: 'index' };
|
|
146
|
+
if (constraintStr === 'email')
|
|
147
|
+
return { kind: 'email' };
|
|
148
|
+
if (constraintStr === 'url')
|
|
149
|
+
return { kind: 'url' };
|
|
150
|
+
if (constraintStr === 'nullable')
|
|
151
|
+
return { kind: 'nullable' };
|
|
152
|
+
if (constraintStr === 'immutable')
|
|
153
|
+
return { kind: 'immutable' };
|
|
154
|
+
// Parameterized constraints: min(18), max(100), default(true), length(10,100), pattern(/regex/)
|
|
155
|
+
const paramMatch = constraintStr.match(/^([a-z]+)\((.+)\)$/);
|
|
156
|
+
if (paramMatch) {
|
|
157
|
+
const [, kind, value] = paramMatch;
|
|
158
|
+
if (kind === 'min') {
|
|
159
|
+
const numValue = Number(value);
|
|
160
|
+
if (isNaN(numValue)) {
|
|
161
|
+
throw new Error(`Invalid min constraint value: ${value}`);
|
|
162
|
+
}
|
|
163
|
+
return { kind: 'min', value: numValue };
|
|
164
|
+
}
|
|
165
|
+
if (kind === 'max') {
|
|
166
|
+
const numValue = Number(value);
|
|
167
|
+
if (isNaN(numValue)) {
|
|
168
|
+
throw new Error(`Invalid max constraint value: ${value}`);
|
|
169
|
+
}
|
|
170
|
+
return { kind: 'max', value: numValue };
|
|
171
|
+
}
|
|
172
|
+
if (kind === 'default') {
|
|
173
|
+
return { kind: 'default', value };
|
|
174
|
+
}
|
|
175
|
+
if (kind === 'length') {
|
|
176
|
+
// Support both length(max) and length(min,max)
|
|
177
|
+
const parts = value.split(',').map(v => v.trim());
|
|
178
|
+
if (parts.length === 1) {
|
|
179
|
+
const maxValue = Number(parts[0]);
|
|
180
|
+
if (isNaN(maxValue)) {
|
|
181
|
+
throw new Error(`Invalid length constraint value: ${parts[0]}`);
|
|
182
|
+
}
|
|
183
|
+
return { kind: 'length', max: maxValue };
|
|
184
|
+
}
|
|
185
|
+
else if (parts.length === 2) {
|
|
186
|
+
const minValue = Number(parts[0]);
|
|
187
|
+
const maxValue = Number(parts[1]);
|
|
188
|
+
if (isNaN(minValue) || isNaN(maxValue)) {
|
|
189
|
+
throw new Error(`Invalid length constraint values: ${value}`);
|
|
190
|
+
}
|
|
191
|
+
return { kind: 'length', min: minValue, max: maxValue };
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new Error(`Invalid length constraint format: ${value}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (kind === 'pattern') {
|
|
198
|
+
// Remove surrounding slashes if present
|
|
199
|
+
const regex = value.replace(/^\/(.+)\/$/, '$1');
|
|
200
|
+
return { kind: 'pattern', regex };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`Unknown constraint: ${constraintStr}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Validate Entity DSL specification
|
|
208
|
+
*/
|
|
209
|
+
export function validateSpec(spec) {
|
|
210
|
+
try {
|
|
211
|
+
const parser = new EntityParser();
|
|
212
|
+
const parsed = parser.parse(spec);
|
|
213
|
+
return { valid: true, parsed };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
valid: false,
|
|
218
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Helper to check if field has specific constraint
|
|
224
|
+
*/
|
|
225
|
+
export function hasConstraint(field, kind) {
|
|
226
|
+
return field.constraints.some(c => c.kind === kind);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Helper to get constraint value
|
|
230
|
+
*/
|
|
231
|
+
export function getConstraintValue(field, kind) {
|
|
232
|
+
return field.constraints.find(c => c.kind === kind);
|
|
233
|
+
}
|