forge-sql-orm 2.1.23 → 2.1.24
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 +1 -0
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +4 -0
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +17 -21
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts +16 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.js +60 -28
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -28
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js +20 -47
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/Rovo.d.ts +32 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +116 -67
- package/dist/core/Rovo.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +168 -118
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +0 -8
- package/dist/utils/cacheTableUtils.d.ts.map +1 -1
- package/dist/utils/cacheTableUtils.js +183 -126
- package/dist/utils/cacheTableUtils.js.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.js +31 -20
- package/dist/utils/forgeDriverProxy.js.map +1 -1
- package/dist/utils/sqlHints.d.ts.map +1 -1
- package/dist/utils/sqlHints.js +19 -29
- package/dist/utils/sqlHints.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +0 -29
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +107 -78
- package/dist/utils/sqlUtils.js.map +1 -1
- package/package.json +8 -8
- package/src/core/ForgeSQLAnalyseOperations.ts +18 -21
- package/src/core/ForgeSQLCrudOperations.ts +83 -33
- package/src/core/ForgeSQLQueryBuilder.ts +59 -154
- package/src/core/Rovo.ts +158 -98
- package/src/lib/drizzle/extensions/additionalActions.ts +287 -382
- package/src/utils/cacheTableUtils.ts +202 -144
- package/src/utils/forgeDriverProxy.ts +39 -21
- package/src/utils/sqlHints.ts +21 -26
- package/src/utils/sqlUtils.ts +151 -101
package/src/core/Rovo.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// qlty-ignore: +qlty:file-complexity
|
|
1
2
|
import {
|
|
2
3
|
ForgeSqlOperation,
|
|
3
4
|
ForgeSqlOrmOptions,
|
|
@@ -11,6 +12,18 @@ import { Parser, Select } from "node-sql-parser";
|
|
|
11
12
|
import { AnyMySqlTable, MySqlColumn } from "drizzle-orm/mysql-core";
|
|
12
13
|
import { getTableName } from "drizzle-orm/table";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for RovoIntegrationSettingImpl.
|
|
17
|
+
*/
|
|
18
|
+
interface RovoIntegrationSettingConfig {
|
|
19
|
+
accountId: string;
|
|
20
|
+
tableName: string;
|
|
21
|
+
contextParam: Record<string, string>;
|
|
22
|
+
rls: boolean;
|
|
23
|
+
rlsFields: string[];
|
|
24
|
+
rlsWherePart: (alias: string) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* Implementation of RovoIntegrationSetting interface.
|
|
16
29
|
* Stores configuration for Rovo query execution including user context, table name, and RLS settings.
|
|
@@ -29,27 +42,15 @@ class RovoIntegrationSettingImpl implements RovoIntegrationSetting {
|
|
|
29
42
|
/**
|
|
30
43
|
* Creates a new RovoIntegrationSettingImpl instance.
|
|
31
44
|
*
|
|
32
|
-
* @param
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tableName: string,
|
|
42
|
-
contextParam: Record<string, string>,
|
|
43
|
-
rls: boolean,
|
|
44
|
-
rlsFields: string[],
|
|
45
|
-
rlsWherePart: (alias: string) => string,
|
|
46
|
-
) {
|
|
47
|
-
this.accountId = accountId;
|
|
48
|
-
this.tableName = tableName;
|
|
49
|
-
this.contextParam = contextParam;
|
|
50
|
-
this.rls = rls;
|
|
51
|
-
this.rlsFields = rlsFields;
|
|
52
|
-
this.rlsWherePart = rlsWherePart;
|
|
45
|
+
* @param config - Configuration object for the setting
|
|
46
|
+
*/
|
|
47
|
+
constructor(config: RovoIntegrationSettingConfig) {
|
|
48
|
+
this.accountId = config.accountId;
|
|
49
|
+
this.tableName = config.tableName;
|
|
50
|
+
this.contextParam = config.contextParam;
|
|
51
|
+
this.rls = config.rls;
|
|
52
|
+
this.rlsFields = config.rlsFields;
|
|
53
|
+
this.rlsWherePart = config.rlsWherePart;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -350,14 +351,14 @@ class RovoIntegrationSettingCreatorImpl implements RovoIntegrationSettingCreator
|
|
|
350
351
|
*/
|
|
351
352
|
async build(): Promise<RovoIntegrationSetting> {
|
|
352
353
|
const useRls = this.isUseRls ? await this.isUseRlsConditional() : false;
|
|
353
|
-
return new RovoIntegrationSettingImpl(
|
|
354
|
-
this.accountId,
|
|
355
|
-
this.tableName,
|
|
356
|
-
this.contextParam,
|
|
357
|
-
useRls,
|
|
358
|
-
this.rlsFields,
|
|
359
|
-
this.wherePart,
|
|
360
|
-
);
|
|
354
|
+
return new RovoIntegrationSettingImpl({
|
|
355
|
+
accountId: this.accountId,
|
|
356
|
+
tableName: this.tableName,
|
|
357
|
+
contextParam: this.contextParam,
|
|
358
|
+
rls: useRls,
|
|
359
|
+
rlsFields: this.rlsFields,
|
|
360
|
+
rlsWherePart: this.wherePart,
|
|
361
|
+
});
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
|
|
@@ -593,6 +594,21 @@ export class Rovo implements RovoIntegration {
|
|
|
593
594
|
return trimmedQuery;
|
|
594
595
|
}
|
|
595
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Validates that AST represents a single SELECT statement.
|
|
599
|
+
*/
|
|
600
|
+
private validateSelectAst(ast: any): void {
|
|
601
|
+
if (Array.isArray(ast)) {
|
|
602
|
+
if (ast.length !== 1 || ast[0].type !== "select") {
|
|
603
|
+
throw new Error(
|
|
604
|
+
"Only a single SELECT query is allowed. Multiple statements or non-SELECT statements are not permitted.",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
} else if (ast && ast.type !== "select") {
|
|
608
|
+
throw new Error("Only SELECT queries are allowed.");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
596
612
|
/**
|
|
597
613
|
* Normalizes SQL query using AST parsing and stringification.
|
|
598
614
|
* This ensures consistent formatting and validates the query structure.
|
|
@@ -606,15 +622,7 @@ export class Rovo implements RovoIntegration {
|
|
|
606
622
|
const parser = new Parser();
|
|
607
623
|
const ast = parser.astify(sql.trim());
|
|
608
624
|
|
|
609
|
-
|
|
610
|
-
if (ast.length !== 1 || ast[0].type !== "select") {
|
|
611
|
-
throw new Error(
|
|
612
|
-
"Only a single SELECT query is allowed. Multiple statements or non-SELECT statements are not permitted.",
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
} else if (ast && ast.type !== "select") {
|
|
616
|
-
throw new Error("Only SELECT queries are allowed.");
|
|
617
|
-
}
|
|
625
|
+
this.validateSelectAst(ast);
|
|
618
626
|
|
|
619
627
|
const normalized = parser.sqlify(Array.isArray(ast) ? ast[0] : ast);
|
|
620
628
|
return normalized.trim();
|
|
@@ -655,14 +663,9 @@ export class Rovo implements RovoIntegration {
|
|
|
655
663
|
}
|
|
656
664
|
|
|
657
665
|
/**
|
|
658
|
-
* Validates query
|
|
659
|
-
* Checks that only the specified table is referenced and no scalar subqueries are present.
|
|
660
|
-
*
|
|
661
|
-
* @param {Select} selectAst - The parsed SELECT AST node
|
|
662
|
-
* @param {string} tableName - The expected table name
|
|
663
|
-
* @throws {Error} If query references other tables or contains scalar subqueries
|
|
666
|
+
* Validates that query only references the expected table.
|
|
664
667
|
*/
|
|
665
|
-
private
|
|
668
|
+
private validateTableReferences(selectAst: Select, tableName: string): void {
|
|
666
669
|
const upperTableName = tableName.toUpperCase();
|
|
667
670
|
const tablesInQuery = this.extractTables(selectAst);
|
|
668
671
|
const uniqueTables = [...new Set(tablesInQuery)];
|
|
@@ -675,7 +678,12 @@ export class Rovo implements RovoIntegration {
|
|
|
675
678
|
"JOINs, subqueries, or references to other tables are not permitted for security reasons.",
|
|
676
679
|
);
|
|
677
680
|
}
|
|
681
|
+
}
|
|
678
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Validates that SELECT columns don't contain scalar subqueries.
|
|
685
|
+
*/
|
|
686
|
+
private validateNoSubqueriesInColumns(selectAst: Select): void {
|
|
679
687
|
if (selectAst.columns && Array.isArray(selectAst.columns)) {
|
|
680
688
|
const hasSubqueryInColumns = selectAst.columns.some((col: any) => {
|
|
681
689
|
if (col.expr) {
|
|
@@ -694,6 +702,19 @@ export class Rovo implements RovoIntegration {
|
|
|
694
702
|
}
|
|
695
703
|
}
|
|
696
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Validates query structure for security compliance.
|
|
707
|
+
* Checks that only the specified table is referenced and no scalar subqueries are present.
|
|
708
|
+
*
|
|
709
|
+
* @param {Select} selectAst - The parsed SELECT AST node
|
|
710
|
+
* @param {string} tableName - The expected table name
|
|
711
|
+
* @throws {Error} If query references other tables or contains scalar subqueries
|
|
712
|
+
*/
|
|
713
|
+
private validateQueryStructure(selectAst: Select, tableName: string): void {
|
|
714
|
+
this.validateTableReferences(selectAst, tableName);
|
|
715
|
+
this.validateNoSubqueriesInColumns(selectAst);
|
|
716
|
+
}
|
|
717
|
+
|
|
697
718
|
/**
|
|
698
719
|
* Validates query execution plan for security violations.
|
|
699
720
|
* Uses EXPLAIN to detect JOINs, window functions, and references to other tables.
|
|
@@ -774,31 +795,14 @@ export class Rovo implements RovoIntegration {
|
|
|
774
795
|
}
|
|
775
796
|
|
|
776
797
|
/**
|
|
777
|
-
* Validates
|
|
778
|
-
* Ensures that required RLS fields are present and all fields originate from the correct table.
|
|
779
|
-
*
|
|
780
|
-
* @param {Result<unknown>} result - The query execution result
|
|
781
|
-
* @param {RovoIntegrationSetting} settings - Rovo settings containing RLS field requirements
|
|
782
|
-
* @param {string} upperTableName - The expected table name in uppercase
|
|
783
|
-
* @throws {Error} If required RLS fields are missing or fields originate from other tables
|
|
798
|
+
* Validates that required RLS fields are present and from correct table.
|
|
784
799
|
*/
|
|
785
|
-
private
|
|
786
|
-
|
|
787
|
-
|
|
800
|
+
private validateRlsFields(
|
|
801
|
+
fields: Array<{ name: string; orgTable?: string }>,
|
|
802
|
+
rlsFields: string[],
|
|
788
803
|
upperTableName: string,
|
|
789
804
|
): void {
|
|
790
|
-
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const fields = result.metadata.fields as Array<{
|
|
795
|
-
name: string;
|
|
796
|
-
schema?: string;
|
|
797
|
-
table?: string;
|
|
798
|
-
orgTable?: string;
|
|
799
|
-
}>;
|
|
800
|
-
|
|
801
|
-
settings.userScopeFields().forEach((field) => {
|
|
805
|
+
rlsFields.forEach((field) => {
|
|
802
806
|
const actualFields = fields.filter((f) => f.name.toLowerCase() === field?.toLowerCase());
|
|
803
807
|
if (actualFields.length === 0) {
|
|
804
808
|
throw new Error(
|
|
@@ -814,7 +818,15 @@ export class Rovo implements RovoIntegration {
|
|
|
814
818
|
);
|
|
815
819
|
}
|
|
816
820
|
});
|
|
821
|
+
}
|
|
817
822
|
|
|
823
|
+
/**
|
|
824
|
+
* Validates that all fields come from the expected table.
|
|
825
|
+
*/
|
|
826
|
+
private validateAllFieldsFromTable(
|
|
827
|
+
fields: Array<{ orgTable?: string }>,
|
|
828
|
+
upperTableName: string,
|
|
829
|
+
): void {
|
|
818
830
|
const fieldsFromOtherTables = fields.filter(
|
|
819
831
|
(f) => f.orgTable && f.orgTable.toUpperCase() !== upperTableName,
|
|
820
832
|
);
|
|
@@ -827,6 +839,77 @@ export class Rovo implements RovoIntegration {
|
|
|
827
839
|
}
|
|
828
840
|
}
|
|
829
841
|
|
|
842
|
+
/**
|
|
843
|
+
* Validates query results for RLS compliance.
|
|
844
|
+
* Ensures that required RLS fields are present and all fields originate from the correct table.
|
|
845
|
+
*
|
|
846
|
+
* @param {Result<unknown>} result - The query execution result
|
|
847
|
+
* @param {RovoIntegrationSetting} settings - Rovo settings containing RLS field requirements
|
|
848
|
+
* @param {string} upperTableName - The expected table name in uppercase
|
|
849
|
+
* @throws {Error} If required RLS fields are missing or fields originate from other tables
|
|
850
|
+
*/
|
|
851
|
+
private validateQueryResults(
|
|
852
|
+
result: Result<unknown>,
|
|
853
|
+
settings: RovoIntegrationSetting,
|
|
854
|
+
upperTableName: string,
|
|
855
|
+
): void {
|
|
856
|
+
if (!result?.metadata?.fields) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const fields = result.metadata.fields as Array<{
|
|
861
|
+
name: string;
|
|
862
|
+
schema?: string;
|
|
863
|
+
table?: string;
|
|
864
|
+
orgTable?: string;
|
|
865
|
+
}>;
|
|
866
|
+
|
|
867
|
+
this.validateRlsFields(fields, settings.userScopeFields(), upperTableName);
|
|
868
|
+
this.validateAllFieldsFromTable(fields, upperTableName);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Normalizes SQL query with error handling.
|
|
873
|
+
*/
|
|
874
|
+
private normalizeQueryWithErrorHandling(query: string): string {
|
|
875
|
+
try {
|
|
876
|
+
return this.normalizeSqlString(query);
|
|
877
|
+
} catch (error: any) {
|
|
878
|
+
if (
|
|
879
|
+
error.message &&
|
|
880
|
+
(error.message.includes("Only") || error.message.includes("single SELECT"))
|
|
881
|
+
) {
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
if (error.message?.includes("SQL parsing error")) {
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
throw new Error(
|
|
888
|
+
`SQL parsing error: ${error.message || "Invalid SQL syntax"}. Please check your query syntax.`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Replaces context parameters in the query string.
|
|
895
|
+
*/
|
|
896
|
+
private replaceQueryParameters(normalized: string, parameters: Record<string, string>): string {
|
|
897
|
+
let result = normalized.replaceAll("ari:cloud:identity::user/", "");
|
|
898
|
+
Object.entries(parameters).forEach(([key, value]) => {
|
|
899
|
+
result = result.replaceAll(key, value);
|
|
900
|
+
});
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Performs all security validations on the query.
|
|
906
|
+
*/
|
|
907
|
+
private async performSecurityValidations(normalized: string, tableName: string): Promise<void> {
|
|
908
|
+
const selectAst = this.parseSqlQuery(normalized);
|
|
909
|
+
this.validateQueryStructure(selectAst, tableName);
|
|
910
|
+
await this.validateExecutionPlan(normalized, tableName);
|
|
911
|
+
}
|
|
912
|
+
|
|
830
913
|
/**
|
|
831
914
|
* Executes a dynamic SQL query with comprehensive security validations.
|
|
832
915
|
*
|
|
@@ -865,23 +948,7 @@ export class Rovo implements RovoIntegration {
|
|
|
865
948
|
const trimmedQuery = this.validateInputs(dynamicSql, tableName);
|
|
866
949
|
|
|
867
950
|
// Normalize SQL
|
|
868
|
-
let normalized
|
|
869
|
-
try {
|
|
870
|
-
normalized = this.normalizeSqlString(trimmedQuery);
|
|
871
|
-
} catch (error: any) {
|
|
872
|
-
if (
|
|
873
|
-
error.message &&
|
|
874
|
-
(error.message.includes("Only") || error.message.includes("single SELECT"))
|
|
875
|
-
) {
|
|
876
|
-
throw error;
|
|
877
|
-
}
|
|
878
|
-
if (error.message?.includes("SQL parsing error")) {
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
throw new Error(
|
|
882
|
-
`SQL parsing error: ${error.message || "Invalid SQL syntax"}. Please check your query syntax.`,
|
|
883
|
-
);
|
|
884
|
-
}
|
|
951
|
+
let normalized = this.normalizeQueryWithErrorHandling(trimmedQuery);
|
|
885
952
|
|
|
886
953
|
// Validate table name and account
|
|
887
954
|
this.validateTableName(normalized, tableName);
|
|
@@ -892,25 +959,18 @@ export class Rovo implements RovoIntegration {
|
|
|
892
959
|
);
|
|
893
960
|
}
|
|
894
961
|
|
|
895
|
-
//
|
|
896
|
-
normalized = normalized.replaceAll("ari:cloud:identity::user/", "");
|
|
897
|
-
Object.entries(parameters).forEach(([key, value]) => {
|
|
898
|
-
normalized = normalized.replaceAll(key, value);
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
// Validate query structure
|
|
902
|
-
const selectAst = this.parseSqlQuery(normalized);
|
|
903
|
-
this.validateQueryStructure(selectAst, tableName);
|
|
904
|
-
|
|
905
|
-
// Validate execution plan
|
|
906
|
-
await this.validateExecutionPlan(normalized, tableName);
|
|
907
|
-
|
|
962
|
+
// Perform security validations
|
|
908
963
|
// Apply RLS filtering if needed
|
|
909
964
|
const isUseRLSFiltering = settings.isUseRLS();
|
|
910
965
|
if (isUseRLSFiltering) {
|
|
911
|
-
normalized = this.applyRLSFiltering(normalized, settings);
|
|
966
|
+
normalized = this.applyRLSFiltering(normalized, settings) ?? normalized;
|
|
912
967
|
}
|
|
913
968
|
|
|
969
|
+
// Replace parameters in query
|
|
970
|
+
normalized = this.replaceQueryParameters(normalized, parameters);
|
|
971
|
+
|
|
972
|
+
await this.performSecurityValidations(normalized, tableName);
|
|
973
|
+
|
|
914
974
|
if (this.options.logRawSqlQuery) {
|
|
915
975
|
// eslint-disable-next-line no-console
|
|
916
976
|
console.debug("Rovo query: " + normalized);
|