pg-meta-triggers.ts223 lines · main
| 1 | import { z } from 'zod' |
| 2 | |
| 3 | import { DEFAULT_SYSTEM_SCHEMAS } from './constants' |
| 4 | import { filterByList } from './helpers' |
| 5 | import { |
| 6 | ident, |
| 7 | joinSqlFragments, |
| 8 | keyword, |
| 9 | literal, |
| 10 | safeSql, |
| 11 | type SafeSqlFragment, |
| 12 | } from './pg-format' |
| 13 | import { TRIGGERS_SQL } from './sql/triggers' |
| 14 | |
| 15 | type TriggerIdentifier = Pick<PGTrigger, 'id'> | Pick<PGTrigger, 'name' | 'schema' | 'table'> |
| 16 | |
| 17 | function getIdentifierWhereClause(identifier: TriggerIdentifier): SafeSqlFragment { |
| 18 | if ('id' in identifier && identifier.id) { |
| 19 | return safeSql`${ident('id')} = ${literal(identifier.id)}` |
| 20 | } |
| 21 | if ('name' in identifier && identifier.name && identifier.table && identifier.schema) { |
| 22 | return safeSql`${ident('name')} = ${literal(identifier.name)} and ${ident('schema')} = ${literal(identifier.schema)} and ${ident('table')} = ${literal(identifier.table)}` |
| 23 | } |
| 24 | throw new Error('Must provide either id or name, schema and table') |
| 25 | } |
| 26 | |
| 27 | export const pgTriggerZod = z.object({ |
| 28 | id: z.number(), |
| 29 | table_id: z.number(), |
| 30 | enabled_mode: z.enum(['DISABLED', 'ORIGIN', 'REPLICA', 'ALWAYS']), |
| 31 | function_args: z.array(z.string()), |
| 32 | name: z.string(), |
| 33 | table: z.string(), |
| 34 | schema: z.string(), |
| 35 | condition: z.string().nullable(), |
| 36 | orientation: z.enum(['ROW', 'STATEMENT']), |
| 37 | activation: z.enum(['BEFORE', 'AFTER', 'INSTEAD OF']), |
| 38 | events: z.array(z.string()), |
| 39 | function_name: z.string(), |
| 40 | function_schema: z.string(), |
| 41 | }) |
| 42 | |
| 43 | export type PGTrigger = z.infer<typeof pgTriggerZod> |
| 44 | |
| 45 | export const pgTriggerArrayZod = z.array(pgTriggerZod) |
| 46 | export const pgTriggerOptionalZod = z.optional(pgTriggerZod) |
| 47 | |
| 48 | export function list({ |
| 49 | includeSystemSchemas = false, |
| 50 | includedSchemas, |
| 51 | excludedSchemas, |
| 52 | limit, |
| 53 | offset, |
| 54 | }: { |
| 55 | includeSystemSchemas?: boolean |
| 56 | includedSchemas?: string[] |
| 57 | excludedSchemas?: string[] |
| 58 | limit?: number |
| 59 | offset?: number |
| 60 | } = {}): { |
| 61 | sql: SafeSqlFragment |
| 62 | zod: typeof pgTriggerArrayZod |
| 63 | } { |
| 64 | let sql = safeSql`with triggers as (${TRIGGERS_SQL}) select * from triggers` |
| 65 | const filter = filterByList( |
| 66 | includedSchemas, |
| 67 | excludedSchemas, |
| 68 | !includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined |
| 69 | ) |
| 70 | if (filter) { |
| 71 | sql = safeSql`${sql} where schema ${filter}` |
| 72 | } |
| 73 | if (limit) { |
| 74 | sql = safeSql`${sql} limit ${literal(limit)}` |
| 75 | } |
| 76 | if (offset) { |
| 77 | sql = safeSql`${sql} offset ${literal(offset)}` |
| 78 | } |
| 79 | return { |
| 80 | sql, |
| 81 | zod: pgTriggerArrayZod, |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | type TriggersRetrieveReturn = { |
| 86 | sql: SafeSqlFragment |
| 87 | zod: typeof pgTriggerOptionalZod |
| 88 | } |
| 89 | |
| 90 | export function retrieve(identifier: TriggerIdentifier): TriggersRetrieveReturn |
| 91 | export function retrieve(params: TriggerIdentifier): TriggersRetrieveReturn { |
| 92 | const whereIdentifierCondition = getIdentifierWhereClause(params) |
| 93 | const sql = safeSql`with triggers as (${TRIGGERS_SQL}) select * from triggers where ${whereIdentifierCondition};` |
| 94 | return { |
| 95 | sql, |
| 96 | zod: pgTriggerOptionalZod, |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | export const pgTriggerCreateZod = z.object({ |
| 101 | name: z.string(), |
| 102 | schema: z.string().optional().default('public'), |
| 103 | table: z.string(), |
| 104 | function_schema: z.string().optional().default('public'), |
| 105 | function_name: z.string(), |
| 106 | function_args: z.array(z.string()).optional(), |
| 107 | activation: z.enum(['BEFORE', 'AFTER', 'INSTEAD OF']), |
| 108 | events: z.array(z.string()), |
| 109 | orientation: z.enum(['ROW', 'STATEMENT']).optional(), |
| 110 | condition: z.string().optional(), |
| 111 | }) |
| 112 | |
| 113 | // Zod validates `condition` as a runtime string; the SafeSqlFragment brand is a |
| 114 | // separate compile-time trust check. A parsed string is not automatically safe — |
| 115 | // callers must promote untrusted input via acceptUntrustedSql/rawSql before it can |
| 116 | // satisfy this type. |
| 117 | export type PGTriggerCreate = Omit<z.infer<typeof pgTriggerCreateZod>, 'condition'> & { |
| 118 | condition?: SafeSqlFragment |
| 119 | } |
| 120 | |
| 121 | export function create({ |
| 122 | name, |
| 123 | schema = 'public', |
| 124 | table, |
| 125 | function_schema = 'public', |
| 126 | function_name, |
| 127 | function_args = [], |
| 128 | activation, |
| 129 | events, |
| 130 | orientation, |
| 131 | condition, |
| 132 | }: PGTriggerCreate): { |
| 133 | sql: SafeSqlFragment |
| 134 | zod: z.ZodType<void> |
| 135 | } { |
| 136 | const qualifiedTableName = safeSql`${ident(schema)}.${ident(table)}` |
| 137 | const qualifiedFunctionName = safeSql`${ident(function_schema)}.${ident(function_name)}` |
| 138 | const triggerEvents = joinSqlFragments(events.map(keyword), ' or ') |
| 139 | const triggerOrientation = orientation ? safeSql`for each ${keyword(orientation)}` : safeSql`` |
| 140 | const triggerCondition = condition ? safeSql`when (${condition})` : safeSql`` |
| 141 | const functionArgsFragment = |
| 142 | function_args.length > 0 ? joinSqlFragments(function_args.map(literal), ',') : safeSql`` |
| 143 | |
| 144 | const sql = safeSql`create trigger ${ident(name)} ${keyword(activation)} ${triggerEvents} on ${qualifiedTableName} ${triggerOrientation} ${triggerCondition} execute function ${qualifiedFunctionName}(${functionArgsFragment});` |
| 145 | |
| 146 | return { |
| 147 | sql, |
| 148 | zod: z.void(), |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | export const pgTriggerUpdateZod = z.object({ |
| 153 | name: z.string().optional(), |
| 154 | enabled_mode: z.enum(['ORIGIN', 'REPLICA', 'ALWAYS', 'DISABLED']).optional(), |
| 155 | }) |
| 156 | |
| 157 | export type PGTriggerUpdate = z.infer<typeof pgTriggerUpdateZod> |
| 158 | |
| 159 | export function update( |
| 160 | id: { name: string; schema: string; table: string }, |
| 161 | params: PGTriggerUpdate |
| 162 | ): { |
| 163 | sql: SafeSqlFragment |
| 164 | zod: z.ZodType<void> |
| 165 | } { |
| 166 | const qualifiedTableName = safeSql`${ident(id.schema)}.${ident(id.table)}` |
| 167 | |
| 168 | let enabledModeSql = safeSql`` |
| 169 | |
| 170 | switch (params.enabled_mode) { |
| 171 | case 'ORIGIN': |
| 172 | enabledModeSql = safeSql`alter table ${qualifiedTableName} enable trigger ${ident(id.name)};` |
| 173 | break |
| 174 | case 'DISABLED': |
| 175 | enabledModeSql = safeSql`alter table ${qualifiedTableName} disable trigger ${ident(id.name)};` |
| 176 | break |
| 177 | case 'REPLICA': |
| 178 | case 'ALWAYS': |
| 179 | enabledModeSql = safeSql`alter table ${qualifiedTableName} enable ${keyword(params.enabled_mode)} trigger ${ident(id.name)};` |
| 180 | break |
| 181 | default: |
| 182 | break |
| 183 | } |
| 184 | |
| 185 | const updateNameSql = |
| 186 | params.name && params.name !== id.name |
| 187 | ? safeSql`alter trigger ${ident(id.name)} on ${qualifiedTableName} rename to ${ident(params.name)};` |
| 188 | : safeSql`` |
| 189 | |
| 190 | // updateNameSql must be last |
| 191 | const sql = safeSql`begin; ${enabledModeSql}; ${updateNameSql}; commit;` |
| 192 | |
| 193 | return { |
| 194 | sql, |
| 195 | zod: z.void(), |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | export function remove( |
| 200 | id: { name: string; schema: string; table: string }, |
| 201 | { cascade = false } = {} |
| 202 | ): { |
| 203 | sql: SafeSqlFragment |
| 204 | zod: z.ZodType<void> |
| 205 | } { |
| 206 | const qualifiedTableName = safeSql`${ident(id.schema)}.${ident(id.table)}` |
| 207 | |
| 208 | const sql = safeSql`drop trigger ${ident(id.name)} on ${qualifiedTableName} ${cascade ? safeSql`cascade` : safeSql``};` |
| 209 | |
| 210 | return { |
| 211 | sql, |
| 212 | zod: z.void(), |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | export default { |
| 217 | list, |
| 218 | retrieve, |
| 219 | create, |
| 220 | update, |
| 221 | remove, |
| 222 | zod: pgTriggerZod, |
| 223 | } |