Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
|
||||
export const deleteRowAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'delete_row',
|
||||
displayName: 'Delete Row',
|
||||
description: 'Delete rows from an Oracle table',
|
||||
props: {
|
||||
tableName: oracleDbProps.tableName(),
|
||||
filter: Property.Object({
|
||||
displayName: 'Filter (WHERE)',
|
||||
description: 'Conditions to match rows for deletion',
|
||||
required: true,
|
||||
defaultValue: { ID: 101 },
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { tableName, filter } = context.propsValue;
|
||||
|
||||
if (
|
||||
typeof filter !== 'object' ||
|
||||
filter === null ||
|
||||
Array.isArray(filter)
|
||||
) {
|
||||
throw new Error('Filter must be a valid object');
|
||||
}
|
||||
|
||||
if (Object.keys(filter).length === 0) {
|
||||
throw new Error(
|
||||
'Filter cannot be empty. Use Run Custom SQL action to delete all rows.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
return await client.deleteRow(tableName, filter as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to delete rows from ${tableName}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
|
||||
export const findRowAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'find_row',
|
||||
displayName: 'Find Row',
|
||||
description: 'Find rows in an Oracle table',
|
||||
props: {
|
||||
tableName: oracleDbProps.tableName(),
|
||||
filter: Property.Object({
|
||||
displayName: 'Filter (WHERE)',
|
||||
description: 'Conditions to match rows',
|
||||
required: true,
|
||||
defaultValue: { ID: 101 },
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { tableName, filter } = context.propsValue;
|
||||
|
||||
if (
|
||||
typeof filter !== 'object' ||
|
||||
filter === null ||
|
||||
Array.isArray(filter)
|
||||
) {
|
||||
throw new Error('Filter must be a valid object');
|
||||
}
|
||||
|
||||
if (Object.keys(filter).length === 0) {
|
||||
throw new Error(
|
||||
'Filter cannot be empty. Use Run Custom SQL action to fetch all rows.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
return await client.findRow(
|
||||
tableName,
|
||||
filter as Record<string, unknown>
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to find rows in ${tableName}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
|
||||
export const insertRowAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'insert_row',
|
||||
displayName: 'Insert Row',
|
||||
description: 'Insert a row into an Oracle table',
|
||||
props: {
|
||||
tableName: oracleDbProps.tableName(),
|
||||
row: Property.Object({
|
||||
displayName: 'Row',
|
||||
description: 'Column names and values to insert',
|
||||
required: true,
|
||||
defaultValue: {
|
||||
COLUMN_NAME: 'value',
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { tableName, row } = context.propsValue;
|
||||
|
||||
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
||||
throw new Error("Row must be a valid object with column names as keys");
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
return await client.insertRow(tableName, row as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to insert row into ${tableName}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
|
||||
export const insertRowsAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'insert_rows',
|
||||
displayName: 'Insert Rows',
|
||||
description: 'Insert multiple rows into an Oracle table',
|
||||
props: {
|
||||
tableName: oracleDbProps.tableName(),
|
||||
rows: Property.Array({
|
||||
displayName: 'Rows',
|
||||
description: 'Array of objects with column names and values',
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{ COLUMN_1: 'value_a', COLUMN_2: 1 },
|
||||
{ COLUMN_1: 'value_b', COLUMN_2: 2 },
|
||||
],
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { tableName, rows } = context.propsValue;
|
||||
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
throw new Error('Rows must be a non-empty array of objects');
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
return await client.insertRows(
|
||||
tableName,
|
||||
rows as Record<string, unknown>[]
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to insert rows into ${tableName}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import oracledb from 'oracledb';
|
||||
|
||||
export const runCustomSqlAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'run_custom_sql',
|
||||
displayName: 'Run Custom SQL',
|
||||
description: 'Execute custom SQL or PL/SQL in Oracle',
|
||||
props: {
|
||||
markdown: Property.MarkDown({
|
||||
value: `**DO NOT** insert dynamic input directly into the query. Use bind parameters (:param) to prevent **SQL injection**.`,
|
||||
}),
|
||||
sql: Property.LongText({
|
||||
displayName: 'SQL Query',
|
||||
description: 'SQL or PL/SQL to execute. Use :param for bind parameters.',
|
||||
required: true,
|
||||
defaultValue: 'SELECT * FROM employees WHERE department_id = :dept_id',
|
||||
}),
|
||||
binds: Property.Object({
|
||||
displayName: 'Bind Parameters',
|
||||
description: 'Key-value pairs for bind variables',
|
||||
required: false,
|
||||
defaultValue: { dept_id: 90 },
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { sql, binds } = context.propsValue;
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
const bindParams = (binds as oracledb.BindParameters) || {};
|
||||
return await client.execute(sql, bindParams);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to execute SQL: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
|
||||
export const updateRowAction = createAction({
|
||||
auth: oracleDbAuth,
|
||||
name: 'update_row',
|
||||
displayName: 'Update Row',
|
||||
description: 'Update rows in an Oracle table',
|
||||
props: {
|
||||
tableName: oracleDbProps.tableName(),
|
||||
values: Property.Object({
|
||||
displayName: 'Values',
|
||||
description: 'Column names and new values to set',
|
||||
required: true,
|
||||
defaultValue: { SALARY: 8000 },
|
||||
}),
|
||||
filter: Property.Object({
|
||||
displayName: 'Filter (WHERE)',
|
||||
description: 'Conditions to match rows. Empty object updates ALL rows.',
|
||||
required: true,
|
||||
defaultValue: { ID: 101 },
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { tableName, values, filter } = context.propsValue;
|
||||
|
||||
if (
|
||||
typeof values !== 'object' ||
|
||||
values === null ||
|
||||
Array.isArray(values) ||
|
||||
Object.keys(values).length === 0
|
||||
) {
|
||||
throw new Error('Values must be a non-empty object');
|
||||
}
|
||||
if (
|
||||
typeof filter !== 'object' ||
|
||||
filter === null ||
|
||||
Array.isArray(filter)
|
||||
) {
|
||||
throw new Error('Filter must be a valid object');
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OracleDbClient(context.auth.props);
|
||||
return await client.updateRow(
|
||||
tableName,
|
||||
values as Record<string, unknown>,
|
||||
filter as Record<string, unknown>
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to update rows in ${tableName}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
PieceAuth,
|
||||
Property,
|
||||
StaticPropsValue,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import oracledb from 'oracledb';
|
||||
|
||||
try {
|
||||
oracledb.initOracleClient();
|
||||
} catch (e) {
|
||||
console.log('Oracle client already initialized or failed to initialize.');
|
||||
}
|
||||
|
||||
export const oracleDbAuth = PieceAuth.CustomAuth({
|
||||
description: `Connect to Oracle Database using either Service Name (host/port/service) or a full connection string.`,
|
||||
required: true,
|
||||
props: {
|
||||
connectionType: Property.StaticDropdown({
|
||||
displayName: 'Connection Type',
|
||||
description: 'How you want to connect',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Service Name', value: 'serviceName' },
|
||||
{ label: 'Connection String', value: 'connectionString' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'serviceName',
|
||||
}),
|
||||
host: Property.ShortText({
|
||||
displayName: 'Host',
|
||||
description: 'Database server hostname or IP',
|
||||
required: false,
|
||||
}),
|
||||
port: Property.Number({
|
||||
displayName: 'Port',
|
||||
description: 'Database port',
|
||||
required: false,
|
||||
defaultValue: 1521,
|
||||
}),
|
||||
serviceName: Property.ShortText({
|
||||
displayName: 'Service Name',
|
||||
description: 'Oracle service name',
|
||||
required: false,
|
||||
}),
|
||||
connectionString: Property.LongText({
|
||||
displayName: 'Connection String',
|
||||
description: 'Full connection string (e.g., host:port/serviceName)',
|
||||
required: false,
|
||||
}),
|
||||
user: Property.ShortText({
|
||||
displayName: 'Username',
|
||||
required: true,
|
||||
}),
|
||||
password: PieceAuth.SecretText({
|
||||
displayName: 'Password',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
|
||||
validate: async ({ auth }) => {
|
||||
let connection: oracledb.Connection | undefined;
|
||||
const typedAuth = auth as StaticPropsValue<(typeof oracleDbAuth)['props']>;
|
||||
|
||||
try {
|
||||
let connectString: string | undefined;
|
||||
|
||||
if (typedAuth.connectionType === 'serviceName') {
|
||||
if (!typedAuth.host || !typedAuth.port || !typedAuth.serviceName) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Host, Port, and Service Name are required for this connection type.',
|
||||
};
|
||||
}
|
||||
connectString = `${typedAuth.host}:${typedAuth.port}/${typedAuth.serviceName}`;
|
||||
} else {
|
||||
if (!typedAuth.connectionString) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Connection String is required for this connection type.',
|
||||
};
|
||||
}
|
||||
connectString = typedAuth.connectionString;
|
||||
}
|
||||
|
||||
connection = await oracledb.getConnection({
|
||||
user: typedAuth.user,
|
||||
password: typedAuth.password,
|
||||
connectString: connectString,
|
||||
});
|
||||
|
||||
return { valid: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
valid: false,
|
||||
error: (e as Error)?.message || 'Unknown connection error.',
|
||||
};
|
||||
} finally {
|
||||
if (connection) {
|
||||
try {
|
||||
await connection.close();
|
||||
} catch (e) {
|
||||
console.error('Failed to close Oracle DB connection:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type OracleDbAuth = StaticPropsValue<typeof oracleDbAuth.props>;
|
||||
@@ -0,0 +1,389 @@
|
||||
import { OracleDbAuth } from './types';
|
||||
import oracledb from 'oracledb';
|
||||
|
||||
interface ExecuteManyResult {
|
||||
rowsAffected?: number;
|
||||
}
|
||||
|
||||
export class OracleDbClient {
|
||||
private readonly auth: OracleDbAuth;
|
||||
private connection: oracledb.Connection | undefined;
|
||||
|
||||
constructor(auth: OracleDbAuth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
const connectString =
|
||||
this.auth.connectionType === 'serviceName'
|
||||
? `${this.auth.host}:${this.auth.port}/${this.auth.serviceName}`
|
||||
: this.auth.connectionString;
|
||||
|
||||
this.connection = await oracledb.getConnection({
|
||||
user: this.auth.user,
|
||||
password: this.auth.password,
|
||||
connectString: connectString,
|
||||
});
|
||||
}
|
||||
|
||||
public async getTables(): Promise<{ label: string; value: string }[]> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const result = await this.connection.execute<{ TABLE_NAME: string }>(
|
||||
`SELECT table_name FROM user_tables ORDER BY table_name`,
|
||||
[],
|
||||
{ outFormat: oracledb.OUT_FORMAT_OBJECT }
|
||||
);
|
||||
await this.close();
|
||||
|
||||
return (
|
||||
result.rows?.map((row) => ({
|
||||
label: row.TABLE_NAME,
|
||||
value: row.TABLE_NAME,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
public async insertRow(
|
||||
tableName: string,
|
||||
rowData: Record<string, unknown>
|
||||
): Promise<{ success: boolean; rowsAffected: number }> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const columns = Object.keys(rowData);
|
||||
const values = Object.values(rowData);
|
||||
const placeholders = columns.map((_, i) => `:${i + 1}`).join(', ');
|
||||
const quotedColumns = columns.map((c) => `"${c}"`).join(', ');
|
||||
const quotedTableName = `"${tableName}"`;
|
||||
const sql = `INSERT INTO ${quotedTableName} (${quotedColumns}) VALUES (${placeholders})`;
|
||||
|
||||
try {
|
||||
const result = await this.connection.execute(sql, values, {
|
||||
autoCommit: true,
|
||||
});
|
||||
await this.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async insertRows(
|
||||
tableName: string,
|
||||
rowsData: Record<string, unknown>[]
|
||||
): Promise<{ success: boolean; rowsAffected: number }> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const columns = Object.keys(rowsData[0]);
|
||||
const quotedColumns = columns.map((c) => `"${c}"`).join(', ');
|
||||
const quotedTableName = `"${tableName}"`;
|
||||
|
||||
const placeholders = columns.map((_, i) => `:${i + 1}`).join(', ');
|
||||
const sql = `INSERT INTO ${quotedTableName} (${quotedColumns}) VALUES (${placeholders})`;
|
||||
|
||||
const bindData = rowsData.map((row) => columns.map((col) => row[col]));
|
||||
|
||||
try {
|
||||
const result = await this.connection.executeMany(sql, bindData, {
|
||||
autoCommit: true,
|
||||
});
|
||||
await this.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateRow(
|
||||
tableName: string,
|
||||
values: Record<string, unknown>,
|
||||
filter: Record<string, unknown>
|
||||
): Promise<{ success: boolean; rowsAffected: number }> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const valueKeys = Object.keys(values);
|
||||
const filterKeys = Object.keys(filter);
|
||||
|
||||
const setClause = valueKeys.map((k) => `"${k}" = :set_${k}`).join(', ');
|
||||
|
||||
const whereClause = filterKeys
|
||||
.map((k) => `"${k}" = :whr_${k}`)
|
||||
.join(' AND ');
|
||||
|
||||
const binds: oracledb.BindParameters = {};
|
||||
|
||||
for (const key of valueKeys) {
|
||||
binds[`set_${key}`] = values[key] as any;
|
||||
}
|
||||
for (const key of filterKeys) {
|
||||
binds[`whr_${key}`] = filter[key] as any;
|
||||
}
|
||||
|
||||
let sql = `UPDATE "${tableName}" SET ${setClause}`;
|
||||
if (whereClause) {
|
||||
sql += ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
autoCommit: true,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteRow(
|
||||
tableName: string,
|
||||
filter: Record<string, unknown>
|
||||
): Promise<{ success: boolean; rowsAffected: number }> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const filterKeys = Object.keys(filter);
|
||||
|
||||
const whereClause = filterKeys
|
||||
.map((k) => `"${k}" = :whr_${k}`)
|
||||
.join(' AND ');
|
||||
|
||||
const binds: oracledb.BindParameters = {};
|
||||
for (const key of filterKeys) {
|
||||
binds[`whr_${key}`] = filter[key] as any;
|
||||
}
|
||||
|
||||
const sql = `DELETE FROM "${tableName}" WHERE ${whereClause}`;
|
||||
|
||||
try {
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
autoCommit: true,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async findRow(
|
||||
tableName: string,
|
||||
filter: Record<string, unknown>
|
||||
): Promise<unknown[]> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const filterKeys = Object.keys(filter);
|
||||
|
||||
const whereClause = filterKeys
|
||||
.map((k) => `"${k}" = :whr_${k}`)
|
||||
.join(' AND ');
|
||||
|
||||
const binds: oracledb.BindParameters = {};
|
||||
for (const key of filterKeys) {
|
||||
binds[`whr_${key}`] = filter[key] as any;
|
||||
}
|
||||
|
||||
const sql = `SELECT * FROM "${tableName}" WHERE ${whereClause}`;
|
||||
|
||||
try {
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
return (result.rows as unknown[]) || [];
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async execute(
|
||||
sql: string,
|
||||
binds: oracledb.BindParameters
|
||||
): Promise<{ rows: unknown[]; rowsAffected?: number }> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
autoCommit: true,
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
|
||||
return {
|
||||
rows: (result.rows as unknown[]) || [],
|
||||
rowsAffected: result.rowsAffected,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await this.close();
|
||||
throw this.handleOracleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getNewRows(
|
||||
tableName: string,
|
||||
orderByColumn: string,
|
||||
lastValue: unknown,
|
||||
filter: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
await this.connect();
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const filterKeys = Object.keys(filter);
|
||||
const whereConditions = filterKeys.map((k) => `"${k}" = :whr_${k}`);
|
||||
whereConditions.push(`"${orderByColumn}" > :lastValue`);
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
const binds: Record<string, any> = { lastValue };
|
||||
for (const key of filterKeys) {
|
||||
binds[`whr_${key}`] = filter[key];
|
||||
}
|
||||
|
||||
const sql = `SELECT * FROM "${tableName}" WHERE ${whereClause} ORDER BY "${orderByColumn}" ASC`;
|
||||
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
return result.rows as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
public async getLatestRows(
|
||||
tableName: string,
|
||||
orderByColumn: string,
|
||||
filter: Record<string, unknown>
|
||||
): Promise<oracledb.Result<unknown>> {
|
||||
await this.connect();
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
|
||||
const filterKeys = Object.keys(filter);
|
||||
const whereClause = filterKeys
|
||||
.map((k) => `"${k}" = :whr_${k}`)
|
||||
.join(' AND ');
|
||||
|
||||
const binds: oracledb.BindParameters = {};
|
||||
for (const key of filterKeys) {
|
||||
binds[`whr_${key}`] = filter[key] as any;
|
||||
}
|
||||
|
||||
let sql = `SELECT * FROM "${tableName}"`;
|
||||
if (whereClause) {
|
||||
sql += ` WHERE ${whereClause}`;
|
||||
}
|
||||
sql += ` ORDER BY "${orderByColumn}" DESC FETCH FIRST 5 ROWS ONLY`;
|
||||
|
||||
const result = await this.connection.execute(sql, binds, {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||
});
|
||||
|
||||
await this.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getColumns(
|
||||
tableName: string
|
||||
): Promise<{ label: string; value: string }[]> {
|
||||
await this.connect();
|
||||
if (!this.connection) {
|
||||
throw new Error('Database connection failed and was not established.');
|
||||
}
|
||||
const result = await this.connection.execute<{ COLUMN_NAME: string }>(
|
||||
`SELECT column_name FROM user_tab_columns WHERE table_name = :tableName ORDER BY column_id`,
|
||||
{ tableName },
|
||||
{ outFormat: oracledb.OUT_FORMAT_OBJECT }
|
||||
);
|
||||
await this.close();
|
||||
return (
|
||||
result.rows?.map((row) => ({
|
||||
label: row.COLUMN_NAME,
|
||||
value: row.COLUMN_NAME,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
if (this.connection) {
|
||||
await this.connection.close();
|
||||
this.connection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private handleOracleError(error: any): Error {
|
||||
const errorNum = error?.errorNum;
|
||||
let message = error?.message || 'Unknown Oracle error';
|
||||
|
||||
// Common Oracle error codes
|
||||
if (errorNum === 1) {
|
||||
message = `Unique constraint violated: ${message}`;
|
||||
} else if (errorNum === 2290 || errorNum === 2291 || errorNum === 2292) {
|
||||
message = `Constraint violation: ${message}`;
|
||||
} else if (errorNum === 1400) {
|
||||
message = `Required column missing: ${message}`;
|
||||
} else if (errorNum === 904 || errorNum === 942) {
|
||||
message = `Invalid column or table: ${message}`;
|
||||
} else if (errorNum === 1722) {
|
||||
message = `Invalid number format: ${message}`;
|
||||
} else if (errorNum === 12899) {
|
||||
message = `Value too large for column: ${message}`;
|
||||
}
|
||||
|
||||
return new Error(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { OracleDbClient } from './client';
|
||||
import { OracleDbAuth } from './types';
|
||||
import { oracleDbAuth } from './auth';
|
||||
|
||||
export const oracleDbProps = {
|
||||
tableName: () =>
|
||||
Property.Dropdown({
|
||||
auth: oracleDbAuth,
|
||||
displayName: 'Table Name',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async (propsValue) => {
|
||||
const auth = propsValue.auth;
|
||||
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
const client = new OracleDbClient(auth.props);
|
||||
const tables = await client.getTables();
|
||||
return {
|
||||
disabled: false,
|
||||
options: tables.map((table) => ({
|
||||
label: table.label,
|
||||
value: table.value,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
orderBy: () =>
|
||||
Property.Dropdown({
|
||||
auth: oracleDbAuth,
|
||||
displayName: 'Order By Column',
|
||||
description: 'Column that increases over time (ID or timestamp)',
|
||||
required: true,
|
||||
refreshers: ['tableName'],
|
||||
options: async (propsValue) => {
|
||||
const tableName = propsValue['tableName'] as string | undefined;
|
||||
const auth = propsValue.auth;
|
||||
|
||||
if (!auth || !tableName) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please select a table first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
const client = new OracleDbClient(auth.props);
|
||||
const columns = await client.getColumns(tableName);
|
||||
return {
|
||||
disabled: false,
|
||||
options: columns.map((col: { label: string; value: string }) => ({
|
||||
label: col.label,
|
||||
value: col.value,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { StaticPropsValue } from '@activepieces/pieces-framework';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
|
||||
export type OracleDbAuth = StaticPropsValue<(typeof oracleDbAuth)['props']>;
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
AppConnectionValueForAuthProperty,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import crypto from 'crypto';
|
||||
import dayjs from 'dayjs';
|
||||
import { oracleDbAuth } from '../common/auth';
|
||||
import { OracleDbClient } from '../common/client';
|
||||
import { oracleDbProps } from '../common/props';
|
||||
import oracledb from 'oracledb';
|
||||
|
||||
type OrderDirection = 'ASC' | 'DESC';
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof oracleDbAuth>,
|
||||
{
|
||||
tableName: string;
|
||||
orderBy: string;
|
||||
orderDirection: OrderDirection | undefined;
|
||||
}
|
||||
> = {
|
||||
strategy: DedupeStrategy.LAST_ITEM,
|
||||
items: async ({ auth, propsValue, lastItemId }) => {
|
||||
const client = new OracleDbClient(auth.props);
|
||||
await client['connect']();
|
||||
|
||||
if (!client['connection']) {
|
||||
throw new Error('Database connection failed');
|
||||
}
|
||||
|
||||
const lastItem = lastItemId as string;
|
||||
const lastOrderKey = lastItem ? lastItem.split('|')[0] : null;
|
||||
const direction = propsValue.orderDirection || 'DESC';
|
||||
|
||||
let sql: string;
|
||||
const binds: oracledb.BindParameters = {};
|
||||
|
||||
if (lastOrderKey === null) {
|
||||
sql = `SELECT * FROM "${propsValue.tableName}" ORDER BY "${propsValue.orderBy}" ${direction} FETCH FIRST 5 ROWS ONLY`;
|
||||
} else {
|
||||
const operator = direction === 'DESC' ? '>=' : '<=';
|
||||
sql = `SELECT * FROM "${propsValue.tableName}" WHERE "${propsValue.orderBy}" ${operator} :lastKey ORDER BY "${propsValue.orderBy}" ${direction}`;
|
||||
binds['lastKey'] = lastOrderKey;
|
||||
}
|
||||
|
||||
const result = await client['connection'].execute(sql, binds, {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||
});
|
||||
|
||||
await client.close();
|
||||
|
||||
const rows = (result.rows as Record<string, any>[]) || [];
|
||||
const items = rows.map((row) => {
|
||||
const rowHash = crypto
|
||||
.createHash('md5')
|
||||
.update(JSON.stringify(row))
|
||||
.digest('hex');
|
||||
const isTimestamp = dayjs(row[propsValue.orderBy]).isValid();
|
||||
const orderValue = isTimestamp
|
||||
? dayjs(row[propsValue.orderBy]).toISOString()
|
||||
: row[propsValue.orderBy];
|
||||
return {
|
||||
id: orderValue + '|' + rowHash,
|
||||
data: row,
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
};
|
||||
|
||||
export const newRowTrigger = createTrigger({
|
||||
auth: oracleDbAuth,
|
||||
name: 'new_row',
|
||||
displayName: 'New Row',
|
||||
description: 'Triggers when a new row is created',
|
||||
props: {
|
||||
description: Property.MarkDown({
|
||||
value: `**NOTE:** Fetches latest rows using the order column (newest first), then keeps polling for new rows.`,
|
||||
}),
|
||||
tableName: oracleDbProps.tableName(),
|
||||
orderBy: oracleDbProps.orderBy(),
|
||||
orderDirection: Property.StaticDropdown<OrderDirection>({
|
||||
displayName: 'Order Direction',
|
||||
description: 'Sort direction to fetch newest rows first',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Ascending', value: 'ASC' },
|
||||
{ label: 'Descending', value: 'DESC' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'DESC',
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
sampleData: {},
|
||||
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth,
|
||||
});
|
||||
},
|
||||
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
auth: context.auth,
|
||||
});
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user