From 2ea9d7a84e6d5847faa0a08ccf02b7f54f541287 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:24:41 -0500 Subject: [PATCH 01/29] Change to postgres 18 --- .github/workflows/ci.yml | 2 +- README.md | 2 +- docker-compose.yml | 2 +- internal/testutils/container.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff3d935..cc63677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: services: # Optional: We use testcontainers, but having a service is good for fallback or specific DB tests postgres: - image: postgres:16-alpine + image: postgres:18-alpine env: POSTGRES_USER: test POSTGRES_PASSWORD: test diff --git a/README.md b/README.md index 3c16334..70fb4aa 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ To run the full stack (API + Postgres + Cloudflare Tunnel), update your `.env` f ```yaml services: db: - image: postgres:16-alpine + image: postgres:18-alpine env_file: - .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index 9f5f0a7..bfaf25d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:16-alpine + image: postgres:18-alpine env_file: - .env environment: diff --git a/internal/testutils/container.go b/internal/testutils/container.go index 7cfdd0b..e2cf174 100644 --- a/internal/testutils/container.go +++ b/internal/testutils/container.go @@ -24,7 +24,7 @@ func SetupTestDB(t *testing.T) *pgxpool.Pool { schemaPath := filepath.Join(projectRoot, "schema.sql") pgContainer, err := postgres.Run(ctx, - "postgres:16-alpine", + "postgres:18-alpine", postgres.WithInitScripts(schemaPath), postgres.WithDatabase("test_db"), postgres.WithUsername("test"), From 64b200d5aa46ec369c72a95ea6e53e9cff94d685 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:26:30 -0500 Subject: [PATCH 02/29] Add link shortener and tracking --- docs/schema/schema.json | 3344 ++++++++++++++++++++-------- go.mod | 6 + go.sum | 43 + internal/database/mocks/Querier.go | 216 ++ internal/database/models.go | 15 + internal/database/querier.go | 9 + internal/database/queries.sql | 34 + internal/database/queries.sql.go | 161 ++ internal/dto/dto.go | 28 + internal/handler/links.go | 311 +++ internal/handler/links_test.go | 180 ++ internal/router/router.go | 12 + schema.sql | 23 +- tests/benchmarks/suite_test.go | 2 +- 14 files changed, 3463 insertions(+), 921 deletions(-) create mode 100644 internal/handler/links.go create mode 100644 internal/handler/links_test.go diff --git a/docs/schema/schema.json b/docs/schema/schema.json index 67eaee4..f26baa0 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -1135,85 +1135,33 @@ } ], "comment": "" - } - ], - "enums": [ - { - "name": "user_role", - "vals": [ - "student", - "alumni", - "faculty", - "external" - ], - "comment": "" - } - ], - "composite_types": [] - }, - { - "comment": "", - "name": "pg_temp", - "tables": [], - "enums": [], - "composite_types": [] - }, - { - "comment": "", - "name": "pg_catalog", - "tables": [ + }, { "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "links" }, "columns": [ { - "name": "tableoid", + "name": "lid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" - }, - "table_alias": "", - "type": { "catalog": "", "schema": "", - "name": "oid" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "cmax", - "not_null": true, - "is_array": false, - "comment": "", - "length": 4, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "name": "links" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1222,24 +1170,24 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "endpoint_url", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "links" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -1248,24 +1196,24 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "dest_url", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "links" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -1274,24 +1222,24 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "oid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "links" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1300,50 +1248,60 @@ "array_dims": 0 }, { - "name": "ctid", - "not_null": true, + "name": "created_at", + "not_null": false, "is_array": false, "comment": "", - "length": 6, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "links" }, "table_alias": "", "type": { "catalog": "", - "schema": "", - "name": "tid" + "schema": "pg_catalog", + "name": "timestamp" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "columns": [ { - "name": "aggfnoid", + "name": "lvid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "link_visits" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1352,24 +1310,24 @@ "array_dims": 0 }, { - "name": "aggkind", + "name": "lid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "link_visits" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "char" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1378,24 +1336,24 @@ "array_dims": 0 }, { - "name": "aggnumdirectargs", - "not_null": true, + "name": "uid", + "not_null": false, "is_array": false, "comment": "", - "length": 2, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "link_visits" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1404,33 +1362,69 @@ "array_dims": 0 }, { - "name": "aggtransfn", - "not_null": true, + "name": "created_at", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_aggregate" + "catalog": "", + "schema": "", + "name": "link_visits" }, "table_alias": "", "type": { "catalog": "", - "schema": "", - "name": "regproc" + "schema": "pg_catalog", + "name": "timestamp" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + } + ], + "enums": [ + { + "name": "user_role", + "vals": [ + "student", + "alumni", + "faculty", + "external" + ], + "comment": "" + } + ], + "composite_types": [] + }, + { + "comment": "", + "name": "pg_temp", + "tables": [], + "enums": [], + "composite_types": [] + }, + { + "comment": "", + "name": "pg_catalog", + "tables": [ + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_aggregate" + }, + "columns": [ { - "name": "aggfinalfn", + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -1447,7 +1441,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1456,7 +1450,7 @@ "array_dims": 0 }, { - "name": "aggcombinefn", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", @@ -1473,7 +1467,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1482,7 +1476,7 @@ "array_dims": 0 }, { - "name": "aggserialfn", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", @@ -1499,7 +1493,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1508,7 +1502,7 @@ "array_dims": 0 }, { - "name": "aggdeserialfn", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", @@ -1525,7 +1519,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1534,7 +1528,7 @@ "array_dims": 0 }, { - "name": "aggmtransfn", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", @@ -1551,7 +1545,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1560,11 +1554,11 @@ "array_dims": 0 }, { - "name": "aggminvtransfn", + "name": "ctid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 6, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1577,7 +1571,7 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "tid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1586,7 +1580,7 @@ "array_dims": 0 }, { - "name": "aggmfinalfn", + "name": "aggfnoid", "not_null": true, "is_array": false, "comment": "", @@ -1612,7 +1606,7 @@ "array_dims": 0 }, { - "name": "aggfinalextra", + "name": "aggkind", "not_null": true, "is_array": false, "comment": "", @@ -1629,7 +1623,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -1638,11 +1632,11 @@ "array_dims": 0 }, { - "name": "aggmfinalextra", + "name": "aggnumdirectargs", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 2, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1655,7 +1649,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "int2" }, "is_sqlc_slice": false, "embed_table": null, @@ -1664,11 +1658,11 @@ "array_dims": 0 }, { - "name": "aggfinalmodify", + "name": "aggtransfn", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1681,7 +1675,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1690,11 +1684,11 @@ "array_dims": 0 }, { - "name": "aggmfinalmodify", + "name": "aggfinalfn", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1707,7 +1701,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1716,7 +1710,7 @@ "array_dims": 0 }, { - "name": "aggsortop", + "name": "aggcombinefn", "not_null": true, "is_array": false, "comment": "", @@ -1733,7 +1727,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1742,7 +1736,7 @@ "array_dims": 0 }, { - "name": "aggtranstype", + "name": "aggserialfn", "not_null": true, "is_array": false, "comment": "", @@ -1759,7 +1753,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1768,7 +1762,7 @@ "array_dims": 0 }, { - "name": "aggtransspace", + "name": "aggdeserialfn", "not_null": true, "is_array": false, "comment": "", @@ -1785,7 +1779,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1794,7 +1788,7 @@ "array_dims": 0 }, { - "name": "aggmtranstype", + "name": "aggmtransfn", "not_null": true, "is_array": false, "comment": "", @@ -1811,7 +1805,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1820,7 +1814,7 @@ "array_dims": 0 }, { - "name": "aggmtransspace", + "name": "aggminvtransfn", "not_null": true, "is_array": false, "comment": "", @@ -1837,7 +1831,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1846,11 +1840,11 @@ "array_dims": 0 }, { - "name": "agginitval", - "not_null": false, + "name": "aggmfinalfn", + "not_null": true, "is_array": false, "comment": "", - "length": -1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1863,7 +1857,7 @@ "type": { "catalog": "", "schema": "", - "name": "text" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -1872,11 +1866,11 @@ "array_dims": 0 }, { - "name": "aggminitval", - "not_null": false, + "name": "aggfinalextra", + "not_null": true, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -1889,43 +1883,33 @@ "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_am" - }, - "columns": [ + }, { - "name": "tableoid", + "name": "aggmfinalextra", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -1934,24 +1918,24 @@ "array_dims": 0 }, { - "name": "cmax", + "name": "aggfinalmodify", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -1960,24 +1944,24 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "aggmfinalmodify", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -1986,7 +1970,7 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "aggsortop", "not_null": true, "is_array": false, "comment": "", @@ -1997,13 +1981,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2012,7 +1996,7 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "aggtranstype", "not_null": true, "is_array": false, "comment": "", @@ -2023,13 +2007,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2038,24 +2022,24 @@ "array_dims": 0 }, { - "name": "ctid", + "name": "aggtransspace", "not_null": true, "is_array": false, "comment": "", - "length": 6, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -2064,7 +2048,7 @@ "array_dims": 0 }, { - "name": "oid", + "name": "aggmtranstype", "not_null": true, "is_array": false, "comment": "", @@ -2075,7 +2059,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { @@ -2090,24 +2074,24 @@ "array_dims": 0 }, { - "name": "amname", + "name": "aggmtransspace", "not_null": true, "is_array": false, "comment": "", - "length": 64, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "name" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -2116,24 +2100,24 @@ "array_dims": 0 }, { - "name": "amhandler", - "not_null": true, + "name": "agginitval", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -2142,24 +2126,24 @@ "array_dims": 0 }, { - "name": "amtype", - "not_null": true, + "name": "aggminitval", + "not_null": false, "is_array": false, "comment": "", - "length": 1, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_am" + "name": "pg_aggregate" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "char" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -2174,7 +2158,7 @@ "rel": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "columns": [ { @@ -2189,7 +2173,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2215,7 +2199,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2241,7 +2225,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2267,7 +2251,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2293,7 +2277,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2319,7 +2303,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2345,7 +2329,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { @@ -2360,7 +2344,33 @@ "array_dims": 0 }, { - "name": "amopfamily", + "name": "amname", + "not_null": true, + "is_array": false, + "comment": "", + "length": 64, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_am" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "name" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "amhandler", "not_null": true, "is_array": false, "comment": "", @@ -2371,13 +2381,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amop" + "name": "pg_am" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, @@ -2386,7 +2396,43 @@ "array_dims": 0 }, { - "name": "amoplefttype", + "name": "amtype", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_am" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "char" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_amop" + }, + "columns": [ + { + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -2412,7 +2458,7 @@ "array_dims": 0 }, { - "name": "amoprighttype", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", @@ -2429,7 +2475,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2438,11 +2484,11 @@ "array_dims": 0 }, { - "name": "amopstrategy", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", - "length": 2, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -2455,7 +2501,7 @@ "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2464,11 +2510,11 @@ "array_dims": 0 }, { - "name": "amoppurpose", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -2481,7 +2527,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2490,7 +2536,7 @@ "array_dims": 0 }, { - "name": "amopopr", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", @@ -2507,7 +2553,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2516,7 +2562,33 @@ "array_dims": 0 }, { - "name": "amopmethod", + "name": "ctid", + "not_null": true, + "is_array": false, + "comment": "", + "length": 6, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_amop" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "tid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", "not_null": true, "is_array": false, "comment": "", @@ -2542,7 +2614,7 @@ "array_dims": 0 }, { - "name": "amopsortfamily", + "name": "amopfamily", "not_null": true, "is_array": false, "comment": "", @@ -2566,19 +2638,9 @@ "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_amproc" - }, - "columns": [ + }, { - "name": "tableoid", + "name": "amoplefttype", "not_null": true, "is_array": false, "comment": "", @@ -2589,7 +2651,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { @@ -2604,7 +2666,7 @@ "array_dims": 0 }, { - "name": "cmax", + "name": "amoprighttype", "not_null": true, "is_array": false, "comment": "", @@ -2615,13 +2677,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2630,24 +2692,24 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "amopstrategy", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 2, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "int2" }, "is_sqlc_slice": false, "embed_table": null, @@ -2656,24 +2718,24 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "amoppurpose", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -2682,7 +2744,7 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "amopopr", "not_null": true, "is_array": false, "comment": "", @@ -2693,13 +2755,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2708,24 +2770,24 @@ "array_dims": 0 }, { - "name": "ctid", + "name": "amopmethod", "not_null": true, "is_array": false, "comment": "", - "length": 6, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2734,7 +2796,7 @@ "array_dims": 0 }, { - "name": "oid", + "name": "amopsortfamily", "not_null": true, "is_array": false, "comment": "", @@ -2745,7 +2807,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_amproc" + "name": "pg_amop" }, "table_alias": "", "type": { @@ -2758,9 +2820,19 @@ "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_amproc" + }, + "columns": [ { - "name": "amprocfamily", + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -2786,7 +2858,7 @@ "array_dims": 0 }, { - "name": "amproclefttype", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", @@ -2803,7 +2875,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2812,7 +2884,7 @@ "array_dims": 0 }, { - "name": "amprocrighttype", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", @@ -2829,7 +2901,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2838,11 +2910,11 @@ "array_dims": 0 }, { - "name": "amprocnum", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", - "length": 2, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -2855,7 +2927,7 @@ "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2864,7 +2936,7 @@ "array_dims": 0 }, { - "name": "amproc", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", @@ -2881,43 +2953,33 @@ "type": { "catalog": "", "schema": "", - "name": "regproc" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attrdef" - }, - "columns": [ + }, { - "name": "tableoid", + "name": "ctid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 6, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "tid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2926,7 +2988,7 @@ "array_dims": 0 }, { - "name": "cmax", + "name": "oid", "not_null": true, "is_array": false, "comment": "", @@ -2937,13 +2999,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2952,7 +3014,7 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "amprocfamily", "not_null": true, "is_array": false, "comment": "", @@ -2963,13 +3025,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -2978,7 +3040,7 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "amproclefttype", "not_null": true, "is_array": false, "comment": "", @@ -2989,13 +3051,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3004,7 +3066,7 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "amprocrighttype", "not_null": true, "is_array": false, "comment": "", @@ -3015,13 +3077,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3030,24 +3092,24 @@ "array_dims": 0 }, { - "name": "ctid", + "name": "amprocnum", "not_null": true, "is_array": false, "comment": "", - "length": 6, + "length": 2, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "int2" }, "is_sqlc_slice": false, "embed_table": null, @@ -3056,7 +3118,7 @@ "array_dims": 0 }, { - "name": "oid", + "name": "amproc", "not_null": true, "is_array": false, "comment": "", @@ -3067,22 +3129,32 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attrdef" + "name": "pg_amproc" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "regproc" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_attrdef" + }, + "columns": [ { - "name": "adrelid", + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -3107,94 +3179,6 @@ "unsigned": false, "array_dims": 0 }, - { - "name": "adnum", - "not_null": true, - "is_array": false, - "comment": "", - "length": 2, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attrdef" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "int2" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "adbin", - "not_null": true, - "is_array": false, - "comment": "", - "length": -1, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attrdef" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "pg_node_tree" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attribute" - }, - "columns": [ - { - "name": "tableoid", - "not_null": true, - "is_array": false, - "comment": "", - "length": 4, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attribute" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "oid" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, { "name": "cmax", "not_null": true, @@ -3207,7 +3191,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3233,7 +3217,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3259,7 +3243,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3285,7 +3269,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3311,7 +3295,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3326,7 +3310,7 @@ "array_dims": 0 }, { - "name": "attrelid", + "name": "oid", "not_null": true, "is_array": false, "comment": "", @@ -3337,7 +3321,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3352,33 +3336,7 @@ "array_dims": 0 }, { - "name": "attname", - "not_null": true, - "is_array": false, - "comment": "", - "length": 64, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attribute" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "name" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "atttypid", + "name": "adrelid", "not_null": true, "is_array": false, "comment": "", @@ -3389,7 +3347,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3404,33 +3362,7 @@ "array_dims": 0 }, { - "name": "attstattarget", - "not_null": true, - "is_array": false, - "comment": "", - "length": 4, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_attribute" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "int4" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "attlen", + "name": "adnum", "not_null": true, "is_array": false, "comment": "", @@ -3441,7 +3373,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { @@ -3456,33 +3388,43 @@ "array_dims": 0 }, { - "name": "attnum", + "name": "adbin", "not_null": true, "is_array": false, "comment": "", - "length": 2, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_attribute" + "name": "pg_attrdef" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "pg_node_tree" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_attribute" + }, + "columns": [ { - "name": "attndims", + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -3499,7 +3441,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3508,7 +3450,7 @@ "array_dims": 0 }, { - "name": "attcacheoff", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", @@ -3525,7 +3467,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3534,7 +3476,7 @@ "array_dims": 0 }, { - "name": "atttypmod", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", @@ -3551,7 +3493,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3560,11 +3502,11 @@ "array_dims": 0 }, { - "name": "attbyval", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3577,7 +3519,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3586,11 +3528,11 @@ "array_dims": 0 }, { - "name": "attalign", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3603,7 +3545,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3612,11 +3554,11 @@ "array_dims": 0 }, { - "name": "attstorage", + "name": "ctid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 6, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3629,7 +3571,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "tid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3638,11 +3580,11 @@ "array_dims": 0 }, { - "name": "attcompression", + "name": "attrelid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3655,7 +3597,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3664,11 +3606,11 @@ "array_dims": 0 }, { - "name": "attnotnull", + "name": "attname", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 64, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3681,7 +3623,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "name" }, "is_sqlc_slice": false, "embed_table": null, @@ -3690,11 +3632,11 @@ "array_dims": 0 }, { - "name": "atthasdef", + "name": "atttypid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3707,7 +3649,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -3716,11 +3658,11 @@ "array_dims": 0 }, { - "name": "atthasmissing", + "name": "attstattarget", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3733,7 +3675,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -3742,11 +3684,11 @@ "array_dims": 0 }, { - "name": "attidentity", + "name": "attlen", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 2, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3759,7 +3701,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "int2" }, "is_sqlc_slice": false, "embed_table": null, @@ -3768,11 +3710,11 @@ "array_dims": 0 }, { - "name": "attgenerated", + "name": "attnum", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 2, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3785,7 +3727,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "int2" }, "is_sqlc_slice": false, "embed_table": null, @@ -3794,11 +3736,11 @@ "array_dims": 0 }, { - "name": "attisdropped", + "name": "attndims", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3811,7 +3753,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -3820,11 +3762,11 @@ "array_dims": 0 }, { - "name": "attislocal", + "name": "attcacheoff", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3837,7 +3779,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -3846,7 +3788,7 @@ "array_dims": 0 }, { - "name": "attinhcount", + "name": "atttypmod", "not_null": true, "is_array": false, "comment": "", @@ -3872,11 +3814,11 @@ "array_dims": 0 }, { - "name": "attcollation", + "name": "attbyval", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3889,7 +3831,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -3898,11 +3840,11 @@ "array_dims": 0 }, { - "name": "attacl", - "not_null": false, - "is_array": true, + "name": "attalign", + "not_null": true, + "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3915,7 +3857,7 @@ "type": { "catalog": "", "schema": "", - "name": "_aclitem" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -3924,11 +3866,11 @@ "array_dims": 0 }, { - "name": "attoptions", - "not_null": false, - "is_array": true, + "name": "attstorage", + "not_null": true, + "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3941,7 +3883,7 @@ "type": { "catalog": "", "schema": "", - "name": "_text" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -3950,11 +3892,11 @@ "array_dims": 0 }, { - "name": "attfdwoptions", - "not_null": false, - "is_array": true, + "name": "attcompression", + "not_null": true, + "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3967,7 +3909,7 @@ "type": { "catalog": "", "schema": "", - "name": "_text" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -3976,11 +3918,11 @@ "array_dims": 0 }, { - "name": "attmissingval", - "not_null": false, + "name": "attnotnull", + "not_null": true, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -3993,43 +3935,33 @@ "type": { "catalog": "", "schema": "", - "name": "anyarray" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_auth_members" - }, - "columns": [ + }, { - "name": "tableoid", + "name": "atthasdef", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4038,24 +3970,24 @@ "array_dims": 0 }, { - "name": "cmax", + "name": "atthasmissing", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4064,24 +3996,24 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "attidentity", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -4090,24 +4022,24 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "attgenerated", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "char" }, "is_sqlc_slice": false, "embed_table": null, @@ -4116,24 +4048,24 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "attisdropped", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4142,24 +4074,24 @@ "array_dims": 0 }, { - "name": "ctid", + "name": "attislocal", "not_null": true, "is_array": false, "comment": "", - "length": 6, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4168,7 +4100,7 @@ "array_dims": 0 }, { - "name": "roleid", + "name": "attinhcount", "not_null": true, "is_array": false, "comment": "", @@ -4179,13 +4111,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -4194,7 +4126,7 @@ "array_dims": 0 }, { - "name": "member", + "name": "attcollation", "not_null": true, "is_array": false, "comment": "", @@ -4205,7 +4137,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { @@ -4220,24 +4152,24 @@ "array_dims": 0 }, { - "name": "grantor", - "not_null": true, - "is_array": false, + "name": "attacl", + "not_null": false, + "is_array": true, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "_aclitem" }, "is_sqlc_slice": false, "embed_table": null, @@ -4246,24 +4178,76 @@ "array_dims": 0 }, { - "name": "admin_option", - "not_null": true, + "name": "attoptions", + "not_null": false, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_attribute" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "_text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "attfdwoptions", + "not_null": false, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_attribute" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "_text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "attmissingval", + "not_null": false, "is_array": false, "comment": "", - "length": 1, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_auth_members" + "name": "pg_attribute" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "anyarray" }, "is_sqlc_slice": false, "embed_table": null, @@ -4278,7 +4262,7 @@ "rel": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "columns": [ { @@ -4293,7 +4277,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4319,7 +4303,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4345,7 +4329,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4371,7 +4355,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4397,7 +4381,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4423,7 +4407,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_authid" + "name": "pg_auth_members" }, "table_alias": "", "type": { @@ -4438,7 +4422,121 @@ "array_dims": 0 }, { - "name": "oid", + "name": "roleid", + "not_null": true, + "is_array": false, + "comment": "", + "length": 4, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_auth_members" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "oid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "member", + "not_null": true, + "is_array": false, + "comment": "", + "length": 4, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_auth_members" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "oid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "grantor", + "not_null": true, + "is_array": false, + "comment": "", + "length": 4, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_auth_members" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "oid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "admin_option", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_auth_members" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "bool" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_authid" + }, + "columns": [ + { + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -4464,11 +4562,11 @@ "array_dims": 0 }, { - "name": "rolname", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", - "length": 64, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4481,7 +4579,7 @@ "type": { "catalog": "", "schema": "", - "name": "name" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4490,11 +4588,11 @@ "array_dims": 0 }, { - "name": "rolsuper", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4507,7 +4605,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4516,11 +4614,11 @@ "array_dims": 0 }, { - "name": "rolinherit", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4533,7 +4631,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4542,11 +4640,11 @@ "array_dims": 0 }, { - "name": "rolcreaterole", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4559,7 +4657,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4568,11 +4666,11 @@ "array_dims": 0 }, { - "name": "rolcreatedb", + "name": "ctid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 6, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4585,7 +4683,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "tid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4594,11 +4692,11 @@ "array_dims": 0 }, { - "name": "rolcanlogin", + "name": "oid", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4611,7 +4709,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -4620,11 +4718,11 @@ "array_dims": 0 }, { - "name": "rolreplication", + "name": "rolname", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 64, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4637,7 +4735,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "name" }, "is_sqlc_slice": false, "embed_table": null, @@ -4646,7 +4744,7 @@ "array_dims": 0 }, { - "name": "rolbypassrls", + "name": "rolsuper", "not_null": true, "is_array": false, "comment": "", @@ -4672,37 +4770,11 @@ "array_dims": 0 }, { - "name": "rolconnlimit", + "name": "rolinherit", "not_null": true, "is_array": false, "comment": "", - "length": 4, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_authid" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "int4" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "rolpassword", - "not_null": false, - "is_array": false, - "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4715,7 +4787,7 @@ "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4724,11 +4796,11 @@ "array_dims": 0 }, { - "name": "rolvaliduntil", - "not_null": false, + "name": "rolcreaterole", + "not_null": true, "is_array": false, "comment": "", - "length": 8, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -4741,69 +4813,7 @@ "type": { "catalog": "", "schema": "", - "name": "timestamptz" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_available_extension_versions" - }, - "columns": [ - { - "name": "name", - "not_null": false, - "is_array": false, - "comment": "", - "length": 64, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_available_extension_versions" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "name" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "", - "unsigned": false, - "array_dims": 0 - }, - { - "name": "version", - "not_null": false, - "is_array": false, - "comment": "", - "length": -1, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_available_extension_versions" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -4812,8 +4822,8 @@ "array_dims": 0 }, { - "name": "installed", - "not_null": false, + "name": "rolcreatedb", + "not_null": true, "is_array": false, "comment": "", "length": 1, @@ -4823,7 +4833,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { @@ -4838,8 +4848,8 @@ "array_dims": 0 }, { - "name": "superuser", - "not_null": false, + "name": "rolcanlogin", + "not_null": true, "is_array": false, "comment": "", "length": 1, @@ -4849,7 +4859,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { @@ -4864,8 +4874,8 @@ "array_dims": 0 }, { - "name": "trusted", - "not_null": false, + "name": "rolreplication", + "not_null": true, "is_array": false, "comment": "", "length": 1, @@ -4875,7 +4885,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { @@ -4890,8 +4900,8 @@ "array_dims": 0 }, { - "name": "relocatable", - "not_null": false, + "name": "rolbypassrls", + "not_null": true, "is_array": false, "comment": "", "length": 1, @@ -4901,7 +4911,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { @@ -4916,24 +4926,24 @@ "array_dims": 0 }, { - "name": "schema", - "not_null": false, + "name": "rolconnlimit", + "not_null": true, "is_array": false, "comment": "", - "length": 64, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "name" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -4942,9 +4952,9 @@ "array_dims": 0 }, { - "name": "requires", + "name": "rolpassword", "not_null": false, - "is_array": true, + "is_array": false, "comment": "", "length": -1, "is_named_param": false, @@ -4953,13 +4963,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "_name" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -4968,24 +4978,24 @@ "array_dims": 0 }, { - "name": "comment", + "name": "rolvaliduntil", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extension_versions" + "name": "pg_authid" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "timestamptz" }, "is_sqlc_slice": false, "embed_table": null, @@ -5000,7 +5010,7 @@ "rel": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extensions" + "name": "pg_available_extension_versions" }, "columns": [ { @@ -5015,7 +5025,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extensions" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { @@ -5030,7 +5040,7 @@ "array_dims": 0 }, { - "name": "default_version", + "name": "version", "not_null": false, "is_array": false, "comment": "", @@ -5041,7 +5051,7 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extensions" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { @@ -5056,24 +5066,24 @@ "array_dims": 0 }, { - "name": "installed_version", + "name": "installed", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extensions" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -5082,60 +5092,50 @@ "array_dims": 0 }, { - "name": "comment", + "name": "superuser", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_available_extensions" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" - }, - "columns": [ + }, { - "name": "name", + "name": "trusted", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -5144,24 +5144,24 @@ "array_dims": 0 }, { - "name": "ident", + "name": "relocatable", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -5170,24 +5170,24 @@ "array_dims": 0 }, { - "name": "parent", + "name": "schema", "not_null": false, "is_array": false, "comment": "", - "length": -1, + "length": 64, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "name" }, "is_sqlc_slice": false, "embed_table": null, @@ -5196,24 +5196,24 @@ "array_dims": 0 }, { - "name": "level", + "name": "requires", "not_null": false, - "is_array": false, + "is_array": true, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "_name" }, "is_sqlc_slice": false, "embed_table": null, @@ -5222,50 +5222,60 @@ "array_dims": 0 }, { - "name": "total_bytes", + "name": "comment", "not_null": false, "is_array": false, "comment": "", - "length": 8, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extension_versions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int8" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_available_extensions" + }, + "columns": [ { - "name": "total_nblocks", + "name": "name", "not_null": false, "is_array": false, "comment": "", - "length": 8, + "length": 64, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extensions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int8" + "name": "name" }, "is_sqlc_slice": false, "embed_table": null, @@ -5274,24 +5284,24 @@ "array_dims": 0 }, { - "name": "free_bytes", + "name": "default_version", "not_null": false, "is_array": false, "comment": "", - "length": 8, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extensions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int8" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5300,24 +5310,24 @@ "array_dims": 0 }, { - "name": "free_chunks", + "name": "installed_version", "not_null": false, "is_array": false, "comment": "", - "length": 8, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extensions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int8" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5326,24 +5336,24 @@ "array_dims": 0 }, { - "name": "used_bytes", + "name": "comment", "not_null": false, "is_array": false, "comment": "", - "length": 8, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_backend_memory_contexts" + "name": "pg_available_extensions" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "int8" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5358,28 +5368,28 @@ "rel": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "columns": [ { - "name": "tableoid", - "not_null": true, + "name": "name", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5388,24 +5398,24 @@ "array_dims": 0 }, { - "name": "cmax", - "not_null": true, + "name": "ident", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5414,24 +5424,24 @@ "array_dims": 0 }, { - "name": "xmax", - "not_null": true, + "name": "parent", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": -1, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -5440,8 +5450,8 @@ "array_dims": 0 }, { - "name": "cmin", - "not_null": true, + "name": "level", + "not_null": false, "is_array": false, "comment": "", "length": 4, @@ -5451,13 +5461,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -5466,24 +5476,24 @@ "array_dims": 0 }, { - "name": "xmin", - "not_null": true, + "name": "total_bytes", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "int8" }, "is_sqlc_slice": false, "embed_table": null, @@ -5492,24 +5502,24 @@ "array_dims": 0 }, { - "name": "ctid", - "not_null": true, + "name": "total_nblocks", + "not_null": false, "is_array": false, "comment": "", - "length": 6, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "int8" }, "is_sqlc_slice": false, "embed_table": null, @@ -5518,24 +5528,24 @@ "array_dims": 0 }, { - "name": "oid", - "not_null": true, + "name": "free_bytes", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "int8" }, "is_sqlc_slice": false, "embed_table": null, @@ -5544,24 +5554,24 @@ "array_dims": 0 }, { - "name": "castsource", - "not_null": true, + "name": "free_chunks", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "int8" }, "is_sqlc_slice": false, "embed_table": null, @@ -5570,33 +5580,43 @@ "array_dims": 0 }, { - "name": "casttarget", - "not_null": true, + "name": "used_bytes", + "not_null": false, "is_array": false, "comment": "", - "length": 4, + "length": 8, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_cast" + "name": "pg_backend_memory_contexts" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "int8" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - }, + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_cast" + }, + "columns": [ { - "name": "castfunc", + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -5622,11 +5642,11 @@ "array_dims": 0 }, { - "name": "castcontext", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -5639,7 +5659,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5648,11 +5668,11 @@ "array_dims": 0 }, { - "name": "castmethod", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -5665,26 +5685,16 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 - } - ], - "comment": "" - }, - { - "rel": { - "catalog": "pg_catalog", - "schema": "pg_catalog", - "name": "pg_class" - }, - "columns": [ + }, { - "name": "tableoid", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", @@ -5695,13 +5705,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5710,7 +5720,7 @@ "array_dims": 0 }, { - "name": "cmax", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", @@ -5721,13 +5731,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5736,7 +5746,33 @@ "array_dims": 0 }, { - "name": "xmax", + "name": "ctid", + "not_null": true, + "is_array": false, + "comment": "", + "length": 6, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_cast" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "tid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", "not_null": true, "is_array": false, "comment": "", @@ -5747,13 +5783,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5762,7 +5798,7 @@ "array_dims": 0 }, { - "name": "cmin", + "name": "castsource", "not_null": true, "is_array": false, "comment": "", @@ -5773,13 +5809,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "cid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5788,7 +5824,7 @@ "array_dims": 0 }, { - "name": "xmin", + "name": "casttarget", "not_null": true, "is_array": false, "comment": "", @@ -5799,13 +5835,13 @@ "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "xid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5814,24 +5850,24 @@ "array_dims": 0 }, { - "name": "ctid", + "name": "castfunc", "not_null": true, "is_array": false, "comment": "", - "length": 6, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", "table": { "catalog": "pg_catalog", "schema": "pg_catalog", - "name": "pg_class" + "name": "pg_cast" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "tid" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5840,7 +5876,69 @@ "array_dims": 0 }, { - "name": "oid", + "name": "castcontext", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_cast" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "char" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "castmethod", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_cast" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "char" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "columns": [ + { + "name": "tableoid", "not_null": true, "is_array": false, "comment": "", @@ -5866,11 +5964,11 @@ "array_dims": 0 }, { - "name": "relname", + "name": "cmax", "not_null": true, "is_array": false, "comment": "", - "length": 64, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -5883,7 +5981,7 @@ "type": { "catalog": "", "schema": "", - "name": "name" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5892,7 +5990,7 @@ "array_dims": 0 }, { - "name": "relnamespace", + "name": "xmax", "not_null": true, "is_array": false, "comment": "", @@ -5909,7 +6007,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5918,7 +6016,7 @@ "array_dims": 0 }, { - "name": "reltype", + "name": "cmin", "not_null": true, "is_array": false, "comment": "", @@ -5935,7 +6033,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "cid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5944,7 +6042,7 @@ "array_dims": 0 }, { - "name": "reloftype", + "name": "xmin", "not_null": true, "is_array": false, "comment": "", @@ -5961,7 +6059,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "xid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5970,11 +6068,11 @@ "array_dims": 0 }, { - "name": "relowner", + "name": "ctid", "not_null": true, "is_array": false, "comment": "", - "length": 4, + "length": 6, "is_named_param": false, "is_func_call": false, "scope": "", @@ -5987,7 +6085,7 @@ "type": { "catalog": "", "schema": "", - "name": "oid" + "name": "tid" }, "is_sqlc_slice": false, "embed_table": null, @@ -5996,7 +6094,7 @@ "array_dims": 0 }, { - "name": "relam", + "name": "oid", "not_null": true, "is_array": false, "comment": "", @@ -6022,7 +6120,33 @@ "array_dims": 0 }, { - "name": "relfilenode", + "name": "relname", + "not_null": true, + "is_array": false, + "comment": "", + "length": 64, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "name" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relnamespace", "not_null": true, "is_array": false, "comment": "", @@ -6048,7 +6172,7 @@ "array_dims": 0 }, { - "name": "reltablespace", + "name": "reltype", "not_null": true, "is_array": false, "comment": "", @@ -6074,7 +6198,7 @@ "array_dims": 0 }, { - "name": "relpages", + "name": "reloftype", "not_null": true, "is_array": false, "comment": "", @@ -6091,7 +6215,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -6100,7 +6224,7 @@ "array_dims": 0 }, { - "name": "reltuples", + "name": "relowner", "not_null": true, "is_array": false, "comment": "", @@ -6117,7 +6241,7 @@ "type": { "catalog": "", "schema": "", - "name": "float4" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -6126,7 +6250,7 @@ "array_dims": 0 }, { - "name": "relallvisible", + "name": "relam", "not_null": true, "is_array": false, "comment": "", @@ -6143,7 +6267,7 @@ "type": { "catalog": "", "schema": "", - "name": "int4" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -6152,7 +6276,7 @@ "array_dims": 0 }, { - "name": "reltoastrelid", + "name": "relfilenode", "not_null": true, "is_array": false, "comment": "", @@ -6178,11 +6302,11 @@ "array_dims": 0 }, { - "name": "relhasindex", + "name": "reltablespace", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6195,7 +6319,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -6204,11 +6328,11 @@ "array_dims": 0 }, { - "name": "relisshared", + "name": "relpages", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6221,7 +6345,7 @@ "type": { "catalog": "", "schema": "", - "name": "bool" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -6230,11 +6354,11 @@ "array_dims": 0 }, { - "name": "relpersistence", + "name": "reltuples", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6247,7 +6371,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "float4" }, "is_sqlc_slice": false, "embed_table": null, @@ -6256,11 +6380,11 @@ "array_dims": 0 }, { - "name": "relkind", + "name": "relallvisible", "not_null": true, "is_array": false, "comment": "", - "length": 1, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6273,7 +6397,7 @@ "type": { "catalog": "", "schema": "", - "name": "char" + "name": "int4" }, "is_sqlc_slice": false, "embed_table": null, @@ -6282,11 +6406,11 @@ "array_dims": 0 }, { - "name": "relnatts", + "name": "reltoastrelid", "not_null": true, "is_array": false, "comment": "", - "length": 2, + "length": 4, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6299,7 +6423,7 @@ "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "oid" }, "is_sqlc_slice": false, "embed_table": null, @@ -6308,11 +6432,11 @@ "array_dims": 0 }, { - "name": "relchecks", + "name": "relhasindex", "not_null": true, "is_array": false, "comment": "", - "length": 2, + "length": 1, "is_named_param": false, "is_func_call": false, "scope": "", @@ -6325,7 +6449,7 @@ "type": { "catalog": "", "schema": "", - "name": "int2" + "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, @@ -6334,7 +6458,137 @@ "array_dims": 0 }, { - "name": "relhasrules", + "name": "relisshared", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "bool" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relpersistence", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "char" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relkind", + "not_null": true, + "is_array": false, + "comment": "", + "length": 1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "char" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relnatts", + "not_null": true, + "is_array": false, + "comment": "", + "length": 2, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "int2" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relchecks", + "not_null": true, + "is_array": false, + "comment": "", + "length": 2, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "pg_catalog", + "schema": "pg_catalog", + "name": "pg_class" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "int2" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "relhasrules", "not_null": true, "is_array": false, "comment": "", @@ -72561,6 +72815,1264 @@ "comments": [], "filename": "queries.sql", "insert_into_table": null + }, + { + "text": "\nINSERT INTO links (endpoint_url, dest_url, oid)\nVALUES ($1, $2, $3)\nRETURNING lid, endpoint_url, dest_url, oid, created_at", + "name": "CreateLink", + "cmd": ":one", + "columns": [ + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 2, + "column": { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 3, + "column": { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [ + " Link Queries" + ], + "filename": "queries.sql", + "insert_into_table": { + "catalog": "", + "schema": "", + "name": "links" + } + }, + { + "text": "SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE lid = $1", + "name": "GetLinkByLID", + "cmd": ":one", + "columns": [ + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null + }, + { + "text": "SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE endpoint_url = $1", + "name": "GetLinkByEndpointURL", + "cmd": ":one", + "columns": [ + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null + }, + { + "text": "UPDATE links\nSET endpoint_url = COALESCE($2, endpoint_url),\n dest_url = COALESCE($3, dest_url)\nWHERE lid = $1\nRETURNING lid, endpoint_url, dest_url, oid, created_at", + "name": "UpdateLink", + "cmd": ":one", + "columns": [ + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 2, + "column": { + "name": "endpoint_url", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": true, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 3, + "column": { + "name": "dest_url", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": true, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null + }, + { + "text": "DELETE FROM links WHERE lid = $1", + "name": "DeleteLink", + "cmd": ":exec", + "columns": [], + "params": [ + { + "number": 1, + "column": { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null + }, + { + "text": "SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE oid = $1 ORDER BY created_at DESC", + "name": "ListLinksByOrg", + "cmd": ":many", + "columns": [ + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "endpoint_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "dest_url", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "oid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null + }, + { + "text": "INSERT INTO link_visits (lid, uid)\nVALUES ($1, $2)\nRETURNING lvid, lid, uid, created_at", + "name": "LogLinkVisit", + "cmd": ":one", + "columns": [ + { + "name": "lvid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lvid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "uid", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "uid", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "created_at", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 2, + "column": { + "name": "uid", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "uid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": { + "catalog": "", + "schema": "", + "name": "link_visits" + } + }, + { + "text": "SELECT COUNT(*) FROM link_visits WHERE lid = $1", + "name": "GetTotalVisits", + "cmd": ":one", + "columns": [ + { + "name": "count", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": true, + "scope": "", + "table": null, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "bigint" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "lid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "lid", + "unsigned": false, + "array_dims": 0 + } + } + ], + "comments": [], + "filename": "queries.sql", + "insert_into_table": null } ], "sqlc_version": "v1.30.0", diff --git a/go.mod b/go.mod index 1473260..c502022 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( github.com/swaggo/swag v1.16.6 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 + github.com/yeqown/go-qrcode/v2 v2.2.5 + github.com/yeqown/go-qrcode/writer/standard v1.3.0 golang.org/x/crypto v0.47.0 golang.org/x/oauth2 v0.34.0 ) @@ -38,6 +40,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fogleman/gg v1.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect @@ -45,6 +48,7 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.19.15 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -73,6 +77,7 @@ require ( github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect @@ -80,6 +85,7 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/image v0.10.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 28cb530..08e6c84 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0o github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -64,6 +66,8 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -164,6 +168,13 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk= +github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw= +github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34= +github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ= +github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= +github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -184,32 +195,64 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M= +golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= diff --git a/internal/database/mocks/Querier.go b/internal/database/mocks/Querier.go index f8822b0..6ac680c 100644 --- a/internal/database/mocks/Querier.go +++ b/internal/database/mocks/Querier.go @@ -110,6 +110,34 @@ func (_m *Querier) CreateEvent(ctx context.Context, arg database.CreateEventPara return r0, r1 } +// CreateLink provides a mock function with given fields: ctx, arg +func (_m *Querier) CreateLink(ctx context.Context, arg database.CreateLinkParams) (database.Link, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for CreateLink") + } + + var r0 database.Link + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.CreateLinkParams) (database.Link, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.CreateLinkParams) database.Link); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(database.Link) + } + + if rf, ok := ret.Get(1).(func(context.Context, database.CreateLinkParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateOrganization provides a mock function with given fields: ctx, name func (_m *Querier) CreateOrganization(ctx context.Context, name string) (database.Organization, error) { ret := _m.Called(ctx, name) @@ -184,6 +212,24 @@ func (_m *Querier) DeleteEvent(ctx context.Context, eid uuid.UUID) error { return r0 } +// DeleteLink provides a mock function with given fields: ctx, lid +func (_m *Querier) DeleteLink(ctx context.Context, lid uuid.UUID) error { + ret := _m.Called(ctx, lid) + + if len(ret) == 0 { + panic("no return value specified for DeleteLink") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok { + r0 = rf(ctx, lid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteOrganization provides a mock function with given fields: ctx, oid func (_m *Querier) DeleteOrganization(ctx context.Context, oid uuid.UUID) error { ret := _m.Called(ctx, oid) @@ -306,6 +352,62 @@ func (_m *Querier) GetEventRegistrations(ctx context.Context, eid uuid.UUID) ([] return r0, r1 } +// GetLinkByEndpointURL provides a mock function with given fields: ctx, endpointUrl +func (_m *Querier) GetLinkByEndpointURL(ctx context.Context, endpointUrl string) (database.Link, error) { + ret := _m.Called(ctx, endpointUrl) + + if len(ret) == 0 { + panic("no return value specified for GetLinkByEndpointURL") + } + + var r0 database.Link + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (database.Link, error)); ok { + return rf(ctx, endpointUrl) + } + if rf, ok := ret.Get(0).(func(context.Context, string) database.Link); ok { + r0 = rf(ctx, endpointUrl) + } else { + r0 = ret.Get(0).(database.Link) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, endpointUrl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLinkByLID provides a mock function with given fields: ctx, lid +func (_m *Querier) GetLinkByLID(ctx context.Context, lid uuid.UUID) (database.Link, error) { + ret := _m.Called(ctx, lid) + + if len(ret) == 0 { + panic("no return value specified for GetLinkByLID") + } + + var r0 database.Link + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.Link, error)); ok { + return rf(ctx, lid) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.Link); ok { + r0 = rf(ctx, lid) + } else { + r0 = ret.Get(0).(database.Link) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, lid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetOrgMembers provides a mock function with given fields: ctx, oid func (_m *Querier) GetOrgMembers(ctx context.Context, oid uuid.UUID) ([]database.GetOrgMembersRow, error) { ret := _m.Called(ctx, oid) @@ -364,6 +466,34 @@ func (_m *Querier) GetOrganizationByID(ctx context.Context, oid uuid.UUID) (data return r0, r1 } +// GetTotalVisits provides a mock function with given fields: ctx, lid +func (_m *Querier) GetTotalVisits(ctx context.Context, lid uuid.UUID) (int64, error) { + ret := _m.Called(ctx, lid) + + if len(ret) == 0 { + panic("no return value specified for GetTotalVisits") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (int64, error)); ok { + return rf(ctx, lid) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) int64); ok { + r0 = rf(ctx, lid) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, lid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetUserByEmail provides a mock function with given fields: ctx, personalEmail func (_m *Querier) GetUserByEmail(ctx context.Context, personalEmail pgtype.Text) (database.User, error) { ret := _m.Called(ctx, personalEmail) @@ -626,6 +756,36 @@ func (_m *Querier) ListEventsByOrg(ctx context.Context, arg database.ListEventsB return r0, r1 } +// ListLinksByOrg provides a mock function with given fields: ctx, oid +func (_m *Querier) ListLinksByOrg(ctx context.Context, oid uuid.UUID) ([]database.Link, error) { + ret := _m.Called(ctx, oid) + + if len(ret) == 0 { + panic("no return value specified for ListLinksByOrg") + } + + var r0 []database.Link + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]database.Link, error)); ok { + return rf(ctx, oid) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []database.Link); ok { + r0 = rf(ctx, oid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.Link) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, oid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListOrganizations provides a mock function with given fields: ctx, arg func (_m *Querier) ListOrganizations(ctx context.Context, arg database.ListOrganizationsParams) ([]database.Organization, error) { ret := _m.Called(ctx, arg) @@ -686,6 +846,34 @@ func (_m *Querier) ListUsers(ctx context.Context, arg database.ListUsersParams) return r0, r1 } +// LogLinkVisit provides a mock function with given fields: ctx, arg +func (_m *Querier) LogLinkVisit(ctx context.Context, arg database.LogLinkVisitParams) (database.LinkVisit, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for LogLinkVisit") + } + + var r0 database.LinkVisit + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.LogLinkVisitParams) (database.LinkVisit, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.LogLinkVisitParams) database.LinkVisit); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(database.LinkVisit) + } + + if rf, ok := ret.Get(1).(func(context.Context, database.LogLinkVisitParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RegisterForEvent provides a mock function with given fields: ctx, arg func (_m *Querier) RegisterForEvent(ctx context.Context, arg database.RegisterForEventParams) error { ret := _m.Called(ctx, arg) @@ -804,6 +992,34 @@ func (_m *Querier) UpdateEvent(ctx context.Context, arg database.UpdateEventPara return r0, r1 } +// UpdateLink provides a mock function with given fields: ctx, arg +func (_m *Querier) UpdateLink(ctx context.Context, arg database.UpdateLinkParams) (database.Link, error) { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for UpdateLink") + } + + var r0 database.Link + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateLinkParams) (database.Link, error)); ok { + return rf(ctx, arg) + } + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateLinkParams) database.Link); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Get(0).(database.Link) + } + + if rf, ok := ret.Get(1).(func(context.Context, database.UpdateLinkParams) error); ok { + r1 = rf(ctx, arg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateOrganization provides a mock function with given fields: ctx, arg func (_m *Querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { ret := _m.Called(ctx, arg) diff --git a/internal/database/models.go b/internal/database/models.go index a54ed5a..df83674 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -89,6 +89,21 @@ type EventRegistration struct { DateRegistered pgtype.Date `json:"date_registered"` } +type Link struct { + Lid uuid.UUID `json:"lid"` + EndpointUrl string `json:"endpoint_url"` + DestUrl string `json:"dest_url"` + Oid uuid.UUID `json:"oid"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + +type LinkVisit struct { + Lvid uuid.UUID `json:"lvid"` + Lid uuid.UUID `json:"lid"` + Uid pgtype.UUID `json:"uid"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type OrgMember struct { Uid uuid.UUID `json:"uid"` Oid uuid.UUID `json:"oid"` diff --git a/internal/database/querier.go b/internal/database/querier.go index fa8a0c9..bd8beac 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -16,17 +16,23 @@ type Querier interface { AddOrgMember(ctx context.Context, arg AddOrgMemberParams) error CreateBotToken(ctx context.Context, arg CreateBotTokenParams) (BotToken, error) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) + // Link Queries + CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error) CreateOrganization(ctx context.Context, name string) (Organization, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteEvent(ctx context.Context, eid uuid.UUID) error + DeleteLink(ctx context.Context, lid uuid.UUID) error DeleteOrganization(ctx context.Context, oid uuid.UUID) error DeleteUser(ctx context.Context, uid uuid.UUID) error // Bot Token Queries GetBotTokenByHash(ctx context.Context, tokenHash string) (BotToken, error) GetEventByID(ctx context.Context, eid uuid.UUID) (Event, error) GetEventRegistrations(ctx context.Context, eid uuid.UUID) ([]GetEventRegistrationsRow, error) + GetLinkByEndpointURL(ctx context.Context, endpointUrl string) (Link, error) + GetLinkByLID(ctx context.Context, lid uuid.UUID) (Link, error) GetOrgMembers(ctx context.Context, oid uuid.UUID) ([]GetOrgMembersRow, error) GetOrganizationByID(ctx context.Context, oid uuid.UUID) (Organization, error) + GetTotalVisits(ctx context.Context, lid uuid.UUID) (int64, error) GetUserByEmail(ctx context.Context, personalEmail pgtype.Text) (User, error) GetUserByID(ctx context.Context, uid uuid.UUID) (User, error) GetUserEvents(ctx context.Context, uid uuid.UUID) ([]GetUserEventsRow, error) @@ -36,14 +42,17 @@ type Querier interface { ListBotTokens(ctx context.Context) ([]ListBotTokensRow, error) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event, error) ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams) ([]Event, error) + ListLinksByOrg(ctx context.Context, oid uuid.UUID) ([]Link, error) ListOrganizations(ctx context.Context, arg ListOrganizationsParams) ([]Organization, error) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) + LogLinkVisit(ctx context.Context, arg LogLinkVisitParams) (LinkVisit, error) RegisterForEvent(ctx context.Context, arg RegisterForEventParams) error RemoveOrgMember(ctx context.Context, arg RemoveOrgMemberParams) error RevokeBotToken(ctx context.Context, tokenID uuid.UUID) error UnregisterFromEvent(ctx context.Context, arg UnregisterFromEventParams) error UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) error UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) + UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) } diff --git a/internal/database/queries.sql b/internal/database/queries.sql index 426505f..8b29c7e 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -152,3 +152,37 @@ UPDATE bot_tokens SET is_active = false WHERE token_id = $1; -- name: UpdateBotTokenLastUsed :exec UPDATE bot_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token_id = $1; + +-- Link Queries + +-- name: CreateLink :one +INSERT INTO links (endpoint_url, dest_url, oid) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: GetLinkByLID :one +SELECT * FROM links WHERE lid = $1; + +-- name: GetLinkByEndpointURL :one +SELECT * FROM links WHERE endpoint_url = $1; + +-- name: UpdateLink :one +UPDATE links +SET endpoint_url = COALESCE(sqlc.narg('endpoint_url'), endpoint_url), + dest_url = COALESCE(sqlc.narg('dest_url'), dest_url) +WHERE lid = $1 +RETURNING *; + +-- name: DeleteLink :exec +DELETE FROM links WHERE lid = $1; + +-- name: ListLinksByOrg :many +SELECT * FROM links WHERE oid = $1 ORDER BY created_at DESC; + +-- name: LogLinkVisit :one +INSERT INTO link_visits (lid, uid) +VALUES ($1, $2) +RETURNING *; + +-- name: GetTotalVisits :one +SELECT COUNT(*) FROM link_visits WHERE lid = $1; diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index d9f267d..9a3aadc 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -105,6 +105,33 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event return i, err } +const createLink = `-- name: CreateLink :one + +INSERT INTO links (endpoint_url, dest_url, oid) +VALUES ($1, $2, $3) +RETURNING lid, endpoint_url, dest_url, oid, created_at +` + +type CreateLinkParams struct { + EndpointUrl string `json:"endpoint_url"` + DestUrl string `json:"dest_url"` + Oid uuid.UUID `json:"oid"` +} + +// Link Queries +func (q *Queries) CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error) { + row := q.db.QueryRow(ctx, createLink, arg.EndpointUrl, arg.DestUrl, arg.Oid) + var i Link + err := row.Scan( + &i.Lid, + &i.EndpointUrl, + &i.DestUrl, + &i.Oid, + &i.CreatedAt, + ) + return i, err +} + const createOrganization = `-- name: CreateOrganization :one INSERT INTO organizations (name) VALUES ($1) @@ -174,6 +201,15 @@ func (q *Queries) DeleteEvent(ctx context.Context, eid uuid.UUID) error { return err } +const deleteLink = `-- name: DeleteLink :exec +DELETE FROM links WHERE lid = $1 +` + +func (q *Queries) DeleteLink(ctx context.Context, lid uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteLink, lid) + return err +} + const deleteOrganization = `-- name: DeleteOrganization :exec DELETE FROM organizations WHERE oid = $1 ` @@ -290,6 +326,40 @@ func (q *Queries) GetEventRegistrations(ctx context.Context, eid uuid.UUID) ([]G return items, nil } +const getLinkByEndpointURL = `-- name: GetLinkByEndpointURL :one +SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE endpoint_url = $1 +` + +func (q *Queries) GetLinkByEndpointURL(ctx context.Context, endpointUrl string) (Link, error) { + row := q.db.QueryRow(ctx, getLinkByEndpointURL, endpointUrl) + var i Link + err := row.Scan( + &i.Lid, + &i.EndpointUrl, + &i.DestUrl, + &i.Oid, + &i.CreatedAt, + ) + return i, err +} + +const getLinkByLID = `-- name: GetLinkByLID :one +SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE lid = $1 +` + +func (q *Queries) GetLinkByLID(ctx context.Context, lid uuid.UUID) (Link, error) { + row := q.db.QueryRow(ctx, getLinkByLID, lid) + var i Link + err := row.Scan( + &i.Lid, + &i.EndpointUrl, + &i.DestUrl, + &i.Oid, + &i.CreatedAt, + ) + return i, err +} + const getOrgMembers = `-- name: GetOrgMembers :many SELECT u.uid, u.first_name, u.last_name, u.personal_email, u.school_email, u.phone, u.grad_year, u.role, u.date_created, u.date_modified, om.is_admin, om.date_joined, om.last_active FROM users u @@ -364,6 +434,17 @@ func (q *Queries) GetOrganizationByID(ctx context.Context, oid uuid.UUID) (Organ return i, err } +const getTotalVisits = `-- name: GetTotalVisits :one +SELECT COUNT(*) FROM link_visits WHERE lid = $1 +` + +func (q *Queries) GetTotalVisits(ctx context.Context, lid uuid.UUID) (int64, error) { + row := q.db.QueryRow(ctx, getTotalVisits, lid) + var count int64 + err := row.Scan(&count) + return count, err +} + const getUserByEmail = `-- name: GetUserByEmail :one SELECT uid, first_name, last_name, personal_email, school_email, phone, grad_year, role, date_created, date_modified FROM users WHERE personal_email = $1 OR school_email = $1 ` @@ -656,6 +737,36 @@ func (q *Queries) ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams return items, nil } +const listLinksByOrg = `-- name: ListLinksByOrg :many +SELECT lid, endpoint_url, dest_url, oid, created_at FROM links WHERE oid = $1 ORDER BY created_at DESC +` + +func (q *Queries) ListLinksByOrg(ctx context.Context, oid uuid.UUID) ([]Link, error) { + rows, err := q.db.Query(ctx, listLinksByOrg, oid) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Link{} + for rows.Next() { + var i Link + if err := rows.Scan( + &i.Lid, + &i.EndpointUrl, + &i.DestUrl, + &i.Oid, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listOrganizations = `-- name: ListOrganizations :many SELECT oid, name, date_created, date_modified FROM organizations ORDER BY name LIMIT $1 OFFSET $2 ` @@ -730,6 +841,29 @@ func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, e return items, nil } +const logLinkVisit = `-- name: LogLinkVisit :one +INSERT INTO link_visits (lid, uid) +VALUES ($1, $2) +RETURNING lvid, lid, uid, created_at +` + +type LogLinkVisitParams struct { + Lid uuid.UUID `json:"lid"` + Uid pgtype.UUID `json:"uid"` +} + +func (q *Queries) LogLinkVisit(ctx context.Context, arg LogLinkVisitParams) (LinkVisit, error) { + row := q.db.QueryRow(ctx, logLinkVisit, arg.Lid, arg.Uid) + var i LinkVisit + err := row.Scan( + &i.Lvid, + &i.Lid, + &i.Uid, + &i.CreatedAt, + ) + return i, err +} + const registerForEvent = `-- name: RegisterForEvent :exec INSERT INTO event_registrations (uid, eid, is_attending) VALUES ($1, $2, $3) @@ -828,6 +962,33 @@ func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event return i, err } +const updateLink = `-- name: UpdateLink :one +UPDATE links +SET endpoint_url = COALESCE($2, endpoint_url), + dest_url = COALESCE($3, dest_url) +WHERE lid = $1 +RETURNING lid, endpoint_url, dest_url, oid, created_at +` + +type UpdateLinkParams struct { + Lid uuid.UUID `json:"lid"` + EndpointUrl pgtype.Text `json:"endpoint_url"` + DestUrl pgtype.Text `json:"dest_url"` +} + +func (q *Queries) UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error) { + row := q.db.QueryRow(ctx, updateLink, arg.Lid, arg.EndpointUrl, arg.DestUrl) + var i Link + err := row.Scan( + &i.Lid, + &i.EndpointUrl, + &i.DestUrl, + &i.Oid, + &i.CreatedAt, + ) + return i, err +} + const updateOrganization = `-- name: UpdateOrganization :one UPDATE organizations SET name = COALESCE($2, name) diff --git a/internal/dto/dto.go b/internal/dto/dto.go index cabfa00..398db56 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -118,6 +118,34 @@ type RegisterEventRequest struct { IsAttending bool `json:"is_attending"` } +// ============================================================================ +// Link DTOs +// ============================================================================ + +type CreateLinkRequest struct { + EndpointURL string `json:"endpoint_url" validate:"required,alphanumhyphen"` + DestURL string `json:"dest_url" validate:"required,url"` + OrgID uuid.UUID `json:"org_id" validate:"required"` +} + +type UpdateLinkRequest struct { + EndpointURL *string `json:"endpoint_url,omitempty" validate:"omitempty,alphanumhyphen"` + DestURL *string `json:"dest_url,omitempty" validate:"omitempty,url"` +} + +type LinkResponse struct { + LID uuid.UUID `json:"lid"` + EndpointURL string `json:"endpoint_url"` + DestURL string `json:"dest_url"` + OrgID uuid.UUID `json:"org_id"` + CreatedAt *time.Time `json:"created_at,omitempty"` +} + +type VisitCountResponse struct { + LID uuid.UUID `json:"lid"` + Count int64 `json:"count"` +} + // ============================================================================ // Pagination // ============================================================================ diff --git a/internal/handler/links.go b/internal/handler/links.go new file mode 100644 index 0000000..c0713f5 --- /dev/null +++ b/internal/handler/links.go @@ -0,0 +1,311 @@ +package handler + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/capyrpi/api/internal/database" + "github.com/capyrpi/api/internal/dto" + "github.com/capyrpi/api/internal/middleware" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/yeqown/go-qrcode/v2" + "github.com/yeqown/go-qrcode/writer/standard" +) + +// CreateLink creates a new dynamic link +// @Summary Create link +// @Description Creates a new dynamic link for an organization. Requires org_admin role. +// @Tags links +// @Accept json +// @Produce json +// @Param body body dto.CreateLinkRequest true "Link data" +// @Success 201 {object} dto.LinkResponse +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Security CookieAuth +// @Router /links [post] +func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) { + var req dto.CreateLinkRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + if req.OrgID == uuid.Nil { + h.respondError(w, http.StatusBadRequest, "org_id is required") + return + } + + // Check if user is admin of the org + claims, ok := middleware.GetUserClaims(r.Context()) + if !ok { + h.respondError(w, http.StatusUnauthorized, "Unauthorized") + return + } + uid, _ := uuid.Parse(claims.UserID) + + isAdmin, err := h.queries.IsOrgAdmin(r.Context(), database.IsOrgAdminParams{ + Uid: uid, + Oid: req.OrgID, + }) + if err != nil { + h.handleDBError(w, err) + return + } + if !isAdmin.Bool { + h.respondError(w, http.StatusForbidden, "Only org admins can create links") + return + } + + link, err := h.queries.CreateLink(r.Context(), database.CreateLinkParams{ + EndpointUrl: req.EndpointURL, + DestUrl: req.DestURL, + Oid: req.OrgID, + }) + if err != nil { + h.handleDBError(w, err) + return + } + + h.respondJSON(w, http.StatusCreated, toLinkResponse(link)) +} + +// UpdateLink updates an existing dynamic link +// @Summary Update link +// @Description Updates a dynamic link's destination or endpoint URL. Requires org_admin role. +// @Tags links +// @Accept json +// @Produce json +// @Param lid path string true "Link UUID" +// @Param body body dto.UpdateLinkRequest true "Update data" +// @Success 200 {object} dto.LinkResponse +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Security CookieAuth +// @Router /links/{lid} [put] +func (h *Handler) UpdateLink(w http.ResponseWriter, r *http.Request) { + lidStr := chi.URLParam(r, "lid") + lid, err := uuid.Parse(lidStr) + if err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid link ID") + return + } + + var req dto.UpdateLinkRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid request body") + return + } + + link, err := h.queries.GetLinkByLID(r.Context(), lid) + if err != nil { + h.handleDBError(w, err) + return + } + + // Permission check + claims, ok := middleware.GetUserClaims(r.Context()) + if !ok { + h.respondError(w, http.StatusUnauthorized, "Unauthorized") + return + } + uid, _ := uuid.Parse(claims.UserID) + + isAdmin, err := h.queries.IsOrgAdmin(r.Context(), database.IsOrgAdminParams{ + Uid: uid, + Oid: link.Oid, + }) + if err != nil { + h.handleDBError(w, err) + return + } + if !isAdmin.Bool { + h.respondError(w, http.StatusForbidden, "Only org admins can update links") + return + } + + updatedLink, err := h.queries.UpdateLink(r.Context(), database.UpdateLinkParams{ + Lid: lid, + EndpointUrl: toPgText(req.EndpointURL), + DestUrl: toPgText(req.DestURL), + }) + if err != nil { + h.handleDBError(w, err) + return + } + + h.respondJSON(w, http.StatusOK, toLinkResponse(updatedLink)) +} + +// ResolveLink resolves a dynamic link, logs a visit, and redirects +// @Summary Resolve link +// @Description Redirects to the destination URL and logs a visit +// @Tags links +// @Param endpoint_url path string true "Dynamic link endpoint URL" +// @Success 302 +// @Failure 404 {object} ErrorResponse +// @Router /r/{endpoint_url} [get] +func (h *Handler) ResolveLink(w http.ResponseWriter, r *http.Request) { + endpointURL := chi.URLParam(r, "endpoint_url") + + link, err := h.queries.GetLinkByEndpointURL(r.Context(), endpointURL) + if err != nil { + h.handleDBError(w, err) + return + } + + // Log visit asynchronously to not block the redirect + go func() { + // Create a detached context so the DB query isn't cancelled when the HTTP request ends + ctx := context.Background() + + var uid pgtype.UUID + claims, ok := middleware.GetUserClaims(r.Context()) + if ok { + parsedUID, err := uuid.Parse(claims.UserID) + if err == nil { + uid = pgtype.UUID{Bytes: parsedUID, Valid: true} + } + } + + _, err := h.queries.LogLinkVisit(ctx, database.LogLinkVisitParams{ + Lid: link.Lid, + Uid: uid, + }) + if err != nil { + slog.Error("failed to log link visit", "lid", link.Lid, "error", err) + } + }() + + http.Redirect(w, r, link.DestUrl, http.StatusFound) +} + +// ListOrgLinks lists all links for an organization +// @Summary List org links +// @Description Returns all dynamic links owned by an organization +// @Tags links +// @Accept json +// @Produce json +// @Param oid path string true "Organization UUID" +// @Success 200 {array} dto.LinkResponse +// @Failure 404 {object} ErrorResponse +// @Security CookieAuth +// @Router /organizations/{oid}/links [get] +func (h *Handler) ListOrgLinks(w http.ResponseWriter, r *http.Request) { + oidStr := chi.URLParam(r, "oid") + oid, err := uuid.Parse(oidStr) + if err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid organization ID") + return + } + + links, err := h.queries.ListLinksByOrg(r.Context(), oid) + if err != nil { + h.handleDBError(w, err) + return + } + + response := make([]dto.LinkResponse, len(links)) + for i, l := range links { + response[i] = toLinkResponse(l) + } + + h.respondJSON(w, http.StatusOK, response) +} + +// GetTotalVisits returns the total number of visits for a link +// @Summary Get visit count +// @Description Returns the total number of visits logged for a link +// @Tags links +// @Produce json +// @Param lid path string true "Link UUID" +// @Success 200 {object} dto.VisitCountResponse +// @Failure 404 {object} ErrorResponse +// @Security CookieAuth +// @Router /links/{lid}/visits [get] +func (h *Handler) GetTotalVisits(w http.ResponseWriter, r *http.Request) { + lidStr := chi.URLParam(r, "lid") + lid, err := uuid.Parse(lidStr) + if err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid link ID") + return + } + + count, err := h.queries.GetTotalVisits(r.Context(), lid) + if err != nil { + h.handleDBError(w, err) + return + } + + h.respondJSON(w, http.StatusOK, dto.VisitCountResponse{ + LID: lid, + Count: count, + }) +} + +type nopWriteCloser struct { + io.Writer +} + +func (nopWriteCloser) Close() error { return nil } + +// GetQRCode generates a QR code for a link's destination URL +// @Summary Get QR code +// @Description Generates and returns a QR code image for the link's destination URL +// @Tags links +// @Produce image/png +// @Param lid path string true "Link UUID" +// @Success 200 {file} image/png +// @Failure 404 {object} ErrorResponse +// @Router /links/{lid}/qrcode [get] +func (h *Handler) GetQRCode(w http.ResponseWriter, r *http.Request) { + lidStr := chi.URLParam(r, "lid") + lid, err := uuid.Parse(lidStr) + if err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid link ID") + return + } + + link, err := h.queries.GetLinkByLID(r.Context(), lid) + if err != nil { + h.handleDBError(w, err) + return + } + + qrc, err := qrcode.New(link.DestUrl) + if err != nil { + slog.Error("failed to generate QR code", "error", err) + h.respondError(w, http.StatusInternalServerError, "Failed to generate QR code") + return + } + + w.Header().Set("Content-Type", "image/png") + wr := standard.NewWithWriter(nopWriteCloser{w}) + if err != nil { + slog.Error("failed to create standard writer for QR code", "error", err) + h.respondError(w, http.StatusInternalServerError, "Failed to create QR code writer") + return + } + + if err = qrc.Save(wr); err != nil { + slog.Error("failed to write QR code to response", "error", err) + // Header already set, but we can't do much now if it partially wrote + } +} + +// Helper functions for link conversion +func toLinkResponse(link database.Link) dto.LinkResponse { + return dto.LinkResponse{ + LID: link.Lid, + EndpointURL: link.EndpointUrl, + DestURL: link.DestUrl, + OrgID: link.Oid, + CreatedAt: fromPgTimestamp(link.CreatedAt), + } +} diff --git a/internal/handler/links_test.go b/internal/handler/links_test.go new file mode 100644 index 0000000..7744488 --- /dev/null +++ b/internal/handler/links_test.go @@ -0,0 +1,180 @@ +package handler_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/capyrpi/api/internal/config" + "github.com/capyrpi/api/internal/database" + "github.com/capyrpi/api/internal/database/mocks" + "github.com/capyrpi/api/internal/dto" + "github.com/capyrpi/api/internal/handler" + "github.com/capyrpi/api/internal/middleware" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestCreateLink(t *testing.T) { + uid := uuid.New() + oid := uuid.New() + lid := uuid.New() + + tests := []struct { + name string + requestBody interface{} + setupMock func(*mocks.Querier) + setupContext func() context.Context + expectedStatus int + }{ + { + name: "Success", + requestBody: dto.CreateLinkRequest{ + EndpointURL: "promo-2024", + DestURL: "https://capyrpi.org/promo", + OrgID: oid, + }, + setupMock: func(m *mocks.Querier) { + m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{ + Uid: uid, + Oid: oid, + }).Return(pgtype.Bool{Bool: true, Valid: true}, nil) + + m.On("CreateLink", mock.Anything, database.CreateLinkParams{ + EndpointUrl: "promo-2024", + DestUrl: "https://capyrpi.org/promo", + Oid: oid, + }).Return(database.Link{ + Lid: lid, + EndpointUrl: "promo-2024", + DestUrl: "https://capyrpi.org/promo", + Oid: oid, + }, nil) + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: uid.String()} + return context.WithValue(ctx, middleware.UserClaimsKey, claims) + }, + expectedStatus: http.StatusCreated, + }, + { + name: "Forbidden - Not Admin", + requestBody: dto.CreateLinkRequest{ + EndpointURL: "promo-2024", + DestURL: "https://capyrpi.org/promo", + OrgID: oid, + }, + setupMock: func(m *mocks.Querier) { + m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{ + Uid: uid, + Oid: oid, + }).Return(pgtype.Bool{Bool: false, Valid: true}, nil) + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: uid.String()} + return context.WithValue(ctx, middleware.UserClaimsKey, claims) + }, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockQueries := mocks.NewQuerier(t) + if tt.setupMock != nil { + tt.setupMock(mockQueries) + } + + h := handler.New(mockQueries, &config.Config{}) + + body, _ := json.Marshal(tt.requestBody) + req := httptest.NewRequest("POST", "/links", bytes.NewBuffer(body)) + req = req.WithContext(tt.setupContext()) + rr := httptest.NewRecorder() + + http.HandlerFunc(h.CreateLink).ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + if tt.expectedStatus == http.StatusCreated { + var res dto.LinkResponse + err := json.Unmarshal(rr.Body.Bytes(), &res) + assert.NoError(t, err) + assert.Equal(t, lid, res.LID) + assert.Equal(t, "promo-2024", res.EndpointURL) + } + }) + } +} + +func TestResolveLink(t *testing.T) { + lid := uuid.New() + endpoint := "my-link" + dest := "https://example.com" + + tests := []struct { + name string + endpointParam string + setupMock func(*mocks.Querier) + setupContext func() context.Context + expectedStatus int + expectedLoc string + }{ + { + name: "Success Redirect", + endpointParam: endpoint, + setupMock: func(m *mocks.Querier) { + m.On("GetLinkByEndpointURL", mock.Anything, endpoint).Return(database.Link{ + Lid: lid, + EndpointUrl: endpoint, + DestUrl: dest, + }, nil) + + // We don't strictly mock the background Context visit log here + // since it happens in a goroutine and is hard to sync without sleep or waitgroups, + // but let's mock it to avoid panic if the mock is strict. + m.On("LogLinkVisit", mock.Anything, mock.MatchedBy(func(p database.LogLinkVisitParams) bool { + return p.Lid == lid + })).Return(database.LinkVisit{}, nil).Maybe() + }, + setupContext: func() context.Context { + return context.Background() + }, + expectedStatus: http.StatusFound, // 302 + expectedLoc: dest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockQueries := mocks.NewQuerier(t) + tt.setupMock(mockQueries) + + h := handler.New(mockQueries, &config.Config{}) + + req := httptest.NewRequest("GET", "/r/"+tt.endpointParam, nil) + req = req.WithContext(tt.setupContext()) + + // Setup chi router context so chi.URLParam works + rctx := chi.NewRouteContext() + rctx.URLParams.Add("endpoint_url", tt.endpointParam) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + rr := httptest.NewRecorder() + + http.HandlerFunc(h.ResolveLink).ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + if tt.expectedStatus == http.StatusFound { + assert.Equal(t, tt.expectedLoc, rr.Header().Get("Location")) + } + }) + } +} diff --git a/internal/router/router.go b/internal/router/router.go index 97c2cb1..cfa0dce 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -25,6 +25,9 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed // Health check (public) r.Get("/health", h.Health) + // Link resolution (public) + r.Get("/r/{endpoint_url}", h.ResolveLink) + // Swagger UI (public) - Only in non-production environments if h.Config.Env != "production" { r.Get("/swagger/*", httpSwagger.WrapHandler) @@ -72,6 +75,7 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed r.Post("/{oid}/members", h.AddOrgMember) r.Delete("/{oid}/members/{uid}", h.RemoveOrgMember) r.Get("/{oid}/events", h.ListOrgEvents) + r.Get("/{oid}/links", h.ListOrgLinks) }) // Events @@ -87,6 +91,14 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed r.Delete("/{eid}/register", h.UnregisterFromEvent) }) + // Links + r.Route("/links", func(r chi.Router) { + r.Post("/", h.CreateLink) + r.Put("/{lid}", h.UpdateLink) + r.Get("/{lid}/visits", h.GetTotalVisits) + r.Get("/{lid}/qrcode", h.GetQRCode) + }) + // Bot token management (human auth only) r.Route("/bot/tokens", func(r chi.Router) { r.Get("/", h.ListBotTokens) diff --git a/schema.sql b/schema.sql index 160c511..d8a6b3f 100644 --- a/schema.sql +++ b/schema.sql @@ -14,7 +14,7 @@ $$ language 'plpgsql'; -- 2. Tables CREATE TABLE IF NOT EXISTS users ( - uid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uid UUID PRIMARY KEY DEFAULT uuidv4(), first_name TEXT NOT NULL, last_name TEXT NOT NULL, personal_email TEXT UNIQUE, @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS users ( ); CREATE TABLE IF NOT EXISTS organizations ( - oid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + oid UUID PRIMARY KEY DEFAULT uuidv4(), name TEXT NOT NULL, date_created DATE DEFAULT CURRENT_DATE, date_modified DATE DEFAULT CURRENT_DATE @@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS org_members ( ); CREATE TABLE IF NOT EXISTS events ( - eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + eid UUID PRIMARY KEY DEFAULT uuidv4(), location TEXT, event_time TIMESTAMP, description TEXT, @@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS event_registrations ( -- 3. Bot Tokens (global access for M2M authentication) CREATE TABLE IF NOT EXISTS bot_tokens ( - token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token_id UUID PRIMARY KEY DEFAULT uuidv4(), token_hash TEXT NOT NULL, -- bcrypt hash of the token name TEXT NOT NULL, -- human-readable name for the bot created_by UUID NOT NULL REFERENCES users(uid), @@ -78,6 +78,21 @@ CREATE TABLE IF NOT EXISTS bot_tokens ( is_active BOOLEAN DEFAULT TRUE ); +CREATE TABLE IF NOT EXISTS links ( + lid UUID PRIMARY KEY DEFAULT uuidv4(), + endpoint_url TEXT NOT NULL UNIQUE, + dest_url TEXT NOT NULL, + oid UUID NOT NULL REFERENCES organizations(oid) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS link_visits ( + lvid UUID PRIMARY KEY DEFAULT uuidv7(), + lid UUID NOT NULL REFERENCES links(lid) ON DELETE CASCADE, + uid UUID REFERENCES users(uid) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + CREATE INDEX IF NOT EXISTS idx_bot_tokens_active ON bot_tokens(is_active) WHERE is_active = TRUE; -- 4. Triggers diff --git a/tests/benchmarks/suite_test.go b/tests/benchmarks/suite_test.go index 6d950d3..5c63e37 100644 --- a/tests/benchmarks/suite_test.go +++ b/tests/benchmarks/suite_test.go @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { log.Printf("Using schema from: %s", schemaPath) pgContainer, err := postgres.Run(ctx, - "postgres:16-alpine", + "postgres:18-alpine", postgres.WithInitScripts(schemaPath), postgres.WithDatabase("bench_db"), postgres.WithUsername("bench"), From 9fc6d69955976703ef632fe1db5b53db367944d5 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:30:21 -0500 Subject: [PATCH 03/29] Make QR Code return endpoint url If the QR code returns the dest url, visits to that URL cannot be tracked. Use endpoint url instead. --- internal/handler/links.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/links.go b/internal/handler/links.go index c0713f5..e1573ca 100644 --- a/internal/handler/links.go +++ b/internal/handler/links.go @@ -278,7 +278,7 @@ func (h *Handler) GetQRCode(w http.ResponseWriter, r *http.Request) { return } - qrc, err := qrcode.New(link.DestUrl) + qrc, err := qrcode.New(link.EndpointUrl) if err != nil { slog.Error("failed to generate QR code", "error", err) h.respondError(w, http.StatusInternalServerError, "Failed to generate QR code") From 31ab83036d6be98bda39edd208ec0bc7d4a8f989 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:30:38 -0500 Subject: [PATCH 04/29] Add link integration tests --- tests/integration/links_test.go | 177 ++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/integration/links_test.go diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go new file mode 100644 index 0000000..bead82a --- /dev/null +++ b/tests/integration/links_test.go @@ -0,0 +1,177 @@ +//go:build integration + +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/capyrpi/api/internal/config" + "github.com/capyrpi/api/internal/database" + "github.com/capyrpi/api/internal/dto" + "github.com/capyrpi/api/internal/handler" + "github.com/capyrpi/api/internal/middleware" + "github.com/capyrpi/api/internal/router" + "github.com/capyrpi/api/internal/testutils" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLinkFlow(t *testing.T) { + // 1. Setup DB and Server + pool := testutils.SetupTestDB(t) + defer pool.Close() + + queries := database.New(pool) + cfg := &config.Config{JWT: config.JWTConfig{Secret: "test-secret", ExpiryHours: 1}} + h := handler.New(queries, cfg) + r := router.New(h, queries, cfg.JWT.Secret, []string{}) + server := httptest.NewServer(r) + defer server.Close() + client := server.Client() + + // Disable redirects on the client so we can test the 302 response directly + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + ctx := context.Background() + + // 2. Create User + user, err := queries.CreateUser(ctx, database.CreateUserParams{ + FirstName: "Link", + LastName: "Tester", + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }) + require.NoError(t, err) + + // 3. Generate Auth Token + claims := middleware.UserClaims{ + UserID: user.Uid.String(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(cfg.JWT.Secret)) + require.NoError(t, err) + cookie := &http.Cookie{Name: "capy_auth", Value: tokenString} + + // 4. Create Organization + orgBody := []byte(`{"name":"Link Org","slug":"link-org"}`) + req, _ := http.NewRequest("POST", server.URL+"/v1/organizations", bytes.NewBuffer(orgBody)) + req.AddCookie(cookie) + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusCreated, resp.StatusCode) + + var orgResp dto.OrganizationResponse + err = json.NewDecoder(resp.Body).Decode(&orgResp) + require.NoError(t, err) + oid := orgResp.OID.String() + + // 5. Create Link + linkReq := dto.CreateLinkRequest{ + EndpointURL: "my-promo", + DestURL: "https://example.com/dest", + OrgID: orgResp.OID, + } + linkBody, _ := json.Marshal(linkReq) + req, _ = http.NewRequest("POST", server.URL+"/v1/links", bytes.NewBuffer(linkBody)) + req.AddCookie(cookie) + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusCreated, resp.StatusCode) + + var linkResp dto.LinkResponse + err = json.NewDecoder(resp.Body).Decode(&linkResp) + require.NoError(t, err) + require.Equal(t, "my-promo", linkResp.EndpointURL) + lid := linkResp.LID.String() + + // 6. Resolve Link (Simulate user clicking it) + req, _ = http.NewRequest("GET", server.URL+"/r/my-promo", nil) + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // Check redirect status and location + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, "https://example.com/dest", resp.Header.Get("Location")) + + // Give the async goroutine a tiny bit of time to log the visit in DB + time.Sleep(100 * time.Millisecond) + + // 7. Check Visit Count + req, _ = http.NewRequest("GET", fmt.Sprintf("%s/v1/links/%s/visits", server.URL, lid), nil) + req.AddCookie(cookie) + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + var visitResp dto.VisitCountResponse + err = json.NewDecoder(resp.Body).Decode(&visitResp) + require.NoError(t, err) + assert.Equal(t, int64(1), visitResp.Count) + + // 8. Update Link + updateReq := dto.UpdateLinkRequest{ + EndpointURL: nil, + DestURL: toPtr("https://example.com/updated"), + } + updateBody, _ := json.Marshal(updateReq) + req, _ = http.NewRequest("PUT", fmt.Sprintf("%s/v1/links/%s", server.URL, lid), bytes.NewBuffer(updateBody)) + req.AddCookie(cookie) + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + var updatedLink dto.LinkResponse + err = json.NewDecoder(resp.Body).Decode(&updatedLink) + require.NoError(t, err) + assert.Equal(t, "https://example.com/updated", updatedLink.DestURL) + + // 9. List Org Links + req, _ = http.NewRequest("GET", fmt.Sprintf("%s/v1/organizations/%s/links", server.URL, oid), nil) + req.AddCookie(cookie) + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + var links []dto.LinkResponse + err = json.NewDecoder(resp.Body).Decode(&links) + require.NoError(t, err) + require.Len(t, links, 1) + + // 10. Get QR Code + req, _ = http.NewRequest("GET", fmt.Sprintf("%s/v1/links/%s/qrcode", server.URL, lid), nil) + // We don't usually need auth for QR code, but the endpoint currently requires CookieAuth based on swagger tags + // Actually looking at router.go, /links/{lid}/qrcode is under protected group. + req.AddCookie(cookie) + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "image/png", resp.Header.Get("Content-Type")) +} + +func toPtr(s string) *string { + return &s +} From 746f0e629de6d88cda58eea8dc2369783e6f0423 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:05:57 -0400 Subject: [PATCH 05/29] Add dots to QR code It looks terrible this is going away --- internal/handler/links.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/handler/links.go b/internal/handler/links.go index e1573ca..b19ff95 100644 --- a/internal/handler/links.go +++ b/internal/handler/links.go @@ -17,6 +17,9 @@ import ( "github.com/yeqown/go-qrcode/writer/standard" ) +const QR_FG_COLOR = "#067b76" +const QR_BG_COLOR = "#fcfdfe" + // CreateLink creates a new dynamic link // @Summary Create link // @Description Creates a new dynamic link for an organization. Requires org_admin role. @@ -278,15 +281,21 @@ func (h *Handler) GetQRCode(w http.ResponseWriter, r *http.Request) { return } - qrc, err := qrcode.New(link.EndpointUrl) + qrc, err := qrcode.NewWith(link.EndpointUrl) if err != nil { slog.Error("failed to generate QR code", "error", err) h.respondError(w, http.StatusInternalServerError, "Failed to generate QR code") return } + qrcOpts := []standard.ImageOption{ + standard.WithBgColorRGBHex(QR_BG_COLOR), + standard.WithFgColorRGBHex(QR_FG_COLOR), + standard.WithCircleShape(), + } + w.Header().Set("Content-Type", "image/png") - wr := standard.NewWithWriter(nopWriteCloser{w}) + wr := standard.NewWithWriter(nopWriteCloser{w}, qrcOpts...) if err != nil { slog.Error("failed to create standard writer for QR code", "error", err) h.respondError(w, http.StatusInternalServerError, "Failed to create QR code writer") From ec56a9343f687d07f0628eb848e90a3f70dacc62 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Wed, 25 Mar 2026 22:45:03 -0400 Subject: [PATCH 06/29] localhost domain routing --- internal/handler/auth.go | 28 +++++++------ internal/handler/handler.go | 32 +++++++++++++++ internal/handler/localhost_test.go | 63 ++++++++++++++++++++++++++++++ internal/oauth/google.go | 11 ++++-- internal/oauth/microsoft.go | 11 ++++-- 5 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 internal/handler/localhost_test.go diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 11c92f9..74797ec 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -75,9 +75,10 @@ func (h *Handler) GoogleAuth(w http.ResponseWriter, r *http.Request) { } // Set state cookie to verify callback - h.setStateCookie(w, state) + h.setStateCookie(w, r, state) - http.Redirect(w, r, h.googleAuth.GetAuthURL(state), http.StatusFound) + redirectURL := h.getOAuthRedirectURL(r, h.Config.OAuth.Google.RedirectURL) + http.Redirect(w, r, h.googleAuth.GetAuthURL(state, redirectURL), http.StatusFound) } // GoogleCallback handles Google OAuth callback @@ -121,7 +122,7 @@ func (h *Handler) GoogleCallback(w http.ResponseWriter, r *http.Request) { return } - h.setAuthCookie(w, token) + h.setAuthCookie(w, r, token) h.respondWithCloseWindow(w) } @@ -138,8 +139,9 @@ func (h *Handler) MicrosoftAuth(w http.ResponseWriter, r *http.Request) { return } - h.setStateCookie(w, state) - http.Redirect(w, r, h.microsoftAuth.GetAuthURL(state), http.StatusFound) + h.setStateCookie(w, r, state) + redirectURL := h.getOAuthRedirectURL(r, h.Config.OAuth.Microsoft.RedirectURL) + http.Redirect(w, r, h.microsoftAuth.GetAuthURL(state, redirectURL), http.StatusFound) } // MicrosoftCallback handles Microsoft OAuth callback @@ -188,7 +190,7 @@ func (h *Handler) MicrosoftCallback(w http.ResponseWriter, r *http.Request) { return } - h.setAuthCookie(w, token) + h.setAuthCookie(w, r, token) h.respondWithCloseWindow(w) } @@ -246,7 +248,7 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { Name: "capy_auth", Value: "", Path: "/", - Domain: h.Config.Cookie.Domain, + Domain: h.getCookieDomain(r), MaxAge: -1, Secure: h.Config.Cookie.Secure, HttpOnly: true, @@ -292,7 +294,7 @@ func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) { } // Set new cookie - h.setAuthCookie(w, token) + h.setAuthCookie(w, r, token) h.respondJSON(w, http.StatusOK, AuthResponse{ User: UserAuthResponse{ @@ -486,12 +488,12 @@ func (h *Handler) generateJWT(user database.User) (string, error) { return token.SignedString([]byte(h.Config.JWT.Secret)) } -func (h *Handler) setAuthCookie(w http.ResponseWriter, token string) { +func (h *Handler) setAuthCookie(w http.ResponseWriter, r *http.Request, token string) { http.SetCookie(w, &http.Cookie{ Name: "capy_auth", Value: token, Path: "/", - Domain: h.Config.Cookie.Domain, + Domain: h.getCookieDomain(r), MaxAge: h.Config.JWT.ExpiryHours * 3600, Secure: h.Config.Cookie.Secure, HttpOnly: true, @@ -551,12 +553,12 @@ func (h *Handler) respondWithCloseWindow(w http.ResponseWriter) { `)) } -func (h *Handler) setStateCookie(w http.ResponseWriter, state string) { +func (h *Handler) setStateCookie(w http.ResponseWriter, r *http.Request, state string) { http.SetCookie(w, &http.Cookie{ Name: "oauth_state", Value: state, Path: "/api/v1/auth", - Domain: h.Config.Cookie.Domain, + Domain: h.getCookieDomain(r), MaxAge: 300, // 5 minutes Secure: h.Config.Cookie.Secure, HttpOnly: true, @@ -574,7 +576,7 @@ func (h *Handler) verifyStateCookie(w http.ResponseWriter, r *http.Request, stat Name: "oauth_state", Value: "", Path: "/api/v1/auth", - Domain: h.Config.Cookie.Domain, + Domain: h.getCookieDomain(r), MaxAge: -1, Secure: h.Config.Cookie.Secure, HttpOnly: true, diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 66c2447..d97e547 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -31,6 +31,38 @@ func New(queries database.Querier, cfg *config.Config) *Handler { } } +func (h *Handler) isLocalhost(r *http.Request) bool { + if h.Config.Env != "development" { + return false + } + host := r.Host + return strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") +} + +func (h *Handler) getCookieDomain(r *http.Request) string { + if h.isLocalhost(r) { + return "localhost" + } + return h.Config.Cookie.Domain +} + +func (h *Handler) getOAuthRedirectURL(r *http.Request, providerRedirectURL string) string { + if !h.isLocalhost(r) { + return "" + } + + // If we're on localhost in dev mode, try to use localhost for the redirect URL + // We assume the port is the same as the current request + if strings.Contains(providerRedirectURL, "://") { + // Replace the host part with localhost:port + parts := strings.SplitN(providerRedirectURL, "/", 4) + if len(parts) >= 4 { + return "http://" + r.Host + "/" + parts[3] + } + } + return "" +} + func normalizeEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) } diff --git a/internal/handler/localhost_test.go b/internal/handler/localhost_test.go new file mode 100644 index 0000000..36d3461 --- /dev/null +++ b/internal/handler/localhost_test.go @@ -0,0 +1,63 @@ +package handler + +import ( + "net/http/httptest" + "testing" + + "github.com/capyrpi/api/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestLocalhostDetection(t *testing.T) { + tests := []struct { + name string + env string + host string + expected bool + }{ + {"localhost in dev", "development", "localhost:8080", true}, + {"127.0.0.1 in dev", "development", "127.0.0.1:8080", true}, + {"production host in dev", "development", "api.capyrpi.org", false}, + {"localhost in prod", "production", "localhost:8080", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &Handler{Config: &config.Config{Env: tt.env}} + r := httptest.NewRequest("GET", "/", nil) + r.Host = tt.host + assert.Equal(t, tt.expected, h.isLocalhost(r)) + }) + } +} + +func TestGetCookieDomain(t *testing.T) { + h := &Handler{Config: &config.Config{ + Env: "development", + Cookie: config.CookieConfig{Domain: "capyrpi.org"}, + }} + + rLocal := httptest.NewRequest("GET", "/", nil) + rLocal.Host = "localhost:8080" + assert.Equal(t, "localhost", h.getCookieDomain(rLocal)) + + rProd := httptest.NewRequest("GET", "/", nil) + rProd.Host = "api.capyrpi.org" + assert.Equal(t, "capyrpi.org", h.getCookieDomain(rProd)) +} + +func TestGetOAuthRedirectURL(t *testing.T) { + h := &Handler{Config: &config.Config{ + Env: "development", + }} + + providerURL := "https://api.capyrpi.org/api/v1/auth/google/callback" + + rLocal := httptest.NewRequest("GET", "/", nil) + rLocal.Host = "localhost:8080" + assert.Equal(t, "http://localhost:8080/api/v1/auth/google/callback", h.getOAuthRedirectURL(rLocal, providerURL)) + + rProd := httptest.NewRequest("GET", "/", nil) + rProd.Host = "api.capyrpi.org" + assert.Equal(t, "", h.getOAuthRedirectURL(rProd, providerURL)) +} diff --git a/internal/oauth/google.go b/internal/oauth/google.go index ad4d60d..758324f 100644 --- a/internal/oauth/google.go +++ b/internal/oauth/google.go @@ -47,11 +47,16 @@ func NewGoogleProvider(clientID, clientSecret, redirectURL string) *GoogleProvid } // GetAuthURL generates the OAuth authorization URL with state token -func (p *GoogleProvider) GetAuthURL(state string) string { - return p.config.AuthCodeURL(state, +// If redirectURLOverride is not empty, it will be used instead of the default config +func (p *GoogleProvider) GetAuthURL(state string, redirectURLOverride string) string { + opts := []oauth2.AuthCodeOption{ oauth2.AccessTypeOffline, oauth2.ApprovalForce, - ) + } + if redirectURLOverride != "" { + opts = append(opts, oauth2.SetAuthURLParam("redirect_uri", redirectURLOverride)) + } + return p.config.AuthCodeURL(state, opts...) } // ExchangeCode exchanges the authorization code for a token and fetches user info diff --git a/internal/oauth/microsoft.go b/internal/oauth/microsoft.go index b93b419..a23bd9d 100644 --- a/internal/oauth/microsoft.go +++ b/internal/oauth/microsoft.go @@ -50,11 +50,16 @@ func NewMicrosoftProvider(clientID, clientSecret, redirectURL, tenantID string) } // GetAuthURL generates the OAuth authorization URL with state token -func (p *MicrosoftProvider) GetAuthURL(state string) string { - return p.config.AuthCodeURL(state, +// If redirectURLOverride is not empty, it will be used instead of the default config +func (p *MicrosoftProvider) GetAuthURL(state string, redirectURLOverride string) string { + opts := []oauth2.AuthCodeOption{ oauth2.AccessTypeOffline, oauth2.ApprovalForce, - ) + } + if redirectURLOverride != "" { + opts = append(opts, oauth2.SetAuthURLParam("redirect_uri", redirectURLOverride)) + } + return p.config.AuthCodeURL(state, opts...) } // ExchangeCode exchanges the authorization code for a token and fetches user info From b85e7999b905d5c1b71ee2f04ee2d06b2914ab41 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Wed, 25 Mar 2026 22:46:16 -0400 Subject: [PATCH 07/29] add develop build --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1821f98..a65581c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Docker Publish on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] tags: [ "v*" ] env: From c02f57731f89319aba9a7a826b70e298006ca88f Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Thu, 26 Mar 2026 01:08:40 -0400 Subject: [PATCH 08/29] feat(dev): allow dynamic localhost/dev cookie switching on dev mode --- .github/workflows/ci.yml | 4 +- cmd/server/main.go | 6 +- docker-compose.yml | 9 --- docs/schema/schema.json | 17 +++--- docs/swagger/docs.go | 33 ++++++----- docs/swagger/swagger.json | 35 +++++------ docs/swagger/swagger.yaml | 31 +++++----- internal/database/queries.sql.go | 6 +- internal/handler/auth.go | 6 +- internal/handler/handler.go | 74 ++++++++++++++++++++---- internal/handler/localhost_test.go | 93 ++++++++++++++++++++++++++---- internal/middleware/cors.go | 19 +++--- internal/middleware/cors_test.go | 22 +++++-- internal/oauth/google.go | 13 ++++- internal/oauth/microsoft.go | 13 ++++- 15 files changed, 269 insertions(+), 112 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff3d935..b252456 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: test: diff --git a/cmd/server/main.go b/cmd/server/main.go index 12d91bb..c08d1f1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -40,7 +40,11 @@ import ( // @name X-Bot-Token func main() { // Setup structured logging - slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + level := slog.LevelInfo + if os.Getenv("ENV") == "development" || os.Getenv("ENV") == "staging" || os.Getenv("ENV") == "" { + level = slog.LevelDebug + } + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) // Load configuration cfg, err := config.Load() diff --git a/docker-compose.yml b/docker-compose.yml index 6594f6f..7c643db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,14 +25,5 @@ services: db: condition: service_healthy - tunnel: - image: cloudflare/cloudflared:latest - restart: unless-stopped - command: tunnel run - env_file: - - .env - depends_on: - - api - volumes: pgdata: diff --git a/docs/schema/schema.json b/docs/schema/schema.json index 737f7e8..1e46115 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -1144,7 +1144,8 @@ "student", "alumni", "faculty", - "external" + "external", + "dev" ], "comment": "" } @@ -71328,12 +71329,12 @@ "insert_into_table": null }, { - "text": "SELECT is_admin FROM event_registrations WHERE uid = $1 AND eid = $2", + "text": "SELECT EXISTS (\n SELECT 1\n FROM event_registrations er\n WHERE er.uid = $1\n AND er.eid = $2\n AND er.is_admin = TRUE\n)\nOR EXISTS (\n SELECT 1\n FROM event_hosting eh\n JOIN org_members om ON om.oid = eh.oid\n WHERE eh.eid = $2\n AND om.uid = $1\n AND om.is_admin = TRUE\n)", "name": "IsEventAdmin", "cmd": ":one", "columns": [ { - "name": "is_admin", + "name": "", "not_null": false, "is_array": false, "comment": "", @@ -71341,20 +71342,16 @@ "is_named_param": false, "is_func_call": false, "scope": "", - "table": { - "catalog": "", - "schema": "", - "name": "event_registrations" - }, + "table": null, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", + "schema": "", "name": "bool" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "is_admin", + "original_name": "", "unsigned": false, "array_dims": 0 } diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index d6a0539..7116052 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -218,7 +218,7 @@ const docTemplate = `{ "BotToken": [] } ], - "description": "Returns information about the current bot token. Authenticate with X-Bot-Token: ., for example: curl -H 'X-Bot-Token: ' http://localhost:8080/api/v1/bot/me", + "description": "Returns information about the current bot token. Authenticate with X-Bot-Token: \u003ctoken_id\u003e.\u003csecret\u003e, for example: curl -H 'X-Bot-Token: \u003ctoken\u003e' http://localhost:8080/api/v1/bot/me", "consumes": [ "application/json" ], @@ -252,7 +252,7 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Returns all bot tokens (requires faculty role)", + "description": "Returns all bot tokens (requires dev role)", "consumes": [ "application/json" ], @@ -287,7 +287,7 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Creates a new bot token (requires faculty role). The raw token is returned only once and must be stored by the caller.", + "description": "Creates a new bot token (requires dev role). The raw token is returned only once and must be stored by the caller.", "consumes": [ "application/json" ], @@ -338,7 +338,7 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Revokes a bot token (requires faculty role)", + "description": "Revokes a bot token (requires dev role)", "consumes": [ "application/json" ], @@ -1750,7 +1750,8 @@ const docTemplate = `{ "student", "alumni", "faculty", - "external" + "external", + "dev" ] }, "school_email": { @@ -1805,42 +1806,42 @@ const docTemplate = `{ } } }, - "handler.BotTokenResponse": { + "handler.BotMeResponse": { "type": "object", "properties": { - "created_at": { + "auth_type": { "type": "string" }, "expires_at": { "type": "string" }, - "is_active": { - "type": "boolean" - }, "name": { "type": "string" }, - "token": { - "description": "Only on creation. Store it immediately; it is not returned again.", - "type": "string" - }, "token_id": { "type": "string" } } }, - "handler.BotMeResponse": { + "handler.BotTokenResponse": { "type": "object", "properties": { - "auth_type": { + "created_at": { "type": "string" }, "expires_at": { "type": "string" }, + "is_active": { + "type": "boolean" + }, "name": { "type": "string" }, + "token": { + "description": "Only on creation", + "type": "string" + }, "token_id": { "type": "string" } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index e57d7c0..ef1715f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -212,7 +212,7 @@ "BotToken": [] } ], - "description": "Returns information about the current bot token. Authenticate with X-Bot-Token: ., for example: curl -H 'X-Bot-Token: ' http://localhost:8080/api/v1/bot/me", + "description": "Returns information about the current bot token. Authenticate with X-Bot-Token: \u003ctoken_id\u003e.\u003csecret\u003e, for example: curl -H 'X-Bot-Token: \u003ctoken\u003e' http://localhost:8080/api/v1/bot/me", "consumes": [ "application/json" ], @@ -246,7 +246,7 @@ "CookieAuth": [] } ], - "description": "Returns all bot tokens (requires faculty role)", + "description": "Returns all bot tokens (requires dev role)", "consumes": [ "application/json" ], @@ -281,7 +281,7 @@ "CookieAuth": [] } ], - "description": "Creates a new bot token (requires faculty role). The raw token is returned only once and must be stored by the caller.", + "description": "Creates a new bot token (requires dev role). The raw token is returned only once and must be stored by the caller.", "consumes": [ "application/json" ], @@ -332,7 +332,7 @@ "CookieAuth": [] } ], - "description": "Revokes a bot token (requires faculty role)", + "description": "Revokes a bot token (requires dev role)", "consumes": [ "application/json" ], @@ -1744,7 +1744,8 @@ "student", "alumni", "faculty", - "external" + "external", + "dev" ] }, "school_email": { @@ -1799,42 +1800,42 @@ } } }, - "handler.BotTokenResponse": { + "handler.BotMeResponse": { "type": "object", "properties": { - "created_at": { + "auth_type": { "type": "string" }, "expires_at": { "type": "string" }, - "is_active": { - "type": "boolean" - }, "name": { "type": "string" }, - "token": { - "description": "Only on creation. Store it immediately; it is not returned again.", - "type": "string" - }, "token_id": { "type": "string" } } }, - "handler.BotMeResponse": { + "handler.BotTokenResponse": { "type": "object", "properties": { - "auth_type": { + "created_at": { "type": "string" }, "expires_at": { "type": "string" }, + "is_active": { + "type": "boolean" + }, "name": { "type": "string" }, + "token": { + "description": "Only on creation", + "type": "string" + }, "token_id": { "type": "string" } @@ -1900,4 +1901,4 @@ "in": "cookie" } } -} +} \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index a7e12ef..73c8faf 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -141,6 +141,7 @@ definitions: - alumni - faculty - external + - dev type: string school_email: type: string @@ -176,30 +177,30 @@ definitions: user: $ref: '#/definitions/handler.UserAuthResponse' type: object - handler.BotTokenResponse: + handler.BotMeResponse: properties: - created_at: + auth_type: type: string expires_at: type: string - is_active: - type: boolean name: type: string - token: - description: Only on creation. Store it immediately; it is not returned again. - type: string token_id: type: string type: object - handler.BotMeResponse: + handler.BotTokenResponse: properties: - auth_type: + created_at: type: string expires_at: type: string + is_active: + type: boolean name: type: string + token: + description: Only on creation + type: string token_id: type: string type: object @@ -371,8 +372,9 @@ paths: get: consumes: - application/json - description: 'Returns information about the current bot token. Authenticate with X-Bot-Token: - ., for example: curl -H ''X-Bot-Token: '' http://localhost:8080/api/v1/bot/me' + description: 'Returns information about the current bot token. Authenticate + with X-Bot-Token: ., for example: curl -H ''X-Bot-Token: + '' http://localhost:8080/api/v1/bot/me' produces: - application/json responses: @@ -393,7 +395,7 @@ paths: get: consumes: - application/json - description: Returns all bot tokens (requires faculty role) + description: Returns all bot tokens (requires dev role) produces: - application/json responses: @@ -415,7 +417,8 @@ paths: post: consumes: - application/json - description: Creates a new bot token (requires faculty role). The raw token is returned only once and must be stored by the caller. + description: Creates a new bot token (requires dev role). The raw token is returned + only once and must be stored by the caller. parameters: - description: Token data in: body @@ -447,7 +450,7 @@ paths: delete: consumes: - application/json - description: Revokes a bot token (requires faculty role) + description: Revokes a bot token (requires dev role) parameters: - description: Token UUID in: path diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index d495a7e..457aa5b 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -527,9 +527,9 @@ type IsEventAdminParams struct { func (q *Queries) IsEventAdmin(ctx context.Context, arg IsEventAdminParams) (pgtype.Bool, error) { row := q.db.QueryRow(ctx, isEventAdmin, arg.Uid, arg.Eid) - var is_admin pgtype.Bool - err := row.Scan(&is_admin) - return is_admin, err + var column_1 pgtype.Bool + err := row.Scan(&column_1) + return column_1, err } const isOrgAdmin = `-- name: IsOrgAdmin :one diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 74797ec..0e571ba 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -104,7 +104,8 @@ func (h *Handler) GoogleCallback(w http.ResponseWriter, r *http.Request) { return } - userInfo, err := h.googleAuth.ExchangeCode(r.Context(), code) + redirectURL := h.getOAuthRedirectURL(r, h.Config.OAuth.Google.RedirectURL) + userInfo, err := h.googleAuth.ExchangeCode(r.Context(), code, redirectURL) if err != nil { h.respondError(w, http.StatusInternalServerError, "Failed to exchange code") return @@ -166,7 +167,8 @@ func (h *Handler) MicrosoftCallback(w http.ResponseWriter, r *http.Request) { return } - userInfo, err := h.microsoftAuth.ExchangeCode(r.Context(), code) + redirectURL := h.getOAuthRedirectURL(r, h.Config.OAuth.Microsoft.RedirectURL) + userInfo, err := h.microsoftAuth.ExchangeCode(r.Context(), code, redirectURL) if err != nil { h.respondError(w, http.StatusInternalServerError, "Failed to exchange code") return diff --git a/internal/handler/handler.go b/internal/handler/handler.go index d97e547..e504d04 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -31,33 +31,85 @@ func New(queries database.Querier, cfg *config.Config) *Handler { } } -func (h *Handler) isLocalhost(r *http.Request) bool { - if h.Config.Env != "development" { - return false +func (h *Handler) isDev(r *http.Request) bool { + // 1. Explicit dev/staging environment + if h.Config.Env == "development" || h.Config.Env == "staging" || h.Config.Env == "" { + return true } + + // 2. Custom dev header (use this to bypass Cloudflare rewriting X-Forwarded-Host) + if r.Header.Get("X-Dev-Host") != "" { + return true + } + + // 3. Aggressively trust localhost/127.0.0.1 in forwarded host + fh := r.Header.Get("X-Forwarded-Host") + if strings.HasPrefix(fh, "localhost") || strings.HasPrefix(fh, "127.0.0.1") { + return true + } + + // 4. Fallback to host-based checks for dev subdomains host := r.Host - return strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") + if fh != "" { + host = fh + } + return strings.HasPrefix(host, "dev.") || + strings.HasPrefix(r.Host, "dev.") || + strings.HasPrefix(r.Host, "localhost") || + strings.HasPrefix(r.Host, "127.0.0.1") +} + +func (h *Handler) getActualHost(r *http.Request) string { + // 1. Pay attention to custom dev header first + if devHost := r.Header.Get("X-Dev-Host"); devHost != "" { + return devHost + } + + // 2. Standard flow + if h.isDev(r) { + if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" { + return forwardedHost + } + } + return r.Host +} + +func (h *Handler) getBaseURL(r *http.Request) string { + // 1. Pay attention to custom dev proto first + if devProto := r.Header.Get("X-Dev-Proto"); devProto != "" { + return devProto + "://" + h.getActualHost(r) + } + + // 2. Standard flow + scheme := "http" + if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { + scheme = "https" + } + return scheme + "://" + h.getActualHost(r) } func (h *Handler) getCookieDomain(r *http.Request) string { - if h.isLocalhost(r) { - return "localhost" + if h.isDev(r) { + host := h.getActualHost(r) + if strings.Contains(host, ":") { + host, _, _ = strings.Cut(host, ":") + } + return host } return h.Config.Cookie.Domain } func (h *Handler) getOAuthRedirectURL(r *http.Request, providerRedirectURL string) string { - if !h.isLocalhost(r) { + if !h.isDev(r) { return "" } - // If we're on localhost in dev mode, try to use localhost for the redirect URL - // We assume the port is the same as the current request + // Use dynamic BaseURL if on a dev host if strings.Contains(providerRedirectURL, "://") { - // Replace the host part with localhost:port + // Replace the host part with current dynamic BaseURL parts := strings.SplitN(providerRedirectURL, "/", 4) if len(parts) >= 4 { - return "http://" + r.Host + "/" + parts[3] + return h.getBaseURL(r) + "/" + parts[3] } } return "" diff --git a/internal/handler/localhost_test.go b/internal/handler/localhost_test.go index 36d3461..3089aab 100644 --- a/internal/handler/localhost_test.go +++ b/internal/handler/localhost_test.go @@ -8,17 +8,21 @@ import ( "github.com/stretchr/testify/assert" ) -func TestLocalhostDetection(t *testing.T) { +func TestDevDetection(t *testing.T) { tests := []struct { name string env string host string expected bool }{ - {"localhost in dev", "development", "localhost:8080", true}, - {"127.0.0.1 in dev", "development", "127.0.0.1:8080", true}, - {"production host in dev", "development", "api.capyrpi.org", false}, - {"localhost in prod", "production", "localhost:8080", false}, + {"dev environment", "development", "api.capyrpi.org", true}, + {"staging environment", "staging", "api.capyrpi.org", true}, + {"empty environment", "", "api.capyrpi.org", true}, + {"production env with dev host", "production", "dev.capyrpi.org", true}, + {"production env with localhost", "production", "localhost:8080", true}, + {"production env with dev forwarded host", "production", "api.capyrpi.org", true}, + {"production env with X-Dev-Host", "production", "api.capyrpi.org", true}, + {"production environment", "production", "api.capyrpi.org", false}, } for _, tt := range tests { @@ -26,24 +30,60 @@ func TestLocalhostDetection(t *testing.T) { h := &Handler{Config: &config.Config{Env: tt.env}} r := httptest.NewRequest("GET", "/", nil) r.Host = tt.host - assert.Equal(t, tt.expected, h.isLocalhost(r)) + if tt.name == "production env with dev forwarded host" { + r.Header.Set("X-Forwarded-Host", "dev.localhost") + } + if tt.name == "production env with X-Dev-Host" { + r.Header.Set("X-Dev-Host", "localhost:5173") + } + assert.Equal(t, tt.expected, h.isDev(r)) }) } } func TestGetCookieDomain(t *testing.T) { h := &Handler{Config: &config.Config{ - Env: "development", + Env: "development", Cookie: config.CookieConfig{Domain: "capyrpi.org"}, }} + // Direct localhost rLocal := httptest.NewRequest("GET", "/", nil) - rLocal.Host = "localhost:8080" + rLocal.Host = "localhost:3000" assert.Equal(t, "localhost", h.getCookieDomain(rLocal)) + // Proxied to dev.capyrpi.org + rProxied := httptest.NewRequest("GET", "/", nil) + rProxied.Host = "dev.capyrpi.org" + rProxied.Header.Set("X-Forwarded-Host", "localhost:3000") + assert.Equal(t, "localhost", h.getCookieDomain(rProxied)) + + // Production (not in dev mode) + hProd := &Handler{Config: &config.Config{ + Env: "production", + Cookie: config.CookieConfig{Domain: "capyrpi.org"}, + }} rProd := httptest.NewRequest("GET", "/", nil) rProd.Host = "api.capyrpi.org" - assert.Equal(t, "capyrpi.org", h.getCookieDomain(rProd)) + assert.Equal(t, "capyrpi.org", hProd.getCookieDomain(rProd)) +} + +func TestGetBaseURL(t *testing.T) { + h := &Handler{Config: &config.Config{ + Env: "development", + }} + + // Direct localhost + rLocal := httptest.NewRequest("GET", "/", nil) + rLocal.Host = "localhost:3000" + assert.Equal(t, "http://localhost:3000", h.getBaseURL(rLocal)) + + // Proxied with HTTPS + rProxied := httptest.NewRequest("GET", "/", nil) + rProxied.Host = "dev.capyrpi.org" + rProxied.Header.Set("X-Forwarded-Host", "localhost:3000") + rProxied.Header.Set("X-Forwarded-Proto", "https") + assert.Equal(t, "https://localhost:3000", h.getBaseURL(rProxied)) } func TestGetOAuthRedirectURL(t *testing.T) { @@ -53,11 +93,40 @@ func TestGetOAuthRedirectURL(t *testing.T) { providerURL := "https://api.capyrpi.org/api/v1/auth/google/callback" + // Direct localhost rLocal := httptest.NewRequest("GET", "/", nil) - rLocal.Host = "localhost:8080" - assert.Equal(t, "http://localhost:8080/api/v1/auth/google/callback", h.getOAuthRedirectURL(rLocal, providerURL)) + rLocal.Host = "localhost:3000" + assert.Equal(t, "http://localhost:3000/api/v1/auth/google/callback", h.getOAuthRedirectURL(rLocal, providerURL)) + + // Proxied + rProxied := httptest.NewRequest("GET", "/", nil) + rProxied.Host = "dev.capyrpi.org" + rProxied.Header.Set("X-Forwarded-Host", "localhost:3000") + assert.Equal(t, "http://localhost:3000/api/v1/auth/google/callback", h.getOAuthRedirectURL(rProxied, providerURL)) + + // Production env with dev host (should work now) + hDevHost := &Handler{Config: &config.Config{Env: "production"}} + rDevHost := httptest.NewRequest("GET", "/", nil) + rDevHost.Host = "dev.capyrpi.org" + rDevHost.Header.Set("X-Forwarded-Host", "localhost:3000") + assert.Equal(t, "http://localhost:3000/api/v1/auth/google/callback", hDevHost.getOAuthRedirectURL(rDevHost, providerURL)) + + // Production env with X-Dev-Host (bypassing proxy) + rXDev := httptest.NewRequest("GET", "/", nil) + rXDev.Host = "api.capyrpi.org" + rXDev.Header.Set("X-Dev-Host", "localhost:5173") + assert.Equal(t, "http://localhost:5173/api/v1/auth/google/callback", hDevHost.getOAuthRedirectURL(rXDev, providerURL)) + + // Production env with X-Dev-Proto + rXProto := httptest.NewRequest("GET", "/", nil) + rXProto.Host = "api.capyrpi.org" + rXProto.Header.Set("X-Dev-Host", "localhost:5173") + rXProto.Header.Set("X-Dev-Proto", "https") + assert.Equal(t, "https://localhost:5173/api/v1/auth/google/callback", hDevHost.getOAuthRedirectURL(rXProto, providerURL)) + // Production (not in dev mode and not a dev host) + hProd := &Handler{Config: &config.Config{Env: "production"}} rProd := httptest.NewRequest("GET", "/", nil) rProd.Host = "api.capyrpi.org" - assert.Equal(t, "", h.getOAuthRedirectURL(rProd, providerURL)) + assert.Equal(t, "", hProd.getOAuthRedirectURL(rProd, providerURL)) } diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go index 47a0713..eddb581 100644 --- a/internal/middleware/cors.go +++ b/internal/middleware/cors.go @@ -12,9 +12,16 @@ func CORS(allowedOrigins []string, isDev bool) func(http.Handler) http.Handler { origin := r.Header.Get("Origin") allowed := false - // In development, allow any localhost or 127.0.0.1 origin - isLocal := strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") - if isDev && isLocal { + // Trust local development origins + isLocal := strings.HasPrefix(origin, "http://localhost") || + strings.HasPrefix(origin, "http://127.0.0.1") || + strings.HasPrefix(origin, "https://localhost") + + // In development, also trust origin if it matches X-Forwarded-Host + forwardedHost := r.Header.Get("X-Forwarded-Host") + isForwarded := forwardedHost != "" && strings.Contains(origin, forwardedHost) + + if isDev && (isLocal || isForwarded) { allowed = true } @@ -31,10 +38,8 @@ func CORS(allowedOrigins []string, isDev bool) func(http.Handler) http.Handler { } } - // If no allowed origins specified, allow all (development mode) - // But for credentials to work with *, strict browsers block it. - // So good practice: if development, echo back origin. - if !allowed && len(allowedOrigins) == 0 { + // If no allowed origins specified and we are in dev, allow all + if !allowed && isDev && len(allowedOrigins) == 0 { allowed = true } diff --git a/internal/middleware/cors_test.go b/internal/middleware/cors_test.go index 8933612..d29413a 100644 --- a/internal/middleware/cors_test.go +++ b/internal/middleware/cors_test.go @@ -20,6 +20,7 @@ func TestCORS(t *testing.T) { isDev bool expectedOrigin string expectedCreds string + forwardedHost string }{ { name: "AllowedOrigin", @@ -61,18 +62,28 @@ func TestCORS(t *testing.T) { name: "DevModeAllowAll", origin: "https://random.com", method: "GET", - setupOrigins: []string{}, // Empty = allow all - isDev: false, // Though typically true in dev + setupOrigins: []string{}, // Empty = allow all in dev + isDev: true, expectedOrigin: "https://random.com", expectedCreds: "true", }, { name: "AllowedLocalhostInDev", - origin: "http://localhost:5173", + origin: "http://localhost:9999", + method: "GET", + setupOrigins: []string{"https://app.example.com"}, + isDev: true, + expectedOrigin: "http://localhost:9999", + expectedCreds: "true", + }, + { + name: "AllowedForwardedHostInDev", + origin: "https://my-frontend.local", method: "GET", setupOrigins: []string{"https://app.example.com"}, isDev: true, - expectedOrigin: "http://localhost:5173", + forwardedHost: "my-frontend.local", + expectedOrigin: "https://my-frontend.local", expectedCreds: "true", }, { @@ -96,6 +107,9 @@ func TestCORS(t *testing.T) { if tt.origin != "" { req.Header.Set("Origin", tt.origin) } + if tt.forwardedHost != "" { + req.Header.Set("X-Forwarded-Host", tt.forwardedHost) + } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) diff --git a/internal/oauth/google.go b/internal/oauth/google.go index 758324f..98d9345 100644 --- a/internal/oauth/google.go +++ b/internal/oauth/google.go @@ -60,8 +60,17 @@ func (p *GoogleProvider) GetAuthURL(state string, redirectURLOverride string) st } // ExchangeCode exchanges the authorization code for a token and fetches user info -func (p *GoogleProvider) ExchangeCode(ctx context.Context, code string) (*GoogleUserInfo, error) { - token, err := p.config.Exchange(ctx, code) +// If redirectURLOverride is not empty, it will be used instead of the default config +func (p *GoogleProvider) ExchangeCode(ctx context.Context, code string, redirectURLOverride string) (*GoogleUserInfo, error) { + conf := p.config + if redirectURLOverride != "" { + // Clone and override + c := *p.config + c.RedirectURL = redirectURLOverride + conf = &c + } + + token, err := conf.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("failed to exchange code: %w", err) } diff --git a/internal/oauth/microsoft.go b/internal/oauth/microsoft.go index a23bd9d..1041946 100644 --- a/internal/oauth/microsoft.go +++ b/internal/oauth/microsoft.go @@ -63,8 +63,17 @@ func (p *MicrosoftProvider) GetAuthURL(state string, redirectURLOverride string) } // ExchangeCode exchanges the authorization code for a token and fetches user info -func (p *MicrosoftProvider) ExchangeCode(ctx context.Context, code string) (*MicrosoftUserInfo, error) { - token, err := p.config.Exchange(ctx, code) +// If redirectURLOverride is not empty, it will be used instead of the default config +func (p *MicrosoftProvider) ExchangeCode(ctx context.Context, code string, redirectURLOverride string) (*MicrosoftUserInfo, error) { + conf := p.config + if redirectURLOverride != "" { + // Clone and override + c := *p.config + c.RedirectURL = redirectURLOverride + conf = &c + } + + token, err := conf.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("failed to exchange code: %w", err) } From 6201956a2d7ab028972b52bf0763d0f601958362 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 26 Mar 2026 12:58:12 -0400 Subject: [PATCH 09/29] fix(user): relaxed user update permissions --- internal/handler/users.go | 18 ++++- internal/handler/users_test.go | 130 +++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/internal/handler/users.go b/internal/handler/users.go index 340a592..13dba98 100644 --- a/internal/handler/users.go +++ b/internal/handler/users.go @@ -40,7 +40,7 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { // UpdateUser updates a user's profile // @Summary Update user -// @Description Updates a user's profile. Users can only update their own profile. +// @Description Updates a user's profile. Only role changes require the caller to have the dev role. // @Tags users // @Accept json // @Produce json @@ -60,7 +60,7 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { return } - authenticatedUser, ok := h.requireSelfOrDev(w, r, uid) + authenticatedUser, _, ok := h.requireAuthenticatedUserRecord(w, r) if !ok { return } @@ -71,13 +71,23 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { return } + targetUser, err := h.queries.GetUserByID(r.Context(), uid) + if err != nil { + h.handleDBError(w, err) + return + } + var role database.NullUserRole if req.Role != nil { - if !authenticatedUser.Role.Valid || authenticatedUser.Role.UserRole != database.UserRoleDev { + requestedRole := database.UserRole(*req.Role) + roleChanged := !targetUser.Role.Valid || targetUser.Role.UserRole != requestedRole + if roleChanged && (!authenticatedUser.Role.Valid || authenticatedUser.Role.UserRole != database.UserRoleDev) { h.respondError(w, http.StatusForbidden, "Only dev may update user roles") return } - role = database.NullUserRole{UserRole: database.UserRole(*req.Role), Valid: true} + if roleChanged { + role = database.NullUserRole{UserRole: requestedRole, Valid: true} + } } user, err := h.queries.UpdateUser(r.Context(), database.UpdateUserParams{ diff --git a/internal/handler/users_test.go b/internal/handler/users_test.go index 44fe5a3..62c8b19 100644 --- a/internal/handler/users_test.go +++ b/internal/handler/users_test.go @@ -1,6 +1,9 @@ package handler_test import ( + "bytes" + "context" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -9,7 +12,9 @@ import ( "github.com/capyrpi/api/internal/config" "github.com/capyrpi/api/internal/database" "github.com/capyrpi/api/internal/database/mocks" + "github.com/capyrpi/api/internal/dto" "github.com/capyrpi/api/internal/handler" + "github.com/capyrpi/api/internal/middleware" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -83,3 +88,128 @@ func TestGetUser(t *testing.T) { }) } } + +func TestUpdateUser(t *testing.T) { + targetUID := uuid.New() + authenticatedUID := uuid.New() + firstName := "Updated" + currentRole := "student" + role := "faculty" + + tests := []struct { + name string + requestBody dto.UpdateUserRequest + mockSetup func(*mocks.Querier) + setupContext func() context.Context + expectedStatus int + }{ + { + name: "NonDevCanUpdateWhenSubmittedRoleMatchesCurrentRole", + requestBody: dto.UpdateUserRequest{ + FirstName: &firstName, + Role: ¤tRole, + }, + mockSetup: func(m *mocks.Querier) { + m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ + Uid: authenticatedUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ + Uid: targetUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + m.On("UpdateUser", mock.Anything, mock.MatchedBy(func(arg database.UpdateUserParams) bool { + return arg.Uid == targetUID && + arg.FirstName.Valid && arg.FirstName.String == firstName && + !arg.Role.Valid + })).Return(database.User{ + Uid: targetUID, + FirstName: firstName, + LastName: "Doe", + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: authenticatedUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") + }, + expectedStatus: http.StatusOK, + }, + { + name: "NonDevCannotUpdateRole", + requestBody: dto.UpdateUserRequest{ + Role: &role, + }, + mockSetup: func(m *mocks.Querier) { + m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ + Uid: authenticatedUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ + Uid: targetUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: authenticatedUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") + }, + expectedStatus: http.StatusForbidden, + }, + { + name: "DevCanUpdateRole", + requestBody: dto.UpdateUserRequest{ + Role: &role, + }, + mockSetup: func(m *mocks.Querier) { + m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ + Uid: authenticatedUID, + Role: database.NullUserRole{UserRole: database.UserRoleDev, Valid: true}, + }, nil) + m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ + Uid: targetUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + m.On("UpdateUser", mock.Anything, mock.MatchedBy(func(arg database.UpdateUserParams) bool { + return arg.Uid == targetUID && + arg.Role.Valid && + arg.Role.UserRole == database.UserRoleFaculty + })).Return(database.User{ + Uid: targetUID, + Role: database.NullUserRole{UserRole: database.UserRoleFaculty, Valid: true}, + }, nil) + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: authenticatedUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") + }, + expectedStatus: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockQueries := mocks.NewQuerier(t) + tt.mockSetup(mockQueries) + + h := handler.New(mockQueries, &config.Config{}) + r := chi.NewRouter() + r.Put("/users/{uid}", h.UpdateUser) + + body, _ := json.Marshal(tt.requestBody) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/users/%s", targetUID), bytes.NewBuffer(body)) + req = req.WithContext(tt.setupContext()) + rr := httptest.NewRecorder() + + r.ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + }) + } +} From 28d103b2ef19fa1ab1e2ff7cf49749ade2e5f2ea Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 26 Mar 2026 13:11:36 -0400 Subject: [PATCH 10/29] fix(user): tightened perms to not allow auth users to update other users :/ --- internal/handler/users_test.go | 84 +++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/internal/handler/users_test.go b/internal/handler/users_test.go index 62c8b19..78eabcf 100644 --- a/internal/handler/users_test.go +++ b/internal/handler/users_test.go @@ -91,33 +91,39 @@ func TestGetUser(t *testing.T) { func TestUpdateUser(t *testing.T) { targetUID := uuid.New() - authenticatedUID := uuid.New() + otherUID := uuid.New() firstName := "Updated" currentRole := "student" - role := "faculty" + newRole := "faculty" tests := []struct { name string requestBody dto.UpdateUserRequest - mockSetup func(*mocks.Querier) setupContext func() context.Context + mockSetup func(*mocks.Querier) expectedStatus int }{ { - name: "NonDevCanUpdateWhenSubmittedRoleMatchesCurrentRole", + name: "SelfCanUpdateWhenRoleUnchanged", requestBody: dto.UpdateUserRequest{ FirstName: &firstName, Role: ¤tRole, }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: targetUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") + }, mockSetup: func(m *mocks.Querier) { - m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ - Uid: authenticatedUID, + m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ + Uid: targetUID, Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, - }, nil) + }, nil).Once() m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ Uid: targetUID, Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, - }, nil) + }, nil).Once() m.On("UpdateUser", mock.Anything, mock.MatchedBy(func(arg database.UpdateUserParams) bool { return arg.Uid == targetUID && arg.FirstName.Valid && arg.FirstName.String == firstName && @@ -129,45 +135,65 @@ func TestUpdateUser(t *testing.T) { Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, }, nil) }, + expectedStatus: http.StatusOK, + }, + { + name: "NonDevCannotUpdateAnotherUser", + requestBody: dto.UpdateUserRequest{ + FirstName: &firstName, + Role: ¤tRole, + }, setupContext: func() context.Context { ctx := context.Background() - claims := &middleware.UserClaims{UserID: authenticatedUID.String()} + claims := &middleware.UserClaims{UserID: otherUID.String()} ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) return context.WithValue(ctx, middleware.AuthTypeKey, "human") }, - expectedStatus: http.StatusOK, + mockSetup: func(m *mocks.Querier) { + m.On("GetUserByID", mock.Anything, otherUID).Return(database.User{ + Uid: otherUID, + Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, + }, nil) + }, + expectedStatus: http.StatusForbidden, }, { - name: "NonDevCannotUpdateRole", + name: "NonDevCannotChangeOwnRole", requestBody: dto.UpdateUserRequest{ - Role: &role, + Role: &newRole, + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: targetUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") }, mockSetup: func(m *mocks.Querier) { - m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ - Uid: authenticatedUID, + m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ + Uid: targetUID, Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, - }, nil) + }, nil).Once() m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ Uid: targetUID, Role: database.NullUserRole{UserRole: database.UserRoleStudent, Valid: true}, - }, nil) - }, - setupContext: func() context.Context { - ctx := context.Background() - claims := &middleware.UserClaims{UserID: authenticatedUID.String()} - ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) - return context.WithValue(ctx, middleware.AuthTypeKey, "human") + }, nil).Once() }, expectedStatus: http.StatusForbidden, }, { - name: "DevCanUpdateRole", + name: "DevCanChangeAnotherUsersRole", requestBody: dto.UpdateUserRequest{ - Role: &role, + Role: &newRole, + }, + setupContext: func() context.Context { + ctx := context.Background() + claims := &middleware.UserClaims{UserID: otherUID.String()} + ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) + return context.WithValue(ctx, middleware.AuthTypeKey, "human") }, mockSetup: func(m *mocks.Querier) { - m.On("GetUserByID", mock.Anything, authenticatedUID).Return(database.User{ - Uid: authenticatedUID, + m.On("GetUserByID", mock.Anything, otherUID).Return(database.User{ + Uid: otherUID, Role: database.NullUserRole{UserRole: database.UserRoleDev, Valid: true}, }, nil) m.On("GetUserByID", mock.Anything, targetUID).Return(database.User{ @@ -183,12 +209,6 @@ func TestUpdateUser(t *testing.T) { Role: database.NullUserRole{UserRole: database.UserRoleFaculty, Valid: true}, }, nil) }, - setupContext: func() context.Context { - ctx := context.Background() - claims := &middleware.UserClaims{UserID: authenticatedUID.String()} - ctx = context.WithValue(ctx, middleware.UserClaimsKey, claims) - return context.WithValue(ctx, middleware.AuthTypeKey, "human") - }, expectedStatus: http.StatusOK, }, } From 93719de7f9b4cf4ae1bca26a2e9055cfe98f76c9 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 26 Mar 2026 13:16:02 -0400 Subject: [PATCH 11/29] fix(user): another pass --- internal/handler/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/users.go b/internal/handler/users.go index 13dba98..2ecbfca 100644 --- a/internal/handler/users.go +++ b/internal/handler/users.go @@ -60,7 +60,7 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { return } - authenticatedUser, _, ok := h.requireAuthenticatedUserRecord(w, r) + authenticatedUser, ok := h.requireSelfOrDev(w, r, uid) if !ok { return } From 2038bd5a7a1cdacc0b91865abf5e3a388b27b5eb Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:14:15 -0400 Subject: [PATCH 12/29] Use hardcoded public link endpoint --- internal/handler/links.go | 7 +++++-- internal/router/router.go | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/handler/links.go b/internal/handler/links.go index b19ff95..57c13b0 100644 --- a/internal/handler/links.go +++ b/internal/handler/links.go @@ -20,6 +20,9 @@ import ( const QR_FG_COLOR = "#067b76" const QR_BG_COLOR = "#fcfdfe" +// TODO this should be a global variable, this is very brittle +const PUBLIC_LINK_ENDPOINT = "www.capyrpi.org/api/r/" + // CreateLink creates a new dynamic link // @Summary Create link // @Description Creates a new dynamic link for an organization. Requires org_admin role. @@ -281,7 +284,7 @@ func (h *Handler) GetQRCode(w http.ResponseWriter, r *http.Request) { return } - qrc, err := qrcode.NewWith(link.EndpointUrl) + qrc, err := qrcode.NewWith(PUBLIC_LINK_ENDPOINT + link.EndpointUrl) if err != nil { slog.Error("failed to generate QR code", "error", err) h.respondError(w, http.StatusInternalServerError, "Failed to generate QR code") @@ -291,7 +294,7 @@ func (h *Handler) GetQRCode(w http.ResponseWriter, r *http.Request) { qrcOpts := []standard.ImageOption{ standard.WithBgColorRGBHex(QR_BG_COLOR), standard.WithFgColorRGBHex(QR_FG_COLOR), - standard.WithCircleShape(), + // standard.WithCircleShape(), } w.Header().Set("Content-Type", "image/png") diff --git a/internal/router/router.go b/internal/router/router.go index cfa0dce..6d43cf9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -26,6 +26,7 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed r.Get("/health", h.Health) // Link resolution (public) + // TODO Use a global variable to link this with links.go r.Get("/r/{endpoint_url}", h.ResolveLink) // Swagger UI (public) - Only in non-production environments From c245764200d59ae5d614e5f5f669f2b0fc7f232f Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 26 Mar 2026 22:29:48 -0400 Subject: [PATCH 13/29] updated event data to include title --- docs/schema/schema.json | 260 +++++++++++++++++- docs/swagger/docs.go | 11 +- docs/swagger/swagger.json | 11 +- docs/swagger/swagger.yaml | 9 +- internal/database/models.go | 1 + internal/database/queries.sql | 7 +- internal/database/queries.sql.go | 40 ++- internal/dto/dto.go | 3 + internal/handler/events.go | 3 + internal/handler/users.go | 1 + .../20260316192946_initial_schema.up.sql | 1 + .../20260327022209_add_event_title.down.sql | 2 + .../20260327022209_add_event_title.up.sql | 2 + schema.sql | 1 + 14 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 migrations/20260327022209_add_event_title.down.sql create mode 100644 migrations/20260327022209_add_event_title.up.sql diff --git a/docs/schema/schema.json b/docs/schema/schema.json index 1e46115..cf6234a 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -583,6 +583,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -69461,7 +69487,7 @@ "insert_into_table": null }, { - "text": "SELECT eid, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1", + "text": "SELECT eid, title, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1", "name": "GetEventByID", "cmd": ":one", "columns": [ @@ -69491,6 +69517,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -69658,7 +69710,7 @@ "insert_into_table": null }, { - "text": "SELECT eid, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2", + "text": "SELECT eid, title, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2", "name": "ListEvents", "cmd": ":many", "columns": [ @@ -69688,6 +69740,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -69876,7 +69954,7 @@ "insert_into_table": null }, { - "text": "SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified\nFROM events e\nJOIN event_hosting eh ON e.eid = eh.eid\nWHERE eh.oid = $1\nORDER BY e.event_time DESC\nLIMIT $2 OFFSET $3", + "text": "SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified\nFROM events e\nJOIN event_hosting eh ON e.eid = eh.eid\nWHERE eh.oid = $1\nORDER BY e.event_time DESC\nLIMIT $2 OFFSET $3", "name": "ListEventsByOrg", "cmd": ":many", "columns": [ @@ -69906,6 +69984,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -70123,7 +70227,7 @@ "insert_into_table": null }, { - "text": "INSERT INTO events (location, event_time, description)\nVALUES ($1, $2, $3)\nRETURNING eid, location, event_time, description, date_created, date_modified", + "text": "INSERT INTO events (title, location, event_time, description)\nVALUES ($1, $2, $3, $4)\nRETURNING eid, title, location, event_time, description, date_created, date_modified", "name": "CreateEvent", "cmd": ":one", "columns": [ @@ -70153,6 +70257,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -70287,6 +70417,35 @@ "params": [ { "number": 1, + "column": { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 2, "column": { "name": "location", "not_null": false, @@ -70315,7 +70474,7 @@ } }, { - "number": 2, + "number": 3, "column": { "name": "event_time", "not_null": false, @@ -70344,7 +70503,7 @@ } }, { - "number": 3, + "number": 4, "column": { "name": "description", "not_null": false, @@ -70382,7 +70541,7 @@ } }, { - "text": "UPDATE events\nSET location = COALESCE($2, location),\n event_time = COALESCE($3, event_time),\n description = COALESCE($4, description)\nWHERE eid = $1\nRETURNING eid, location, event_time, description, date_created, date_modified", + "text": "UPDATE events\nSET title = COALESCE($2, title),\n location = COALESCE($3, location),\n event_time = COALESCE($4, event_time),\n description = COALESCE($5, description)\nWHERE eid = $1\nRETURNING eid, title, location, event_time, description, date_created, date_modified", "name": "UpdateEvent", "cmd": ":one", "columns": [ @@ -70412,6 +70571,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, @@ -70575,6 +70760,35 @@ }, { "number": 2, + "column": { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": true, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "public", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + } + }, + { + "number": 3, "column": { "name": "location", "not_null": false, @@ -70603,7 +70817,7 @@ } }, { - "number": 3, + "number": 4, "column": { "name": "event_time", "not_null": false, @@ -70632,7 +70846,7 @@ } }, { - "number": 4, + "number": 5, "column": { "name": "description", "not_null": false, @@ -71421,7 +71635,7 @@ "insert_into_table": null }, { - "text": "SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered\nFROM events e\nJOIN event_registrations er ON e.eid = er.eid\nWHERE er.uid = $1\nORDER BY e.event_time DESC", + "text": "SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered\nFROM events e\nJOIN event_registrations er ON e.eid = er.eid\nWHERE er.uid = $1\nORDER BY e.event_time DESC", "name": "GetUserEvents", "cmd": ":many", "columns": [ @@ -71451,6 +71665,32 @@ "unsigned": false, "array_dims": 0 }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, { "name": "location", "not_null": false, diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 7116052..8903547 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -1348,7 +1348,7 @@ const docTemplate = `{ "CookieAuth": [] } ], - "description": "Updates a user's profile. Users can only update their own profile.", + "description": "Updates a user's profile. Only role changes require the caller to have the dev role.", "consumes": [ "application/json" ], @@ -1575,6 +1575,9 @@ const docTemplate = `{ "org_id": { "description": "Which org is hosting", "type": "string" + }, + "title": { + "type": "string" } } }, @@ -1638,6 +1641,9 @@ const docTemplate = `{ }, "location": { "type": "string" + }, + "title": { + "type": "string" } } }, @@ -1707,6 +1713,9 @@ const docTemplate = `{ }, "location": { "type": "string" + }, + "title": { + "type": "string" } } }, diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index ef1715f..e6a4d10 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1342,7 +1342,7 @@ "CookieAuth": [] } ], - "description": "Updates a user's profile. Users can only update their own profile.", + "description": "Updates a user's profile. Only role changes require the caller to have the dev role.", "consumes": [ "application/json" ], @@ -1569,6 +1569,9 @@ "org_id": { "description": "Which org is hosting", "type": "string" + }, + "title": { + "type": "string" } } }, @@ -1632,6 +1635,9 @@ }, "location": { "type": "string" + }, + "title": { + "type": "string" } } }, @@ -1701,6 +1707,9 @@ }, "location": { "type": "string" + }, + "title": { + "type": "string" } } }, diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 73c8faf..e2cccbc 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -20,6 +20,8 @@ definitions: org_id: description: Which org is hosting type: string + title: + type: string required: - org_id type: object @@ -64,6 +66,8 @@ definitions: type: string location: type: string + title: + type: string type: object dto.OrgMemberResponse: properties: @@ -109,6 +113,8 @@ definitions: type: string location: type: string + title: + type: string type: object dto.UpdateOrganizationRequest: properties: @@ -1120,7 +1126,8 @@ paths: put: consumes: - application/json - description: Updates a user's profile. Users can only update their own profile. + description: Updates a user's profile. Only role changes require the caller + to have the dev role. parameters: - description: User UUID in: path diff --git a/internal/database/models.go b/internal/database/models.go index 16ba637..bfd5811 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -70,6 +70,7 @@ type BotToken struct { type Event struct { Eid uuid.UUID `json:"eid"` + Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` diff --git a/internal/database/queries.sql b/internal/database/queries.sql index 3344cfd..67119fc 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -87,13 +87,14 @@ ORDER BY e.event_time DESC LIMIT $2 OFFSET $3; -- name: CreateEvent :one -INSERT INTO events (location, event_time, description) -VALUES ($1, $2, $3) +INSERT INTO events (title, location, event_time, description) +VALUES ($1, $2, $3, $4) RETURNING *; -- name: UpdateEvent :one UPDATE events -SET location = COALESCE(sqlc.narg('location'), location), +SET title = COALESCE(sqlc.narg('title'), title), + location = COALESCE(sqlc.narg('location'), location), event_time = COALESCE(sqlc.narg('event_time'), event_time), description = COALESCE(sqlc.narg('description'), description) WHERE eid = $1 diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 457aa5b..145df82 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -80,22 +80,29 @@ func (q *Queries) CreateBotToken(ctx context.Context, arg CreateBotTokenParams) } const createEvent = `-- name: CreateEvent :one -INSERT INTO events (location, event_time, description) -VALUES ($1, $2, $3) -RETURNING eid, location, event_time, description, date_created, date_modified +INSERT INTO events (title, location, event_time, description) +VALUES ($1, $2, $3, $4) +RETURNING eid, title, location, event_time, description, date_created, date_modified ` type CreateEventParams struct { + Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` } func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { - row := q.db.QueryRow(ctx, createEvent, arg.Location, arg.EventTime, arg.Description) + row := q.db.QueryRow(ctx, createEvent, + arg.Title, + arg.Location, + arg.EventTime, + arg.Description, + ) var i Event err := row.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, @@ -215,7 +222,7 @@ func (q *Queries) GetBotTokenByID(ctx context.Context, tokenID uuid.UUID) (BotTo } const getEventByID = `-- name: GetEventByID :one -SELECT eid, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1 +SELECT eid, title, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1 ` func (q *Queries) GetEventByID(ctx context.Context, eid uuid.UUID) (Event, error) { @@ -223,6 +230,7 @@ func (q *Queries) GetEventByID(ctx context.Context, eid uuid.UUID) (Event, error var i Event err := row.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, @@ -409,7 +417,7 @@ func (q *Queries) GetUserByID(ctx context.Context, uid uuid.UUID) (User, error) } const getUserEvents = `-- name: GetUserEvents :many -SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered +SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered FROM events e JOIN event_registrations er ON e.eid = er.eid WHERE er.uid = $1 @@ -418,6 +426,7 @@ ORDER BY e.event_time DESC type GetUserEventsRow struct { Eid uuid.UUID `json:"eid"` + Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` @@ -439,6 +448,7 @@ func (q *Queries) GetUserEvents(ctx context.Context, uid uuid.UUID) ([]GetUserEv var i GetUserEventsRow if err := rows.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, @@ -593,7 +603,7 @@ func (q *Queries) ListBotTokens(ctx context.Context) ([]ListBotTokensRow, error) } const listEvents = `-- name: ListEvents :many -SELECT eid, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2 +SELECT eid, title, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2 ` type ListEventsParams struct { @@ -612,6 +622,7 @@ func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event var i Event if err := rows.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, @@ -629,7 +640,7 @@ func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event } const listEventsByOrg = `-- name: ListEventsByOrg :many -SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified +SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified FROM events e JOIN event_hosting eh ON e.eid = eh.eid WHERE eh.oid = $1 @@ -654,6 +665,7 @@ func (q *Queries) ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams var i Event if err := rows.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, @@ -809,15 +821,17 @@ func (q *Queries) UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) const updateEvent = `-- name: UpdateEvent :one UPDATE events -SET location = COALESCE($2, location), - event_time = COALESCE($3, event_time), - description = COALESCE($4, description) +SET title = COALESCE($2, title), + location = COALESCE($3, location), + event_time = COALESCE($4, event_time), + description = COALESCE($5, description) WHERE eid = $1 -RETURNING eid, location, event_time, description, date_created, date_modified +RETURNING eid, title, location, event_time, description, date_created, date_modified ` type UpdateEventParams struct { Eid uuid.UUID `json:"eid"` + Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` @@ -826,6 +840,7 @@ type UpdateEventParams struct { func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) { row := q.db.QueryRow(ctx, updateEvent, arg.Eid, + arg.Title, arg.Location, arg.EventTime, arg.Description, @@ -833,6 +848,7 @@ func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event var i Event err := row.Scan( &i.Eid, + &i.Title, &i.Location, &i.EventTime, &i.Description, diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 692c076..facf79e 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -83,6 +83,7 @@ type AddMemberRequest struct { // ============================================================================ type CreateEventRequest struct { + Title string `json:"title,omitempty"` Location string `json:"location,omitempty"` EventTime *time.Time `json:"event_time,omitempty"` Description string `json:"description,omitempty"` @@ -90,6 +91,7 @@ type CreateEventRequest struct { } type UpdateEventRequest struct { + Title *string `json:"title,omitempty"` Location *string `json:"location,omitempty"` EventTime *time.Time `json:"event_time,omitempty"` Description *string `json:"description,omitempty"` @@ -97,6 +99,7 @@ type UpdateEventRequest struct { type EventResponse struct { EID uuid.UUID `json:"eid"` + Title *string `json:"title,omitempty"` Location *string `json:"location,omitempty"` EventTime *time.Time `json:"event_time,omitempty"` Description *string `json:"description,omitempty"` diff --git a/internal/handler/events.go b/internal/handler/events.go index 81c58ef..46d6a5d 100644 --- a/internal/handler/events.go +++ b/internal/handler/events.go @@ -72,6 +72,7 @@ func (h *Handler) CreateEvent(w http.ResponseWriter, r *http.Request) { } event, err := h.queries.CreateEvent(r.Context(), database.CreateEventParams{ + Title: toPgTextFromString(req.Title), Location: toPgTextFromString(req.Location), EventTime: toPgTimestamp(req.EventTime), Description: toPgTextFromString(req.Description), @@ -155,6 +156,7 @@ func (h *Handler) UpdateEvent(w http.ResponseWriter, r *http.Request) { event, err := h.queries.UpdateEvent(r.Context(), database.UpdateEventParams{ Eid: eid, + Title: toPgText(req.Title), Location: toPgText(req.Location), EventTime: toPgTimestamp(req.EventTime), Description: toPgText(req.Description), @@ -418,6 +420,7 @@ func (h *Handler) ListEventsByOrg(w http.ResponseWriter, r *http.Request) { func toEventResponse(event database.Event) dto.EventResponse { return dto.EventResponse{ EID: event.Eid, + Title: fromPgText(event.Title), Location: fromPgText(event.Location), EventTime: fromPgTimestamp(event.EventTime), Description: fromPgText(event.Description), diff --git a/internal/handler/users.go b/internal/handler/users.go index 2ecbfca..066b0a3 100644 --- a/internal/handler/users.go +++ b/internal/handler/users.go @@ -205,6 +205,7 @@ func (h *Handler) GetUserEvents(w http.ResponseWriter, r *http.Request) { for i, event := range events { response[i] = dto.EventResponse{ EID: event.Eid, + Title: fromPgText(event.Title), Location: fromPgText(event.Location), EventTime: fromPgTimestamp(event.EventTime), Description: fromPgText(event.Description), diff --git a/migrations/20260316192946_initial_schema.up.sql b/migrations/20260316192946_initial_schema.up.sql index aff59d4..a2997a6 100644 --- a/migrations/20260316192946_initial_schema.up.sql +++ b/migrations/20260316192946_initial_schema.up.sql @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS org_members ( CREATE TABLE IF NOT EXISTS events ( eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT, location TEXT, event_time TIMESTAMP, description TEXT, diff --git a/migrations/20260327022209_add_event_title.down.sql b/migrations/20260327022209_add_event_title.down.sql new file mode 100644 index 0000000..34e6221 --- /dev/null +++ b/migrations/20260327022209_add_event_title.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE events +DROP COLUMN title; diff --git a/migrations/20260327022209_add_event_title.up.sql b/migrations/20260327022209_add_event_title.up.sql new file mode 100644 index 0000000..8ae34a2 --- /dev/null +++ b/migrations/20260327022209_add_event_title.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE events +ADD COLUMN title TEXT; diff --git a/schema.sql b/schema.sql index d67c444..5853ab2 100644 --- a/schema.sql +++ b/schema.sql @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS org_members ( CREATE TABLE IF NOT EXISTS events ( eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT, location TEXT, event_time TIMESTAMP, description TEXT, From 5c6cc40bc98126701a06377a3bd720b4ef72f32e Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 26 Mar 2026 22:34:14 -0400 Subject: [PATCH 14/29] reverted init_scheme for version history --- migrations/20260316192946_initial_schema.up.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations/20260316192946_initial_schema.up.sql b/migrations/20260316192946_initial_schema.up.sql index a2997a6..aff59d4 100644 --- a/migrations/20260316192946_initial_schema.up.sql +++ b/migrations/20260316192946_initial_schema.up.sql @@ -44,7 +44,6 @@ CREATE TABLE IF NOT EXISTS org_members ( CREATE TABLE IF NOT EXISTS events ( eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT, location TEXT, event_time TIMESTAMP, description TEXT, From 7ddf171f560018206b81996f669861ea34e1f906 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:08:24 -0400 Subject: [PATCH 15/29] Revert to postgres 16 and add migrate scripts --- .github/workflows/ci.yml | 2 +- README.md | 2 +- docker-compose.yml | 2 +- internal/router/router.go | 2 ++ internal/testutils/container.go | 2 +- migrations/20260326224918_add_links.down.sql | 2 ++ migrations/20260326224918_add_links.up.sql | 14 ++++++++++++++ tests/benchmarks/suite_test.go | 2 +- 8 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 migrations/20260326224918_add_links.down.sql create mode 100644 migrations/20260326224918_add_links.up.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 781d3ca..b252456 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: services: # Optional: We use testcontainers, but having a service is good for fallback or specific DB tests postgres: - image: postgres:18-alpine + image: postgres:16-alpine env: POSTGRES_USER: test POSTGRES_PASSWORD: test diff --git a/README.md b/README.md index ac0b90a..4ba4678 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ To run the full stack (API + Postgres + Cloudflare Tunnel), update your `.env` f ```yaml services: db: - image: postgres:18-alpine + image: postgres:16-alpine env_file: - .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index 428fcf6..7c643db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:18-alpine + image: postgres:16-alpine env_file: - .env environment: diff --git a/internal/router/router.go b/internal/router/router.go index 832527a..f70a12b 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -107,6 +107,8 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed }) }) + mountProtectedRoutes(r, h, jwtSecret) + // Bot routes (M2M auth) r.Group(func(r chi.Router) { r.Use(middleware.M2MAuth(queries)) diff --git a/internal/testutils/container.go b/internal/testutils/container.go index 920da82..8f507df 100644 --- a/internal/testutils/container.go +++ b/internal/testutils/container.go @@ -54,7 +54,7 @@ func SetupTestDB(t *testing.T) *pgxpool.Pool { schemaPath := filepath.Join(projectRoot, "schema.sql") pgContainer, err := postgres.Run(ctx, - "postgres:18-alpine", + "postgres:16-alpine", postgres.WithInitScripts(schemaPath), postgres.WithDatabase("test_db"), postgres.WithUsername("test"), diff --git a/migrations/20260326224918_add_links.down.sql b/migrations/20260326224918_add_links.down.sql new file mode 100644 index 0000000..108a49b --- /dev/null +++ b/migrations/20260326224918_add_links.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS link_visits; +DROP TABLE IF EXISTS links; diff --git a/migrations/20260326224918_add_links.up.sql b/migrations/20260326224918_add_links.up.sql new file mode 100644 index 0000000..8d15324 --- /dev/null +++ b/migrations/20260326224918_add_links.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS links ( + lid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + endpoint_url TEXT NOT NULL UNIQUE, + dest_url TEXT NOT NULL, + oid UUID NOT NULL REFERENCES organizations(oid) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS link_visits ( + lvid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lid UUID NOT NULL REFERENCES links(lid) ON DELETE CASCADE, + uid UUID REFERENCES users(uid) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/tests/benchmarks/suite_test.go b/tests/benchmarks/suite_test.go index 3e8b8de..66f574b 100644 --- a/tests/benchmarks/suite_test.go +++ b/tests/benchmarks/suite_test.go @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { log.Printf("Using schema from: %s", schemaPath) pgContainer, err := postgres.Run(ctx, - "postgres:18-alpine", + "postgres:16-alpine", postgres.WithInitScripts(schemaPath), postgres.WithDatabase("bench_db"), postgres.WithUsername("bench"), From 9dd7817629a80d4fdde7d244444de1258415ad87 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:11:18 -0400 Subject: [PATCH 16/29] Revert schema.sql to use postgres 16 --- schema.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/schema.sql b/schema.sql index 2ee9abf..e648e17 100644 --- a/schema.sql +++ b/schema.sql @@ -14,7 +14,7 @@ $$ language 'plpgsql'; -- 2. Tables CREATE TABLE IF NOT EXISTS users ( - uid UUID PRIMARY KEY DEFAULT uuidv4(), + uid UUID PRIMARY KEY DEFAULT gen_random_uuid(), first_name TEXT NOT NULL, last_name TEXT NOT NULL, personal_email TEXT UNIQUE, @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS users ( ); CREATE TABLE IF NOT EXISTS organizations ( - oid UUID PRIMARY KEY DEFAULT uuidv4(), + oid UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, date_created DATE DEFAULT CURRENT_DATE, date_modified DATE DEFAULT CURRENT_DATE @@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS org_members ( ); CREATE TABLE IF NOT EXISTS events ( - eid UUID PRIMARY KEY DEFAULT uuidv4(), + eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), location TEXT, event_time TIMESTAMP, description TEXT, @@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS event_registrations ( -- 3. Bot Tokens (global access for M2M authentication) CREATE TABLE IF NOT EXISTS bot_tokens ( - token_id UUID PRIMARY KEY DEFAULT uuidv4(), + token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), token_hash TEXT NOT NULL, -- bcrypt hash of the token name TEXT NOT NULL, -- human-readable name for the bot created_by UUID NOT NULL REFERENCES users(uid), @@ -79,7 +79,7 @@ CREATE TABLE IF NOT EXISTS bot_tokens ( ); CREATE TABLE IF NOT EXISTS links ( - lid UUID PRIMARY KEY DEFAULT uuidv4(), + lid UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint_url TEXT NOT NULL UNIQUE, dest_url TEXT NOT NULL, oid UUID NOT NULL REFERENCES organizations(oid) ON DELETE CASCADE, @@ -87,7 +87,7 @@ CREATE TABLE IF NOT EXISTS links ( ); CREATE TABLE IF NOT EXISTS link_visits ( - lvid UUID PRIMARY KEY DEFAULT uuidv7(), + lvid UUID PRIMARY KEY DEFAULT gen_random_uuid(), lid UUID NOT NULL REFERENCES links(lid) ON DELETE CASCADE, uid UUID REFERENCES users(uid) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP From 26aa7558fe06c86175d144cdbea88f14c7d6d85c Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 27 Mar 2026 14:24:26 -0400 Subject: [PATCH 17/29] user joining orgs --- internal/handler/organizations.go | 22 +++++++++++++++---- internal/handler/organizations_test.go | 29 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/handler/organizations.go b/internal/handler/organizations.go index 28f6eaa..c6c3607 100644 --- a/internal/handler/organizations.go +++ b/internal/handler/organizations.go @@ -259,16 +259,30 @@ func (h *Handler) AddOrgMember(w http.ResponseWriter, r *http.Request) { return } - if _, ok := h.requireOrgAdmin(w, r, oid); !ok { - return - } - var req dto.AddMemberRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.respondError(w, http.StatusBadRequest, "Invalid request body") return } + switch middleware.GetAuthType(r.Context()) { + case "bot": + // Bots retain full access to add members on behalf of users. + default: + authenticatedUID, _, ok := h.requireAuthenticatedUser(w, r) + if !ok { + return + } + + // Allow self-join through the existing members endpoint, but never allow + // a human user to self-promote to admin. + if req.UID != authenticatedUID || req.IsAdmin { + if _, ok := h.requireOrgAdmin(w, r, oid); !ok { + return + } + } + } + if err := h.queries.AddOrgMember(r.Context(), database.AddOrgMemberParams{ Uid: req.UID, Oid: oid, diff --git a/internal/handler/organizations_test.go b/internal/handler/organizations_test.go index f302cf8..e6db509 100644 --- a/internal/handler/organizations_test.go +++ b/internal/handler/organizations_test.go @@ -14,6 +14,7 @@ import ( "github.com/capyrpi/api/internal/dto" "github.com/capyrpi/api/internal/handler" "github.com/capyrpi/api/internal/middleware" + "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -128,3 +129,31 @@ func TestListOrganizations(t *testing.T) { }) } } + +func TestAddOrgMemberAllowsSelfJoin(t *testing.T) { + uid := uuid.New() + oid := uuid.New() + + mockQueries := mocks.NewQuerier(t) + mockQueries.On("AddOrgMember", mock.Anything, mock.MatchedBy(func(arg database.AddOrgMemberParams) bool { + return arg.Oid == oid && arg.Uid == uid && arg.IsAdmin.Valid && !arg.IsAdmin.Bool + })).Return(nil) + + h := handler.New(mockQueries, &config.Config{}) + + body, _ := json.Marshal(dto.AddMemberRequest{ + UID: uid, + IsAdmin: false, + }) + + req := httptest.NewRequest("POST", "/organizations/"+oid.String()+"/members", bytes.NewBuffer(body)) + req = req.WithContext(context.WithValue(context.Background(), middleware.UserClaimsKey, &middleware.UserClaims{UserID: uid.String()})) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("oid", oid.String()) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + rr := httptest.NewRecorder() + http.HandlerFunc(h.AddOrgMember).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusCreated, rr.Code) +} From fa57a75fc80c883fbbf99f9694ddcf0b94896b4c Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:53:05 -0400 Subject: [PATCH 18/29] Point sqlc.yaml to migrations --- docs/schema/schema.json | 2 +- schema.sql | 107 ---------------------------------------- 2 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 schema.sql diff --git a/docs/schema/schema.json b/docs/schema/schema.json index 57db3d8..7fa7e7d 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -3,7 +3,7 @@ "version": "2", "engine": "postgresql", "schema": [ - "schema.sql" + "migrations" ], "queries": [ "internal/database/queries.sql" diff --git a/schema.sql b/schema.sql deleted file mode 100644 index 0edc1b7..0000000 --- a/schema.sql +++ /dev/null @@ -1,107 +0,0 @@ --- schema.sql --- Database Schema for CAPY (Club Assistant in Python) - --- 1. ENUMs & Functions -CREATE TYPE user_role AS ENUM ('student', 'alumni', 'faculty', 'external', 'dev'); - -CREATE OR REPLACE FUNCTION update_modified_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.date_modified = CURRENT_DATE; - RETURN NEW; -END; -$$ language 'plpgsql'; - --- 2. Tables -CREATE TABLE IF NOT EXISTS users ( - uid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - personal_email TEXT UNIQUE, - school_email TEXT UNIQUE, - phone TEXT, - grad_year INT, - role user_role DEFAULT 'student', - date_created DATE DEFAULT CURRENT_DATE, - date_modified DATE DEFAULT CURRENT_DATE -); - -CREATE TABLE IF NOT EXISTS organizations ( - oid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - date_created DATE DEFAULT CURRENT_DATE, - date_modified DATE DEFAULT CURRENT_DATE -); - -CREATE TABLE IF NOT EXISTS org_members ( - uid UUID REFERENCES users(uid) ON DELETE CASCADE, - oid UUID REFERENCES organizations(oid) ON DELETE CASCADE, - is_admin BOOLEAN DEFAULT FALSE, - date_joined DATE DEFAULT CURRENT_DATE, - last_active DATE DEFAULT CURRENT_DATE, - PRIMARY KEY (uid, oid) -); - -CREATE TABLE IF NOT EXISTS events ( - eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT, - location TEXT, - event_time TIMESTAMP, - description TEXT, - date_created DATE DEFAULT CURRENT_DATE, - date_modified DATE DEFAULT CURRENT_DATE -); - -CREATE TABLE IF NOT EXISTS event_hosting ( - eid UUID REFERENCES events(eid) ON DELETE CASCADE, - oid UUID REFERENCES organizations(oid) ON DELETE CASCADE, - PRIMARY KEY (eid, oid) -); - -CREATE TABLE IF NOT EXISTS event_registrations ( - uid UUID REFERENCES users(uid) ON DELETE CASCADE, - eid UUID REFERENCES events(eid) ON DELETE CASCADE, - is_attending BOOLEAN DEFAULT FALSE, - is_admin BOOLEAN DEFAULT FALSE, - date_registered DATE DEFAULT CURRENT_DATE, - PRIMARY KEY (uid, eid) -); - --- 3. Bot Tokens (global access for M2M authentication) -CREATE TABLE IF NOT EXISTS bot_tokens ( - token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - token_hash TEXT NOT NULL, -- bcrypt hash of the token - name TEXT NOT NULL, -- human-readable name for the bot - created_by UUID NOT NULL REFERENCES users(uid), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_used_at TIMESTAMP, - expires_at TIMESTAMP, -- NULL = never expires - is_active BOOLEAN DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS links ( - lid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - endpoint_url TEXT NOT NULL UNIQUE, - dest_url TEXT NOT NULL, - oid UUID NOT NULL REFERENCES organizations(oid) ON DELETE CASCADE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS link_visits ( - lvid UUID PRIMARY KEY DEFAULT gen_random_uuid(), - lid UUID NOT NULL REFERENCES links(lid) ON DELETE CASCADE, - uid UUID REFERENCES users(uid) ON DELETE SET NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_bot_tokens_active ON bot_tokens(is_active) WHERE is_active = TRUE; - --- 4. Triggers -DROP TRIGGER IF EXISTS update_users_modtime ON users; -CREATE TRIGGER update_users_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_modified_column(); - -DROP TRIGGER IF EXISTS update_orgs_modtime ON organizations; -CREATE TRIGGER update_orgs_modtime BEFORE UPDATE ON organizations FOR EACH ROW EXECUTE FUNCTION update_modified_column(); - -DROP TRIGGER IF EXISTS update_events_modtime ON events; -CREATE TRIGGER update_events_modtime BEFORE UPDATE ON events FOR EACH ROW EXECUTE FUNCTION update_modified_column(); From 6bcdba1b572f3d10f347cc6f750ad7bb40881e24 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:03:52 -0400 Subject: [PATCH 19/29] Edit sqlc.yaml Previously pushed an edit to the generated sqlc.json instead of sqlc.yaml. Oops! --- sqlc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlc.yaml b/sqlc.yaml index b21bd96..c7a941b 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -2,7 +2,7 @@ version: "2" sql: - engine: "postgresql" queries: "internal/database/queries.sql" - schema: "schema.sql" + schema: "migrations" gen: go: package: "database" From c07ca82b4f0f5460681c9b47bad4d382b7986829 Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:42:29 -0400 Subject: [PATCH 20/29] Add and use event with orgids view Tests still need to be updated --- Makefile | 6 + docs/schema/schema.json | 1324 ++++++++++++++--- internal/database/models.go | 39 +- internal/database/querier.go | 10 +- internal/database/queries.sql | 36 +- internal/database/queries.sql.go | 85 +- internal/dto/dto.go | 15 +- internal/handler/events.go | 18 +- .../20260327180817_add_event_view.down.sql | 1 + .../20260327180817_add_event_view.up.sql | 8 + 10 files changed, 1265 insertions(+), 277 deletions(-) create mode 100644 migrations/20260327180817_add_event_view.down.sql create mode 100644 migrations/20260327180817_add_event_view.up.sql diff --git a/Makefile b/Makefile index 9003174..4d96c9e 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,12 @@ migrate-version: test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \ docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" version +migrate-force: + @test -n "$(version)" || (echo "Usage: make migrate-version version=20260327180817" && exit 1) + @db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \ + test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \ + docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" force $(version) + # Build build: generate go build -o bin/capy-server ./cmd/server diff --git a/docs/schema/schema.json b/docs/schema/schema.json index 7fa7e7d..c0ec31e 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -584,7 +584,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -610,7 +610,7 @@ "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -626,8 +626,8 @@ "table_alias": "", "type": { "catalog": "", - "schema": "", - "name": "text" + "schema": "pg_catalog", + "name": "timestamp" }, "is_sqlc_slice": false, "embed_table": null, @@ -636,7 +636,7 @@ "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -652,8 +652,8 @@ "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -662,7 +662,7 @@ "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -679,7 +679,7 @@ "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, @@ -688,7 +688,7 @@ "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -714,7 +714,7 @@ "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -731,7 +731,7 @@ "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -1162,6 +1162,546 @@ ], "comment": "" }, + { + "rel": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "columns": [ + { + "name": "id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "bigserial" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "correlation_id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "timestamp", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "timestamptz" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "received_at", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "timestamptz" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "interaction_type", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "user_id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "int8" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "command_name", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "guild_id", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "int8" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "guild_name", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "channel_id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "int8" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "options", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "jsonb" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "bot_version", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_interactions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "columns": [ + { + "name": "id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "bigserial" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "correlation_id", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "timestamp", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "timestamptz" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "received_at", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "timestamptz" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "command_name", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "status", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "duration_ms", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "numeric" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "error_type", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "telemetry_completions" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "varchar" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, { "rel": { "catalog": "", @@ -1196,8 +1736,86 @@ "array_dims": 0 }, { - "name": "endpoint_url", - "not_null": true, + "name": "endpoint_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "dest_url", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "oid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "links" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "created_at", + "not_null": false, "is_array": false, "comment": "", "length": -1, @@ -1211,9 +1829,45 @@ }, "table_alias": "", "type": { + "catalog": "", + "schema": "pg_catalog", + "name": "timestamp" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + } + ], + "comment": "" + }, + { + "rel": { + "catalog": "", + "schema": "", + "name": "link_visits" + }, + "columns": [ + { + "name": "lvid", + "not_null": true, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { "catalog": "", "schema": "", - "name": "text" + "name": "link_visits" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1222,7 +1876,7 @@ "array_dims": 0 }, { - "name": "dest_url", + "name": "lid", "not_null": true, "is_array": false, "comment": "", @@ -1233,13 +1887,13 @@ "table": { "catalog": "", "schema": "", - "name": "links" + "name": "link_visits" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "uuid" }, "is_sqlc_slice": false, "embed_table": null, @@ -1248,8 +1902,8 @@ "array_dims": 0 }, { - "name": "oid", - "not_null": true, + "name": "uid", + "not_null": false, "is_array": false, "comment": "", "length": -1, @@ -1259,7 +1913,7 @@ "table": { "catalog": "", "schema": "", - "name": "links" + "name": "link_visits" }, "table_alias": "", "type": { @@ -1285,7 +1939,7 @@ "table": { "catalog": "", "schema": "", - "name": "links" + "name": "link_visits" }, "table_alias": "", "type": { @@ -1306,11 +1960,11 @@ "rel": { "catalog": "", "schema": "", - "name": "link_visits" + "name": "events_with_org_ids" }, "columns": [ { - "name": "lvid", + "name": "eid", "not_null": true, "is_array": false, "comment": "", @@ -1321,7 +1975,7 @@ "table": { "catalog": "", "schema": "", - "name": "link_visits" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -1336,8 +1990,8 @@ "array_dims": 0 }, { - "name": "lid", - "not_null": true, + "name": "location", + "not_null": false, "is_array": false, "comment": "", "length": -1, @@ -1347,13 +2001,13 @@ "table": { "catalog": "", "schema": "", - "name": "link_visits" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "uuid" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, @@ -1362,7 +2016,7 @@ "array_dims": 0 }, { - "name": "uid", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -1373,13 +2027,13 @@ "table": { "catalog": "", "schema": "", - "name": "link_visits" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "uuid" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, @@ -1388,7 +2042,7 @@ "array_dims": 0 }, { - "name": "created_at", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -1399,19 +2053,123 @@ "table": { "catalog": "", "schema": "", - "name": "link_visits" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, "original_name": "", "unsigned": false, "array_dims": 0 + }, + { + "name": "date_created", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "date" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "date_modified", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "date" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "", + "unsigned": false, + "array_dims": 1 } ], "comment": "" @@ -69741,7 +70499,7 @@ "insert_into_table": null }, { - "text": "SELECT eid, title, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1", + "text": "SELECT eid, location, event_time, description, date_created, date_modified, title, org_ids FROM events_with_org_ids WHERE eid = $1", "name": "GetEventByID", "cmd": ":one", "columns": [ @@ -69757,7 +70515,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -69772,7 +70530,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -69783,7 +70541,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -69793,12 +70551,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "title", + "original_name": "location", "unsigned": false, "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -69809,22 +70567,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "location", + "original_name": "event_time", "unsigned": false, "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -69835,22 +70593,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "event_time", + "original_name": "description", "unsigned": false, "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -69861,22 +70619,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "description", + "original_name": "date_created", "unsigned": false, "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -69887,7 +70645,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -69897,12 +70655,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_created", + "original_name": "date_modified", "unsigned": false, "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -69913,19 +70671,45 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_modified", + "original_name": "title", "unsigned": false, "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 } ], "params": [ @@ -69943,7 +70727,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -69964,7 +70748,7 @@ "insert_into_table": null }, { - "text": "SELECT eid, title, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2", + "text": "SELECT eid, location, event_time, description, date_created, date_modified, title, org_ids FROM events_with_org_ids ORDER BY event_time DESC LIMIT $1 OFFSET $2", "name": "ListEvents", "cmd": ":many", "columns": [ @@ -69980,7 +70764,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -69994,32 +70778,6 @@ "unsigned": false, "array_dims": 0 }, - { - "name": "title", - "not_null": false, - "is_array": false, - "comment": "", - "length": -1, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "", - "schema": "", - "name": "events" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "text" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "title", - "unsigned": false, - "array_dims": 0 - }, { "name": "location", "not_null": false, @@ -70032,7 +70790,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70058,13 +70816,13 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, @@ -70084,7 +70842,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70110,7 +70868,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70136,7 +70894,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70149,6 +70907,58 @@ "original_name": "date_modified", "unsigned": false, "array_dims": 0 + }, + { + "name": "title", + "not_null": false, + "is_array": false, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "text" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "title", + "unsigned": false, + "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 } ], "params": [ @@ -70208,7 +71018,7 @@ "insert_into_table": null }, { - "text": "SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified\nFROM events e\nJOIN event_hosting eh ON e.eid = eh.eid\nWHERE eh.oid = $1\nORDER BY e.event_time DESC\nLIMIT $2 OFFSET $3", + "text": "SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, e.title, e.org_ids\nFROM events_with_org_ids e\nJOIN event_hosting eh ON e.eid = eh.eid\nWHERE eh.oid = $1\nORDER BY e.event_time DESC\nLIMIT $2 OFFSET $3", "name": "ListEventsByOrg", "cmd": ":many", "columns": [ @@ -70224,7 +71034,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70239,7 +71049,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -70250,7 +71060,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70260,12 +71070,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "title", + "original_name": "location", "unsigned": false, "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -70276,22 +71086,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "location", + "original_name": "event_time", "unsigned": false, "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -70302,22 +71112,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "event_time", + "original_name": "description", "unsigned": false, "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -70328,22 +71138,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "description", + "original_name": "date_created", "unsigned": false, "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -70354,7 +71164,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70364,12 +71174,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_created", + "original_name": "date_modified", "unsigned": false, "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -70380,19 +71190,45 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_modified", + "original_name": "title", "unsigned": false, "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 } ], "params": [ @@ -70481,7 +71317,7 @@ "insert_into_table": null }, { - "text": "INSERT INTO events (title, location, event_time, description)\nVALUES ($1, $2, $3, $4)\nRETURNING eid, title, location, event_time, description, date_created, date_modified", + "text": "WITH updated AS (\n INSERT INTO events (title, location, event_time, description)\n VALUES ($1, $2, $3, $4)\n RETURNING eid, location, event_time, description, date_created, date_modified, title\n)\nSELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v\nWHERE v.eid = (SELECT eid FROM updated)", "name": "CreateEvent", "cmd": ":one", "columns": [ @@ -70497,7 +71333,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70512,7 +71348,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -70523,7 +71359,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70533,12 +71369,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "title", + "original_name": "location", "unsigned": false, "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -70549,22 +71385,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "location", + "original_name": "event_time", "unsigned": false, "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -70575,22 +71411,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "event_time", + "original_name": "description", "unsigned": false, "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -70601,22 +71437,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "description", + "original_name": "date_created", "unsigned": false, "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -70627,7 +71463,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70637,12 +71473,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_created", + "original_name": "date_modified", "unsigned": false, "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -70653,19 +71489,45 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_modified", + "original_name": "title", "unsigned": false, "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 } ], "params": [ @@ -70788,14 +71650,10 @@ ], "comments": [], "filename": "queries.sql", - "insert_into_table": { - "catalog": "", - "schema": "", - "name": "events" - } + "insert_into_table": null }, { - "text": "UPDATE events\nSET title = COALESCE($2, title),\n location = COALESCE($3, location),\n event_time = COALESCE($4, event_time),\n description = COALESCE($5, description)\nWHERE eid = $1\nRETURNING eid, title, location, event_time, description, date_created, date_modified", + "text": "WITH updated AS (\n UPDATE events\n SET title = COALESCE($2, title),\n location = COALESCE($3, location),\n event_time = COALESCE($4, event_time),\n description = COALESCE($5, description)\n WHERE eid = $1\n RETURNING eid, location, event_time, description, date_created, date_modified, title\n)\nSELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v\nWHERE v.eid = $1", "name": "UpdateEvent", "cmd": ":one", "columns": [ @@ -70811,7 +71669,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70826,7 +71684,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -70837,7 +71695,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70847,12 +71705,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "title", + "original_name": "location", "unsigned": false, "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -70863,22 +71721,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "location", + "original_name": "event_time", "unsigned": false, "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -70889,22 +71747,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "event_time", + "original_name": "description", "unsigned": false, "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -70915,22 +71773,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "description", + "original_name": "date_created", "unsigned": false, "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -70941,7 +71799,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -70951,12 +71809,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_created", + "original_name": "date_modified", "unsigned": false, "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -70967,19 +71825,45 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_modified", + "original_name": "title", "unsigned": false, "array_dims": 0 + }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 } ], "params": [ @@ -70997,7 +71881,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71026,7 +71910,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71055,7 +71939,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71084,7 +71968,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71113,7 +71997,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71889,7 +72773,7 @@ "insert_into_table": null }, { - "text": "SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered\nFROM events e\nJOIN event_registrations er ON e.eid = er.eid\nWHERE er.uid = $1\nORDER BY e.event_time DESC", + "text": "SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, e.title, e.org_ids, er.is_attending, er.is_admin, er.date_registered\nFROM events_with_org_ids e\nJOIN event_registrations er ON e.eid = er.eid\nWHERE er.uid = $1\nORDER BY e.event_time DESC", "name": "GetUserEvents", "cmd": ":many", "columns": [ @@ -71905,7 +72789,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71920,7 +72804,7 @@ "array_dims": 0 }, { - "name": "title", + "name": "location", "not_null": false, "is_array": false, "comment": "", @@ -71931,7 +72815,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -71941,12 +72825,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "title", + "original_name": "location", "unsigned": false, "array_dims": 0 }, { - "name": "location", + "name": "event_time", "not_null": false, "is_array": false, "comment": "", @@ -71957,22 +72841,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "pg_catalog.timestamp" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "location", + "original_name": "event_time", "unsigned": false, "array_dims": 0 }, { - "name": "event_time", + "name": "description", "not_null": false, "is_array": false, "comment": "", @@ -71983,22 +72867,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", - "schema": "pg_catalog", - "name": "timestamp" + "schema": "", + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "event_time", + "original_name": "description", "unsigned": false, "array_dims": 0 }, { - "name": "description", + "name": "date_created", "not_null": false, "is_array": false, "comment": "", @@ -72009,22 +72893,22 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "text" + "name": "date" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "description", + "original_name": "date_created", "unsigned": false, "array_dims": 0 }, { - "name": "date_created", + "name": "date_modified", "not_null": false, "is_array": false, "comment": "", @@ -72035,7 +72919,7 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { @@ -72045,12 +72929,12 @@ }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_created", + "original_name": "date_modified", "unsigned": false, "array_dims": 0 }, { - "name": "date_modified", + "name": "title", "not_null": false, "is_array": false, "comment": "", @@ -72061,20 +72945,46 @@ "table": { "catalog": "", "schema": "", - "name": "events" + "name": "events_with_org_ids" }, "table_alias": "", "type": { "catalog": "", "schema": "", - "name": "date" + "name": "text" }, "is_sqlc_slice": false, "embed_table": null, - "original_name": "date_modified", + "original_name": "title", "unsigned": false, "array_dims": 0 }, + { + "name": "org_ids", + "not_null": true, + "is_array": true, + "comment": "", + "length": -1, + "is_named_param": false, + "is_func_call": false, + "scope": "", + "table": { + "catalog": "", + "schema": "", + "name": "events_with_org_ids" + }, + "table_alias": "", + "type": { + "catalog": "", + "schema": "", + "name": "uuid" + }, + "is_sqlc_slice": false, + "embed_table": null, + "original_name": "org_ids", + "unsigned": false, + "array_dims": 1 + }, { "name": "is_attending", "not_null": false, diff --git a/internal/database/models.go b/internal/database/models.go index a264815..9e8e147 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -70,12 +70,12 @@ type BotToken struct { type Event struct { Eid uuid.UUID `json:"eid"` - Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` DateCreated pgtype.Date `json:"date_created"` DateModified pgtype.Date `json:"date_modified"` + Title pgtype.Text `json:"title"` } type EventHosting struct { @@ -91,6 +91,17 @@ type EventRegistration struct { DateRegistered pgtype.Date `json:"date_registered"` } +type EventsWithOrgID struct { + Eid uuid.UUID `json:"eid"` + Location pgtype.Text `json:"location"` + EventTime pgtype.Timestamp `json:"event_time"` + Description pgtype.Text `json:"description"` + DateCreated pgtype.Date `json:"date_created"` + DateModified pgtype.Date `json:"date_modified"` + Title pgtype.Text `json:"title"` + OrgIds []uuid.UUID `json:"org_ids"` +} + type Link struct { Lid uuid.UUID `json:"lid"` EndpointUrl string `json:"endpoint_url"` @@ -121,6 +132,32 @@ type Organization struct { DateModified pgtype.Date `json:"date_modified"` } +type TelemetryCompletion struct { + ID int64 `json:"id"` + CorrelationID string `json:"correlation_id"` + Timestamp pgtype.Timestamptz `json:"timestamp"` + ReceivedAt pgtype.Timestamptz `json:"received_at"` + CommandName string `json:"command_name"` + Status string `json:"status"` + DurationMs pgtype.Numeric `json:"duration_ms"` + ErrorType pgtype.Text `json:"error_type"` +} + +type TelemetryInteraction struct { + ID int64 `json:"id"` + CorrelationID string `json:"correlation_id"` + Timestamp pgtype.Timestamptz `json:"timestamp"` + ReceivedAt pgtype.Timestamptz `json:"received_at"` + InteractionType string `json:"interaction_type"` + UserID int64 `json:"user_id"` + CommandName pgtype.Text `json:"command_name"` + GuildID pgtype.Int8 `json:"guild_id"` + GuildName pgtype.Text `json:"guild_name"` + ChannelID int64 `json:"channel_id"` + Options []byte `json:"options"` + BotVersion string `json:"bot_version"` +} + type User struct { Uid uuid.UUID `json:"uid"` FirstName string `json:"first_name"` diff --git a/internal/database/querier.go b/internal/database/querier.go index 7284a32..ce2f4d6 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -15,7 +15,7 @@ type Querier interface { AddEventHost(ctx context.Context, arg AddEventHostParams) error AddOrgMember(ctx context.Context, arg AddOrgMemberParams) error CreateBotToken(ctx context.Context, arg CreateBotTokenParams) (BotToken, error) - CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) + CreateEvent(ctx context.Context, arg CreateEventParams) (EventsWithOrgID, error) // Link Queries CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error) CreateOrganization(ctx context.Context, name string) (Organization, error) @@ -26,7 +26,7 @@ type Querier interface { DeleteUser(ctx context.Context, uid uuid.UUID) error // Bot Token Queries GetBotTokenByID(ctx context.Context, tokenID uuid.UUID) (BotToken, error) - GetEventByID(ctx context.Context, eid uuid.UUID) (Event, error) + GetEventByID(ctx context.Context, eid uuid.UUID) (EventsWithOrgID, error) GetEventRegistrations(ctx context.Context, eid uuid.UUID) ([]GetEventRegistrationsRow, error) GetLinkByEndpointURL(ctx context.Context, endpointUrl string) (Link, error) GetLinkByLID(ctx context.Context, lid uuid.UUID) (Link, error) @@ -40,8 +40,8 @@ type Querier interface { IsEventAdmin(ctx context.Context, arg IsEventAdminParams) (pgtype.Bool, error) IsOrgAdmin(ctx context.Context, arg IsOrgAdminParams) (pgtype.Bool, error) ListBotTokens(ctx context.Context) ([]ListBotTokensRow, error) - ListEvents(ctx context.Context, arg ListEventsParams) ([]Event, error) - ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams) ([]Event, error) + ListEvents(ctx context.Context, arg ListEventsParams) ([]EventsWithOrgID, error) + ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams) ([]EventsWithOrgID, error) ListLinksByOrg(ctx context.Context, oid uuid.UUID) ([]Link, error) ListOrganizations(ctx context.Context, arg ListOrganizationsParams) ([]Organization, error) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) @@ -51,7 +51,7 @@ type Querier interface { RevokeBotToken(ctx context.Context, tokenID uuid.UUID) error UnregisterFromEvent(ctx context.Context, arg UnregisterFromEventParams) error UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) error - UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) + UpdateEvent(ctx context.Context, arg UpdateEventParams) (EventsWithOrgID, error) UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) diff --git a/internal/database/queries.sql b/internal/database/queries.sql index b47282a..5159491 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -73,32 +73,40 @@ WHERE om.uid = $1 ORDER BY o.name; -- name: GetEventByID :one -SELECT * FROM events WHERE eid = $1; +SELECT * FROM events_with_org_ids WHERE eid = $1; -- name: ListEvents :many -SELECT * FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2; +SELECT * FROM events_with_org_ids ORDER BY event_time DESC LIMIT $1 OFFSET $2; -- name: ListEventsByOrg :many SELECT e.* -FROM events e +FROM events_with_org_ids e JOIN event_hosting eh ON e.eid = eh.eid WHERE eh.oid = $1 ORDER BY e.event_time DESC LIMIT $2 OFFSET $3; -- name: CreateEvent :one -INSERT INTO events (title, location, event_time, description) -VALUES ($1, $2, $3, $4) -RETURNING *; +WITH updated AS ( + INSERT INTO events (title, location, event_time, description) + VALUES ($1, $2, $3, $4) + RETURNING * +) +SELECT v.* FROM events_with_org_ids v +WHERE v.eid = (SELECT eid FROM updated); -- name: UpdateEvent :one -UPDATE events -SET title = COALESCE(sqlc.narg('title'), title), - location = COALESCE(sqlc.narg('location'), location), - event_time = COALESCE(sqlc.narg('event_time'), event_time), - description = COALESCE(sqlc.narg('description'), description) -WHERE eid = $1 -RETURNING *; +WITH updated AS ( + UPDATE events + SET title = COALESCE(sqlc.narg('title'), title), + location = COALESCE(sqlc.narg('location'), location), + event_time = COALESCE(sqlc.narg('event_time'), event_time), + description = COALESCE(sqlc.narg('description'), description) + WHERE eid = $1 + RETURNING * +) +SELECT v.* FROM events_with_org_ids v +WHERE v.eid = $1; -- name: DeleteEvent :exec DELETE FROM events WHERE eid = $1; @@ -142,7 +150,7 @@ OR EXISTS ( -- name: GetUserEvents :many SELECT e.*, er.is_attending, er.is_admin, er.date_registered -FROM events e +FROM events_with_org_ids e JOIN event_registrations er ON e.eid = er.eid WHERE er.uid = $1 ORDER BY e.event_time DESC; diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 6150227..9fbc29d 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -80,9 +80,13 @@ func (q *Queries) CreateBotToken(ctx context.Context, arg CreateBotTokenParams) } const createEvent = `-- name: CreateEvent :one -INSERT INTO events (title, location, event_time, description) -VALUES ($1, $2, $3, $4) -RETURNING eid, title, location, event_time, description, date_created, date_modified +WITH updated AS ( + INSERT INTO events (title, location, event_time, description) + VALUES ($1, $2, $3, $4) + RETURNING eid, location, event_time, description, date_created, date_modified, title +) +SELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v +WHERE v.eid = (SELECT eid FROM updated) ` type CreateEventParams struct { @@ -92,22 +96,23 @@ type CreateEventParams struct { Description pgtype.Text `json:"description"` } -func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { +func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (EventsWithOrgID, error) { row := q.db.QueryRow(ctx, createEvent, arg.Title, arg.Location, arg.EventTime, arg.Description, ) - var i Event + var i EventsWithOrgID err := row.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, ) return i, err } @@ -258,20 +263,21 @@ func (q *Queries) GetBotTokenByID(ctx context.Context, tokenID uuid.UUID) (BotTo } const getEventByID = `-- name: GetEventByID :one -SELECT eid, title, location, event_time, description, date_created, date_modified FROM events WHERE eid = $1 +SELECT eid, location, event_time, description, date_created, date_modified, title, org_ids FROM events_with_org_ids WHERE eid = $1 ` -func (q *Queries) GetEventByID(ctx context.Context, eid uuid.UUID) (Event, error) { +func (q *Queries) GetEventByID(ctx context.Context, eid uuid.UUID) (EventsWithOrgID, error) { row := q.db.QueryRow(ctx, getEventByID, eid) - var i Event + var i EventsWithOrgID err := row.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, ) return i, err } @@ -498,8 +504,8 @@ func (q *Queries) GetUserByID(ctx context.Context, uid uuid.UUID) (User, error) } const getUserEvents = `-- name: GetUserEvents :many -SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified, er.is_attending, er.is_admin, er.date_registered -FROM events e +SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, e.title, e.org_ids, er.is_attending, er.is_admin, er.date_registered +FROM events_with_org_ids e JOIN event_registrations er ON e.eid = er.eid WHERE er.uid = $1 ORDER BY e.event_time DESC @@ -507,12 +513,13 @@ ORDER BY e.event_time DESC type GetUserEventsRow struct { Eid uuid.UUID `json:"eid"` - Title pgtype.Text `json:"title"` Location pgtype.Text `json:"location"` EventTime pgtype.Timestamp `json:"event_time"` Description pgtype.Text `json:"description"` DateCreated pgtype.Date `json:"date_created"` DateModified pgtype.Date `json:"date_modified"` + Title pgtype.Text `json:"title"` + OrgIds []uuid.UUID `json:"org_ids"` IsAttending pgtype.Bool `json:"is_attending"` IsAdmin pgtype.Bool `json:"is_admin"` DateRegistered pgtype.Date `json:"date_registered"` @@ -529,12 +536,13 @@ func (q *Queries) GetUserEvents(ctx context.Context, uid uuid.UUID) ([]GetUserEv var i GetUserEventsRow if err := rows.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, &i.IsAttending, &i.IsAdmin, &i.DateRegistered, @@ -684,7 +692,7 @@ func (q *Queries) ListBotTokens(ctx context.Context) ([]ListBotTokensRow, error) } const listEvents = `-- name: ListEvents :many -SELECT eid, title, location, event_time, description, date_created, date_modified FROM events ORDER BY event_time DESC LIMIT $1 OFFSET $2 +SELECT eid, location, event_time, description, date_created, date_modified, title, org_ids FROM events_with_org_ids ORDER BY event_time DESC LIMIT $1 OFFSET $2 ` type ListEventsParams struct { @@ -692,23 +700,24 @@ type ListEventsParams struct { Offset int32 `json:"offset"` } -func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event, error) { +func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]EventsWithOrgID, error) { rows, err := q.db.Query(ctx, listEvents, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() - items := []Event{} + items := []EventsWithOrgID{} for rows.Next() { - var i Event + var i EventsWithOrgID if err := rows.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, ); err != nil { return nil, err } @@ -721,8 +730,8 @@ func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event } const listEventsByOrg = `-- name: ListEventsByOrg :many -SELECT e.eid, e.title, e.location, e.event_time, e.description, e.date_created, e.date_modified -FROM events e +SELECT e.eid, e.location, e.event_time, e.description, e.date_created, e.date_modified, e.title, e.org_ids +FROM events_with_org_ids e JOIN event_hosting eh ON e.eid = eh.eid WHERE eh.oid = $1 ORDER BY e.event_time DESC @@ -735,23 +744,24 @@ type ListEventsByOrgParams struct { Offset int32 `json:"offset"` } -func (q *Queries) ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams) ([]Event, error) { +func (q *Queries) ListEventsByOrg(ctx context.Context, arg ListEventsByOrgParams) ([]EventsWithOrgID, error) { rows, err := q.db.Query(ctx, listEventsByOrg, arg.Oid, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() - items := []Event{} + items := []EventsWithOrgID{} for rows.Next() { - var i Event + var i EventsWithOrgID if err := rows.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, ); err != nil { return nil, err } @@ -954,13 +964,17 @@ func (q *Queries) UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) } const updateEvent = `-- name: UpdateEvent :one -UPDATE events -SET title = COALESCE($2, title), - location = COALESCE($3, location), - event_time = COALESCE($4, event_time), - description = COALESCE($5, description) -WHERE eid = $1 -RETURNING eid, title, location, event_time, description, date_created, date_modified +WITH updated AS ( + UPDATE events + SET title = COALESCE($2, title), + location = COALESCE($3, location), + event_time = COALESCE($4, event_time), + description = COALESCE($5, description) + WHERE eid = $1 + RETURNING eid, location, event_time, description, date_created, date_modified, title +) +SELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v +WHERE v.eid = $1 ` type UpdateEventParams struct { @@ -971,7 +985,7 @@ type UpdateEventParams struct { Description pgtype.Text `json:"description"` } -func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) { +func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (EventsWithOrgID, error) { row := q.db.QueryRow(ctx, updateEvent, arg.Eid, arg.Title, @@ -979,15 +993,16 @@ func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event arg.EventTime, arg.Description, ) - var i Event + var i EventsWithOrgID err := row.Scan( &i.Eid, - &i.Title, &i.Location, &i.EventTime, &i.Description, &i.DateCreated, &i.DateModified, + &i.Title, + &i.OrgIds, ) return i, err } diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 4563dca..6b1781f 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -98,13 +98,14 @@ type UpdateEventRequest struct { } type EventResponse struct { - EID uuid.UUID `json:"eid"` - Title *string `json:"title,omitempty"` - Location *string `json:"location,omitempty"` - EventTime *time.Time `json:"event_time,omitempty"` - Description *string `json:"description,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateModified *time.Time `json:"date_modified,omitempty"` + EID uuid.UUID `json:"eid"` + Organizations []uuid.UUID `json:"oids"` + Title *string `json:"title,omitempty"` + Location *string `json:"location,omitempty"` + EventTime *time.Time `json:"event_time,omitempty"` + Description *string `json:"description,omitempty"` + DateCreated *time.Time `json:"date_created,omitempty"` + DateModified *time.Time `json:"date_modified,omitempty"` } type EventRegistrationResponse struct { diff --git a/internal/handler/events.go b/internal/handler/events.go index 46d6a5d..b237796 100644 --- a/internal/handler/events.go +++ b/internal/handler/events.go @@ -417,14 +417,16 @@ func (h *Handler) ListEventsByOrg(w http.ResponseWriter, r *http.Request) { } // Helper functions -func toEventResponse(event database.Event) dto.EventResponse { +func toEventResponse(event database.EventsWithOrgID) dto.EventResponse { + return dto.EventResponse{ - EID: event.Eid, - Title: fromPgText(event.Title), - Location: fromPgText(event.Location), - EventTime: fromPgTimestamp(event.EventTime), - Description: fromPgText(event.Description), - DateCreated: fromPgDate(event.DateCreated), - DateModified: fromPgDate(event.DateModified), + EID: event.Eid, + Organizations: event.OrgIds, + Title: fromPgText(event.Title), + Location: fromPgText(event.Location), + EventTime: fromPgTimestamp(event.EventTime), + Description: fromPgText(event.Description), + DateCreated: fromPgDate(event.DateCreated), + DateModified: fromPgDate(event.DateModified), } } diff --git a/migrations/20260327180817_add_event_view.down.sql b/migrations/20260327180817_add_event_view.down.sql new file mode 100644 index 0000000..bdeb469 --- /dev/null +++ b/migrations/20260327180817_add_event_view.down.sql @@ -0,0 +1 @@ +DROP VIEW events_with_org_ids \ No newline at end of file diff --git a/migrations/20260327180817_add_event_view.up.sql b/migrations/20260327180817_add_event_view.up.sql new file mode 100644 index 0000000..3bd1bc4 --- /dev/null +++ b/migrations/20260327180817_add_event_view.up.sql @@ -0,0 +1,8 @@ +CREATE VIEW events_with_org_ids AS + SELECT + e.*, + ARRAY_AGG(eh.oid)::uuid[] AS org_ids + FROM events e + LEFT JOIN event_hosting eh ON e.eid = eh.eid + GROUP BY + e.eid; From a08891b4da0f6aaaefb30766ebe2c99726fdb7fb Mon Sep 17 00:00:00 2001 From: Jeremy <87028711+jgoldberger26@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:39:03 -0400 Subject: [PATCH 21/29] remove SQL Schema from dockerfile --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e77d7ec..b72af86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,6 @@ WORKDIR /app RUN apk add --no-cache ca-certificates tzdata wget COPY --from=builder /capy-server . COPY --from=builder /app/docs ./docs -COPY --from=builder /app/schema.sql ./schema.sql COPY --from=builder /app/migrations ./migrations RUN adduser -D -g '' appuser USER appuser From 313dc74681bdf9c7cd595ee23c984698f57b1ce9 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 10:35:39 -0400 Subject: [PATCH 22/29] feat(routes): relaxed permissions to view rec event and orgs for demo --- internal/database/mocks/Querier.go | 50 ++++++++++++++--------------- internal/handler/events.go | 1 - internal/handler/organizations.go | 1 - internal/router/router.go | 50 ++++++++++++++--------------- internal/router/router_auth_test.go | 28 ++++++++++++++++ 5 files changed, 78 insertions(+), 52 deletions(-) diff --git a/internal/database/mocks/Querier.go b/internal/database/mocks/Querier.go index fa0b713..f0070ab 100644 --- a/internal/database/mocks/Querier.go +++ b/internal/database/mocks/Querier.go @@ -83,22 +83,22 @@ func (_m *Querier) CreateBotToken(ctx context.Context, arg database.CreateBotTok } // CreateEvent provides a mock function with given fields: ctx, arg -func (_m *Querier) CreateEvent(ctx context.Context, arg database.CreateEventParams) (database.Event, error) { +func (_m *Querier) CreateEvent(ctx context.Context, arg database.CreateEventParams) (database.EventsWithOrgID, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for CreateEvent") } - var r0 database.Event + var r0 database.EventsWithOrgID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) (database.Event, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) (database.EventsWithOrgID, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) database.Event); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) database.EventsWithOrgID); ok { r0 = rf(ctx, arg) } else { - r0 = ret.Get(0).(database.Event) + r0 = ret.Get(0).(database.EventsWithOrgID) } if rf, ok := ret.Get(1).(func(context.Context, database.CreateEventParams) error); ok { @@ -295,22 +295,22 @@ func (_m *Querier) GetBotTokenByID(ctx context.Context, tokenID uuid.UUID) (data } // GetEventByID provides a mock function with given fields: ctx, eid -func (_m *Querier) GetEventByID(ctx context.Context, eid uuid.UUID) (database.Event, error) { +func (_m *Querier) GetEventByID(ctx context.Context, eid uuid.UUID) (database.EventsWithOrgID, error) { ret := _m.Called(ctx, eid) if len(ret) == 0 { panic("no return value specified for GetEventByID") } - var r0 database.Event + var r0 database.EventsWithOrgID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.Event, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.EventsWithOrgID, error)); ok { return rf(ctx, eid) } - if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.Event); ok { + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.EventsWithOrgID); ok { r0 = rf(ctx, eid) } else { - r0 = ret.Get(0).(database.Event) + r0 = ret.Get(0).(database.EventsWithOrgID) } if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { @@ -697,23 +697,23 @@ func (_m *Querier) ListBotTokens(ctx context.Context) ([]database.ListBotTokensR } // ListEvents provides a mock function with given fields: ctx, arg -func (_m *Querier) ListEvents(ctx context.Context, arg database.ListEventsParams) ([]database.Event, error) { +func (_m *Querier) ListEvents(ctx context.Context, arg database.ListEventsParams) ([]database.EventsWithOrgID, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for ListEvents") } - var r0 []database.Event + var r0 []database.EventsWithOrgID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsParams) ([]database.Event, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsParams) ([]database.EventsWithOrgID, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsParams) []database.Event); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsParams) []database.EventsWithOrgID); ok { r0 = rf(ctx, arg) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]database.Event) + r0 = ret.Get(0).([]database.EventsWithOrgID) } } @@ -727,23 +727,23 @@ func (_m *Querier) ListEvents(ctx context.Context, arg database.ListEventsParams } // ListEventsByOrg provides a mock function with given fields: ctx, arg -func (_m *Querier) ListEventsByOrg(ctx context.Context, arg database.ListEventsByOrgParams) ([]database.Event, error) { +func (_m *Querier) ListEventsByOrg(ctx context.Context, arg database.ListEventsByOrgParams) ([]database.EventsWithOrgID, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for ListEventsByOrg") } - var r0 []database.Event + var r0 []database.EventsWithOrgID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsByOrgParams) ([]database.Event, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsByOrgParams) ([]database.EventsWithOrgID, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsByOrgParams) []database.Event); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.ListEventsByOrgParams) []database.EventsWithOrgID); ok { r0 = rf(ctx, arg) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]database.Event) + r0 = ret.Get(0).([]database.EventsWithOrgID) } } @@ -965,22 +965,22 @@ func (_m *Querier) UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID } // UpdateEvent provides a mock function with given fields: ctx, arg -func (_m *Querier) UpdateEvent(ctx context.Context, arg database.UpdateEventParams) (database.Event, error) { +func (_m *Querier) UpdateEvent(ctx context.Context, arg database.UpdateEventParams) (database.EventsWithOrgID, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for UpdateEvent") } - var r0 database.Event + var r0 database.EventsWithOrgID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) (database.Event, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) (database.EventsWithOrgID, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) database.Event); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) database.EventsWithOrgID); ok { r0 = rf(ctx, arg) } else { - r0 = ret.Get(0).(database.Event) + r0 = ret.Get(0).(database.EventsWithOrgID) } if rf, ok := ret.Get(1).(func(context.Context, database.UpdateEventParams) error); ok { diff --git a/internal/handler/events.go b/internal/handler/events.go index b237796..afc732d 100644 --- a/internal/handler/events.go +++ b/internal/handler/events.go @@ -21,7 +21,6 @@ import ( // @Param limit query int false "Limit (default 20, max 100)" // @Param offset query int false "Offset (default 0)" // @Success 200 {array} dto.EventResponse -// @Security CookieAuth // @Router /events [get] func (h *Handler) ListEvents(w http.ResponseWriter, r *http.Request) { limit, offset := parsePagination(r) diff --git a/internal/handler/organizations.go b/internal/handler/organizations.go index c6c3607..001af24 100644 --- a/internal/handler/organizations.go +++ b/internal/handler/organizations.go @@ -22,7 +22,6 @@ import ( // @Param limit query int false "Limit (default 20, max 100)" // @Param offset query int false "Offset (default 0)" // @Success 200 {array} dto.OrganizationResponse -// @Security CookieAuth // @Router /organizations [get] func (h *Handler) ListOrganizations(w http.ResponseWriter, r *http.Request) { limit, offset := parsePagination(r) diff --git a/internal/router/router.go b/internal/router/router.go index f70a12b..e4522cc 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -22,31 +22,6 @@ func mountProtectedRoutes(r chi.Router, h *handler.Handler, jwtSecret string) { r.Get("/{uid}/events", h.GetUserEvents) }) - r.Route("/organizations", func(r chi.Router) { - r.Get("/", h.ListOrganizations) - r.Post("/", h.CreateOrganization) - r.Get("/{oid}", h.GetOrganization) - r.Put("/{oid}", h.UpdateOrganization) - r.Delete("/{oid}", h.DeleteOrganization) - r.Get("/{oid}/members", h.ListOrgMembers) - r.Post("/{oid}/members", h.AddOrgMember) - r.Delete("/{oid}/members/{uid}", h.RemoveOrgMember) - r.Get("/{oid}/events", h.ListOrgEvents) - r.Get("/{oid}/links", h.ListOrgLinks) - }) - - r.Route("/events", func(r chi.Router) { - r.Get("/", h.ListEvents) - r.Post("/", h.CreateEvent) - r.Get("/org/{oid}", h.ListEventsByOrg) - r.Get("/{eid}", h.GetEvent) - r.Put("/{eid}", h.UpdateEvent) - r.Delete("/{eid}", h.DeleteEvent) - r.Get("/{eid}/registrations", h.ListEventRegistrations) - r.Post("/{eid}/register", h.RegisterForEvent) - r.Delete("/{eid}/register", h.UnregisterFromEvent) - }) - // Links r.Route("/links", func(r chi.Router) { r.Post("/", h.CreateLink) @@ -61,6 +36,25 @@ func mountProtectedRoutes(r chi.Router, h *handler.Handler, jwtSecret string) { r.Delete("/{token_id}", h.RevokeBotToken) }) }) + + r.With(middleware.Auth(jwtSecret)).Post("/organizations", h.CreateOrganization) + r.With(middleware.Auth(jwtSecret)).Get("/organizations/{oid}", h.GetOrganization) + r.With(middleware.Auth(jwtSecret)).Put("/organizations/{oid}", h.UpdateOrganization) + r.With(middleware.Auth(jwtSecret)).Delete("/organizations/{oid}", h.DeleteOrganization) + r.With(middleware.Auth(jwtSecret)).Get("/organizations/{oid}/members", h.ListOrgMembers) + r.With(middleware.Auth(jwtSecret)).Post("/organizations/{oid}/members", h.AddOrgMember) + r.With(middleware.Auth(jwtSecret)).Delete("/organizations/{oid}/members/{uid}", h.RemoveOrgMember) + r.With(middleware.Auth(jwtSecret)).Get("/organizations/{oid}/events", h.ListOrgEvents) + r.With(middleware.Auth(jwtSecret)).Get("/organizations/{oid}/links", h.ListOrgLinks) + + r.With(middleware.Auth(jwtSecret)).Post("/events", h.CreateEvent) + r.With(middleware.Auth(jwtSecret)).Get("/events/org/{oid}", h.ListEventsByOrg) + r.With(middleware.Auth(jwtSecret)).Get("/events/{eid}", h.GetEvent) + r.With(middleware.Auth(jwtSecret)).Put("/events/{eid}", h.UpdateEvent) + r.With(middleware.Auth(jwtSecret)).Delete("/events/{eid}", h.DeleteEvent) + r.With(middleware.Auth(jwtSecret)).Get("/events/{eid}/registrations", h.ListEventRegistrations) + r.With(middleware.Auth(jwtSecret)).Post("/events/{eid}/register", h.RegisterForEvent) + r.With(middleware.Auth(jwtSecret)).Delete("/events/{eid}/register", h.UnregisterFromEvent) } // New creates a new chi router with all routes configured @@ -107,6 +101,10 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed }) }) + // Public read-only collection routes + r.Get("/organizations", h.ListOrganizations) + r.Get("/events", h.ListEvents) + mountProtectedRoutes(r, h, jwtSecret) // Bot routes (M2M auth) @@ -156,6 +154,8 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed }) r.Route("/v1", func(r chi.Router) { + r.Get("/organizations", h.ListOrganizations) + r.Get("/events", h.ListEvents) mountProtectedRoutes(r, h, jwtSecret) }) diff --git a/internal/router/router_auth_test.go b/internal/router/router_auth_test.go index 43a359d..30ab6eb 100644 --- a/internal/router/router_auth_test.go +++ b/internal/router/router_auth_test.go @@ -265,6 +265,34 @@ func TestBotTokenManagementUsesDatabaseRole(t *testing.T) { assert.Equal(t, http.StatusOK, res.Code) } +func TestPublicCollectionRoutesDoNotRequireAuth(t *testing.T) { + mockQueries := mocks.NewQuerier(t) + routerUnderTest := newTestRouter(mockQueries) + + mockQueries.On("ListOrganizations", mock.Anything, mock.MatchedBy(func(arg database.ListOrganizationsParams) bool { + return arg.Limit == 20 && arg.Offset == 0 + })).Return([]database.Organization{}, nil).Once() + + mockQueries.On("ListEvents", mock.Anything, mock.MatchedBy(func(arg database.ListEventsParams) bool { + return arg.Limit == 20 && arg.Offset == 0 + })).Return([]database.EventsWithOrgID{}, nil).Once() + + orgReq := httptest.NewRequest(http.MethodGet, "/api/v1/organizations", nil) + orgRes := httptest.NewRecorder() + routerUnderTest.ServeHTTP(orgRes, orgReq) + assert.Equal(t, http.StatusOK, orgRes.Code) + + eventReq := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil) + eventRes := httptest.NewRecorder() + routerUnderTest.ServeHTTP(eventRes, eventReq) + assert.Equal(t, http.StatusOK, eventRes.Code) + + protectedReq := httptest.NewRequest(http.MethodPost, "/api/v1/organizations", bytes.NewBufferString(`{"name":"Still Protected"}`)) + protectedRes := httptest.NewRecorder() + routerUnderTest.ServeHTTP(protectedRes, protectedReq) + assert.Equal(t, http.StatusUnauthorized, protectedRes.Code) +} + func newTestRouter(queries database.Querier) http.Handler { cfg := &config.Config{ Env: "test", From 3691eb96fba7b993898e2d1e9ea1ad7404c0e4b1 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 11:33:28 -0400 Subject: [PATCH 23/29] fix(orgs): users can leave without being admin --- internal/handler/organizations.go | 20 +++++-- internal/handler/organizations_test.go | 78 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/internal/handler/organizations.go b/internal/handler/organizations.go index 001af24..4e7e378 100644 --- a/internal/handler/organizations.go +++ b/internal/handler/organizations.go @@ -315,10 +315,6 @@ func (h *Handler) RemoveOrgMember(w http.ResponseWriter, r *http.Request) { return } - if _, ok := h.requireOrgAdmin(w, r, oid); !ok { - return - } - uidStr := chi.URLParam(r, "uid") uid, err := uuid.Parse(uidStr) if err != nil { @@ -326,6 +322,22 @@ func (h *Handler) RemoveOrgMember(w http.ResponseWriter, r *http.Request) { return } + switch middleware.GetAuthType(r.Context()) { + case "bot": + // Bots retain full access to remove members on behalf of users. + default: + authenticatedUID, _, ok := h.requireAuthenticatedUser(w, r) + if !ok { + return + } + + if uid != authenticatedUID { + if _, ok := h.requireOrgAdmin(w, r, oid); !ok { + return + } + } + } + if err := h.queries.RemoveOrgMember(r.Context(), database.RemoveOrgMemberParams{ Uid: uid, Oid: oid, diff --git a/internal/handler/organizations_test.go b/internal/handler/organizations_test.go index e6db509..140fe24 100644 --- a/internal/handler/organizations_test.go +++ b/internal/handler/organizations_test.go @@ -16,6 +16,7 @@ import ( "github.com/capyrpi/api/internal/middleware" "github.com/go-chi/chi/v5" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -157,3 +158,80 @@ func TestAddOrgMemberAllowsSelfJoin(t *testing.T) { assert.Equal(t, http.StatusCreated, rr.Code) } + +func TestRemoveOrgMemberAuthorization(t *testing.T) { + oid := uuid.New() + selfUID := uuid.New() + otherUID := uuid.New() + + tests := []struct { + name string + authUID uuid.UUID + targetUID uuid.UUID + setupMock func(*mocks.Querier) + expectedStatus int + }{ + { + name: "MemberCanRemoveSelf", + authUID: selfUID, + targetUID: selfUID, + setupMock: func(m *mocks.Querier) { + m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{ + Uid: selfUID, + Oid: oid, + }).Return(nil) + }, + expectedStatus: http.StatusNoContent, + }, + { + name: "AdminCanRemoveOtherMember", + authUID: selfUID, + targetUID: otherUID, + setupMock: func(m *mocks.Querier) { + m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{ + Uid: selfUID, + Oid: oid, + }).Return(pgtype.Bool{Bool: true, Valid: true}, nil) + m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{ + Uid: otherUID, + Oid: oid, + }).Return(nil) + }, + expectedStatus: http.StatusNoContent, + }, + { + name: "NonAdminCannotRemoveOtherMember", + authUID: selfUID, + targetUID: otherUID, + setupMock: func(m *mocks.Querier) { + m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{ + Uid: selfUID, + Oid: oid, + }).Return(pgtype.Bool{Bool: false, Valid: true}, nil) + }, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockQueries := mocks.NewQuerier(t) + tt.setupMock(mockQueries) + + h := handler.New(mockQueries, &config.Config{}) + + req := httptest.NewRequest(http.MethodDelete, "/organizations/"+oid.String()+"/members/"+tt.targetUID.String(), nil) + req = req.WithContext(context.WithValue(context.Background(), middleware.UserClaimsKey, &middleware.UserClaims{UserID: tt.authUID.String()})) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("oid", oid.String()) + rctx.URLParams.Add("uid", tt.targetUID.String()) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + rr := httptest.NewRecorder() + http.HandlerFunc(h.RemoveOrgMember).ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedStatus, rr.Code) + }) + } +} From ce8d905f41c2bfa1f58de25e2c505528f9d493a2 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 12:39:42 -0400 Subject: [PATCH 24/29] fixed resource not found issue --- internal/database/querier.go | 2 +- internal/database/queries.sql | 10 +++------- internal/database/queries.sql.go | 15 +++++---------- internal/handler/events.go | 8 +++++++- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/internal/database/querier.go b/internal/database/querier.go index ce2f4d6..0ca1acf 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -15,7 +15,7 @@ type Querier interface { AddEventHost(ctx context.Context, arg AddEventHostParams) error AddOrgMember(ctx context.Context, arg AddOrgMemberParams) error CreateBotToken(ctx context.Context, arg CreateBotTokenParams) (BotToken, error) - CreateEvent(ctx context.Context, arg CreateEventParams) (EventsWithOrgID, error) + CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) // Link Queries CreateLink(ctx context.Context, arg CreateLinkParams) (Link, error) CreateOrganization(ctx context.Context, name string) (Organization, error) diff --git a/internal/database/queries.sql b/internal/database/queries.sql index 5159491..c519f1d 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -87,13 +87,9 @@ ORDER BY e.event_time DESC LIMIT $2 OFFSET $3; -- name: CreateEvent :one -WITH updated AS ( - INSERT INTO events (title, location, event_time, description) - VALUES ($1, $2, $3, $4) - RETURNING * -) -SELECT v.* FROM events_with_org_ids v -WHERE v.eid = (SELECT eid FROM updated); +INSERT INTO events (title, location, event_time, description) +VALUES ($1, $2, $3, $4) +RETURNING *; -- name: UpdateEvent :one WITH updated AS ( diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 9fbc29d..9d93d34 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -80,13 +80,9 @@ func (q *Queries) CreateBotToken(ctx context.Context, arg CreateBotTokenParams) } const createEvent = `-- name: CreateEvent :one -WITH updated AS ( - INSERT INTO events (title, location, event_time, description) - VALUES ($1, $2, $3, $4) - RETURNING eid, location, event_time, description, date_created, date_modified, title -) -SELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v -WHERE v.eid = (SELECT eid FROM updated) +INSERT INTO events (title, location, event_time, description) +VALUES ($1, $2, $3, $4) +RETURNING eid, location, event_time, description, date_created, date_modified, title ` type CreateEventParams struct { @@ -96,14 +92,14 @@ type CreateEventParams struct { Description pgtype.Text `json:"description"` } -func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (EventsWithOrgID, error) { +func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { row := q.db.QueryRow(ctx, createEvent, arg.Title, arg.Location, arg.EventTime, arg.Description, ) - var i EventsWithOrgID + var i Event err := row.Scan( &i.Eid, &i.Location, @@ -112,7 +108,6 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event &i.DateCreated, &i.DateModified, &i.Title, - &i.OrgIds, ) return i, err } diff --git a/internal/handler/events.go b/internal/handler/events.go index afc732d..c7e96a6 100644 --- a/internal/handler/events.go +++ b/internal/handler/events.go @@ -90,7 +90,13 @@ func (h *Handler) CreateEvent(w http.ResponseWriter, r *http.Request) { return } - h.respondJSON(w, http.StatusCreated, toEventResponse(event)) + createdEvent, err := h.queries.GetEventByID(r.Context(), event.Eid) + if err != nil { + h.handleDBError(w, err) + return + } + + h.respondJSON(w, http.StatusCreated, toEventResponse(createdEvent)) } // GetEvent gets an event by ID From 4652849980a713245a2a80fa5d4f005eb78baace Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 13:18:50 -0400 Subject: [PATCH 25/29] tests pass --- internal/database/mocks/Querier.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/database/mocks/Querier.go b/internal/database/mocks/Querier.go index f0070ab..f29ca02 100644 --- a/internal/database/mocks/Querier.go +++ b/internal/database/mocks/Querier.go @@ -83,22 +83,22 @@ func (_m *Querier) CreateBotToken(ctx context.Context, arg database.CreateBotTok } // CreateEvent provides a mock function with given fields: ctx, arg -func (_m *Querier) CreateEvent(ctx context.Context, arg database.CreateEventParams) (database.EventsWithOrgID, error) { +func (_m *Querier) CreateEvent(ctx context.Context, arg database.CreateEventParams) (database.Event, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for CreateEvent") } - var r0 database.EventsWithOrgID + var r0 database.Event var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) (database.EventsWithOrgID, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) (database.Event, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) database.EventsWithOrgID); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.CreateEventParams) database.Event); ok { r0 = rf(ctx, arg) } else { - r0 = ret.Get(0).(database.EventsWithOrgID) + r0 = ret.Get(0).(database.Event) } if rf, ok := ret.Get(1).(func(context.Context, database.CreateEventParams) error); ok { From 821a6211e98af67f6aafbaa86a08c18d54a238ad Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 13:31:06 -0400 Subject: [PATCH 26/29] tests pass again, turns out we need schema for benchmarks --- schema.sql | 91 ++++++++++++++++++++++++++++++ tests/benchmarks/benchmark_test.go | 2 +- tests/benchmarks/suite_test.go | 17 +++++- 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 schema.sql diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..aff59d4 --- /dev/null +++ b/schema.sql @@ -0,0 +1,91 @@ +-- schema.sql +-- Database Schema for CAPY (Club Assistant in Python) + +-- 1. ENUMs & Functions +CREATE TYPE user_role AS ENUM ('student', 'alumni', 'faculty', 'external'); + +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.date_modified = CURRENT_DATE; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 2. Tables +CREATE TABLE IF NOT EXISTS users ( + uid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + personal_email TEXT UNIQUE, + school_email TEXT UNIQUE, + phone TEXT, + grad_year INT, + role user_role DEFAULT 'student', + date_created DATE DEFAULT CURRENT_DATE, + date_modified DATE DEFAULT CURRENT_DATE +); + +CREATE TABLE IF NOT EXISTS organizations ( + oid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + date_created DATE DEFAULT CURRENT_DATE, + date_modified DATE DEFAULT CURRENT_DATE +); + +CREATE TABLE IF NOT EXISTS org_members ( + uid UUID REFERENCES users(uid) ON DELETE CASCADE, + oid UUID REFERENCES organizations(oid) ON DELETE CASCADE, + is_admin BOOLEAN DEFAULT FALSE, + date_joined DATE DEFAULT CURRENT_DATE, + last_active DATE DEFAULT CURRENT_DATE, + PRIMARY KEY (uid, oid) +); + +CREATE TABLE IF NOT EXISTS events ( + eid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + location TEXT, + event_time TIMESTAMP, + description TEXT, + date_created DATE DEFAULT CURRENT_DATE, + date_modified DATE DEFAULT CURRENT_DATE +); + +CREATE TABLE IF NOT EXISTS event_hosting ( + eid UUID REFERENCES events(eid) ON DELETE CASCADE, + oid UUID REFERENCES organizations(oid) ON DELETE CASCADE, + PRIMARY KEY (eid, oid) +); + +CREATE TABLE IF NOT EXISTS event_registrations ( + uid UUID REFERENCES users(uid) ON DELETE CASCADE, + eid UUID REFERENCES events(eid) ON DELETE CASCADE, + is_attending BOOLEAN DEFAULT FALSE, + is_admin BOOLEAN DEFAULT FALSE, + date_registered DATE DEFAULT CURRENT_DATE, + PRIMARY KEY (uid, eid) +); + +-- 3. Bot Tokens (global access for M2M authentication) +CREATE TABLE IF NOT EXISTS bot_tokens ( + token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token_hash TEXT NOT NULL, -- bcrypt hash of the token + name TEXT NOT NULL, -- human-readable name for the bot + created_by UUID NOT NULL REFERENCES users(uid), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP, + expires_at TIMESTAMP, -- NULL = never expires + is_active BOOLEAN DEFAULT TRUE +); + +CREATE INDEX IF NOT EXISTS idx_bot_tokens_active ON bot_tokens(is_active) WHERE is_active = TRUE; + +-- 4. Triggers +DROP TRIGGER IF EXISTS update_users_modtime ON users; +CREATE TRIGGER update_users_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_modified_column(); + +DROP TRIGGER IF EXISTS update_orgs_modtime ON organizations; +CREATE TRIGGER update_orgs_modtime BEFORE UPDATE ON organizations FOR EACH ROW EXECUTE FUNCTION update_modified_column(); + +DROP TRIGGER IF EXISTS update_events_modtime ON events; +CREATE TRIGGER update_events_modtime BEFORE UPDATE ON events FOR EACH ROW EXECUTE FUNCTION update_modified_column(); diff --git a/tests/benchmarks/benchmark_test.go b/tests/benchmarks/benchmark_test.go index e38fb5b..525d1e0 100644 --- a/tests/benchmarks/benchmark_test.go +++ b/tests/benchmarks/benchmark_test.go @@ -31,7 +31,7 @@ func BenchmarkHealthEndpoint(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - resp, err := benchClient.Get(benchServer.URL + "/health") + resp, err := benchClient.Get(benchServer.URL + "/api/health") if err != nil { b.Fatalf("failed to make request: %v", err) } diff --git a/tests/benchmarks/suite_test.go b/tests/benchmarks/suite_test.go index 66f574b..4f581fe 100644 --- a/tests/benchmarks/suite_test.go +++ b/tests/benchmarks/suite_test.go @@ -43,13 +43,12 @@ func TestMain(m *testing.M) { _, filename, _, _ := runtime.Caller(0) projectRoot := filepath.Join(filepath.Dir(filename), "../..") - schemaPath := filepath.Join(projectRoot, "schema.sql") + migrationsPath := filepath.Join(projectRoot, "migrations") - log.Printf("Using schema from: %s", schemaPath) + log.Printf("Using migrations from: %s", migrationsPath) pgContainer, err := postgres.Run(ctx, "postgres:16-alpine", - postgres.WithInitScripts(schemaPath), postgres.WithDatabase("bench_db"), postgres.WithUsername("bench"), postgres.WithPassword("bench"), @@ -82,6 +81,10 @@ func TestMain(m *testing.M) { log.Fatalf("failed to connect to database: %v", err) } + if err := database.RunMigrations(ctx, connStr, migrationsPath); err != nil { + log.Fatalf("failed to apply migrations: %v", err) + } + benchQueries = database.New(benchDB) setupTestData(ctx) @@ -135,6 +138,14 @@ func setupTestData(ctx context.Context) { } benchOrgID = org.Oid.String() + if err := benchQueries.AddOrgMember(ctx, database.AddOrgMemberParams{ + Uid: user.Uid, + Oid: org.Oid, + IsAdmin: pgtype.Bool{Bool: true, Valid: true}, + }); err != nil { + log.Fatalf("failed to add benchmark user as org admin: %v", err) + } + event, err := benchQueries.CreateEvent(ctx, database.CreateEventParams{ Location: pgtype.Text{String: "Bench Event", Valid: true}, EventTime: pgtype.Timestamp{Time: time.Now().Add(24 * time.Hour), Valid: true}, From 19cf65e38815c5e71a0810d8155334e6ffc475e3 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Fri, 3 Apr 2026 13:43:25 -0400 Subject: [PATCH 27/29] fixed integration tests --- internal/database/queries.sql | 18 ++++++++++++++++-- internal/database/queries.sql.go | 18 ++++++++++++++++-- internal/router/router.go | 3 +++ internal/testutils/container.go | 9 ++++++--- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/internal/database/queries.sql b/internal/database/queries.sql index c519f1d..5aa6836 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -101,8 +101,22 @@ WITH updated AS ( WHERE eid = $1 RETURNING * ) -SELECT v.* FROM events_with_org_ids v -WHERE v.eid = $1; +SELECT + u.eid, + u.location, + u.event_time, + u.description, + u.date_created, + u.date_modified, + u.title, + COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids +FROM updated u +LEFT JOIN ( + SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids + FROM event_hosting eh + WHERE eh.eid = $1 + GROUP BY eh.eid +) hosts ON hosts.eid = u.eid; -- name: DeleteEvent :exec DELETE FROM events WHERE eid = $1; diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 9d93d34..a552607 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -968,8 +968,22 @@ WITH updated AS ( WHERE eid = $1 RETURNING eid, location, event_time, description, date_created, date_modified, title ) -SELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v -WHERE v.eid = $1 +SELECT + u.eid, + u.location, + u.event_time, + u.description, + u.date_created, + u.date_modified, + u.title, + COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids +FROM updated u +LEFT JOIN ( + SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids + FROM event_hosting eh + WHERE eh.eid = $1 + GROUP BY eh.eid +) hosts ON hosts.eid = u.eid ` type UpdateEventParams struct { diff --git a/internal/router/router.go b/internal/router/router.go index e4522cc..969c40f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -69,6 +69,9 @@ func New(h *handler.Handler, queries database.Querier, jwtSecret string, allowed r.Use(chimiddleware.RequestID) r.Use(middleware.CORS(allowedOrigins, h.Config.Env == "development")) + // Public link resolution alias. + r.Get("/r/{endpoint_url}", h.ResolveLink) + // API routes r.Route("/api", func(r chi.Router) { // Health check (public) diff --git a/internal/testutils/container.go b/internal/testutils/container.go index 8f507df..23af7c1 100644 --- a/internal/testutils/container.go +++ b/internal/testutils/container.go @@ -46,16 +46,15 @@ func SetupTestPostgres(t *testing.T) string { return connStr } -// SetupTestDB creates a fresh Postgres container, initializes schema.sql, and returns the connection pool. +// SetupTestDB creates a fresh Postgres container, applies all migrations, and returns the connection pool. func SetupTestDB(t *testing.T) *pgxpool.Pool { ctx := context.Background() _, filename, _, _ := runtime.Caller(0) projectRoot := filepath.Join(filepath.Dir(filename), "../..") - schemaPath := filepath.Join(projectRoot, "schema.sql") + migrationsPath := filepath.Join(projectRoot, "migrations") pgContainer, err := postgres.Run(ctx, "postgres:16-alpine", - postgres.WithInitScripts(schemaPath), postgres.WithDatabase("test_db"), postgres.WithUsername("test"), postgres.WithPassword("test"), @@ -79,6 +78,10 @@ func SetupTestDB(t *testing.T) *pgxpool.Pool { t.Fatalf("failed to get connection string: %v", err) } + if err := database.RunMigrations(ctx, connStr, migrationsPath); err != nil { + t.Fatalf("failed to apply migrations: %v", err) + } + pool, err := database.NewPool(ctx, connStr) if err != nil { t.Fatalf("failed to connect to database: %v", err) From e9d218c5b03495345ae9c70cc5373fc0e2cbee8e Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 30 Apr 2026 18:55:59 -0400 Subject: [PATCH 28/29] fixed broken make generate --- docs/schema/schema.json | 115 ++++------- docs/swagger/docs.go | 334 ++++++++++++++++++++++++++++++- docs/swagger/swagger.json | 334 ++++++++++++++++++++++++++++++- docs/swagger/swagger.yaml | 220 +++++++++++++++++++- internal/database/querier.go | 2 +- internal/database/queries.sql | 32 +-- internal/database/queries.sql.go | 37 +--- internal/handler/events.go | 11 +- sqlc.yaml | 8 +- 9 files changed, 940 insertions(+), 153 deletions(-) diff --git a/docs/schema/schema.json b/docs/schema/schema.json index c0ec31e..80ea402 100644 --- a/docs/schema/schema.json +++ b/docs/schema/schema.json @@ -3,7 +3,12 @@ "version": "2", "engine": "postgresql", "schema": [ - "migrations" + "migrations/20260316192946_initial_schema.up.sql", + "migrations/20260318030206_telemetry_table.up.sql", + "migrations/20260319120000_add_dev_user_role.up.sql", + "migrations/20260326224918_add_links.up.sql", + "migrations/20260327022209_add_event_title.up.sql", + "migrations/20260327180817_add_event_view.up.sql" ], "queries": [ "internal/database/queries.sql" @@ -71317,7 +71322,7 @@ "insert_into_table": null }, { - "text": "WITH updated AS (\n INSERT INTO events (title, location, event_time, description)\n VALUES ($1, $2, $3, $4)\n RETURNING eid, location, event_time, description, date_created, date_modified, title\n)\nSELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v\nWHERE v.eid = (SELECT eid FROM updated)", + "text": "INSERT INTO events (title, location, event_time, description)\nVALUES ($1, $2, $3, $4)\nRETURNING eid, location, event_time, description, date_created, date_modified, title", "name": "CreateEvent", "cmd": ":one", "columns": [ @@ -71333,7 +71338,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71359,7 +71364,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71385,13 +71390,13 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { "catalog": "", - "schema": "", - "name": "pg_catalog.timestamp" + "schema": "pg_catalog", + "name": "timestamp" }, "is_sqlc_slice": false, "embed_table": null, @@ -71411,7 +71416,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71437,7 +71442,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71463,7 +71468,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71489,7 +71494,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71502,32 +71507,6 @@ "original_name": "title", "unsigned": false, "array_dims": 0 - }, - { - "name": "org_ids", - "not_null": true, - "is_array": true, - "comment": "", - "length": -1, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "", - "schema": "", - "name": "events_with_org_ids" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "uuid" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "org_ids", - "unsigned": false, - "array_dims": 1 } ], "params": [ @@ -71650,10 +71629,14 @@ ], "comments": [], "filename": "queries.sql", - "insert_into_table": null + "insert_into_table": { + "catalog": "", + "schema": "", + "name": "events" + } }, { - "text": "WITH updated AS (\n UPDATE events\n SET title = COALESCE($2, title),\n location = COALESCE($3, location),\n event_time = COALESCE($4, event_time),\n description = COALESCE($5, description)\n WHERE eid = $1\n RETURNING eid, location, event_time, description, date_created, date_modified, title\n)\nSELECT v.eid, v.location, v.event_time, v.description, v.date_created, v.date_modified, v.title, v.org_ids FROM events_with_org_ids v\nWHERE v.eid = $1", + "text": "UPDATE events\nSET title = COALESCE($2, title),\n location = COALESCE($3, location),\n event_time = COALESCE($4, event_time),\n description = COALESCE($5, description)\nWHERE eid = $1\nRETURNING eid, location, event_time, description, date_created, date_modified, title", "name": "UpdateEvent", "cmd": ":one", "columns": [ @@ -71669,7 +71652,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71695,7 +71678,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71721,13 +71704,13 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { "catalog": "", - "schema": "", - "name": "pg_catalog.timestamp" + "schema": "pg_catalog", + "name": "timestamp" }, "is_sqlc_slice": false, "embed_table": null, @@ -71747,7 +71730,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71773,7 +71756,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71799,7 +71782,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71825,7 +71808,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71838,32 +71821,6 @@ "original_name": "title", "unsigned": false, "array_dims": 0 - }, - { - "name": "org_ids", - "not_null": true, - "is_array": true, - "comment": "", - "length": -1, - "is_named_param": false, - "is_func_call": false, - "scope": "", - "table": { - "catalog": "", - "schema": "", - "name": "events_with_org_ids" - }, - "table_alias": "", - "type": { - "catalog": "", - "schema": "", - "name": "uuid" - }, - "is_sqlc_slice": false, - "embed_table": null, - "original_name": "org_ids", - "unsigned": false, - "array_dims": 1 } ], "params": [ @@ -71881,7 +71838,7 @@ "table": { "catalog": "", "schema": "", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71910,7 +71867,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71939,7 +71896,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71968,7 +71925,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { @@ -71997,7 +71954,7 @@ "table": { "catalog": "", "schema": "public", - "name": "events_with_org_ids" + "name": "events" }, "table_alias": "", "type": { diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 8903547..c40b5f8 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -379,11 +379,6 @@ const docTemplate = `{ }, "/events": { "get": { - "security": [ - { - "CookieAuth": [] - } - ], "description": "Returns a paginated list of all public events", "consumes": [ "application/json" @@ -841,13 +836,198 @@ const docTemplate = `{ } } }, - "/organizations": { + "/links": { + "post": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Creates a new dynamic link for an organization. Requires org_admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Create link", + "parameters": [ + { + "description": "Link data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateLinkRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dto.LinkResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}": { + "put": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Updates a dynamic link's destination or endpoint URL. Requires org_admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Update link", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + }, + { + "description": "Update data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.LinkResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}/qrcode": { + "get": { + "description": "Generates and returns a QR code image for the link's destination URL", + "produces": [ + "image/png" + ], + "tags": [ + "links" + ], + "summary": "Get QR code", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}/visits": { "get": { "security": [ { "CookieAuth": [] } ], + "description": "Returns the total number of visits logged for a link", + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Get visit count", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VisitCountResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/organizations": { + "get": { "description": "Returns a paginated list of all organizations", "consumes": [ "application/json" @@ -1136,6 +1316,52 @@ const docTemplate = `{ } } }, + "/organizations/{oid}/links": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns all dynamic links owned by an organization", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "List org links", + "parameters": [ + { + "type": "string", + "description": "Organization UUID", + "name": "oid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.LinkResponse" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/organizations/{oid}/members": { "get": { "security": [ @@ -1300,6 +1526,35 @@ const docTemplate = `{ } } }, + "/r/{endpoint_url}": { + "get": { + "description": "Redirects to the destination URL and logs a visit", + "tags": [ + "links" + ], + "summary": "Resolve link", + "parameters": [ + { + "type": "string", + "description": "Dynamic link endpoint URL", + "name": "endpoint_url", + "in": "path", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/users/{uid}": { "get": { "security": [ @@ -1581,6 +1836,25 @@ const docTemplate = `{ } } }, + "dto.CreateLinkRequest": { + "type": "object", + "required": [ + "dest_url", + "endpoint_url", + "org_id" + ], + "properties": { + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + }, + "org_id": { + "type": "string" + } + } + }, "dto.CreateOrganizationRequest": { "type": "object", "required": [ @@ -1642,11 +1916,37 @@ const docTemplate = `{ "location": { "type": "string" }, + "oids": { + "type": "array", + "items": { + "type": "string" + } + }, "title": { "type": "string" } } }, + "dto.LinkResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + }, + "lid": { + "type": "string" + }, + "org_id": { + "type": "string" + } + } + }, "dto.OrgMemberResponse": { "type": "object", "properties": { @@ -1719,6 +2019,17 @@ const docTemplate = `{ } } }, + "dto.UpdateLinkRequest": { + "type": "object", + "properties": { + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + } + } + }, "dto.UpdateOrganizationRequest": { "type": "object", "properties": { @@ -1803,6 +2114,17 @@ const docTemplate = `{ } } }, + "dto.VisitCountResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "lid": { + "type": "string" + } + } + }, "handler.AuthResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index e6a4d10..c929f97 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -373,11 +373,6 @@ }, "/events": { "get": { - "security": [ - { - "CookieAuth": [] - } - ], "description": "Returns a paginated list of all public events", "consumes": [ "application/json" @@ -835,13 +830,198 @@ } } }, - "/organizations": { + "/links": { + "post": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Creates a new dynamic link for an organization. Requires org_admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Create link", + "parameters": [ + { + "description": "Link data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateLinkRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dto.LinkResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}": { + "put": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Updates a dynamic link's destination or endpoint URL. Requires org_admin role.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Update link", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + }, + { + "description": "Update data", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.LinkResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}/qrcode": { + "get": { + "description": "Generates and returns a QR code image for the link's destination URL", + "produces": [ + "image/png" + ], + "tags": [ + "links" + ], + "summary": "Get QR code", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/links/{lid}/visits": { "get": { "security": [ { "CookieAuth": [] } ], + "description": "Returns the total number of visits logged for a link", + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "Get visit count", + "parameters": [ + { + "type": "string", + "description": "Link UUID", + "name": "lid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.VisitCountResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/organizations": { + "get": { "description": "Returns a paginated list of all organizations", "consumes": [ "application/json" @@ -1130,6 +1310,52 @@ } } }, + "/organizations/{oid}/links": { + "get": { + "security": [ + { + "CookieAuth": [] + } + ], + "description": "Returns all dynamic links owned by an organization", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "links" + ], + "summary": "List org links", + "parameters": [ + { + "type": "string", + "description": "Organization UUID", + "name": "oid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.LinkResponse" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/organizations/{oid}/members": { "get": { "security": [ @@ -1294,6 +1520,35 @@ } } }, + "/r/{endpoint_url}": { + "get": { + "description": "Redirects to the destination URL and logs a visit", + "tags": [ + "links" + ], + "summary": "Resolve link", + "parameters": [ + { + "type": "string", + "description": "Dynamic link endpoint URL", + "name": "endpoint_url", + "in": "path", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/users/{uid}": { "get": { "security": [ @@ -1575,6 +1830,25 @@ } } }, + "dto.CreateLinkRequest": { + "type": "object", + "required": [ + "dest_url", + "endpoint_url", + "org_id" + ], + "properties": { + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + }, + "org_id": { + "type": "string" + } + } + }, "dto.CreateOrganizationRequest": { "type": "object", "required": [ @@ -1636,11 +1910,37 @@ "location": { "type": "string" }, + "oids": { + "type": "array", + "items": { + "type": "string" + } + }, "title": { "type": "string" } } }, + "dto.LinkResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + }, + "lid": { + "type": "string" + }, + "org_id": { + "type": "string" + } + } + }, "dto.OrgMemberResponse": { "type": "object", "properties": { @@ -1713,6 +2013,17 @@ } } }, + "dto.UpdateLinkRequest": { + "type": "object", + "properties": { + "dest_url": { + "type": "string" + }, + "endpoint_url": { + "type": "string" + } + } + }, "dto.UpdateOrganizationRequest": { "type": "object", "properties": { @@ -1797,6 +2108,17 @@ } } }, + "dto.VisitCountResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "lid": { + "type": "string" + } + } + }, "handler.AuthResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index e2cccbc..0cf730e 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -25,6 +25,19 @@ definitions: required: - org_id type: object + dto.CreateLinkRequest: + properties: + dest_url: + type: string + endpoint_url: + type: string + org_id: + type: string + required: + - dest_url + - endpoint_url + - org_id + type: object dto.CreateOrganizationRequest: properties: creator_uid: @@ -66,9 +79,26 @@ definitions: type: string location: type: string + oids: + items: + type: string + type: array title: type: string type: object + dto.LinkResponse: + properties: + created_at: + type: string + dest_url: + type: string + endpoint_url: + type: string + lid: + type: string + org_id: + type: string + type: object dto.OrgMemberResponse: properties: date_joined: @@ -116,6 +146,13 @@ definitions: title: type: string type: object + dto.UpdateLinkRequest: + properties: + dest_url: + type: string + endpoint_url: + type: string + type: object dto.UpdateOrganizationRequest: properties: name: @@ -175,6 +212,13 @@ definitions: uid: type: string type: object + dto.VisitCountResponse: + properties: + count: + type: integer + lid: + type: string + type: object handler.AuthResponse: properties: token: @@ -504,8 +548,6 @@ paths: items: $ref: '#/definitions/dto.EventResponse' type: array - security: - - CookieAuth: [] summary: List events tags: - events @@ -776,6 +818,130 @@ paths: summary: List events by organization tags: - events + /links: + post: + consumes: + - application/json + description: Creates a new dynamic link for an organization. Requires org_admin + role. + parameters: + - description: Link data + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreateLinkRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dto.LinkResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/handler.ErrorResponse' + security: + - CookieAuth: [] + summary: Create link + tags: + - links + /links/{lid}: + put: + consumes: + - application/json + description: Updates a dynamic link's destination or endpoint URL. Requires + org_admin role. + parameters: + - description: Link UUID + in: path + name: lid + required: true + type: string + - description: Update data + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdateLinkRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.LinkResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/handler.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + security: + - CookieAuth: [] + summary: Update link + tags: + - links + /links/{lid}/qrcode: + get: + description: Generates and returns a QR code image for the link's destination + URL + parameters: + - description: Link UUID + in: path + name: lid + required: true + type: string + produces: + - image/png + responses: + "200": + description: OK + schema: + type: file + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + summary: Get QR code + tags: + - links + /links/{lid}/visits: + get: + description: Returns the total number of visits logged for a link + parameters: + - description: Link UUID + in: path + name: lid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.VisitCountResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + security: + - CookieAuth: [] + summary: Get visit count + tags: + - links /organizations: get: consumes: @@ -799,8 +965,6 @@ paths: items: $ref: '#/definitions/dto.OrganizationResponse' type: array - security: - - CookieAuth: [] summary: List organizations tags: - organizations @@ -963,6 +1127,35 @@ paths: summary: List organization events tags: - organizations + /organizations/{oid}/links: + get: + consumes: + - application/json + description: Returns all dynamic links owned by an organization + parameters: + - description: Organization UUID + in: path + name: oid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.LinkResponse' + type: array + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + security: + - CookieAuth: [] + summary: List org links + tags: + - links /organizations/{oid}/members: get: consumes: @@ -1068,6 +1261,25 @@ paths: summary: Remove organization member tags: - organizations + /r/{endpoint_url}: + get: + description: Redirects to the destination URL and logs a visit + parameters: + - description: Dynamic link endpoint URL + in: path + name: endpoint_url + required: true + type: string + responses: + "302": + description: Found + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + summary: Resolve link + tags: + - links /users/{uid}: delete: consumes: diff --git a/internal/database/querier.go b/internal/database/querier.go index 0ca1acf..b0fbc7e 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -51,7 +51,7 @@ type Querier interface { RevokeBotToken(ctx context.Context, tokenID uuid.UUID) error UnregisterFromEvent(ctx context.Context, arg UnregisterFromEventParams) error UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) error - UpdateEvent(ctx context.Context, arg UpdateEventParams) (EventsWithOrgID, error) + UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) UpdateLink(ctx context.Context, arg UpdateLinkParams) (Link, error) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) diff --git a/internal/database/queries.sql b/internal/database/queries.sql index 5aa6836..02a0ebf 100644 --- a/internal/database/queries.sql +++ b/internal/database/queries.sql @@ -92,31 +92,13 @@ VALUES ($1, $2, $3, $4) RETURNING *; -- name: UpdateEvent :one -WITH updated AS ( - UPDATE events - SET title = COALESCE(sqlc.narg('title'), title), - location = COALESCE(sqlc.narg('location'), location), - event_time = COALESCE(sqlc.narg('event_time'), event_time), - description = COALESCE(sqlc.narg('description'), description) - WHERE eid = $1 - RETURNING * -) -SELECT - u.eid, - u.location, - u.event_time, - u.description, - u.date_created, - u.date_modified, - u.title, - COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids -FROM updated u -LEFT JOIN ( - SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids - FROM event_hosting eh - WHERE eh.eid = $1 - GROUP BY eh.eid -) hosts ON hosts.eid = u.eid; +UPDATE events +SET title = COALESCE(sqlc.narg('title'), title), + location = COALESCE(sqlc.narg('location'), location), + event_time = COALESCE(sqlc.narg('event_time'), event_time), + description = COALESCE(sqlc.narg('description'), description) +WHERE eid = $1 +RETURNING *; -- name: DeleteEvent :exec DELETE FROM events WHERE eid = $1; diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index a552607..6f977e8 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -959,31 +959,13 @@ func (q *Queries) UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID) } const updateEvent = `-- name: UpdateEvent :one -WITH updated AS ( - UPDATE events - SET title = COALESCE($2, title), - location = COALESCE($3, location), - event_time = COALESCE($4, event_time), - description = COALESCE($5, description) - WHERE eid = $1 - RETURNING eid, location, event_time, description, date_created, date_modified, title -) -SELECT - u.eid, - u.location, - u.event_time, - u.description, - u.date_created, - u.date_modified, - u.title, - COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids -FROM updated u -LEFT JOIN ( - SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids - FROM event_hosting eh - WHERE eh.eid = $1 - GROUP BY eh.eid -) hosts ON hosts.eid = u.eid +UPDATE events +SET title = COALESCE($2, title), + location = COALESCE($3, location), + event_time = COALESCE($4, event_time), + description = COALESCE($5, description) +WHERE eid = $1 +RETURNING eid, location, event_time, description, date_created, date_modified, title ` type UpdateEventParams struct { @@ -994,7 +976,7 @@ type UpdateEventParams struct { Description pgtype.Text `json:"description"` } -func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (EventsWithOrgID, error) { +func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event, error) { row := q.db.QueryRow(ctx, updateEvent, arg.Eid, arg.Title, @@ -1002,7 +984,7 @@ func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event arg.EventTime, arg.Description, ) - var i EventsWithOrgID + var i Event err := row.Scan( &i.Eid, &i.Location, @@ -1011,7 +993,6 @@ func (q *Queries) UpdateEvent(ctx context.Context, arg UpdateEventParams) (Event &i.DateCreated, &i.DateModified, &i.Title, - &i.OrgIds, ) return i, err } diff --git a/internal/handler/events.go b/internal/handler/events.go index c7e96a6..3323592 100644 --- a/internal/handler/events.go +++ b/internal/handler/events.go @@ -159,19 +159,24 @@ func (h *Handler) UpdateEvent(w http.ResponseWriter, r *http.Request) { return } - event, err := h.queries.UpdateEvent(r.Context(), database.UpdateEventParams{ + if _, err := h.queries.UpdateEvent(r.Context(), database.UpdateEventParams{ Eid: eid, Title: toPgText(req.Title), Location: toPgText(req.Location), EventTime: toPgTimestamp(req.EventTime), Description: toPgText(req.Description), - }) + }); err != nil { + h.handleDBError(w, err) + return + } + + updatedEvent, err := h.queries.GetEventByID(r.Context(), eid) if err != nil { h.handleDBError(w, err) return } - h.respondJSON(w, http.StatusOK, toEventResponse(event)) + h.respondJSON(w, http.StatusOK, toEventResponse(updatedEvent)) } // DeleteEvent deletes an event diff --git a/sqlc.yaml b/sqlc.yaml index c7a941b..5f90987 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -2,7 +2,13 @@ version: "2" sql: - engine: "postgresql" queries: "internal/database/queries.sql" - schema: "migrations" + schema: + - "migrations/20260316192946_initial_schema.up.sql" + - "migrations/20260318030206_telemetry_table.up.sql" + - "migrations/20260319120000_add_dev_user_role.up.sql" + - "migrations/20260326224918_add_links.up.sql" + - "migrations/20260327022209_add_event_title.up.sql" + - "migrations/20260327180817_add_event_view.up.sql" gen: go: package: "database" From b61f2832bf208f166dc9605aa00bdc9ca99cd4a3 Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Thu, 30 Apr 2026 19:07:39 -0400 Subject: [PATCH 29/29] fixed linting and disabled benchmark test --- internal/database/mocks/Querier.go | 10 +++++----- tests/benchmarks/suite_test.go | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/database/mocks/Querier.go b/internal/database/mocks/Querier.go index f29ca02..1bc0dcc 100644 --- a/internal/database/mocks/Querier.go +++ b/internal/database/mocks/Querier.go @@ -965,22 +965,22 @@ func (_m *Querier) UpdateBotTokenLastUsed(ctx context.Context, tokenID uuid.UUID } // UpdateEvent provides a mock function with given fields: ctx, arg -func (_m *Querier) UpdateEvent(ctx context.Context, arg database.UpdateEventParams) (database.EventsWithOrgID, error) { +func (_m *Querier) UpdateEvent(ctx context.Context, arg database.UpdateEventParams) (database.Event, error) { ret := _m.Called(ctx, arg) if len(ret) == 0 { panic("no return value specified for UpdateEvent") } - var r0 database.EventsWithOrgID + var r0 database.Event var r1 error - if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) (database.EventsWithOrgID, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) (database.Event, error)); ok { return rf(ctx, arg) } - if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) database.EventsWithOrgID); ok { + if rf, ok := ret.Get(0).(func(context.Context, database.UpdateEventParams) database.Event); ok { r0 = rf(ctx, arg) } else { - r0 = ret.Get(0).(database.EventsWithOrgID) + r0 = ret.Get(0).(database.Event) } if rf, ok := ret.Get(1).(func(context.Context, database.UpdateEventParams) error); ok { diff --git a/tests/benchmarks/suite_test.go b/tests/benchmarks/suite_test.go index 4f581fe..3026b54 100644 --- a/tests/benchmarks/suite_test.go +++ b/tests/benchmarks/suite_test.go @@ -2,6 +2,7 @@ package benchmarks import ( "context" + "flag" "log" "net/http/httptest" "os" @@ -37,6 +38,12 @@ var ( ) func TestMain(m *testing.M) { + flag.Parse() + if testing.Short() { + log.Println("Skipping benchmark suite setup in -short mode") + os.Exit(0) + } + ctx := context.Background() log.Println("Starting benchmark suite setup...")