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.
Files changed (44) hide show
  1. package/README.md +1 -0
  2. package/dist/core/ForgeSQLAnalyseOperations.d.ts +4 -0
  3. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLAnalyseOperations.js +17 -21
  5. package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -1
  6. package/dist/core/ForgeSQLCrudOperations.d.ts +16 -0
  7. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLCrudOperations.js +60 -28
  9. package/dist/core/ForgeSQLCrudOperations.js.map +1 -1
  10. package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -28
  11. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  12. package/dist/core/ForgeSQLQueryBuilder.js +20 -47
  13. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  14. package/dist/core/Rovo.d.ts +32 -0
  15. package/dist/core/Rovo.d.ts.map +1 -1
  16. package/dist/core/Rovo.js +116 -67
  17. package/dist/core/Rovo.js.map +1 -1
  18. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  19. package/dist/lib/drizzle/extensions/additionalActions.js +168 -118
  20. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
  21. package/dist/utils/cacheTableUtils.d.ts +0 -8
  22. package/dist/utils/cacheTableUtils.d.ts.map +1 -1
  23. package/dist/utils/cacheTableUtils.js +183 -126
  24. package/dist/utils/cacheTableUtils.js.map +1 -1
  25. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  26. package/dist/utils/forgeDriverProxy.js +31 -20
  27. package/dist/utils/forgeDriverProxy.js.map +1 -1
  28. package/dist/utils/sqlHints.d.ts.map +1 -1
  29. package/dist/utils/sqlHints.js +19 -29
  30. package/dist/utils/sqlHints.js.map +1 -1
  31. package/dist/utils/sqlUtils.d.ts +0 -29
  32. package/dist/utils/sqlUtils.d.ts.map +1 -1
  33. package/dist/utils/sqlUtils.js +107 -78
  34. package/dist/utils/sqlUtils.js.map +1 -1
  35. package/package.json +8 -8
  36. package/src/core/ForgeSQLAnalyseOperations.ts +18 -21
  37. package/src/core/ForgeSQLCrudOperations.ts +83 -33
  38. package/src/core/ForgeSQLQueryBuilder.ts +59 -154
  39. package/src/core/Rovo.ts +158 -98
  40. package/src/lib/drizzle/extensions/additionalActions.ts +287 -382
  41. package/src/utils/cacheTableUtils.ts +202 -144
  42. package/src/utils/forgeDriverProxy.ts +39 -21
  43. package/src/utils/sqlHints.ts +21 -26
  44. 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 {string} accountId - The account ID of the active user
33
- * @param {string} tableName - The name of the table to query
34
- * @param {Record<string, string>} contextParam - Context parameters for query substitution (parameter name -> value mapping)
35
- * @param {boolean} rls - Whether Row-Level Security is enabled
36
- * @param {string[]} rlsFields - Array of field names required for RLS validation
37
- * @param {(alias: string) => string} rlsWherePart - Function that generates WHERE clause for RLS filtering
38
- */
39
- constructor(
40
- accountId: string,
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
- if (Array.isArray(ast)) {
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 structure for security compliance.
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 validateQueryStructure(selectAst: Select, tableName: string): void {
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 query results for RLS compliance.
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 validateQueryResults(
786
- result: Result<unknown>,
787
- settings: RovoIntegrationSetting,
800
+ private validateRlsFields(
801
+ fields: Array<{ name: string; orgTable?: string }>,
802
+ rlsFields: string[],
788
803
  upperTableName: string,
789
804
  ): void {
790
- if (!result?.metadata?.fields) {
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: string;
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
- // Replace parameters in query
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);