Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ interface SalesforceLead {
[key: string]: any;
}

/** Validate a Salesforce record ID (15 or 18 character alphanumeric). */
function validateSalesforceId(id: string, label: string): string {
if (!/^[a-zA-Z0-9]{15,18}$/.test(id)) {
throw new Error(`Invalid ${label}: must be a 15 or 18 character Salesforce ID`);
}
return id;
}

/** Escape a string value for use in SOQL single-quoted literals. */
function escapeSoql(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}

/** Validate a SOQL field name. */
function validateFieldName(field: string): string {
if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(field)) {
throw new Error(`Invalid SOQL field name: ${field}`);
}
return field;
}

async function salesforceFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = await getAccessToken();
if (!token) {
Expand Down Expand Up @@ -141,6 +162,7 @@ function buildListSoql(params: {
}): string {
const { object, fields, where, limit, offset } = params;

fields.forEach((f) => validateFieldName(f));
let soql = `SELECT ${fields.join(", ")} FROM ${object}`;
if (where) soql += ` WHERE ${where}`;
soql += ` ORDER BY LastModifiedDate DESC LIMIT ${limit} OFFSET ${offset}`;
Expand All @@ -155,6 +177,8 @@ async function getSingleRecord<T>(params: {
notFoundMessage: string;
}): Promise<T> {
const { object, id, fields, notFoundMessage } = params;
fields.forEach((f) => validateFieldName(f));
validateSalesforceId(id, `${object} ID`);
const soql = `SELECT ${fields.join(", ")} FROM ${object} WHERE Id = '${id}'`;
const result = await query<T>(soql);

Expand Down Expand Up @@ -270,7 +294,9 @@ export function listContacts(options?: {
"LastModifiedDate",
];

const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined;
const where = options?.accountId
? (validateSalesforceId(options.accountId, "accountId"), `AccountId = '${options.accountId}'`)
: undefined;

return query<SalesforceContact>(buildListSoql({ object: "Contact", fields, where, limit, offset }));
}
Expand Down Expand Up @@ -356,7 +382,9 @@ export function listOpportunities(options?: {
"LastModifiedDate",
];

const where = options?.accountId ? `AccountId = '${options.accountId}'` : undefined;
const where = options?.accountId
? (validateSalesforceId(options.accountId, "accountId"), `AccountId = '${options.accountId}'`)
: undefined;

return query<SalesforceOpportunity>(
buildListSoql({ object: "Opportunity", fields, where, limit, offset }),
Expand Down Expand Up @@ -441,7 +469,7 @@ export function listLeads(options?: {
"LastModifiedDate",
];

const where = options?.status ? `Status = '${options.status}'` : undefined;
const where = options?.status ? `Status = '${escapeSoql(options.status)}'` : undefined;

return query<SalesforceLead>(buildListSoql({ object: "Lead", fields, where, limit, offset }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ interface SnowflakeError extends Error {
sqlState?: string;
}

/** Validate a Snowflake identifier (database, schema, or table name). */
function validateIdentifier(value: string, label: string): string {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
throw new Error(
`Invalid ${label}: must start with a letter or underscore and contain only letters, numbers, and underscores`,
);
}
return value;
}

async function snowflakeFetch<T>(
endpoint: string,
options: RequestInit = {},
Expand Down Expand Up @@ -241,6 +251,7 @@ export async function listDatabases(): Promise<DatabaseInfo[]> {
}

export async function listSchemas(database: string): Promise<SchemaInfo[]> {
validateIdentifier(database, "database name");
const result = await runQuery(`SHOW SCHEMAS IN DATABASE ${database}`);
return result.rows as SchemaInfo[];
}
Expand All @@ -249,6 +260,8 @@ export async function listTables(
database: string,
schema: string,
): Promise<TableInfo[]> {
validateIdentifier(database, "database name");
validateIdentifier(schema, "schema name");
const result = await runQuery(`SHOW TABLES IN ${database}.${schema}`);
return result.rows as TableInfo[];
}
Expand All @@ -261,6 +274,9 @@ export async function describeTable(
columns: ColumnInfo[];
primaryKeys: string[];
}> {
validateIdentifier(database, "database name");
validateIdentifier(schema, "schema name");
validateIdentifier(table, "table name");
const result = await runQuery(`DESCRIBE TABLE ${database}.${schema}.${table}`);

const columns = result.rows as ColumnInfo[];
Expand All @@ -276,6 +292,9 @@ export async function getTableRowCount(
schema: string,
table: string,
): Promise<number> {
validateIdentifier(database, "database name");
validateIdentifier(schema, "schema name");
validateIdentifier(table, "table name");
const result = await runQuery(
`SELECT COUNT(*) as count FROM ${database}.${schema}.${table}`,
);
Expand Down
Loading