Skip to content

Commit 6594cae

Browse files
committed
Feat: add triggers endpoints
1 parent 7403e4d commit 6594cae

File tree

8 files changed

+419
-94
lines changed

8 files changed

+419
-94
lines changed

src/lib/PostgresMetaTriggers.ts

Lines changed: 211 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {
2-
triggersSql,
3-
} from './sql'
4-
import { PostgresMetaResult, PostgresTable } from './types'
1+
import format, { ident, literal } from 'pg-format'
2+
import { triggersSql } from './sql'
3+
import { PostgresMetaResult, PostgresTrigger } from './types'
54

65
export default class PostgresMetaTriggers {
76
query: (sql: string) => Promise<PostgresMetaResult<any>>
@@ -10,59 +9,216 @@ export default class PostgresMetaTriggers {
109
this.query = query
1110
}
1211

13-
async list({ } = {}): Promise<PostgresMetaResult<PostgresTable[]>> {
14-
const sql = enrichedTriggersSql
15-
return await this.query(sql)
12+
async list(): Promise<PostgresMetaResult<PostgresTrigger[]>> {
13+
return await this.query(enrichedTriggersSql)
1614
}
1715

18-
// async retrieve({ id }: { id: number }): Promise<PostgresMetaResult<PostgresTable>>
19-
// async retrieve({
20-
// name,
21-
// schema,
22-
// }: {
23-
// name: string
24-
// schema: string
25-
// }): Promise<PostgresMetaResult<PostgresTable>>
26-
// async retrieve({
27-
// id,
28-
// name,
29-
// schema = 'public',
30-
// }: {
31-
// id?: number
32-
// name?: string
33-
// schema?: string
34-
// }): Promise<PostgresMetaResult<PostgresTable>> {
35-
// // @TODO
36-
// }
37-
38-
// async create({
39-
// name,
40-
// schema = 'public',
41-
// comment,
42-
// }: {
43-
// name: string
44-
// schema?: string
45-
// comment?: string
46-
// }): Promise<PostgresMetaResult<PostgresTable>> {
47-
// // @TODO
48-
// }
49-
50-
// async update(
51-
// id: number,
52-
// {
53-
// name,
54-
// schema,
55-
// }: {
56-
// name?: string
57-
// schema?: string
58-
// }
59-
// ): Promise<PostgresMetaResult<PostgresTable>> {
60-
// // @TODO
61-
// }
62-
63-
// async remove(id: number, { cascade = false } = {}): Promise<PostgresMetaResult<PostgresTable>> {
64-
// // @TODO
65-
// }
16+
async retrieve({ id }: { id: number }): Promise<PostgresMetaResult<PostgresTrigger>>
17+
async retrieve({
18+
name,
19+
table,
20+
schema,
21+
}: {
22+
name: string
23+
table: string
24+
schema?: string
25+
}): Promise<PostgresMetaResult<PostgresTrigger>>
26+
async retrieve({
27+
id,
28+
name,
29+
schema = 'public',
30+
table,
31+
}: {
32+
id?: number
33+
name?: string
34+
schema?: string
35+
table?: string
36+
}): Promise<PostgresMetaResult<PostgresTrigger>> {
37+
if (id) {
38+
const sql = `${enrichedTriggersSql} WHERE triggers.id = ${literal(id)};`
39+
40+
const { data, error } = await this.query(sql)
41+
42+
if (error) {
43+
return { data: null, error }
44+
}
45+
46+
const triggerRecord = data && data[0]
47+
48+
if (triggerRecord) {
49+
return { data: triggerRecord, error: null }
50+
}
51+
52+
return { data: null, error: { message: `Cannot find a trigger with ID ${id}` } }
53+
}
54+
55+
if (name && schema && table) {
56+
const sql = `${enrichedTriggersSql} WHERE triggers.name = ${literal(
57+
name
58+
)} AND triggers.schema = ${literal(schema)} AND triggers.table = ${literal(table)};`
59+
60+
const { data, error } = await this.query(sql)
61+
62+
if (error) {
63+
return { data: null, error }
64+
}
65+
66+
const triggerRecord = data && data[0]
67+
68+
if (triggerRecord) {
69+
return { data: triggerRecord, error: null }
70+
}
71+
72+
return {
73+
data: null,
74+
error: {
75+
message: `Cannot find a trigger with name ${name} on table "${schema}"."${table}"`,
76+
},
77+
}
78+
}
79+
80+
return { data: null, error: { message: 'Invalid parameters on trigger retrieve' } }
81+
}
82+
83+
/**
84+
* Creates trigger
85+
*
86+
* @param {Object} obj - An object.
87+
* @param {string} obj.name - Trigger name.
88+
* @param {string} obj.schema - Name of schema that trigger is for.
89+
* @param {string} obj.table - Unqualified table, view, or foreign table name that trigger is for.
90+
* @param {string} obj.function_schema - Name of schema that function is for.
91+
* @param {string} obj.function_name - Unqualified name of the function to execute.
92+
* @param {('BEFORE'|'AFTER'|'INSTEAD OF')} obj.activation - Determines when function is called
93+
* during event occurrence.
94+
* @param {Array<string>} obj.events - Event(s) that will fire the trigger. Array of the following options: 'INSERT' | 'UPDATE' | 'UPDATE
95+
* OF column_name1,column_name2' | 'DELETE' | 'TRUNCATE'.
96+
* @param {('ROW'|'STATEMENT')} obj.orientation - Trigger function for every row affected by event or
97+
* once per statement. Defaults to 'STATEMENT'.
98+
* @param {string} obj.condition - Boolean expression that will trigger function.
99+
* For example: '(old.* IS DISTINCT FROM new.*)'
100+
* @param {Array<string>} obj.function_args - array of arguments to be passed to function when trigger is fired.
101+
* For example: ['arg1', 'arg2']
102+
*/
103+
async create({
104+
name,
105+
schema = 'public',
106+
table,
107+
function_schema = 'public',
108+
function_name,
109+
function_args,
110+
activation,
111+
events,
112+
orientation,
113+
condition,
114+
}: {
115+
name: string
116+
table: string
117+
function_name: string
118+
activation: string
119+
events: string[]
120+
function_schema?: string
121+
schema?: string
122+
orientation?: string
123+
condition?: string
124+
function_args?: string[]
125+
}): Promise<PostgresMetaResult<PostgresTrigger>> {
126+
const qualifiedTableName = `${ident(schema)}.${ident(table)}`
127+
const qualifiedFunctionName = `${ident(function_schema)}.${ident(function_name)}`
128+
129+
const triggerOrientation = orientation ? `FOR EACH ${format.string(orientation)}` : ''
130+
const triggerCondition = condition ? `WHEN ( ${format.string(condition)} )` : ''
131+
const triggerEvents = Array.isArray(events) ? `${format.string(events.join(' OR '))}` : ''
132+
const functionArgs = Array.isArray(function_args)
133+
? `${function_args.map((arg) => literal(arg)).join(',')}`
134+
: ''
135+
136+
const sql = `CREATE TRIGGER ${ident(name)} ${format.string(
137+
activation
138+
)} ${triggerEvents} ON ${qualifiedTableName} ${triggerOrientation} ${triggerCondition} EXECUTE FUNCTION ${qualifiedFunctionName} ( ${functionArgs} );`
139+
140+
const { error } = await this.query(sql)
141+
142+
if (error) {
143+
return { data: null, error }
144+
}
145+
146+
return await this.retrieve({
147+
name,
148+
table,
149+
schema,
150+
})
151+
}
152+
153+
async update(
154+
id: number,
155+
{
156+
name: newName,
157+
enabled_mode,
158+
}: {
159+
name: string
160+
enabled_mode: 'ORIGIN' | 'REPLICA' | 'ALWAYS' | 'DISABLED'
161+
}
162+
): Promise<PostgresMetaResult<PostgresTrigger>> {
163+
const { data: triggerRecord, error } = await this.retrieve({ id })
164+
165+
if (error) {
166+
return { data: null, error }
167+
}
168+
169+
let enabledModeSql
170+
const enabledMode = enabled_mode.toUpperCase()
171+
const { name: currentName, schema: schema, table: table } = triggerRecord!
172+
const qualifiedTableName = `${ident(schema)}.${ident(table)}`
173+
const updateNameSql = newName
174+
? `ALTER TRIGGER ${ident(currentName)} ON ${qualifiedTableName} RENAME TO ${ident(newName)};`
175+
: ''
176+
177+
if (['ORIGIN', 'REPLICA', 'ALWAYS', 'DISABLED'].includes(enabledMode)) {
178+
if (enabledMode === 'DISABLED') {
179+
enabledModeSql = `ALTER TABLE ${qualifiedTableName} DISABLE TRIGGER ${ident(currentName)};`
180+
} else {
181+
enabledModeSql = `ALTER TABLE ${qualifiedTableName} ENABLE ${
182+
['REPLICA', 'ALWAYS'].includes(enabledMode) ? enabledMode : ''
183+
} TRIGGER ${ident(currentName)};`
184+
}
185+
}
186+
187+
// updateNameSql must be last
188+
const sql = `BEGIN; ${enabledModeSql} ${updateNameSql} COMMIT;`
189+
190+
{
191+
const { error } = await this.query(sql)
192+
193+
if (error) {
194+
return { data: null, error }
195+
}
196+
}
197+
198+
return await this.retrieve({ id })
199+
}
200+
201+
async remove(id: number, { cascade = false }): Promise<PostgresMetaResult<PostgresTrigger>> {
202+
const { data: triggerRecord, error } = await this.retrieve({ id })
203+
204+
if (error) {
205+
return { data: null, error }
206+
}
207+
208+
const { name, schema, table } = triggerRecord!
209+
const qualifiedTableName = `${ident(schema)}.${ident(table)}`
210+
const sql = `DROP TRIGGER ${ident(name)} ON ${qualifiedTableName} ${cascade ? 'CASCADE' : ''};`
211+
212+
{
213+
const { error } = await this.query(sql)
214+
215+
if (error) {
216+
return { data: null, error }
217+
}
218+
}
219+
220+
return { data: triggerRecord!, error: null }
221+
}
66222
}
67223

68224
const enrichedTriggersSql = `

src/lib/db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { PostgresMetaResult } from './types'
33

44
types.setTypeParser(20, parseInt)
55

6-
export const init: (config: PoolConfig) => {
6+
export const init: (
7+
config: PoolConfig
8+
) => {
79
query: (sql: string) => Promise<PostgresMetaResult<any>>
810
end: () => Promise<void>
911
} = (config) => {

src/lib/sql/schemas.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Adapted from infromation_schema.schemata
1+
-- Adapted from information_schema.schemata
22

33
SELECT
44
n.oid :: int8 AS id,

src/lib/sql/triggers.sql

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
1-
select
2-
trigger_schema as "schema",
3-
trigger_name as "name",
4-
trigger_catalog as "catalog"
5-
from
6-
information_schema.triggers
1+
SELECT
2+
pg_t.oid AS id,
3+
CASE
4+
WHEN pg_t.tgenabled = 'D' THEN 'DISABLED'
5+
WHEN pg_t.tgenabled = 'O' THEN 'ORIGIN'
6+
WHEN pg_t.tgenabled = 'R' THEN 'REPLICA'
7+
WHEN pg_t.tgenabled = 'A' THEN 'ALWAYS'
8+
END AS enabled_mode,
9+
(
10+
STRING_TO_ARRAY(
11+
ENCODE(pg_t.tgargs, 'escape'), '\000'
12+
)
13+
)[:pg_t.tgnargs] AS function_args,
14+
is_t.trigger_name AS name,
15+
is_t.event_object_table AS table,
16+
is_t.event_object_schema AS schema,
17+
is_t.action_condition AS condition,
18+
is_t.action_orientation AS orientation,
19+
is_t.action_timing AS activation,
20+
ARRAY_AGG(is_t.event_manipulation)::text[] AS events,
21+
pg_p.proname AS function_name,
22+
pg_n.nspname AS function_schema
23+
FROM
24+
pg_trigger AS pg_t
25+
JOIN
26+
pg_class AS pg_c
27+
ON pg_t.tgrelid = pg_c.oid
28+
JOIN information_schema.triggers AS is_t
29+
ON is_t.trigger_name = pg_t.tgname
30+
AND pg_c.relname = is_t.event_object_table
31+
JOIN pg_proc AS pg_p
32+
ON pg_t.tgfoid = pg_p.oid
33+
JOIN pg_namespace AS pg_n
34+
ON pg_p.pronamespace = pg_n.oid
35+
GROUP BY
36+
pg_t.oid,
37+
pg_t.tgenabled,
38+
pg_t.tgargs,
39+
pg_t.tgnargs,
40+
is_t.trigger_name,
41+
is_t.event_object_table,
42+
is_t.event_object_schema,
43+
is_t.action_condition,
44+
is_t.action_orientation,
45+
is_t.action_statement,
46+
is_t.action_timing,
47+
pg_p.proname,
48+
pg_n.nspname

src/lib/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ export const postgresPublicationSchema = Type.Object({
150150
})
151151
export type PostgresPublication = Static<typeof postgresPublicationSchema>
152152

153+
export const postgresTriggerSchema = Type.Object({
154+
id: Type.Integer(),
155+
enabled_mode: Type.Union([
156+
Type.Literal('ORIGIN'),
157+
Type.Literal('REPLICA'),
158+
Type.Literal('ALWAYS'),
159+
Type.Literal('DISABLED'),
160+
]),
161+
name: Type.String(),
162+
table: Type.String(),
163+
schema: Type.String(),
164+
condition: Type.Union([Type.String(), Type.Null()]),
165+
orientation: Type.Union([Type.Literal('ROW'), Type.Literal('STATEMENT')]),
166+
activation: Type.Union([
167+
Type.Literal('BEFORE'),
168+
Type.Literal('AFTER'),
169+
Type.Literal('INSTEAD OF'),
170+
]),
171+
events: Type.Array(Type.String()),
172+
function_schema: Type.String(),
173+
function_name: Type.String(),
174+
function_args: Type.Array(Type.String()),
175+
})
176+
export type PostgresTrigger = Static<typeof postgresTriggerSchema>
177+
153178
export const postgresRelationshipSchema = Type.Object({
154179
id: Type.Integer(),
155180
constraint_name: Type.String(),

0 commit comments

Comments
 (0)