forge-sql-orm 2.1.15 → 2.1.16
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 +4 -0
- package/dist/core/ForgeSQLQueryBuilder.d.ts +2 -3
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts +2 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -1
- package/dist/core/Rovo.d.ts +40 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +164 -138
- package/dist/core/Rovo.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +72 -22
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +11 -0
- package/dist/utils/cacheTableUtils.d.ts.map +1 -0
- package/dist/utils/cacheTableUtils.js +450 -0
- package/dist/utils/cacheTableUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +3 -22
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +5 -12
- package/dist/utils/forgeDriver.js.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +53 -31
- package/dist/utils/metadataContextUtils.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +217 -119
- package/dist/utils/sqlUtils.js.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
- package/package.json +9 -9
- package/src/core/ForgeSQLQueryBuilder.ts +2 -2
- package/src/core/ForgeSQLSelectOperations.ts +2 -1
- package/src/core/Rovo.ts +209 -167
- package/src/index.ts +1 -3
- package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
- package/src/utils/cacheTableUtils.ts +511 -0
- package/src/utils/cacheUtils.ts +3 -25
- package/src/utils/forgeDriver.ts +5 -11
- package/src/utils/metadataContextUtils.ts +49 -26
- package/src/utils/sqlUtils.ts +298 -142
- package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
package/src/core/Rovo.ts
CHANGED
|
@@ -168,7 +168,6 @@ class RovoIntegrationSettingCreatorImpl implements RovoIntegrationSettingCreator
|
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
170
|
useRLS(): RlsSettings {
|
|
171
|
-
const _this = this;
|
|
172
171
|
/**
|
|
173
172
|
* Internal implementation of RlsSettings interface.
|
|
174
173
|
* Provides fluent API for configuring Row-Level Security settings.
|
|
@@ -180,7 +179,7 @@ class RovoIntegrationSettingCreatorImpl implements RovoIntegrationSettingCreator
|
|
|
180
179
|
private isUseRlsConditionalSettings: () => Promise<boolean> = async () => true;
|
|
181
180
|
private rlsFieldsSettings: string[] = [];
|
|
182
181
|
private wherePartSettings: (alias: string) => string = () => "";
|
|
183
|
-
|
|
182
|
+
constructor(private readonly parent: RovoIntegrationSettingCreatorImpl) {}
|
|
184
183
|
/**
|
|
185
184
|
* Sets a conditional function to determine if RLS should be applied.
|
|
186
185
|
*
|
|
@@ -255,13 +254,13 @@ class RovoIntegrationSettingCreatorImpl implements RovoIntegrationSettingCreator
|
|
|
255
254
|
* @returns {RovoIntegrationSettingCreator} The parent settings builder
|
|
256
255
|
*/
|
|
257
256
|
finish(): RovoIntegrationSettingCreator {
|
|
258
|
-
|
|
259
|
-
this.rlsFieldsSettings.forEach((columnName) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return
|
|
257
|
+
this.parent.isUseRls = true;
|
|
258
|
+
this.rlsFieldsSettings.forEach((columnName) => this.parent.rlsFields.push(columnName));
|
|
259
|
+
this.parent.wherePart = this.wherePartSettings;
|
|
260
|
+
this.parent.isUseRlsConditional = this.isUseRlsConditionalSettings;
|
|
261
|
+
return this.parent;
|
|
263
262
|
}
|
|
264
|
-
})();
|
|
263
|
+
})(this);
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
/**
|
|
@@ -363,13 +362,41 @@ export class Rovo implements RovoIntegration {
|
|
|
363
362
|
);
|
|
364
363
|
}
|
|
365
364
|
return ast[0];
|
|
366
|
-
} else if (ast
|
|
365
|
+
} else if (ast?.type === "select") {
|
|
367
366
|
return ast;
|
|
368
367
|
} else {
|
|
369
368
|
throw new Error("Only SELECT queries are allowed.");
|
|
370
369
|
}
|
|
371
370
|
}
|
|
372
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Recursively processes array or single node and extracts tables
|
|
374
|
+
* @param items - Array of nodes or single node
|
|
375
|
+
* @param tables - Accumulator array for table names
|
|
376
|
+
*/
|
|
377
|
+
private extractTablesFromItems(items: any, tables: string[]): void {
|
|
378
|
+
if (Array.isArray(items)) {
|
|
379
|
+
items.forEach((item: any) => {
|
|
380
|
+
tables.push(...this.extractTables(item));
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
tables.push(...this.extractTables(items));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Extracts table name from table node
|
|
389
|
+
* @param node - AST node with table information
|
|
390
|
+
* @returns Table name in uppercase or null if not applicable
|
|
391
|
+
*/
|
|
392
|
+
private extractTableName(node: any): string | null {
|
|
393
|
+
if (!node.table) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const tableName = node.table === "dual" ? "dual" : node.table.name || node.table;
|
|
397
|
+
return tableName && tableName !== "dual" ? tableName.toUpperCase() : null;
|
|
398
|
+
}
|
|
399
|
+
|
|
373
400
|
/**
|
|
374
401
|
* Recursively extracts all table names from SQL AST node
|
|
375
402
|
* @param node - AST node to extract tables from
|
|
@@ -378,33 +405,22 @@ export class Rovo implements RovoIntegration {
|
|
|
378
405
|
private extractTables(node: any): string[] {
|
|
379
406
|
const tables: string[] = [];
|
|
380
407
|
|
|
408
|
+
// Extract table name if node is a table type
|
|
381
409
|
if (node.type === "table" || node.type === "dual") {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
tables.push(tableName.toUpperCase());
|
|
386
|
-
}
|
|
410
|
+
const tableName = this.extractTableName(node);
|
|
411
|
+
if (tableName) {
|
|
412
|
+
tables.push(tableName);
|
|
387
413
|
}
|
|
388
414
|
}
|
|
389
415
|
|
|
416
|
+
// Extract tables from FROM clause
|
|
390
417
|
if (node.from) {
|
|
391
|
-
|
|
392
|
-
node.from.forEach((fromItem: any) => {
|
|
393
|
-
tables.push(...this.extractTables(fromItem));
|
|
394
|
-
});
|
|
395
|
-
} else {
|
|
396
|
-
tables.push(...this.extractTables(node.from));
|
|
397
|
-
}
|
|
418
|
+
this.extractTablesFromItems(node.from, tables);
|
|
398
419
|
}
|
|
399
420
|
|
|
421
|
+
// Extract tables from JOIN clause
|
|
400
422
|
if (node.join) {
|
|
401
|
-
|
|
402
|
-
node.join.forEach((joinItem: any) => {
|
|
403
|
-
tables.push(...this.extractTables(joinItem));
|
|
404
|
-
});
|
|
405
|
-
} else {
|
|
406
|
-
tables.push(...this.extractTables(node.join));
|
|
407
|
-
}
|
|
423
|
+
this.extractTablesFromItems(node.join, tables);
|
|
408
424
|
}
|
|
409
425
|
|
|
410
426
|
return tables;
|
|
@@ -418,7 +434,7 @@ export class Rovo implements RovoIntegration {
|
|
|
418
434
|
private hasScalarSubquery(node: any): boolean {
|
|
419
435
|
if (!node) return false;
|
|
420
436
|
|
|
421
|
-
if (node.type === "subquery" ||
|
|
437
|
+
if (node.type === "subquery" || node.ast?.type === "select") {
|
|
422
438
|
return true;
|
|
423
439
|
}
|
|
424
440
|
|
|
@@ -491,23 +507,17 @@ export class Rovo implements RovoIntegration {
|
|
|
491
507
|
* console.log(result.metadata); // Query metadata
|
|
492
508
|
* ```
|
|
493
509
|
*/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
):
|
|
498
|
-
|
|
499
|
-
const tableName = settings.getTableName();
|
|
500
|
-
const accountId = settings.getActiveUser();
|
|
501
|
-
const parameters = settings.getParameters();
|
|
502
|
-
if (!query || !query.trim()) {
|
|
510
|
+
/**
|
|
511
|
+
* Validates basic input parameters
|
|
512
|
+
*/
|
|
513
|
+
private validateInputs(query: string, tableName: string): string {
|
|
514
|
+
if (!query?.trim()) {
|
|
503
515
|
throw new Error("SQL query is required. Please provide a valid SELECT query.");
|
|
504
516
|
}
|
|
505
517
|
if (!tableName) {
|
|
506
518
|
throw new Error("Table Name is required. Please provide a valid Table Name.");
|
|
507
519
|
}
|
|
508
520
|
|
|
509
|
-
// Quick validation: check if query starts with SELECT (case-insensitive)
|
|
510
|
-
// This allows us to fail fast for non-SELECT queries before normalization
|
|
511
521
|
const trimmedQuery = query.trim();
|
|
512
522
|
const quickUpper = trimmedQuery.toUpperCase();
|
|
513
523
|
if (!quickUpper.startsWith("SELECT")) {
|
|
@@ -516,78 +526,50 @@ export class Rovo implements RovoIntegration {
|
|
|
516
526
|
);
|
|
517
527
|
}
|
|
518
528
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// Validate it's a SELECT query before normalizing
|
|
536
|
-
if (Array.isArray(ast)) {
|
|
537
|
-
if (ast.length !== 1 || ast[0].type !== "select") {
|
|
538
|
-
throw new Error(
|
|
539
|
-
"Only a single SELECT query is allowed. Multiple statements or non-SELECT statements are not permitted.",
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
} else if (ast && ast.type !== "select") {
|
|
543
|
-
throw new Error("Only SELECT queries are allowed.");
|
|
544
|
-
}
|
|
545
|
-
// Convert AST back to SQL (this normalizes formatting)
|
|
546
|
-
const normalized = parser.sqlify(Array.isArray(ast) ? ast[0] : ast);
|
|
547
|
-
// trim
|
|
548
|
-
return normalized.trim();
|
|
549
|
-
} catch (error: any) {
|
|
550
|
-
// If it's a validation error we threw, re-throw it
|
|
551
|
-
if (
|
|
552
|
-
error.message &&
|
|
553
|
-
(error.message.includes("Only") || error.message.includes("single SELECT"))
|
|
554
|
-
) {
|
|
555
|
-
throw error;
|
|
556
|
-
}
|
|
557
|
-
// For parsing errors, wrap them in a more user-friendly message
|
|
558
|
-
// Check if error is already wrapped to avoid double wrapping
|
|
559
|
-
if (error.message && error.message.includes("SQL parsing error")) {
|
|
560
|
-
throw error;
|
|
529
|
+
return trimmedQuery;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Normalizes SQL query using AST parsing and stringification
|
|
534
|
+
*/
|
|
535
|
+
private normalizeSqlString(sql: string): string {
|
|
536
|
+
try {
|
|
537
|
+
const parser = new Parser();
|
|
538
|
+
const ast = parser.astify(sql.trim());
|
|
539
|
+
|
|
540
|
+
if (Array.isArray(ast)) {
|
|
541
|
+
if (ast.length !== 1 || ast[0].type !== "select") {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Only a single SELECT query is allowed. Multiple statements or non-SELECT statements are not permitted.",
|
|
544
|
+
);
|
|
561
545
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
546
|
+
} else if (ast && ast.type !== "select") {
|
|
547
|
+
throw new Error("Only SELECT queries are allowed.");
|
|
565
548
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
normalized = normalizeSqlString(trimmedQuery);
|
|
549
|
+
|
|
550
|
+
const normalized = parser.sqlify(Array.isArray(ast) ? ast[0] : ast);
|
|
551
|
+
return normalized.trim();
|
|
570
552
|
} catch (error: any) {
|
|
571
|
-
// Re-throw validation errors as-is
|
|
572
553
|
if (
|
|
573
554
|
error.message &&
|
|
574
555
|
(error.message.includes("Only") || error.message.includes("single SELECT"))
|
|
575
556
|
) {
|
|
576
557
|
throw error;
|
|
577
558
|
}
|
|
578
|
-
|
|
579
|
-
if (error.message && error.message.includes("SQL parsing error")) {
|
|
559
|
+
if (error.message?.includes("SQL parsing error")) {
|
|
580
560
|
throw error;
|
|
581
561
|
}
|
|
582
|
-
// For other errors, wrap them
|
|
583
562
|
throw new Error(
|
|
584
563
|
`SQL parsing error: ${error.message || "Invalid SQL syntax"}. Please check your query syntax.`,
|
|
585
564
|
);
|
|
586
565
|
}
|
|
566
|
+
}
|
|
587
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Validates that query targets the correct table
|
|
570
|
+
*/
|
|
571
|
+
private validateTableName(normalized: string, tableName: string): void {
|
|
588
572
|
const upperTableName = tableName.toUpperCase();
|
|
589
|
-
// Validate table name
|
|
590
|
-
// sqlify may add backticks, so we check for both formats: FROM table_name and FROM `table_name`
|
|
591
573
|
const tableNamePattern = new RegExp(`FROM\\s+[\`]?${upperTableName}[\`]?`, "i");
|
|
592
574
|
if (!tableNamePattern.test(normalized)) {
|
|
593
575
|
throw new Error(
|
|
@@ -596,25 +578,15 @@ export class Rovo implements RovoIntegration {
|
|
|
596
578
|
"' table only. Other tables are not accessible.",
|
|
597
579
|
);
|
|
598
580
|
}
|
|
581
|
+
}
|
|
599
582
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
normalized = normalized.replaceAll("ari:cloud:identity::user/", "");
|
|
606
|
-
Object.entries(parameters).forEach(([key, value]) => {
|
|
607
|
-
normalized = normalized.replaceAll(key, value);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// Parse SQL query to validate structure before execution
|
|
611
|
-
const selectAst = this.parseSqlQuery(normalized);
|
|
612
|
-
|
|
613
|
-
// Extract all tables from the query
|
|
583
|
+
/**
|
|
584
|
+
* Validates query structure (tables, subqueries)
|
|
585
|
+
*/
|
|
586
|
+
private validateQueryStructure(selectAst: Select, tableName: string): void {
|
|
587
|
+
const upperTableName = tableName.toUpperCase();
|
|
614
588
|
const tablesInQuery = this.extractTables(selectAst);
|
|
615
589
|
const uniqueTables = [...new Set(tablesInQuery)];
|
|
616
|
-
|
|
617
|
-
// Check that only table is used
|
|
618
590
|
const invalidTables = uniqueTables.filter((table) => table !== upperTableName);
|
|
619
591
|
|
|
620
592
|
if (invalidTables.length > 0) {
|
|
@@ -625,7 +597,6 @@ export class Rovo implements RovoIntegration {
|
|
|
625
597
|
);
|
|
626
598
|
}
|
|
627
599
|
|
|
628
|
-
// Check for scalar subqueries in SELECT columns
|
|
629
600
|
if (selectAst.columns && Array.isArray(selectAst.columns)) {
|
|
630
601
|
const hasSubqueryInColumns = selectAst.columns.some((col: any) => {
|
|
631
602
|
if (col.expr) {
|
|
@@ -642,8 +613,12 @@ export class Rovo implements RovoIntegration {
|
|
|
642
613
|
);
|
|
643
614
|
}
|
|
644
615
|
}
|
|
616
|
+
}
|
|
645
617
|
|
|
646
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Validates query execution plan for security violations
|
|
620
|
+
*/
|
|
621
|
+
private async validateExecutionPlan(normalized: string, tableName: string): Promise<void> {
|
|
647
622
|
const explainRows = await this.forgeOperations.analyze().explainRaw(normalized, []);
|
|
648
623
|
|
|
649
624
|
const hasJoin = explainRows.some((row) => {
|
|
@@ -664,9 +639,6 @@ export class Rovo implements RovoIntegration {
|
|
|
664
639
|
);
|
|
665
640
|
}
|
|
666
641
|
|
|
667
|
-
// Detect window functions (e.g., COUNT(*) OVER(...), ROW_NUMBER() OVER(...))
|
|
668
|
-
// Window functions are not allowed for security
|
|
669
|
-
// Users should use regular aggregate functions with GROUP BY instead
|
|
670
642
|
const hasWindow = explainRows.some((row) => {
|
|
671
643
|
const id = row.id.toUpperCase();
|
|
672
644
|
const info = (row.operatorInfo ?? "").toUpperCase();
|
|
@@ -680,8 +652,6 @@ export class Rovo implements RovoIntegration {
|
|
|
680
652
|
);
|
|
681
653
|
}
|
|
682
654
|
|
|
683
|
-
// Check for references to other tables in the query execution plan
|
|
684
|
-
// This detects JOINs, subqueries, or any other references to tables other than expected
|
|
685
655
|
const tablesInPlan = explainRows.filter(
|
|
686
656
|
(row) =>
|
|
687
657
|
row.accessObject?.startsWith("table:") &&
|
|
@@ -694,70 +664,142 @@ export class Rovo implements RovoIntegration {
|
|
|
694
664
|
"JOINs, subqueries, or references to other tables are not permitted for security reasons.",
|
|
695
665
|
);
|
|
696
666
|
}
|
|
667
|
+
}
|
|
697
668
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Applies row-level security filtering to query
|
|
671
|
+
*/
|
|
672
|
+
private applyRLSFiltering(normalized: string, settings: RovoIntegrationSetting): string {
|
|
673
|
+
if (normalized.endsWith(";")) {
|
|
674
|
+
normalized = normalized.slice(0, -1);
|
|
675
|
+
}
|
|
704
676
|
|
|
705
|
-
|
|
677
|
+
return `
|
|
706
678
|
SELECT *
|
|
707
679
|
FROM (
|
|
708
680
|
${normalized}
|
|
709
681
|
) AS t
|
|
710
682
|
WHERE (${settings.userScopeWhere("t")})
|
|
711
683
|
`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Validates query results for RLS compliance
|
|
688
|
+
*/
|
|
689
|
+
private validateQueryResults(
|
|
690
|
+
result: Result<unknown>,
|
|
691
|
+
settings: RovoIntegrationSetting,
|
|
692
|
+
upperTableName: string,
|
|
693
|
+
): void {
|
|
694
|
+
if (!result?.metadata?.fields) {
|
|
695
|
+
return;
|
|
712
696
|
}
|
|
713
|
-
if (this.options.logRawSqlQuery) {
|
|
714
|
-
// eslint-disable-next-line no-console
|
|
715
|
-
console.debug("Rovo query: " + normalized);
|
|
716
|
-
}
|
|
717
|
-
const result = await sql.executeRaw(normalized);
|
|
718
697
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
schema?: string;
|
|
726
|
-
table?: string;
|
|
727
|
-
orgTable?: string;
|
|
728
|
-
}>;
|
|
729
|
-
|
|
730
|
-
settings.userScopeFields().forEach((field) => {
|
|
731
|
-
const actualFields = fields.filter((f) => f.name.toLowerCase() === field?.toLowerCase());
|
|
732
|
-
if (actualFields.length === 0) {
|
|
733
|
-
throw new Error(
|
|
734
|
-
`Security validation failed: The query must include ${field} as a raw column in the SELECT statement. This field is required for row-level security enforcement.`,
|
|
735
|
-
);
|
|
736
|
-
}
|
|
737
|
-
const actualField = actualFields.find(
|
|
738
|
-
(f) => !f.orgTable || f.orgTable.toUpperCase() !== upperTableName,
|
|
739
|
-
);
|
|
740
|
-
if (actualField) {
|
|
741
|
-
throw new Error(
|
|
742
|
-
`Security validation failed: '${field}' must come directly from the ${upperTableName} table. Joins, subqueries, or table aliases that change the origin of this column are not allowed.`,
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
});
|
|
698
|
+
const fields = result.metadata.fields as Array<{
|
|
699
|
+
name: string;
|
|
700
|
+
schema?: string;
|
|
701
|
+
table?: string;
|
|
702
|
+
orgTable?: string;
|
|
703
|
+
}>;
|
|
746
704
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
705
|
+
settings.userScopeFields().forEach((field) => {
|
|
706
|
+
const actualFields = fields.filter((f) => f.name.toLowerCase() === field?.toLowerCase());
|
|
707
|
+
if (actualFields.length === 0) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`Security validation failed: The query must include ${field} as a raw column in the SELECT statement. This field is required for row-level security enforcement.`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const actualField = actualFields.find(
|
|
713
|
+
(f) => !f.orgTable || f.orgTable.toUpperCase() !== upperTableName,
|
|
753
714
|
);
|
|
754
|
-
if (
|
|
715
|
+
if (actualField) {
|
|
755
716
|
throw new Error(
|
|
756
|
-
`Security validation failed:
|
|
757
|
-
"Fields from other tables detected, which indicates the use of JOINs, subqueries, or references to other tables. " +
|
|
758
|
-
"This is not allowed for security reasons.",
|
|
717
|
+
`Security validation failed: '${field}' must come directly from the ${upperTableName} table. Joins, subqueries, or table aliases that change the origin of this column are not allowed.`,
|
|
759
718
|
);
|
|
760
719
|
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const fieldsFromOtherTables = fields.filter(
|
|
723
|
+
(f) => f.orgTable && f.orgTable.toUpperCase() !== upperTableName,
|
|
724
|
+
);
|
|
725
|
+
if (fieldsFromOtherTables.length > 0) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Security validation failed: All fields must come from the ${upperTableName} table. ` +
|
|
728
|
+
"Fields from other tables detected, which indicates the use of JOINs, subqueries, or references to other tables. " +
|
|
729
|
+
"This is not allowed for security reasons.",
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async dynamicIsolatedQuery(
|
|
735
|
+
dynamicSql: string,
|
|
736
|
+
settings: RovoIntegrationSetting,
|
|
737
|
+
): Promise<Result<unknown>> {
|
|
738
|
+
const tableName = settings.getTableName();
|
|
739
|
+
const accountId = settings.getActiveUser();
|
|
740
|
+
const parameters = settings.getParameters();
|
|
741
|
+
|
|
742
|
+
// Validate inputs
|
|
743
|
+
const trimmedQuery = this.validateInputs(dynamicSql, tableName);
|
|
744
|
+
|
|
745
|
+
// Normalize SQL
|
|
746
|
+
let normalized: string;
|
|
747
|
+
try {
|
|
748
|
+
normalized = this.normalizeSqlString(trimmedQuery);
|
|
749
|
+
} catch (error: any) {
|
|
750
|
+
if (
|
|
751
|
+
error.message &&
|
|
752
|
+
(error.message.includes("Only") || error.message.includes("single SELECT"))
|
|
753
|
+
) {
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
if (error.message?.includes("SQL parsing error")) {
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
throw new Error(
|
|
760
|
+
`SQL parsing error: ${error.message || "Invalid SQL syntax"}. Please check your query syntax.`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Validate table name and account
|
|
765
|
+
this.validateTableName(normalized, tableName);
|
|
766
|
+
|
|
767
|
+
if (!accountId) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
"Authentication error: User account ID is missing. Please ensure you are logged in.",
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Replace parameters in query
|
|
774
|
+
normalized = normalized.replaceAll("ari:cloud:identity::user/", "");
|
|
775
|
+
Object.entries(parameters).forEach(([key, value]) => {
|
|
776
|
+
normalized = normalized.replaceAll(key, value);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Validate query structure
|
|
780
|
+
const selectAst = this.parseSqlQuery(normalized);
|
|
781
|
+
this.validateQueryStructure(selectAst, tableName);
|
|
782
|
+
|
|
783
|
+
// Validate execution plan
|
|
784
|
+
await this.validateExecutionPlan(normalized, tableName);
|
|
785
|
+
|
|
786
|
+
// Apply RLS filtering if needed
|
|
787
|
+
const isUseRLSFiltering = settings.isUseRLS();
|
|
788
|
+
if (isUseRLSFiltering) {
|
|
789
|
+
normalized = this.applyRLSFiltering(normalized, settings);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (this.options.logRawSqlQuery) {
|
|
793
|
+
// eslint-disable-next-line no-console
|
|
794
|
+
console.debug("Rovo query: " + normalized);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const result = await sql.executeRaw(normalized);
|
|
798
|
+
|
|
799
|
+
// Validate query results for RLS compliance
|
|
800
|
+
if (isUseRLSFiltering) {
|
|
801
|
+
const upperTableName = tableName.toUpperCase();
|
|
802
|
+
this.validateQueryResults(result, settings, upperTableName);
|
|
761
803
|
}
|
|
762
804
|
|
|
763
805
|
return result;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export { default } from "./core/ForgeSQLORM";
|
|
2
2
|
|
|
3
3
|
export * from "./core/ForgeSQLQueryBuilder";
|
|
4
4
|
export * from "./core/ForgeSQLCrudOperations";
|
|
@@ -8,5 +8,3 @@ export * from "./utils/forgeDriver";
|
|
|
8
8
|
export * from "./webtriggers";
|
|
9
9
|
export * from "./lib/drizzle/extensions/additionalActions";
|
|
10
10
|
export * from "./core/SystemTables";
|
|
11
|
-
|
|
12
|
-
export default ForgeSQLORM;
|