triggers.test.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { afterAll, beforeAll, expect, test } from 'vitest'
  2. import pgMeta, { safeSql } from '../src/index'
  3. import { cleanupRoot, createTestDatabase } from './db/utils'
  4. beforeAll(async () => {
  5. // Any global setup if needed
  6. })
  7. afterAll(async () => {
  8. await cleanupRoot()
  9. })
  10. const withTestDatabase = (
  11. name: string,
  12. fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
  13. ) => {
  14. test(name, async () => {
  15. const db = await createTestDatabase()
  16. try {
  17. await fn(db)
  18. } finally {
  19. await db.cleanup()
  20. }
  21. })
  22. }
  23. withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => {
  24. // Create trigger
  25. const { sql: createSql } = pgMeta.triggers.create({
  26. name: 'test_trigger',
  27. schema: 'public',
  28. table: 'users_audit',
  29. function_schema: 'public',
  30. function_name: 'audit_action',
  31. function_args: ['test1', 'test2'],
  32. activation: 'AFTER',
  33. events: ['UPDATE'],
  34. orientation: 'ROW',
  35. condition: safeSql`(old.* IS DISTINCT FROM new.*)`,
  36. })
  37. await executeQuery(createSql)
  38. const { sql: listTriggers, zod: listZod } = pgMeta.triggers.list()
  39. const listedTriggers = await listZod.parse(await executeQuery(listTriggers))
  40. const createdTriggers = listedTriggers.find(
  41. (t) => t.name === 'test_trigger' && t.table === 'users_audit' && t.schema === 'public'
  42. )
  43. expect(createdTriggers!).toMatchInlineSnapshot(
  44. { id: expect.any(Number), table_id: expect.any(Number) },
  45. `
  46. {
  47. "activation": "AFTER",
  48. "condition": "(old.* IS DISTINCT FROM new.*)",
  49. "enabled_mode": "ORIGIN",
  50. "events": [
  51. "UPDATE",
  52. ],
  53. "function_args": [
  54. "test1",
  55. "test2",
  56. ],
  57. "function_name": "audit_action",
  58. "function_schema": "public",
  59. "id": Any<Number>,
  60. "name": "test_trigger",
  61. "orientation": "ROW",
  62. "schema": "public",
  63. "table": "users_audit",
  64. "table_id": Any<Number>,
  65. }
  66. `
  67. )
  68. // Retrieve created trigger by name
  69. const { sql: retrieveSqlByName, zod: retrieveZod } = pgMeta.triggers.retrieve({
  70. name: 'test_trigger',
  71. table: 'users_audit',
  72. schema: 'public',
  73. })
  74. const trigger = retrieveZod.parse((await executeQuery(retrieveSqlByName))[0])
  75. expect(trigger!).toMatchInlineSnapshot(
  76. { id: expect.any(Number), table_id: expect.any(Number) },
  77. `
  78. {
  79. "activation": "AFTER",
  80. "condition": "(old.* IS DISTINCT FROM new.*)",
  81. "enabled_mode": "ORIGIN",
  82. "events": [
  83. "UPDATE",
  84. ],
  85. "function_args": [
  86. "test1",
  87. "test2",
  88. ],
  89. "function_name": "audit_action",
  90. "function_schema": "public",
  91. "id": Any<Number>,
  92. "name": "test_trigger",
  93. "orientation": "ROW",
  94. "schema": "public",
  95. "table": "users_audit",
  96. "table_id": Any<Number>,
  97. }
  98. `
  99. )
  100. // Retrieve created trigger by id
  101. const { sql: retrieveSqlById } = pgMeta.triggers.retrieve({
  102. id: trigger!.id,
  103. })
  104. const triggerById = retrieveZod.parse((await executeQuery(retrieveSqlById))[0])
  105. expect(triggerById!).toMatchInlineSnapshot(
  106. { id: expect.any(Number), table_id: expect.any(Number) },
  107. `
  108. {
  109. "activation": "AFTER",
  110. "condition": "(old.* IS DISTINCT FROM new.*)",
  111. "enabled_mode": "ORIGIN",
  112. "events": [
  113. "UPDATE",
  114. ],
  115. "function_args": [
  116. "test1",
  117. "test2",
  118. ],
  119. "function_name": "audit_action",
  120. "function_schema": "public",
  121. "id": Any<Number>,
  122. "name": "test_trigger",
  123. "orientation": "ROW",
  124. "schema": "public",
  125. "table": "users_audit",
  126. "table_id": Any<Number>,
  127. }
  128. `
  129. )
  130. // Update trigger
  131. const { sql: updateSql } = pgMeta.triggers.update(trigger!, {
  132. name: 'test_trigger_renamed',
  133. enabled_mode: 'DISABLED',
  134. })
  135. await executeQuery(updateSql)
  136. // Verify update
  137. const { sql: retrieveUpdatedSql } = pgMeta.triggers.retrieve({
  138. id: trigger!.id,
  139. })
  140. const updatedTrigger = retrieveZod.parse((await executeQuery(retrieveUpdatedSql))[0])
  141. expect(updatedTrigger).toMatchInlineSnapshot(
  142. { id: expect.any(Number), table_id: expect.any(Number) },
  143. `
  144. {
  145. "activation": "AFTER",
  146. "condition": "(old.* IS DISTINCT FROM new.*)",
  147. "enabled_mode": "DISABLED",
  148. "events": [
  149. "UPDATE",
  150. ],
  151. "function_args": [
  152. "test1",
  153. "test2",
  154. ],
  155. "function_name": "audit_action",
  156. "function_schema": "public",
  157. "id": Any<Number>,
  158. "name": "test_trigger_renamed",
  159. "orientation": "ROW",
  160. "schema": "public",
  161. "table": "users_audit",
  162. "table_id": Any<Number>,
  163. }
  164. `
  165. )
  166. // Update trigger again
  167. const { sql: updateSql2 } = pgMeta.triggers.update(updatedTrigger!, {
  168. enabled_mode: 'REPLICA',
  169. })
  170. await executeQuery(updateSql2)
  171. // Verify second update
  172. const { sql: retrieveUpdated2Sql } = pgMeta.triggers.retrieve({
  173. name: 'test_trigger_renamed',
  174. table: 'users_audit',
  175. schema: 'public',
  176. })
  177. const updatedTrigger2 = retrieveZod.parse((await executeQuery(retrieveUpdated2Sql))[0])
  178. expect(updatedTrigger2).toMatchObject({
  179. enabled_mode: 'REPLICA',
  180. })
  181. // Remove trigger
  182. const { sql: removeSql } = pgMeta.triggers.remove(updatedTrigger2!)
  183. await executeQuery(removeSql)
  184. // Verify removal
  185. const { sql: verifyRemoveSql } = pgMeta.triggers.retrieve({
  186. name: 'test_trigger_renamed',
  187. table: 'users_audit',
  188. schema: 'public',
  189. })
  190. const result = retrieveZod.parse((await executeQuery(verifyRemoveSql))[0])
  191. expect(result).toBeUndefined()
  192. })
  193. withTestDatabase('multi event', async ({ executeQuery }) => {
  194. // Create trigger
  195. const { sql: createSql } = pgMeta.triggers.create({
  196. name: 'test_multi_event_trigger',
  197. schema: 'public',
  198. table: 'users_audit',
  199. function_schema: 'public',
  200. function_name: 'audit_action',
  201. function_args: ['test1', 'test2'],
  202. activation: 'AFTER',
  203. events: ['insert', 'update', 'delete'],
  204. orientation: 'ROW',
  205. })
  206. await executeQuery(createSql)
  207. // Verify created trigger
  208. const { sql: retrieveSql, zod: retrieveZod } = pgMeta.triggers.retrieve({
  209. name: 'test_multi_event_trigger',
  210. table: 'users_audit',
  211. schema: 'public',
  212. })
  213. const trigger = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  214. expect(trigger).toMatchInlineSnapshot(
  215. { id: expect.any(Number), table_id: expect.any(Number) },
  216. `
  217. {
  218. "activation": "AFTER",
  219. "condition": null,
  220. "enabled_mode": "ORIGIN",
  221. "events": [
  222. "INSERT",
  223. "DELETE",
  224. "UPDATE",
  225. ],
  226. "function_args": [
  227. "test1",
  228. "test2",
  229. ],
  230. "function_name": "audit_action",
  231. "function_schema": "public",
  232. "id": Any<Number>,
  233. "name": "test_multi_event_trigger",
  234. "orientation": "ROW",
  235. "schema": "public",
  236. "table": "users_audit",
  237. "table_id": Any<Number>,
  238. }
  239. `
  240. )
  241. // Remove trigger
  242. const { sql: removeSql } = pgMeta.triggers.remove(trigger!)
  243. await executeQuery(removeSql)
  244. // Verify removal
  245. const { sql: verifyRemoveSql } = pgMeta.triggers.retrieve({
  246. name: 'test_multi_event_trigger',
  247. table: 'users_audit',
  248. schema: 'public',
  249. })
  250. const result = retrieveZod.parse((await executeQuery(verifyRemoveSql))[0])
  251. expect(result).toBeUndefined()
  252. })
  253. withTestDatabase('triggers with the same name on different schemas', async ({ executeQuery }) => {
  254. // Create test schemas and triggers
  255. await executeQuery(`
  256. create function tr_f() returns trigger language plpgsql as 'begin end';
  257. create schema s1; create table s1.t(); create trigger tr before insert on s1.t execute function tr_f();
  258. create schema s2; create table s2.t(); create trigger tr before insert on s2.t execute function tr_f();
  259. `)
  260. // List and verify triggers
  261. const { sql: listSql, zod: listZod } = pgMeta.triggers.list()
  262. const triggers = listZod.parse(await executeQuery(listSql))
  263. expect(triggers.map(({ id, table_id, ...trigger }) => trigger)).toMatchInlineSnapshot(`
  264. [
  265. {
  266. "activation": "BEFORE",
  267. "condition": null,
  268. "enabled_mode": "ORIGIN",
  269. "events": [
  270. "INSERT",
  271. ],
  272. "function_args": [],
  273. "function_name": "tr_f",
  274. "function_schema": "public",
  275. "name": "tr",
  276. "orientation": "STATEMENT",
  277. "schema": "s1",
  278. "table": "t",
  279. },
  280. {
  281. "activation": "BEFORE",
  282. "condition": null,
  283. "enabled_mode": "ORIGIN",
  284. "events": [
  285. "INSERT",
  286. ],
  287. "function_args": [],
  288. "function_name": "tr_f",
  289. "function_schema": "public",
  290. "name": "tr",
  291. "orientation": "STATEMENT",
  292. "schema": "s2",
  293. "table": "t",
  294. },
  295. ]
  296. `)
  297. })
  298. withTestDatabase('triggers on capitalized schema and table names', async ({ executeQuery }) => {
  299. // Create test schema and trigger
  300. await executeQuery(`
  301. CREATE SCHEMA "MySchema";
  302. CREATE TABLE "MySchema"."MyTable" (
  303. id SERIAL PRIMARY KEY,
  304. name TEXT NOT NULL,
  305. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  306. updated_at TIMESTAMP
  307. );
  308. CREATE OR REPLACE FUNCTION "MySchema"."my_trigger_function"()
  309. RETURNS TRIGGER AS $$
  310. BEGIN
  311. NEW.updated_at := CURRENT_TIMESTAMP;
  312. RETURN NEW;
  313. END;
  314. $$ LANGUAGE plpgsql;
  315. CREATE TRIGGER "my_trigger"
  316. BEFORE INSERT ON "MySchema"."MyTable"
  317. FOR EACH ROW
  318. EXECUTE FUNCTION "MySchema"."my_trigger_function"();
  319. `)
  320. // List and verify triggers
  321. const { sql: listSql, zod: listZod } = pgMeta.triggers.list()
  322. const triggers = listZod.parse(await executeQuery(listSql))
  323. expect(triggers.map(({ id, table_id, ...trigger }) => trigger)).toMatchInlineSnapshot(`
  324. [
  325. {
  326. "activation": "BEFORE",
  327. "condition": null,
  328. "enabled_mode": "ORIGIN",
  329. "events": [
  330. "INSERT",
  331. ],
  332. "function_args": [],
  333. "function_name": "my_trigger_function",
  334. "function_schema": "MySchema",
  335. "name": "my_trigger",
  336. "orientation": "ROW",
  337. "schema": "MySchema",
  338. "table": "MyTable",
  339. },
  340. ]
  341. `)
  342. })