···1313type Props = {
1414 // this is now a token id not leaflet! Should probs rename
1515 params: Promise<{ leaflet_id: string }>;
1616+ searchParams: Promise<{
1717+ publication_uri: string;
1818+ title: string;
1919+ description: string;
2020+ entitiesToDelete: string;
2121+ }>;
1622};
1723export default async function PublishLeafletPage(props: Props) {
1824 let leaflet_id = (await props.params).leaflet_id;
···2733 *,
2834 documents_in_publications(count)
2935 ),
3030- documents(*))`,
3636+ documents(*)),
3737+ leaflets_to_documents(
3838+ *,
3939+ documents(*)
4040+ )`,
3141 )
3242 .eq("id", leaflet_id)
3343 .single();
3444 let rootEntity = data?.root_entity;
3535- if (!data || !rootEntity || !data.leaflets_in_publications[0])
4545+4646+ // Try to find publication from leaflets_in_publications first
4747+ let publication = data?.leaflets_in_publications[0]?.publications;
4848+4949+ // If not found, check if publication_uri is in searchParams
5050+ if (!publication) {
5151+ let pub_uri = (await props.searchParams).publication_uri;
5252+ if (pub_uri) {
5353+ console.log(decodeURIComponent(pub_uri));
5454+ let { data: pubData, error } = await supabaseServerClient
5555+ .from("publications")
5656+ .select("*, documents_in_publications(count)")
5757+ .eq("uri", decodeURIComponent(pub_uri))
5858+ .single();
5959+ console.log(error);
6060+ publication = pubData;
6161+ }
6262+ }
6363+6464+ // Check basic data requirements
6565+ if (!data || !rootEntity)
3666 return (
3767 <div>
3868 missin something
···42724373 let identity = await getIdentityData();
4474 if (!identity || !identity.atp_did) return null;
4545- let pub = data.leaflets_in_publications[0];
4646- let agent = new AtpAgent({ service: "https://public.api.bsky.app" });
47757676+ // Get title and description from either source
7777+ let title =
7878+ data.leaflets_in_publications[0]?.title ||
7979+ data.leaflets_to_documents[0]?.title ||
8080+ decodeURIComponent((await props.searchParams).title || "");
8181+ let description =
8282+ data.leaflets_in_publications[0]?.description ||
8383+ data.leaflets_to_documents[0]?.description ||
8484+ decodeURIComponent((await props.searchParams).description || "");
8585+8686+ let agent = new AtpAgent({ service: "https://public.api.bsky.app" });
4887 let profile = await agent.getProfile({ actor: identity.atp_did });
8888+8989+ // Parse entitiesToDelete from URL params
9090+ let searchParams = await props.searchParams;
9191+ let entitiesToDelete: string[] = [];
9292+ try {
9393+ if (searchParams.entitiesToDelete) {
9494+ entitiesToDelete = JSON.parse(
9595+ decodeURIComponent(searchParams.entitiesToDelete),
9696+ );
9797+ }
9898+ } catch (e) {
9999+ // If parsing fails, just use empty array
100100+ }
101101+49102 return (
50103 <ReplicacheProvider
51104 rootEntity={rootEntity}
···57110 leaflet_id={leaflet_id}
58111 root_entity={rootEntity}
59112 profile={profile.data}
6060- title={pub.title}
6161- publication_uri={pub.publication}
6262- description={pub.description}
6363- record={pub.publications?.record as PubLeafletPublication.Record}
6464- posts_in_pub={pub.publications?.documents_in_publications[0].count}
113113+ title={title}
114114+ description={description}
115115+ publication_uri={publication?.uri}
116116+ record={publication?.record as PubLeafletPublication.Record | undefined}
117117+ posts_in_pub={publication?.documents_in_publications[0]?.count}
118118+ entitiesToDelete={entitiesToDelete}
65119 />
66120 </ReplicacheProvider>
67121 );
+9-8
app/[leaflet_id]/publish/publishBskyPost.ts
···1212import { createOauthClient } from "src/atproto-oauth";
1313import { supabaseServerClient } from "supabase/serverClient";
1414import { Json } from "supabase/database.types";
1515+import {
1616+ getMicroLinkOgImage,
1717+ getWebpageImage,
1818+} from "src/utils/getMicroLinkOgImage";
15191620export async function publishPostToBsky(args: {
1721 text: string;
···3135 credentialSession.fetchHandler.bind(credentialSession),
3236 );
3337 let newPostUrl = args.url;
3434- let preview_image = await fetch(
3535- `https://pro.microlink.io/?url=${newPostUrl}&screenshot=true&viewport.width=1400&viewport.height=733&meta=false&embed=screenshot.url&force=true`,
3636- {
3737- headers: {
3838- "x-api-key": process.env.MICROLINK_API_KEY!,
3939- },
4040- },
4141- );
3838+ let preview_image = await getWebpageImage(newPostUrl, {
3939+ width: 1400,
4040+ height: 733,
4141+ noCache: true,
4242+ });
42434344 let binary = await preview_image.blob();
4445 let resized_preview_image = await sharp(await binary.arrayBuffer())
-30
app/about/page.tsx
···11-import { LegalContent } from "app/legal/content";
22-import Link from "next/link";
33-44-export default function AboutPage() {
55- return (
66- <div className="flex flex-col gap-2">
77- <div className="flex flex-col h-[80vh] mx-auto sm:px-4 px-3 sm:py-6 py-4 max-w-prose gap-4 text-lg">
88- <p>
99- Leaflet.pub is a web app for instantly creating and collaborating on
1010- documents.{" "}
1111- <Link href="/" prefetch={false}>
1212- Click here
1313- </Link>{" "}
1414- to create one and get started!
1515- </p>
1616-1717- <p>
1818- Leaflet is made by Learning Futures Inc. Previously we built{" "}
1919- <a href="https://hyperlink.academy">hyperlink.academy</a>
2020- </p>
2121- <p>
2222- You can find us on{" "}
2323- <a href="https://bsky.app/profile/leaflet.pub">Bluesky</a> or email as
2424- at <a href="mailto:contact@leaflet.pub">contact@leaflet.pub</a>
2525- </p>
2626- </div>
2727- <LegalContent />
2828- </div>
2929- );
3030-}
···11import {
22 PubLeafletDocument,
33 PubLeafletPagesLinearDocument,
44+ PubLeafletPagesCanvas,
45 PubLeafletBlocksCode,
56} from "lexicons/api";
67import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki";
7889export async function extractCodeBlocks(
99- blocks: PubLeafletPagesLinearDocument.Block[],
1010+ blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[],
1011): Promise<Map<string, string>> {
1112 const codeBlocks = new Map<string, string>();
12131313- // Process all pages in the document
1414+ // Process all blocks (works for both linear and canvas)
1415 for (let i = 0; i < blocks.length; i++) {
1516 const block = blocks[i];
1617 const currentIndex = [i];
···88import { setHours, setMinutes } from "date-fns";
99import { Separator } from "react-aria-components";
1010import { Checkbox } from "components/Checkbox";
1111-import { useInitialPageLoad } from "components/InitialPageLoadProvider";
1111+import { useHasPageLoaded } from "components/InitialPageLoadProvider";
1212import { useSpring, animated } from "@react-spring/web";
1313import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
1414import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall";
15151616export function DateTimeBlock(props: BlockProps) {
1717 const [isClient, setIsClient] = useState(false);
1818- let initialPageLoad = useInitialPageLoad();
1818+ let initialPageLoad = useHasPageLoaded();
19192020 useEffect(() => {
2121 setIsClient(true);
+65-16
components/Blocks/EmbedBlock.tsx
···1010import { Input } from "components/Input";
1111import { isUrl } from "src/utils/isURL";
1212import { elementId } from "src/utils/elementId";
1313-import { deleteBlock } from "./DeleteBlock";
1413import { focusBlock } from "src/utils/focusBlock";
1514import { useDrag } from "src/hooks/useDrag";
1615import { BlockEmbedSmall } from "components/Icons/BlockEmbedSmall";
1716import { CheckTiny } from "components/Icons/CheckTiny";
1717+import { DotLoader } from "components/utils/DotLoader";
1818+import {
1919+ LinkPreviewBody,
2020+ LinkPreviewMetadataResult,
2121+} from "app/api/link_previews/route";
18221923export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => {
2024 let { permissions } = useEntitySetContext();
···132136133137 let entity_set = useEntitySetContext();
134138 let [linkValue, setLinkValue] = useState("");
139139+ let [loading, setLoading] = useState(false);
135140 let { rep } = useReplicache();
136141 let submit = async () => {
137142 let entity = props.entityID;
···149154 }
150155 let link = linkValue;
151156 if (!linkValue.startsWith("http")) link = `https://${linkValue}`;
152152- // these mutations = simpler subset of addLinkBlock
153157 if (!rep) return;
154154- await rep.mutate.assertFact({
155155- entity: entity,
156156- attribute: "block/type",
157157- data: { type: "block-type-union", value: "embed" },
158158- });
159159- await rep?.mutate.assertFact({
160160- entity: entity,
161161- attribute: "embed/url",
162162- data: {
163163- type: "string",
164164- value: link,
165165- },
166166- });
158158+159159+ // Try to get embed URL from iframely, fallback to direct URL
160160+ setLoading(true);
161161+ try {
162162+ let res = await fetch("/api/link_previews", {
163163+ headers: { "Content-Type": "application/json" },
164164+ method: "POST",
165165+ body: JSON.stringify({ url: link, type: "meta" } as LinkPreviewBody),
166166+ });
167167+168168+ let embedUrl = link;
169169+ let embedHeight = 360;
170170+171171+ if (res.status === 200) {
172172+ let data = await (res.json() as LinkPreviewMetadataResult);
173173+ if (data.success && data.data.links?.player?.[0]) {
174174+ let embed = data.data.links.player[0];
175175+ embedUrl = embed.href;
176176+ embedHeight = embed.media?.height || 300;
177177+ }
178178+ }
179179+180180+ await rep.mutate.assertFact([
181181+ {
182182+ entity: entity,
183183+ attribute: "embed/url",
184184+ data: {
185185+ type: "string",
186186+ value: embedUrl,
187187+ },
188188+ },
189189+ {
190190+ entity: entity,
191191+ attribute: "embed/height",
192192+ data: {
193193+ type: "number",
194194+ value: embedHeight,
195195+ },
196196+ },
197197+ ]);
198198+ } catch {
199199+ // On any error, fallback to using the URL directly
200200+ await rep.mutate.assertFact([
201201+ {
202202+ entity: entity,
203203+ attribute: "embed/url",
204204+ data: {
205205+ type: "string",
206206+ value: link,
207207+ },
208208+ },
209209+ ]);
210210+ } finally {
211211+ setLoading(false);
212212+ }
167213 };
168214 let smoker = useSmoker();
169215···171217 <form
172218 onSubmit={(e) => {
173219 e.preventDefault();
220220+ if (loading) return;
174221 let rect = document
175222 .getElementById("embed-block-submit")
176223 ?.getBoundingClientRect();
···212259 <button
213260 type="submit"
214261 id="embed-block-submit"
262262+ disabled={loading}
215263 className={`p-1 ${isSelected && !isLocked ? "text-accent-contrast" : "text-border"}`}
216264 onMouseDown={(e) => {
217265 e.preventDefault();
266266+ if (loading) return;
218267 if (!linkValue || linkValue === "") {
219268 smoker({
220269 error: true,
···234283 submit();
235284 }}
236285 >
237237- <CheckTiny />
286286+ {loading ? <DotLoader /> : <CheckTiny />}
238287 </button>
239288 </div>
240289 </form>
-2
components/Blocks/PublicationPollBlock.tsx
···31313232 const docRecord = publicationData.documents
3333 .data as PubLeafletDocument.Record;
3434- console.log(docRecord);
35343635 // Search through all pages and blocks to find if this poll entity has been published
3736 for (const page of docRecord.pages || []) {
···4039 for (const blockWrapper of linearPage.blocks || []) {
4140 if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) {
4241 const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main;
4343- console.log(pollBlock);
4442 // Check if this poll's rkey matches our entity ID
4543 const rkey = pollBlock.pollRef.uri.split("/").pop();
4644 if (rkey === props.entityID) {
+2-2
components/Blocks/RSVPBlock/SendUpdate.tsx
···99import { sendUpdateToRSVPS } from "actions/sendUpdateToRSVPS";
1010import { useReplicache } from "src/replicache";
1111import { Checkbox } from "components/Checkbox";
1212-import { usePublishLink } from "components/ShareOptions";
1212+import { useReadOnlyShareLink } from "app/[leaflet_id]/actions/ShareOptions";
13131414export function SendUpdateButton(props: { entityID: string }) {
1515- let publishLink = usePublishLink();
1515+ let publishLink = useReadOnlyShareLink();
1616 let { permissions } = useEntitySetContext();
1717 let { permission_token } = useReplicache();
1818 let [input, setInput] = useState("");
···77import { getPollData } from "actions/pollActions";
88import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data";
99import { createContext, useContext } from "react";
1010+import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData";
1111+import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
1212+import { AtUri } from "@atproto/syntax";
10131114export const StaticLeafletDataContext = createContext<
1215 null | GetLeafletDataReturnType["result"]["data"]
···6669};
6770export function useLeafletPublicationData() {
6871 let { data, mutate } = useLeafletData();
7272+7373+ // First check for leaflets in publications
7474+ let pubData = getPublicationMetadataFromLeafletData(data);
7575+6976 return {
7070- data:
7171- data?.leaflets_in_publications?.[0] ||
7272- data?.permission_token_rights[0].entity_sets?.permission_tokens?.find(
7373- (p) => p.leaflets_in_publications.length,
7474- )?.leaflets_in_publications?.[0] ||
7575- null,
7777+ data: pubData || null,
7678 mutate,
7779 };
7880}
···8082 let { data, mutate } = useLeafletData();
8183 return { data: data?.custom_domain_routes, mutate: mutate };
8284}
8585+8686+export function useLeafletPublicationStatus() {
8787+ const data = useContext(StaticLeafletDataContext);
8888+ if (!data) return null;
8989+9090+ const publishedInPublication = data.leaflets_in_publications?.find(
9191+ (l) => l.doc,
9292+ );
9393+ const publishedStandalone = data.leaflets_to_documents?.find(
9494+ (l) => !!l.documents,
9595+ );
9696+9797+ const documentUri =
9898+ publishedInPublication?.documents?.uri ?? publishedStandalone?.document;
9999+100100+ // Compute the full post URL for sharing
101101+ let postShareLink: string | undefined;
102102+ if (publishedInPublication?.publications && publishedInPublication.documents) {
103103+ // Published in a publication - use publication URL + document rkey
104104+ const docUri = new AtUri(publishedInPublication.documents.uri);
105105+ postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`;
106106+ } else if (publishedStandalone?.document) {
107107+ // Standalone published post - use /p/{did}/{rkey} format
108108+ const docUri = new AtUri(publishedStandalone.document);
109109+ postShareLink = `/p/${docUri.host}/${docUri.rkey}`;
110110+ }
111111+112112+ return {
113113+ token: data,
114114+ leafletId: data.root_entity,
115115+ shareLink: data.id,
116116+ // Draft state - in a publication but not yet published
117117+ draftInPublication:
118118+ data.leaflets_in_publications?.[0]?.publication ?? undefined,
119119+ // Published state
120120+ isPublished: !!(publishedInPublication || publishedStandalone),
121121+ publishedAt:
122122+ publishedInPublication?.documents?.indexed_at ??
123123+ publishedStandalone?.documents?.indexed_at,
124124+ documentUri,
125125+ // Full URL for sharing published posts
126126+ postShareLink,
127127+ };
128128+}
+4-1
components/Pages/Page.tsx
···1616import { PageOptions } from "./PageOptions";
1717import { CardThemeProvider } from "components/ThemeManager/ThemeProvider";
1818import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer";
1919+import { usePreserveScroll } from "src/hooks/usePreserveScroll";
19202021export function Page(props: {
2122 entityID: string;
···6061 />
6162 }
6263 >
6363- {props.first && (
6464+ {props.first && pageType === "doc" && (
6465 <>
6566 <PublicationMetadata />
6667 </>
···8384 pageType: "canvas" | "doc";
8485 drawerOpen: boolean | undefined;
8586}) => {
8787+ let { ref } = usePreserveScroll<HTMLDivElement>(props.id);
8688 return (
8789 // this div wraps the contents AND the page options.
8890 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions
···9597 it needs to be a separate div so that the user can scroll from anywhere on the page if there isn't a card border
9698 */}
9799 <div
100100+ ref={ref}
98101 onClick={props.onClickAction}
99102 id={props.id}
100103 className={`
+7-6
components/Pages/PageShareMenu.tsx
···11import { useLeafletDomains } from "components/PageSWRDataProvider";
22-import { ShareButton, usePublishLink } from "components/ShareOptions";
22+import {
33+ ShareButton,
44+ useReadOnlyShareLink,
55+} from "app/[leaflet_id]/actions/ShareOptions";
36import { useEffect, useState } from "react";
4758export const PageShareMenu = (props: { entityID: string }) => {
66- let publishLink = usePublishLink();
99+ let publishLink = useReadOnlyShareLink();
710 let { data: domains } = useLeafletDomains();
811 let [collabLink, setCollabLink] = useState<null | string>(null);
912 useEffect(() => {
···1417 <div>
1518 <ShareButton
1619 text="Share Edit Link"
1717- subtext=""
1818- helptext="recipients can edit the full Leaflet"
2020+ subtext="Recipients can edit the full Leaflet"
1921 smokerText="Collab link copied!"
2022 id="get-page-collab-link"
2123 link={`${collabLink}?page=${props.entityID}`}
2224 />
2325 <ShareButton
2426 text="Share View Link"
2525- subtext=""
2626- helptext="recipients can view the full Leaflet"
2727+ subtext="Recipients can view the full Leaflet"
2728 smokerText="Publish link copied!"
2829 id="get-page-publish-link"
2930 fullLink={
···2525import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
2626import * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote'
2727import * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost'
2828+import * as PubLeafletBlocksButton from './types/pub/leaflet/blocks/button'
2829import * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code'
2930import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header'
3031import * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
···6465export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
6566export * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote'
6667export * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost'
6868+export * as PubLeafletBlocksButton from './types/pub/leaflet/blocks/button'
6769export * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code'
6870export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header'
6971export * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
···9698 'pub.leaflet.pages.linearDocument#textAlignCenter',
9799 LinearDocumentTextAlignRight:
98100 'pub.leaflet.pages.linearDocument#textAlignRight',
101101+ LinearDocumentTextAlignJustify:
102102+ 'pub.leaflet.pages.linearDocument#textAlignJustify',
99103}
100104101105export class AtpBaseClient extends XrpcClient {
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55+import { CID } from 'multiformats/cid'
66+import { validate as _validate } from '../../../../lexicons'
77+import {
88+ type $Typed,
99+ is$typed as _is$typed,
1010+ type OmitKey,
1111+} from '../../../../util'
1212+1313+const is$typed = _is$typed,
1414+ validate = _validate
1515+const id = 'pub.leaflet.blocks.button'
1616+1717+export interface Main {
1818+ $type?: 'pub.leaflet.blocks.button'
1919+ text: string
2020+ url: string
2121+}
2222+2323+const hashMain = 'main'
2424+2525+export function isMain<V>(v: V) {
2626+ return is$typed(v, id, hashMain)
2727+}
2828+2929+export function validateMain<V>(v: V) {
3030+ return validate<Main & V>(v, id, hashMain)
3131+}
+3-1
lexicons/api/types/pub/leaflet/document.ts
···66import { validate as _validate } from '../../../lexicons'
77import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef'
99+import type * as PubLeafletPublication from './publication'
910import type * as PubLeafletPagesLinearDocument from './pages/linearDocument'
1011import type * as PubLeafletPagesCanvas from './pages/canvas'
1112···1920 postRef?: ComAtprotoRepoStrongRef.Main
2021 description?: string
2122 publishedAt?: string
2222- publication: string
2323+ publication?: string
2324 author: string
2525+ theme?: PubLeafletPublication.Theme
2426 pages: (
2527 | $Typed<PubLeafletPagesLinearDocument.Main>
2628 | $Typed<PubLeafletPagesCanvas.Main>
+2
lexicons/api/types/pub/leaflet/pages/canvas.ts
···2222import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost'
2323import type * as PubLeafletBlocksPage from '../blocks/page'
2424import type * as PubLeafletBlocksPoll from '../blocks/poll'
2525+import type * as PubLeafletBlocksButton from '../blocks/button'
25262627const is$typed = _is$typed,
2728 validate = _validate
···5960 | $Typed<PubLeafletBlocksBskyPost.Main>
6061 | $Typed<PubLeafletBlocksPage.Main>
6162 | $Typed<PubLeafletBlocksPoll.Main>
6363+ | $Typed<PubLeafletBlocksButton.Main>
6264 | { $type: string }
6365 x: number
6466 y: number
···11/// <reference types="next" />
22/// <reference types="next/image-types/global" />
33-/// <reference path="./.next/types/routes.d.ts" />
33+import "./.next/dev/types/routes.d.ts";
4455// NOTE: This file should not be edited
66// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
···11+create table "public"."notifications" (
22+ "recipient" text not null,
33+ "created_at" timestamp with time zone not null default now(),
44+ "read" boolean not null default false,
55+ "data" jsonb not null,
66+ "id" uuid not null
77+);
88+99+1010+alter table "public"."notifications" enable row level security;
1111+1212+CREATE UNIQUE INDEX notifications_pkey ON public.notifications USING btree (id);
1313+1414+alter table "public"."notifications" add constraint "notifications_pkey" PRIMARY KEY using index "notifications_pkey";
1515+1616+alter table "public"."notifications" add constraint "notifications_recipient_fkey" FOREIGN KEY (recipient) REFERENCES identities(atp_did) ON UPDATE CASCADE ON DELETE CASCADE not valid;
1717+1818+alter table "public"."notifications" validate constraint "notifications_recipient_fkey";
1919+2020+grant delete on table "public"."notifications" to "anon";
2121+2222+grant insert on table "public"."notifications" to "anon";
2323+2424+grant references on table "public"."notifications" to "anon";
2525+2626+grant select on table "public"."notifications" to "anon";
2727+2828+grant trigger on table "public"."notifications" to "anon";
2929+3030+grant truncate on table "public"."notifications" to "anon";
3131+3232+grant update on table "public"."notifications" to "anon";
3333+3434+grant delete on table "public"."notifications" to "authenticated";
3535+3636+grant insert on table "public"."notifications" to "authenticated";
3737+3838+grant references on table "public"."notifications" to "authenticated";
3939+4040+grant select on table "public"."notifications" to "authenticated";
4141+4242+grant trigger on table "public"."notifications" to "authenticated";
4343+4444+grant truncate on table "public"."notifications" to "authenticated";
4545+4646+grant update on table "public"."notifications" to "authenticated";
4747+4848+grant delete on table "public"."notifications" to "service_role";
4949+5050+grant insert on table "public"."notifications" to "service_role";
5151+5252+grant references on table "public"."notifications" to "service_role";
5353+5454+grant select on table "public"."notifications" to "service_role";
5555+5656+grant trigger on table "public"."notifications" to "service_role";
5757+5858+grant truncate on table "public"."notifications" to "service_role";
5959+6060+grant update on table "public"."notifications" to "service_role";
···11+create table "public"."leaflets_to_documents" (
22+ "leaflet" uuid not null,
33+ "document" text not null,
44+ "created_at" timestamp with time zone not null default now(),
55+ "title" text not null default ''::text,
66+ "description" text not null default ''::text
77+);
88+99+alter table "public"."leaflets_to_documents" enable row level security;
1010+1111+CREATE UNIQUE INDEX leaflets_to_documents_pkey ON public.leaflets_to_documents USING btree (leaflet, document);
1212+1313+alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_pkey" PRIMARY KEY using index "leaflets_to_documents_pkey";
1414+1515+alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid;
1616+1717+alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_document_fkey";
1818+1919+alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_leaflet_fkey" FOREIGN KEY (leaflet) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
2020+2121+alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_leaflet_fkey";
2222+2323+grant delete on table "public"."leaflets_to_documents" to "anon";
2424+2525+grant insert on table "public"."leaflets_to_documents" to "anon";
2626+2727+grant references on table "public"."leaflets_to_documents" to "anon";
2828+2929+grant select on table "public"."leaflets_to_documents" to "anon";
3030+3131+grant trigger on table "public"."leaflets_to_documents" to "anon";
3232+3333+grant truncate on table "public"."leaflets_to_documents" to "anon";
3434+3535+grant update on table "public"."leaflets_to_documents" to "anon";
3636+3737+grant delete on table "public"."leaflets_to_documents" to "authenticated";
3838+3939+grant insert on table "public"."leaflets_to_documents" to "authenticated";
4040+4141+grant references on table "public"."leaflets_to_documents" to "authenticated";
4242+4343+grant select on table "public"."leaflets_to_documents" to "authenticated";
4444+4545+grant trigger on table "public"."leaflets_to_documents" to "authenticated";
4646+4747+grant truncate on table "public"."leaflets_to_documents" to "authenticated";
4848+4949+grant update on table "public"."leaflets_to_documents" to "authenticated";
5050+5151+grant delete on table "public"."leaflets_to_documents" to "service_role";
5252+5353+grant insert on table "public"."leaflets_to_documents" to "service_role";
5454+5555+grant references on table "public"."leaflets_to_documents" to "service_role";
5656+5757+grant select on table "public"."leaflets_to_documents" to "service_role";
5858+5959+grant trigger on table "public"."leaflets_to_documents" to "service_role";
6060+6161+grant truncate on table "public"."leaflets_to_documents" to "service_role";
6262+6363+grant update on table "public"."leaflets_to_documents" to "service_role";