@slates-integrations/postgresql 0.2.0-rc.6 → 0.2.0-rc.9

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.
@@ -1,193 +0,0 @@
1
- import { SlateTool } from '@slates/provider';
2
- import { spec } from '../spec';
3
- import { createClient, escapeIdentifier, qualifiedTableName } from '../lib/helpers';
4
- import { postgresServiceError } from '../lib/errors';
5
- import { z } from 'zod';
6
-
7
- let normalizeViewQuery = (query: string | undefined, allowTableQuery: boolean) => {
8
- if (!query?.trim()) {
9
- throw postgresServiceError('query is required for create action.');
10
- }
11
-
12
- let sql = query.trim().replace(/;\s*$/, '');
13
- if (sql.includes(';') || sql.includes('\0')) {
14
- throw postgresServiceError(
15
- 'View query must be a single SELECT, WITH, VALUES, or TABLE statement.'
16
- );
17
- }
18
-
19
- let allowedPattern = allowTableQuery
20
- ? /^(SELECT|WITH|VALUES|TABLE)\b/i
21
- : /^(SELECT|WITH|VALUES)\b/i;
22
- if (!allowedPattern.test(sql)) {
23
- throw postgresServiceError(
24
- allowTableQuery
25
- ? 'Materialized view query must start with SELECT, WITH, VALUES, or TABLE.'
26
- : 'View query must start with SELECT, WITH, or VALUES.'
27
- );
28
- }
29
-
30
- return sql;
31
- };
32
-
33
- export let manageViews = SlateTool.create(spec, {
34
- name: 'Manage Views',
35
- key: 'manage_views',
36
- description:
37
- 'Create and drop PostgreSQL views or materialized views, and refresh materialized views.',
38
- instructions: [
39
- 'Use regular views for reusable read queries that should always reflect current base-table data.',
40
- 'Use materialized views when the query result should be persisted and refreshed on demand.',
41
- 'Dropping a view requires confirmDrop=true to avoid accidentally removing dependent read models.'
42
- ],
43
- tags: {
44
- destructive: true
45
- }
46
- })
47
- .input(
48
- z.object({
49
- action: z.enum(['create', 'drop', 'refresh']).describe('Action to perform'),
50
- viewType: z
51
- .enum(['view', 'materialized_view'])
52
- .optional()
53
- .default('view')
54
- .describe('Whether to manage a regular view or materialized view'),
55
- viewName: z.string().describe('View name'),
56
- schemaName: z.string().optional().describe('Schema containing the view'),
57
-
58
- // Create options
59
- query: z
60
- .string()
61
- .optional()
62
- .describe('SELECT query used to define the view (create only)'),
63
- columns: z
64
- .array(z.string())
65
- .optional()
66
- .describe('Optional output column names for the view'),
67
- orReplace: z
68
- .boolean()
69
- .optional()
70
- .default(false)
71
- .describe('Use CREATE OR REPLACE VIEW (regular views only)'),
72
- ifNotExists: z
73
- .boolean()
74
- .optional()
75
- .default(false)
76
- .describe('Use IF NOT EXISTS for materialized view creation'),
77
- checkOption: z
78
- .enum(['local', 'cascaded'])
79
- .optional()
80
- .describe('WITH LOCAL/CASCADED CHECK OPTION for updatable regular views'),
81
- withData: z
82
- .boolean()
83
- .optional()
84
- .default(true)
85
- .describe('Populate a materialized view during create/refresh'),
86
-
87
- // Drop/refresh options
88
- ifExists: z.boolean().optional().default(false).describe('Use IF EXISTS for drop'),
89
- cascade: z.boolean().optional().default(false).describe('Use CASCADE for drop'),
90
- confirmDrop: z.boolean().optional().default(false).describe('Must be true for drop'),
91
- concurrently: z
92
- .boolean()
93
- .optional()
94
- .default(false)
95
- .describe('Refresh materialized view without blocking concurrent reads')
96
- })
97
- )
98
- .output(
99
- z.object({
100
- success: z.boolean().describe('Whether the operation completed successfully'),
101
- executedSql: z.string().describe('The SQL statement that was executed'),
102
- viewName: z.string().describe('View affected by the operation'),
103
- schemaName: z.string().describe('Schema containing the view'),
104
- viewType: z.enum(['view', 'materialized_view']).describe('Type of view')
105
- })
106
- )
107
- .handleInvocation(async ctx => {
108
- let client = createClient(ctx.auth, ctx.config);
109
- let schema = ctx.input.schemaName || ctx.config.defaultSchema;
110
- let fullViewName = qualifiedTableName(ctx.input.viewName, schema);
111
- let columnList = ctx.input.columns?.length
112
- ? ` (${ctx.input.columns.map(escapeIdentifier).join(', ')})`
113
- : '';
114
- let sql: string;
115
-
116
- if (ctx.input.action === 'create') {
117
- let query = normalizeViewQuery(
118
- ctx.input.query,
119
- ctx.input.viewType === 'materialized_view'
120
- );
121
-
122
- if (ctx.input.viewType === 'view') {
123
- if (ctx.input.ifNotExists) {
124
- throw postgresServiceError('ifNotExists is only supported for materialized views.');
125
- }
126
-
127
- let orReplace = ctx.input.orReplace ? 'OR REPLACE ' : '';
128
- let checkOption = ctx.input.checkOption
129
- ? ` WITH ${ctx.input.checkOption.toUpperCase()} CHECK OPTION`
130
- : '';
131
- sql = `CREATE ${orReplace}VIEW ${fullViewName}${columnList} AS ${query}${checkOption}`;
132
- } else {
133
- if (ctx.input.orReplace) {
134
- throw postgresServiceError('orReplace is not supported for materialized views.');
135
- }
136
-
137
- if (ctx.input.checkOption) {
138
- throw postgresServiceError('checkOption is only supported for regular views.');
139
- }
140
-
141
- let ifNotExists = ctx.input.ifNotExists ? 'IF NOT EXISTS ' : '';
142
- let withData = ctx.input.withData ? 'WITH DATA' : 'WITH NO DATA';
143
- sql = `CREATE MATERIALIZED VIEW ${ifNotExists}${fullViewName}${columnList} AS ${query} ${withData}`;
144
- }
145
- } else if (ctx.input.action === 'drop') {
146
- if (!ctx.input.confirmDrop) {
147
- throw postgresServiceError('confirmDrop must be true to drop a view.');
148
- }
149
-
150
- let objectKeyword = ctx.input.viewType === 'view' ? 'VIEW' : 'MATERIALIZED VIEW';
151
- let ifExists = ctx.input.ifExists ? 'IF EXISTS ' : '';
152
- let dropBehavior = ctx.input.cascade ? ' CASCADE' : ' RESTRICT';
153
- sql = `DROP ${objectKeyword} ${ifExists}${fullViewName}${dropBehavior}`;
154
- } else if (ctx.input.action === 'refresh') {
155
- if (ctx.input.viewType !== 'materialized_view') {
156
- throw postgresServiceError('Only materialized views can be refreshed.');
157
- }
158
-
159
- if (ctx.input.concurrently && !ctx.input.withData) {
160
- throw postgresServiceError(
161
- 'concurrently cannot be used when withData is false for REFRESH MATERIALIZED VIEW.'
162
- );
163
- }
164
-
165
- let concurrently = ctx.input.concurrently ? 'CONCURRENTLY ' : '';
166
- let withData = ctx.input.withData ? 'WITH DATA' : 'WITH NO DATA';
167
- sql = `REFRESH MATERIALIZED VIEW ${concurrently}${fullViewName} ${withData}`;
168
- } else {
169
- throw postgresServiceError(`Unknown action: ${ctx.input.action}`);
170
- }
171
-
172
- ctx.info(`Executing: ${sql}`);
173
- await client.query(sql, ctx.config.queryTimeout);
174
-
175
- let actionLabel =
176
- ctx.input.action === 'create'
177
- ? 'Created'
178
- : ctx.input.action === 'drop'
179
- ? 'Dropped'
180
- : 'Refreshed';
181
-
182
- return {
183
- output: {
184
- success: true,
185
- executedSql: sql,
186
- viewName: ctx.input.viewName,
187
- schemaName: schema,
188
- viewType: ctx.input.viewType
189
- },
190
- message: `${actionLabel} ${ctx.input.viewType === 'view' ? 'view' : 'materialized view'} \`${schema}.${ctx.input.viewName}\`.`
191
- };
192
- })
193
- .build();
@@ -1,98 +0,0 @@
1
- import { SlateTool } from '@slates/provider';
2
- import { spec } from '../spec';
3
- import {
4
- createClient,
5
- escapeIdentifier,
6
- escapeLiteral,
7
- qualifiedTableName
8
- } from '../lib/helpers';
9
- import { postgresServiceError } from '../lib/errors';
10
- import { z } from 'zod';
11
-
12
- export let updateRows = SlateTool.create(spec, {
13
- name: 'Update Rows',
14
- key: 'update_rows',
15
- description: `Update rows in a PostgreSQL table based on a WHERE condition. Specify the columns to update and their new values.
16
- Supports returning updated rows and allows complex WHERE conditions.`,
17
- instructions: [
18
- 'Always specify a WHERE condition to avoid unintended full-table updates.',
19
- 'Values are automatically escaped to prevent SQL injection.'
20
- ],
21
- constraints: ['If no WHERE condition is provided, all rows in the table will be updated.'],
22
- tags: {
23
- destructive: true
24
- }
25
- })
26
- .input(
27
- z.object({
28
- tableName: z.string().describe('Name of the table to update'),
29
- schemaName: z.string().optional().describe('Schema containing the table'),
30
- set: z
31
- .record(z.string(), z.any())
32
- .describe('Object of column names and their new values'),
33
- where: z
34
- .string()
35
- .optional()
36
- .describe(
37
- 'SQL WHERE condition (without the WHERE keyword). Example: "id = 1 AND status = \'active\'"'
38
- ),
39
- returning: z
40
- .boolean()
41
- .optional()
42
- .default(true)
43
- .describe('Whether to return the updated rows using RETURNING *')
44
- })
45
- )
46
- .output(
47
- z.object({
48
- updatedCount: z.number().describe('Number of rows updated'),
49
- returnedRows: z
50
- .array(z.record(z.string(), z.any()))
51
- .describe('Updated rows (if returning was enabled)')
52
- })
53
- )
54
- .handleInvocation(async ctx => {
55
- let client = createClient(ctx.auth, ctx.config);
56
- let schema = ctx.input.schemaName || ctx.config.defaultSchema;
57
- let fullTableName = qualifiedTableName(ctx.input.tableName, schema);
58
-
59
- let setClauses = Object.entries(ctx.input.set).map(([col, val]) => {
60
- let sqlVal: string;
61
- if (val === null || val === undefined) sqlVal = 'NULL';
62
- else if (typeof val === 'number') sqlVal = String(val);
63
- else if (typeof val === 'boolean') sqlVal = val ? 'TRUE' : 'FALSE';
64
- else if (typeof val === 'object') sqlVal = escapeLiteral(JSON.stringify(val));
65
- else sqlVal = escapeLiteral(String(val));
66
- return `${escapeIdentifier(col)} = ${sqlVal}`;
67
- });
68
-
69
- if (setClauses.length === 0) {
70
- throw postgresServiceError(
71
- 'At least one column must be specified in the "set" parameter'
72
- );
73
- }
74
-
75
- let sql = `UPDATE ${fullTableName} SET ${setClauses.join(', ')}`;
76
-
77
- if (ctx.input.where) {
78
- sql += ` WHERE ${ctx.input.where}`;
79
- }
80
-
81
- if (ctx.input.returning) {
82
- sql += ' RETURNING *';
83
- }
84
-
85
- ctx.info(
86
- `Updating rows in ${fullTableName}${ctx.input.where ? ` where ${ctx.input.where}` : ''}`
87
- );
88
- let result = await client.query(sql, ctx.config.queryTimeout);
89
-
90
- return {
91
- output: {
92
- updatedCount: result.rowCount ?? 0,
93
- returnedRows: result.rows
94
- },
95
- message: `Updated **${result.rowCount ?? 0}** row(s) in \`${ctx.input.tableName}\`.`
96
- };
97
- })
98
- .build();
@@ -1,67 +0,0 @@
1
- import { SlateTrigger } from '@slates/provider';
2
- import { spec } from '../spec';
3
- import { z } from 'zod';
4
-
5
- /**
6
- * Generic inbound webhook for providers without a tailored webhook trigger yet.
7
- * POST JSON is parsed into `payload` (non-objects are wrapped as { _value }).
8
- * Refine in the workflow mapper or replace with a provider-specific trigger.
9
- */
10
- export let inboundWebhook = SlateTrigger.create(spec, {
11
- name: 'Inbound Webhook',
12
- key: 'inbound_webhook',
13
- description:
14
- 'Receives HTTP POST at the Slates webhook URL. Parses JSON into payload (or stores raw body if not JSON). Configure your provider to POST here when supported.'
15
- })
16
- .input(
17
- z.object({
18
- payload: z
19
- .record(z.string(), z.any())
20
- .describe('Parsed JSON object from the request body'),
21
- rawBody: z.string().optional().describe('Raw body when JSON parsing failed'),
22
- contentType: z.string().optional().describe('Content-Type header')
23
- })
24
- )
25
- .output(
26
- z.object({
27
- payload: z.record(z.string(), z.any()),
28
- rawBody: z.string().optional()
29
- })
30
- )
31
- .webhook({
32
- handleRequest: async ctx => {
33
- let contentType = ctx.request.headers.get('content-type') ?? '';
34
- let text = await ctx.request.text();
35
- if (!text || !text.trim()) {
36
- return {
37
- inputs: [{ payload: {}, contentType }]
38
- };
39
- }
40
- try {
41
- let parsed = JSON.parse(text);
42
- let payload =
43
- parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
44
- ? parsed
45
- : { _value: parsed };
46
- return {
47
- inputs: [{ payload, contentType }]
48
- };
49
- } catch {
50
- return {
51
- inputs: [{ payload: {}, rawBody: text, contentType }]
52
- };
53
- }
54
- },
55
-
56
- handleEvent: async ctx => {
57
- return {
58
- type: 'webhook.inbound',
59
- id: `inbound-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
60
- output: {
61
- payload: ctx.input.payload,
62
- rawBody: ctx.input.rawBody
63
- }
64
- };
65
- }
66
- })
67
- .build();
@@ -1,2 +0,0 @@
1
- export { tableChanges } from './table-changes';
2
- export * from './inbound-webhook';
@@ -1,123 +0,0 @@
1
- import { SlateTrigger, SlateDefaultPollingIntervalSeconds } from '@slates/provider';
2
- import { spec } from '../spec';
3
- import { createClient, escapeIdentifier, qualifiedTableName } from '../lib/helpers';
4
- import { z } from 'zod';
5
-
6
- export let tableChanges = SlateTrigger.create(spec, {
7
- name: 'Table Row Changes',
8
- key: 'table_row_changes',
9
- description:
10
- 'Polls a PostgreSQL table for new or updated rows based on a timestamp or auto-incrementing column. Detects INSERT and UPDATE operations by tracking the latest value of the specified column.',
11
- instructions: [
12
- 'The table must have a column that monotonically increases with each insert or update (e.g., a created_at, updated_at, or auto-increment id column).',
13
- 'For detecting both inserts and updates, use a column that updates on modification (e.g., updated_at with a trigger).',
14
- 'The polling interval determines how frequently the table is checked for changes.'
15
- ]
16
- })
17
- .input(
18
- z.object({
19
- changeType: z
20
- .enum(['inserted', 'updated'])
21
- .describe('Whether the row was newly inserted or updated'),
22
- trackingColumnValue: z.string().describe('Value of the tracking column for this row'),
23
- row: z.record(z.string(), z.any()).describe('The full row data'),
24
- tableName: z.string().describe('Table that the change was detected in'),
25
- schemaName: z.string().describe('Schema of the table')
26
- })
27
- )
28
- .output(
29
- z.object({
30
- tableName: z.string().describe('Table where the change occurred'),
31
- schemaName: z.string().describe('Schema of the table'),
32
- changeType: z.enum(['inserted', 'updated']).describe('Type of change detected'),
33
- row: z.record(z.string(), z.any()).describe('The full row data')
34
- })
35
- )
36
- .polling({
37
- options: {
38
- intervalInSeconds: SlateDefaultPollingIntervalSeconds
39
- },
40
-
41
- pollEvents: async ctx => {
42
- let client = createClient(ctx.auth, ctx.config);
43
- let schema = (ctx.input as any).schemaName || ctx.config.defaultSchema;
44
- let tableName = (ctx.input as any).tableName as string;
45
- let trackingColumn = (ctx.input as any).trackingColumn as string;
46
- let trackingColumnType = (ctx.input as any).trackingColumnType as string;
47
- let batchSize = (ctx.input as any).batchSize || 100;
48
- let fullTableName = qualifiedTableName(tableName, schema);
49
- let escapedCol = escapeIdentifier(trackingColumn);
50
-
51
- let lastValue = ctx.state?.lastTrackingValue as string | null | undefined;
52
-
53
- let sql: string;
54
- if (lastValue) {
55
- let comparison: string;
56
- if (trackingColumnType === 'timestamp') {
57
- comparison = `${escapedCol} > '${lastValue.replace(/'/g, "''")}'::timestamptz`;
58
- } else {
59
- comparison = `${escapedCol} > ${parseInt(lastValue, 10)}`;
60
- }
61
- sql = `SELECT * FROM ${fullTableName} WHERE ${comparison} ORDER BY ${escapedCol} ASC LIMIT ${batchSize}`;
62
- } else {
63
- // First poll - get the latest rows to establish a baseline
64
- sql = `SELECT * FROM ${fullTableName} ORDER BY ${escapedCol} DESC LIMIT 1`;
65
- }
66
-
67
- ctx.info(
68
- `Polling ${fullTableName} for changes (tracking: ${trackingColumn}, last: ${lastValue || 'initial'})`
69
- );
70
- let result = await client.query(sql, ctx.config.queryTimeout);
71
-
72
- if (!lastValue) {
73
- // First poll - just capture the current max value, don't emit events
74
- let maxRow = result.rows[0];
75
- let newLastValue = maxRow ? String(maxRow[trackingColumn]) : null;
76
- return {
77
- inputs: [],
78
- updatedState: {
79
- lastTrackingValue: newLastValue
80
- }
81
- };
82
- }
83
-
84
- let inputs = result.rows.map((row: any) => ({
85
- changeType: 'inserted' as const, // We can't distinguish insert from update with this approach
86
- trackingColumnValue: String(row[trackingColumn]),
87
- row,
88
- tableName,
89
- schemaName: schema
90
- }));
91
-
92
- let newLastValue =
93
- result.rows.length > 0
94
- ? String(result.rows[result.rows.length - 1]![trackingColumn])
95
- : lastValue;
96
-
97
- return {
98
- inputs,
99
- updatedState: {
100
- lastTrackingValue: newLastValue
101
- }
102
- };
103
- },
104
-
105
- handleEvent: async ctx => {
106
- let row = ctx.input.row;
107
- // Try to find a suitable ID for deduplication
108
- let rowId =
109
- row['id'] || row['_id'] || row['uid'] || row['uuid'] || ctx.input.trackingColumnValue;
110
-
111
- return {
112
- type: `row.${ctx.input.changeType}`,
113
- id: `${ctx.input.schemaName}.${ctx.input.tableName}:${String(rowId)}:${ctx.input.trackingColumnValue}`,
114
- output: {
115
- tableName: ctx.input.tableName,
116
- schemaName: ctx.input.schemaName,
117
- changeType: ctx.input.changeType,
118
- row: ctx.input.row
119
- }
120
- };
121
- }
122
- })
123
- .build();
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "types": ["node"],
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
- "moduleResolution": "bundler",
11
-
12
- "noEmit": true,
13
- "strict": true,
14
- "skipLibCheck": true,
15
- "noFallthroughCasesInSwitch": true,
16
- "noUncheckedIndexedAccess": true,
17
- "noImplicitOverride": true,
18
- "noUnusedLocals": false,
19
- "noUnusedParameters": false,
20
- "noPropertyAccessFromIndexSignature": false
21
- },
22
- "include": ["src"]
23
- }