|
| 1 | +-- 20260706000000_publication_bibliographic_rollup.sql |
| 2 | +-- |
| 3 | +-- Rolls up bibliographic-field edits from one vault_publications copy to the |
| 4 | +-- canonical publications row and every sibling vault_publications copy |
| 5 | +-- (same original_publication_id, different vault), atomically. Parameters |
| 6 | +-- are p_-prefixed to avoid the ambiguous-column-reference bug fixed in |
| 7 | +-- migration 006 (a bare `vault_id = vault_id` inside a function body is |
| 8 | +-- parsed as comparing the column to itself, not to the parameter, if the |
| 9 | +-- parameter shares the column's name). |
| 10 | +-- |
| 11 | +-- notes is intentionally never part of the bibliographic field list below — |
| 12 | +-- it's vault-local and, like tag_ids, must never propagate. |
| 13 | + |
| 14 | +CREATE OR REPLACE FUNCTION "public"."update_vault_publication_with_rollup"( |
| 15 | + "p_vault_publication_id" "uuid", |
| 16 | + "p_vault_id" "uuid", |
| 17 | + "p_patch" "jsonb", |
| 18 | + "p_actor_user_id" "uuid" |
| 19 | +) RETURNS "void" |
| 20 | + LANGUAGE "plpgsql" |
| 21 | + AS $$ |
| 22 | +DECLARE |
| 23 | + v_original_id uuid; |
| 24 | + v_has_bibliographic_patch boolean; |
| 25 | +BEGIN |
| 26 | + -- This function is only ever called under the service-role key (see |
| 27 | + -- .netlify's handleUpdateItem), which has no JWT context of its own, so |
| 28 | + -- auth.uid() would otherwise resolve to NULL for the rest of this |
| 29 | + -- transaction. vault_publications' own "set updated_by from auth.uid()" |
| 30 | + -- BEFORE UPDATE trigger fires on every UPDATE below regardless -- setting |
| 31 | + -- this makes it record the real actor instead of overwriting every touched |
| 32 | + -- row's updated_by with NULL. |
| 33 | + PERFORM set_config('request.jwt.claim.sub', p_actor_user_id::text, true); |
| 34 | + |
| 35 | + UPDATE vault_publications SET |
| 36 | + title = CASE WHEN p_patch ? 'title' THEN p_patch->>'title' ELSE title END, |
| 37 | + authors = CASE |
| 38 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'array' |
| 39 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'authors')) |
| 40 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'null' |
| 41 | + THEN NULL |
| 42 | + ELSE authors |
| 43 | + END, |
| 44 | + year = CASE |
| 45 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'null' |
| 46 | + THEN NULL |
| 47 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'number' |
| 48 | + THEN (p_patch->>'year')::numeric::integer |
| 49 | + WHEN p_patch ? 'year' |
| 50 | + THEN year |
| 51 | + ELSE year |
| 52 | + END, |
| 53 | + journal = CASE WHEN p_patch ? 'journal' THEN p_patch->>'journal' ELSE journal END, |
| 54 | + volume = CASE WHEN p_patch ? 'volume' THEN p_patch->>'volume' ELSE volume END, |
| 55 | + issue = CASE WHEN p_patch ? 'issue' THEN p_patch->>'issue' ELSE issue END, |
| 56 | + pages = CASE WHEN p_patch ? 'pages' THEN p_patch->>'pages' ELSE pages END, |
| 57 | + doi = CASE WHEN p_patch ? 'doi' THEN p_patch->>'doi' ELSE doi END, |
| 58 | + url = CASE WHEN p_patch ? 'url' THEN p_patch->>'url' ELSE url END, |
| 59 | + abstract = CASE WHEN p_patch ? 'abstract' THEN p_patch->>'abstract' ELSE abstract END, |
| 60 | + pdf_url = CASE WHEN p_patch ? 'pdf_url' THEN p_patch->>'pdf_url' ELSE pdf_url END, |
| 61 | + bibtex_key = CASE WHEN p_patch ? 'bibtex_key' THEN p_patch->>'bibtex_key' ELSE bibtex_key END, |
| 62 | + publication_type = CASE WHEN p_patch ? 'publication_type' THEN p_patch->>'publication_type' ELSE publication_type END, |
| 63 | + notes = CASE WHEN p_patch ? 'notes' THEN p_patch->>'notes' ELSE notes END, |
| 64 | + booktitle = CASE WHEN p_patch ? 'booktitle' THEN p_patch->>'booktitle' ELSE booktitle END, |
| 65 | + chapter = CASE WHEN p_patch ? 'chapter' THEN p_patch->>'chapter' ELSE chapter END, |
| 66 | + edition = CASE WHEN p_patch ? 'edition' THEN p_patch->>'edition' ELSE edition END, |
| 67 | + editor = CASE |
| 68 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'array' |
| 69 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'editor')) |
| 70 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'null' |
| 71 | + THEN NULL |
| 72 | + ELSE editor |
| 73 | + END, |
| 74 | + howpublished = CASE WHEN p_patch ? 'howpublished' THEN p_patch->>'howpublished' ELSE howpublished END, |
| 75 | + institution = CASE WHEN p_patch ? 'institution' THEN p_patch->>'institution' ELSE institution END, |
| 76 | + number = CASE WHEN p_patch ? 'number' THEN p_patch->>'number' ELSE number END, |
| 77 | + organization = CASE WHEN p_patch ? 'organization' THEN p_patch->>'organization' ELSE organization END, |
| 78 | + publisher = CASE WHEN p_patch ? 'publisher' THEN p_patch->>'publisher' ELSE publisher END, |
| 79 | + school = CASE WHEN p_patch ? 'school' THEN p_patch->>'school' ELSE school END, |
| 80 | + series = CASE WHEN p_patch ? 'series' THEN p_patch->>'series' ELSE series END, |
| 81 | + type = CASE WHEN p_patch ? 'type' THEN p_patch->>'type' ELSE type END, |
| 82 | + eid = CASE WHEN p_patch ? 'eid' THEN p_patch->>'eid' ELSE eid END, |
| 83 | + isbn = CASE WHEN p_patch ? 'isbn' THEN p_patch->>'isbn' ELSE isbn END, |
| 84 | + issn = CASE WHEN p_patch ? 'issn' THEN p_patch->>'issn' ELSE issn END, |
| 85 | + keywords = CASE |
| 86 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'array' |
| 87 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'keywords')) |
| 88 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'null' |
| 89 | + THEN NULL |
| 90 | + ELSE keywords |
| 91 | + END, |
| 92 | + version = version + 1, |
| 93 | + updated_at = now() |
| 94 | + WHERE id = p_vault_publication_id AND vault_id = p_vault_id |
| 95 | + RETURNING original_publication_id INTO v_original_id; |
| 96 | + |
| 97 | + IF NOT FOUND THEN |
| 98 | + RAISE EXCEPTION 'vault publication % not found in vault %', p_vault_publication_id, p_vault_id |
| 99 | + USING ERRCODE = 'P0002'; |
| 100 | + END IF; |
| 101 | + |
| 102 | + v_has_bibliographic_patch := p_patch ?| ARRAY[ |
| 103 | + 'title', 'authors', 'year', 'journal', 'volume', 'issue', 'pages', 'doi', 'url', |
| 104 | + 'abstract', 'pdf_url', 'bibtex_key', 'publication_type', 'booktitle', 'chapter', |
| 105 | + 'edition', 'editor', 'howpublished', 'institution', 'number', 'organization', |
| 106 | + 'publisher', 'school', 'series', 'type', 'eid', 'isbn', 'issn', 'keywords' |
| 107 | + ]; |
| 108 | + |
| 109 | + IF v_original_id IS NOT NULL AND v_has_bibliographic_patch THEN |
| 110 | + UPDATE publications SET |
| 111 | + title = CASE WHEN p_patch ? 'title' THEN p_patch->>'title' ELSE title END, |
| 112 | + authors = CASE |
| 113 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'array' |
| 114 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'authors')) |
| 115 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'null' |
| 116 | + THEN NULL |
| 117 | + ELSE authors |
| 118 | + END, |
| 119 | + year = CASE |
| 120 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'null' |
| 121 | + THEN NULL |
| 122 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'number' |
| 123 | + THEN (p_patch->>'year')::numeric::integer |
| 124 | + WHEN p_patch ? 'year' |
| 125 | + THEN year |
| 126 | + ELSE year |
| 127 | + END, |
| 128 | + journal = CASE WHEN p_patch ? 'journal' THEN p_patch->>'journal' ELSE journal END, |
| 129 | + volume = CASE WHEN p_patch ? 'volume' THEN p_patch->>'volume' ELSE volume END, |
| 130 | + issue = CASE WHEN p_patch ? 'issue' THEN p_patch->>'issue' ELSE issue END, |
| 131 | + pages = CASE WHEN p_patch ? 'pages' THEN p_patch->>'pages' ELSE pages END, |
| 132 | + doi = CASE WHEN p_patch ? 'doi' THEN p_patch->>'doi' ELSE doi END, |
| 133 | + url = CASE WHEN p_patch ? 'url' THEN p_patch->>'url' ELSE url END, |
| 134 | + abstract = CASE WHEN p_patch ? 'abstract' THEN p_patch->>'abstract' ELSE abstract END, |
| 135 | + pdf_url = CASE WHEN p_patch ? 'pdf_url' THEN p_patch->>'pdf_url' ELSE pdf_url END, |
| 136 | + bibtex_key = CASE WHEN p_patch ? 'bibtex_key' THEN p_patch->>'bibtex_key' ELSE bibtex_key END, |
| 137 | + publication_type = CASE WHEN p_patch ? 'publication_type' THEN p_patch->>'publication_type' ELSE publication_type END, |
| 138 | + booktitle = CASE WHEN p_patch ? 'booktitle' THEN p_patch->>'booktitle' ELSE booktitle END, |
| 139 | + chapter = CASE WHEN p_patch ? 'chapter' THEN p_patch->>'chapter' ELSE chapter END, |
| 140 | + edition = CASE WHEN p_patch ? 'edition' THEN p_patch->>'edition' ELSE edition END, |
| 141 | + editor = CASE |
| 142 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'array' |
| 143 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'editor')) |
| 144 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'null' |
| 145 | + THEN NULL |
| 146 | + ELSE editor |
| 147 | + END, |
| 148 | + howpublished = CASE WHEN p_patch ? 'howpublished' THEN p_patch->>'howpublished' ELSE howpublished END, |
| 149 | + institution = CASE WHEN p_patch ? 'institution' THEN p_patch->>'institution' ELSE institution END, |
| 150 | + number = CASE WHEN p_patch ? 'number' THEN p_patch->>'number' ELSE number END, |
| 151 | + organization = CASE WHEN p_patch ? 'organization' THEN p_patch->>'organization' ELSE organization END, |
| 152 | + publisher = CASE WHEN p_patch ? 'publisher' THEN p_patch->>'publisher' ELSE publisher END, |
| 153 | + school = CASE WHEN p_patch ? 'school' THEN p_patch->>'school' ELSE school END, |
| 154 | + series = CASE WHEN p_patch ? 'series' THEN p_patch->>'series' ELSE series END, |
| 155 | + type = CASE WHEN p_patch ? 'type' THEN p_patch->>'type' ELSE type END, |
| 156 | + eid = CASE WHEN p_patch ? 'eid' THEN p_patch->>'eid' ELSE eid END, |
| 157 | + isbn = CASE WHEN p_patch ? 'isbn' THEN p_patch->>'isbn' ELSE isbn END, |
| 158 | + issn = CASE WHEN p_patch ? 'issn' THEN p_patch->>'issn' ELSE issn END, |
| 159 | + keywords = CASE |
| 160 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'array' |
| 161 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'keywords')) |
| 162 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'null' |
| 163 | + THEN NULL |
| 164 | + ELSE keywords |
| 165 | + END, |
| 166 | + updated_at = now() |
| 167 | + WHERE id = v_original_id; |
| 168 | + |
| 169 | + UPDATE vault_publications SET |
| 170 | + title = CASE WHEN p_patch ? 'title' THEN p_patch->>'title' ELSE title END, |
| 171 | + authors = CASE |
| 172 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'array' |
| 173 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'authors')) |
| 174 | + WHEN p_patch ? 'authors' AND jsonb_typeof(p_patch->'authors') = 'null' |
| 175 | + THEN NULL |
| 176 | + ELSE authors |
| 177 | + END, |
| 178 | + year = CASE |
| 179 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'null' |
| 180 | + THEN NULL |
| 181 | + WHEN p_patch ? 'year' AND jsonb_typeof(p_patch->'year') = 'number' |
| 182 | + THEN (p_patch->>'year')::numeric::integer |
| 183 | + WHEN p_patch ? 'year' |
| 184 | + THEN year |
| 185 | + ELSE year |
| 186 | + END, |
| 187 | + journal = CASE WHEN p_patch ? 'journal' THEN p_patch->>'journal' ELSE journal END, |
| 188 | + volume = CASE WHEN p_patch ? 'volume' THEN p_patch->>'volume' ELSE volume END, |
| 189 | + issue = CASE WHEN p_patch ? 'issue' THEN p_patch->>'issue' ELSE issue END, |
| 190 | + pages = CASE WHEN p_patch ? 'pages' THEN p_patch->>'pages' ELSE pages END, |
| 191 | + doi = CASE WHEN p_patch ? 'doi' THEN p_patch->>'doi' ELSE doi END, |
| 192 | + url = CASE WHEN p_patch ? 'url' THEN p_patch->>'url' ELSE url END, |
| 193 | + abstract = CASE WHEN p_patch ? 'abstract' THEN p_patch->>'abstract' ELSE abstract END, |
| 194 | + pdf_url = CASE WHEN p_patch ? 'pdf_url' THEN p_patch->>'pdf_url' ELSE pdf_url END, |
| 195 | + bibtex_key = CASE WHEN p_patch ? 'bibtex_key' THEN p_patch->>'bibtex_key' ELSE bibtex_key END, |
| 196 | + publication_type = CASE WHEN p_patch ? 'publication_type' THEN p_patch->>'publication_type' ELSE publication_type END, |
| 197 | + booktitle = CASE WHEN p_patch ? 'booktitle' THEN p_patch->>'booktitle' ELSE booktitle END, |
| 198 | + chapter = CASE WHEN p_patch ? 'chapter' THEN p_patch->>'chapter' ELSE chapter END, |
| 199 | + edition = CASE WHEN p_patch ? 'edition' THEN p_patch->>'edition' ELSE edition END, |
| 200 | + editor = CASE |
| 201 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'array' |
| 202 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'editor')) |
| 203 | + WHEN p_patch ? 'editor' AND jsonb_typeof(p_patch->'editor') = 'null' |
| 204 | + THEN NULL |
| 205 | + ELSE editor |
| 206 | + END, |
| 207 | + howpublished = CASE WHEN p_patch ? 'howpublished' THEN p_patch->>'howpublished' ELSE howpublished END, |
| 208 | + institution = CASE WHEN p_patch ? 'institution' THEN p_patch->>'institution' ELSE institution END, |
| 209 | + number = CASE WHEN p_patch ? 'number' THEN p_patch->>'number' ELSE number END, |
| 210 | + organization = CASE WHEN p_patch ? 'organization' THEN p_patch->>'organization' ELSE organization END, |
| 211 | + publisher = CASE WHEN p_patch ? 'publisher' THEN p_patch->>'publisher' ELSE publisher END, |
| 212 | + school = CASE WHEN p_patch ? 'school' THEN p_patch->>'school' ELSE school END, |
| 213 | + series = CASE WHEN p_patch ? 'series' THEN p_patch->>'series' ELSE series END, |
| 214 | + type = CASE WHEN p_patch ? 'type' THEN p_patch->>'type' ELSE type END, |
| 215 | + eid = CASE WHEN p_patch ? 'eid' THEN p_patch->>'eid' ELSE eid END, |
| 216 | + isbn = CASE WHEN p_patch ? 'isbn' THEN p_patch->>'isbn' ELSE isbn END, |
| 217 | + issn = CASE WHEN p_patch ? 'issn' THEN p_patch->>'issn' ELSE issn END, |
| 218 | + keywords = CASE |
| 219 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'array' |
| 220 | + THEN ARRAY(SELECT jsonb_array_elements_text(p_patch->'keywords')) |
| 221 | + WHEN p_patch ? 'keywords' AND jsonb_typeof(p_patch->'keywords') = 'null' |
| 222 | + THEN NULL |
| 223 | + ELSE keywords |
| 224 | + END, |
| 225 | + updated_at = now() |
| 226 | + WHERE original_publication_id = v_original_id AND id <> p_vault_publication_id; |
| 227 | + END IF; |
| 228 | +END; |
| 229 | +$$; |
| 230 | + |
| 231 | +ALTER FUNCTION "public"."update_vault_publication_with_rollup"("p_vault_publication_id" "uuid", "p_vault_id" "uuid", "p_patch" "jsonb", "p_actor_user_id" "uuid") OWNER TO "postgres"; |
| 232 | + |
| 233 | +-- Unlike copy_publication_to_vault and most other functions in this schema |
| 234 | +-- (granted to anon/authenticated/service_role alike, since they're meant to |
| 235 | +-- be called by regular users), this function trusts its caller completely: |
| 236 | +-- it has no independent authorization of its own, no auth/scope/vault-access |
| 237 | +-- checks -- those already happened in .netlify's handleUpdateItem before the |
| 238 | +-- RPC call. Restricting EXECUTE to service_role only means the only caller |
| 239 | +-- that can ever invoke it is the one that already did those checks, and RLS |
| 240 | +-- (which service_role bypasses) never becomes a factor either way. |
| 241 | +REVOKE ALL ON FUNCTION "public"."update_vault_publication_with_rollup"("p_vault_publication_id" "uuid", "p_vault_id" "uuid", "p_patch" "jsonb", "p_actor_user_id" "uuid") FROM PUBLIC; |
| 242 | +GRANT ALL ON FUNCTION "public"."update_vault_publication_with_rollup"("p_vault_publication_id" "uuid", "p_vault_id" "uuid", "p_patch" "jsonb", "p_actor_user_id" "uuid") TO "service_role"; |
0 commit comments