11import {
2- Component , EventEmitter ,
3- Input , OnInit , Output
2+ Component , EventEmitter , forwardRef ,
3+ Input , OnDestroy , OnInit , Output
44} from '@angular/core' ;
5+ import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
6+ import { SqlValidationService } from '../../services/code-editor/sql-validation.service' ;
57
6- interface ConsoleOptions {
8+ export interface ConsoleOptions {
79 value ?: string ;
810 language ?: 'sql' ;
911 theme ?: 'vs' | 'vs-dark' | 'hc-black' | string ;
@@ -17,7 +19,9 @@ interface ConsoleOptions {
1719 } ;
1820 overviewRulerLanes ?: number ;
1921 wordWrap ?: 'off' | 'on' | 'wordWrapColumn' | 'bounded' ;
20- automaticLayout : boolean ;
22+ automaticLayout ?: boolean ;
23+ lineNumbers ?: 'off' | 'on' ;
24+ cursorSmoothCaretAnimation ?: 'off' | 'on' ;
2125}
2226
2327const SQL_KEYWORDS = [ 'CREATE' , 'DROP' , 'ALTER' , 'TRUNCATE' ,
@@ -28,24 +32,34 @@ const SQL_KEYWORDS = ['CREATE', 'DROP', 'ALTER', 'TRUNCATE',
2832 'FROM' , 'WHERE' , 'GROUP BY' , 'HAVING' , 'ORDER BY' , 'DISTINCT' ,
2933 'JOIN' , 'INNER' , 'LEFT' , 'RIGHT' , 'FULL' , 'UNION' , 'INTERSECT' ,
3034 'NULL' , 'TRUE' , 'FALSE' ,
31- 'AS' , 'CASE' , 'WHEN' , 'THEN' , 'END'
35+ 'AS' , 'CASE' , 'WHEN' , 'THEN' , 'END' ,
36+ 'LIMIT' , 'OFFSET'
3237] ;
3338
3439@Component ( {
3540 selector : 'app-code-editor' ,
3641 templateUrl : './code-editor.component.html' ,
37- styleUrls : [ './code-editor.component.scss' ]
42+ styleUrls : [ './code-editor.component.scss' ] ,
43+ providers : [
44+ {
45+ provide : NG_VALUE_ACCESSOR ,
46+ useExisting : forwardRef ( ( ) => CodeEditorComponent ) ,
47+ multi : true
48+ }
49+ ]
3850} )
39- export class CodeEditorComponent implements OnInit {
51+ export class CodeEditorComponent implements OnInit , OnDestroy , ControlValueAccessor {
52+ @Input ( ) showFullEditor = true ;
53+ @Input ( ) showSuggestions = false ;
4054 @Input ( ) consoleOptions ?: ConsoleOptions ;
4155 @Output ( ) execute = new EventEmitter < string > ( ) ;
4256 @Output ( ) clearData = new EventEmitter < void > ( ) ;
4357 @Input ( ) queryError : string | null = null ;
4458 @Input ( ) customKeywords : string [ ] = [ ] ;
59+
4560 sqlQuery = '' ;
4661 errorMessage = '' ;
4762 successMessage = '' ;
48-
4963 readonly defaultOptions : ConsoleOptions = {
5064 value : this . sqlQuery ,
5165 language : 'sql' ,
@@ -60,17 +74,29 @@ export class CodeEditorComponent implements OnInit {
6074 } ,
6175 overviewRulerLanes : 0 ,
6276 wordWrap : 'on' ,
63- automaticLayout : true
77+ automaticLayout : true ,
78+ lineNumbers : 'on' ,
79+ cursorSmoothCaretAnimation : 'off'
6480 } ;
81+ private completionProvider ?: monaco . IDisposable ;
82+ private onChange = ( _ : any ) => { } ;
83+ private onTouched = ( ) => { } ;
6584
66- constructor ( ) { }
85+ constructor ( private sqlValidationService : SqlValidationService ) { }
6786
6887 ngOnInit ( ) : void {
6988 this . consoleOptions = { ...this . defaultOptions , ...this . consoleOptions } ;
7089 }
7190
72- onEditorInit ( ) {
73- monaco . languages . registerCompletionItemProvider ( 'sql' , {
91+ ngOnDestroy ( ) : void {
92+ if ( this . completionProvider ) {
93+ this . completionProvider . dispose ( ) ;
94+ this . completionProvider = undefined ;
95+ }
96+ }
97+
98+ onEditorInit ( editorInstance : monaco . editor . IStandaloneCodeEditor ) {
99+ this . completionProvider = monaco . languages . registerCompletionItemProvider ( 'sql' , {
74100 provideCompletionItems : ( ) => {
75101 const allKeywords = Array . from ( new Set ( [
76102 ...SQL_KEYWORDS ,
@@ -86,24 +112,24 @@ export class CodeEditorComponent implements OnInit {
86112 return { suggestions } ;
87113 }
88114 } ) ;
115+
116+ editorInstance . onDidChangeModelContent ( ( ) => {
117+ const val = editorInstance . getValue ( ) ;
118+ this . sqlQuery = val ;
119+ this . onChange ( val ) ;
120+ this . onTouched ( ) ;
121+ } ) ;
89122 }
90123
91124 executeQuery ( ) : void {
92- this . resetMessages ( ) ;
93- const query = this . sqlQuery ? this . sqlQuery . trim ( ) : '' ;
94- if ( ! query ) {
95- this . errorMessage = 'The query cannot be empty.' ;
96- return ;
97- }
98-
99- const validationError = this . validateSqlQuery ( query ) ;
100- if ( validationError ) {
101- this . errorMessage = validationError ;
125+ this . clearMessages ( ) ;
126+ this . errorMessage = this . sqlValidationService . validateSqlQuery ( this . sqlQuery ) ;
127+ if ( this . errorMessage ) {
102128 return ;
103129 }
104130
105131 try {
106- const cleanedQuery = query . replace ( / \n / g, ' ' ) ;
132+ const cleanedQuery = this . sqlQuery . replace ( / \n / g, ' ' ) ;
107133 this . execute . emit ( cleanedQuery ) ;
108134 } catch ( err ) {
109135 this . errorMessage = err instanceof Error ? err . message : String ( err ) ;
@@ -112,20 +138,19 @@ export class CodeEditorComponent implements OnInit {
112138
113139 clearQuery ( ) : void {
114140 this . sqlQuery = '' ;
115- this . resetMessages ( ) ;
141+ this . clearMessages ( ) ;
116142 this . clearData . emit ( ) ;
117143 }
118144
119145 formatQuery ( ) : void {
120- this . resetMessages ( ) ;
146+ this . clearMessages ( ) ;
121147 this . sqlQuery = this . formatSql ( this . sqlQuery ) ;
122148 }
123149
124150 private formatSql ( sql : string ) : string {
125- const keywords = [ 'SELECT' , 'FROM' , 'WHERE' , 'JOIN' , 'LEFT' , 'RIGHT' , 'INNER' , 'ON' , 'GROUP BY' , 'ORDER BY' , 'LIMIT' ] ;
126151 let formatted = sql ;
127152
128- keywords . forEach ( keyword => {
153+ SQL_KEYWORDS . forEach ( keyword => {
129154 const regex = new RegExp ( `\\b${ keyword } \\b` , 'gi' ) ;
130155 formatted = formatted . replace ( regex , `\n${ keyword } ` ) ;
131156 } ) ;
@@ -134,148 +159,29 @@ export class CodeEditorComponent implements OnInit {
134159 }
135160
136161 copyQuery ( ) : void {
137- this . resetMessages ( ) ;
162+ this . clearMessages ( ) ;
138163 ( navigator as any ) . clipboard . writeText ( this . sqlQuery ) ;
139164 this . successMessage = 'Query copied to clipboard.' ;
140165 }
141166
142- resetMessages ( ) : void {
167+ clearMessages ( ) : void {
143168 this . errorMessage = '' ;
144169 this . successMessage = '' ;
145170 }
146171
147- private validateSqlQuery ( query : string ) : string | null {
148- const trimmed = query . trim ( ) . replace ( / ; + \s * $ / , '' ) ;
149- const upper = trimmed . toUpperCase ( ) ;
150-
151- const startPattern = / ^ \s * S E L E C T \b / i;
152- if ( ! startPattern . test ( trimmed ) ) {
153- return 'Query must start with SELECT.' ;
154- }
155-
156- const minimalPattern = / ^ \s * S E L E C T \s + .+ \s + F R O M \s + .+ / is;
157- if ( ! minimalPattern . test ( trimmed ) ) {
158- return 'Query must be at least: SELECT <columns> FROM <table>.' ;
159- }
160-
161- const forbiddenPattern = / \b ( I N S E R T | U P D A T E | D E L E T E | D R O P | A L T E R | C R E A T E | R E P L A C E | T R U N C A T E | M E R G E | G R A N T | R E V O K E | E X E C | E X E C U T E | C O M M I T | R O L L B A C K | I N T O ) \b / i;
162- const commentPattern = / ( - - .* ?$ | \/ \* .* ?\* \/ ) / gm;
163- const allowedFunctions = new Set ( [ 'COUNT' , 'AVG' , 'MIN' , 'MAX' , 'SUM' ] ) ;
164-
165- if ( forbiddenPattern . test ( upper ) ) {
166- return 'Query contains forbidden SQL keywords.' ;
167- }
168- if ( commentPattern . test ( trimmed ) ) {
169- return 'Query must not contain SQL comments (-- or /* */).' ;
170- }
171- if ( trimmed . includes ( ';' ) ) {
172- return 'Query must not contain internal semicolons.' ;
173- }
174- if ( ! this . balancedQuotes ( trimmed ) ) {
175- return 'Quotes are not balanced.' ;
176- }
177- if ( ! this . balancedParentheses ( trimmed ) ) {
178- return 'Parentheses are not balanced.' ;
179- }
180-
181- if ( this . hasMisplacedCommas ( trimmed ) ) {
182- return 'Query contains misplaced commas.' ;
183- }
184-
185- if ( this . hasSubqueryWithoutAlias ( trimmed ) ) {
186- return 'Subquery in FROM must have an alias.' ;
187- }
188-
189- const functions = this . extractFunctions ( upper ) ;
190- for ( const func of functions ) {
191- if ( ! allowedFunctions . has ( func ) ) {
192- return `Unsupported SQL function: ${ func } .` ;
193- }
194- }
195-
196- return null ;
197- }
198-
199- private balancedParentheses ( query : string ) : boolean {
200- let count = 0 ;
201- for ( const c of query ) {
202- if ( c === '(' ) {
203- count ++ ;
204- } else if ( c === ')' ) {
205- count -- ;
206- }
207- if ( count < 0 ) {
208- return false ;
209- }
210- }
211- return count === 0 ;
172+ writeValue ( value : any ) : void {
173+ this . sqlQuery = value || '' ;
212174 }
213175
214- private balancedQuotes ( query : string ) : boolean {
215- let sq = 0 ;
216- let dq = 0 ;
217- let escaped = false ; for ( const c of query ) {
218- if ( escaped ) { escaped = false ; continue ; }
219- if ( c === '\\' ) { escaped = true ; continue ; }
220- if ( c === '\'' ) {
221- sq ++ ;
222- } else {
223- if ( c === '"' ) { dq ++ ; }
224- }
225- }
226- return ( sq % 2 === 0 ) && ( dq % 2 === 0 ) ;
176+ registerOnChange ( fn : any ) : void {
177+ this . onChange = fn ;
227178 }
228179
229- private extractFunctions ( upperQuery : string ) : string [ ] {
230- const funcPattern = / \b ( C O U N T | A V G | M I N | M A X | S U M ) \s * \( / g;
231- const funcs : string [ ] = [ ] ;
232-
233- let match : RegExpExecArray | null = funcPattern . exec ( upperQuery ) ;
234- while ( match !== null ) {
235- funcs . push ( match [ 1 ] ) ;
236- match = funcPattern . exec ( upperQuery ) ;
237- }
238-
239- return funcs ;
180+ registerOnTouched ( fn : any ) : void {
181+ this . onTouched = fn ;
240182 }
241183
242- private hasMisplacedCommas ( query : string ) : boolean {
243- const upperQuery = query . toUpperCase ( ) ;
244-
245- if ( upperQuery . startsWith ( 'SELECT ,' ) || upperQuery . includes ( ',,' ) ) {
246- return true ;
247- }
248-
249- if ( / , \s * F R O M / i. test ( upperQuery ) ) {
250- return true ;
251- }
252-
253- const selectPart = query
254- . replace ( / ^ S E L E C T \s + / i, '' )
255- . replace ( / \s + F R O M .* $ / i, '' )
256- . trim ( ) ;
257-
258- if ( selectPart . startsWith ( ',' ) || selectPart . endsWith ( ',' ) ) {
259- return true ;
260- }
261-
262- const fields = selectPart . split ( ',' ) ;
263- for ( const f of fields ) {
264- if ( f . trim ( ) === '' ) {
265- return true ;
266- }
267- }
268-
269- return false ;
270- }
271-
272- private hasSubqueryWithoutAlias ( query : string ) : boolean {
273- const subqueryRegex = / F R O M \s * \( [ ^ ) ] * \) / i;
274- if ( ! subqueryRegex . test ( query ) ) {
275- return false ;
276- }
277- const aliasRegex = / F R O M \s * \( [ ^ ) ] * \) \s + ( A S \s + \w + | \w + ) / i;
278- return ! aliasRegex . test ( query ) ;
184+ setDisabledState ?( isDisabled : boolean ) : void {
185+ // Optional: handle disabled state
279186 }
280-
281187}
0 commit comments