a tool for shared writing and social publishing

re-architect access around permission tokens

Tokens give you permissions over a set of entities. When an entity is
created it defines its set. Permissions are, read, write,
change_entity_set, and create_token. The latter two are unused as of
now.

Tokens also define their "root entities" so they can be used directly in
links to give access. This may later prove to be a bad idea, but works
for now!

In the code we add the context of EntitySetProvider. For now there is
just on at the top level of a doc, but we will need to wrap other
sections of the tree if we implement scoped permissions.

+613 -139
+27 -14
app/[doc_id]/Doc.tsx
··· 1 - import { Fact, ReplicacheProvider } from "src/replicache"; 2 import { Database } from "../../supabase/database.types"; 3 import { Attributes } from "src/replicache/attributes"; 4 import { createServerClient } from "@supabase/ssr"; ··· 8 import { MobileFooter } from "components/MobileFooter"; 9 import { PopUpProvider } from "components/Toast"; 10 import { YJSFragmentToString } from "components/TextBlock/RenderYJSFragment"; 11 export function Doc(props: { 12 initialFacts: Fact<keyof typeof Attributes>[]; 13 doc_id: string; 14 }) { 15 return ( 16 - <ReplicacheProvider name={props.doc_id} initialFacts={props.initialFacts}> 17 - <PopUpProvider> 18 - <ThemeProvider entityID={props.doc_id}> 19 - <SelectionManager /> 20 - <div 21 - className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 22 - id="card-carousel" 23 - > 24 - <Cards rootCard={props.doc_id} /> 25 - </div> 26 - <MobileFooter entityID={props.doc_id} /> 27 - </ThemeProvider> 28 - </PopUpProvider> 29 </ReplicacheProvider> 30 ); 31 }
··· 1 + import { Fact, PermissionToken, ReplicacheProvider } from "src/replicache"; 2 import { Database } from "../../supabase/database.types"; 3 import { Attributes } from "src/replicache/attributes"; 4 import { createServerClient } from "@supabase/ssr"; ··· 8 import { MobileFooter } from "components/MobileFooter"; 9 import { PopUpProvider } from "components/Toast"; 10 import { YJSFragmentToString } from "components/TextBlock/RenderYJSFragment"; 11 + import { 12 + EntitySetContext, 13 + EntitySetProvider, 14 + } from "components/EntitySetProvider"; 15 export function Doc(props: { 16 + token: PermissionToken; 17 initialFacts: Fact<keyof typeof Attributes>[]; 18 doc_id: string; 19 }) { 20 return ( 21 + <ReplicacheProvider 22 + token={props.token} 23 + name={props.doc_id} 24 + initialFacts={props.initialFacts} 25 + > 26 + <EntitySetProvider 27 + set={props.token.permission_token_rights[0].entity_set} 28 + > 29 + <PopUpProvider> 30 + <ThemeProvider entityID={props.doc_id}> 31 + <SelectionManager /> 32 + <div 33 + className="pageContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full" 34 + id="card-carousel" 35 + > 36 + <Cards rootCard={props.doc_id} /> 37 + </div> 38 + <MobileFooter entityID={props.doc_id} /> 39 + </ThemeProvider> 40 + </PopUpProvider> 41 + </EntitySetProvider> 42 </ReplicacheProvider> 43 ); 44 }
+26 -6
app/[doc_id]/page.tsx
··· 24 { cookies: {} }, 25 ); 26 type Props = { 27 params: { doc_id: string }; 28 }; 29 export default async function DocumentPage(props: Props) { 30 - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 31 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 32 - return <Doc initialFacts={initialFacts} doc_id={props.params.doc_id} />; 33 } 34 35 export async function generateMetadata(props: Props): Promise<Metadata> { 36 - let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 37 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 38 let blocks = initialFacts 39 - .filter( 40 - (f) => f.attribute === "card/block" && f.entity === props.params.doc_id, 41 - ) 42 .map((_f) => { 43 let block = _f as Fact<"card/block">; 44 let type = initialFacts.find(
··· 24 { cookies: {} }, 25 ); 26 type Props = { 27 + // this is now a token id not doc! Should probs rename 28 params: { doc_id: string }; 29 }; 30 export default async function DocumentPage(props: Props) { 31 + let res = await supabase 32 + .from("permission_tokens") 33 + .select("*, permission_token_rights(*)") 34 + .eq("id", props.params.doc_id) 35 + .single(); 36 + let rootEntity = res.data?.root_entity; 37 + if (!rootEntity || !res.data) 38 + return <div>404 no rootEntity found idk man</div>; 39 + let { data } = await supabase.rpc("get_facts", { 40 + root: rootEntity, 41 + }); 42 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 43 + return ( 44 + <Doc initialFacts={initialFacts} doc_id={rootEntity} token={res.data} /> 45 + ); 46 } 47 48 export async function generateMetadata(props: Props): Promise<Metadata> { 49 + let res = await supabase 50 + .from("permission_tokens") 51 + .select("*, permission_token_rights(*)") 52 + .eq("id", props.params.doc_id) 53 + .single(); 54 + let rootEntity = res.data?.root_entity; 55 + if (!rootEntity || !res.data) return { title: "Doc not found" }; 56 + let { data } = await supabase.rpc("get_facts", { 57 + root: rootEntity, 58 + }); 59 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 60 let blocks = initialFacts 61 + .filter((f) => f.attribute === "card/block" && f.entity === rootEntity) 62 .map((_f) => { 63 let block = _f as Fact<"card/block">; 64 let type = initialFacts.find(
+45 -4
app/page.tsx
··· 1 import { drizzle } from "drizzle-orm/postgres-js"; 2 - import { entities } from "drizzle/schema"; 3 import { redirect } from "next/navigation"; 4 import postgres from "postgres"; 5 import { Doc } from "./[doc_id]/Doc"; ··· 12 export const fetchCache = "force-no-store"; 13 14 export default async function RootPage() { 15 - let rows = await db.insert(entities).values({}).returning(); 16 return ( 17 <> 18 - <UpdateURL url={`/${rows[0].id}`} /> 19 - <Doc doc_id={rows[0].id} initialFacts={[]} /> 20 </> 21 ); 22 }
··· 1 import { drizzle } from "drizzle-orm/postgres-js"; 2 + import { 3 + entities, 4 + permission_tokens, 5 + permission_token_rights, 6 + entity_sets, 7 + } from "drizzle/schema"; 8 import { redirect } from "next/navigation"; 9 import postgres from "postgres"; 10 import { Doc } from "./[doc_id]/Doc"; ··· 17 export const fetchCache = "force-no-store"; 18 19 export default async function RootPage() { 20 + // Creating a new document 21 + let { permissionToken, rights, entity, entity_set } = await db.transaction( 22 + async (tx) => { 23 + // Create a new entity set 24 + let [entity_set] = await tx.insert(entity_sets).values({}).returning(); 25 + // Create a root-entity 26 + let [entity] = await tx 27 + .insert(entities) 28 + // And add it to that permission set 29 + .values({ set: entity_set.id }) 30 + .returning(); 31 + //Create a new permission token 32 + let [permissionToken] = await tx 33 + .insert(permission_tokens) 34 + .values({ root_entity: entity.id }) 35 + .returning(); 36 + //and give it all the permission on that entity set 37 + let [rights] = await tx 38 + .insert(permission_token_rights) 39 + .values({ 40 + token: permissionToken.id, 41 + entity_set: entity_set.id, 42 + read: true, 43 + write: true, 44 + create_token: true, 45 + change_entity_set: true, 46 + }) 47 + .returning(); 48 + return { permissionToken, rights, entity, entity_set }; 49 + }, 50 + ); 51 + // Here i need to pass the permission token instead of the doc_id 52 + // In the replicache provider I guess I need to fetch the relevant stuff of the permission token? 53 return ( 54 <> 55 + <UpdateURL url={`/${permissionToken.id}`} /> 56 + <Doc 57 + doc_id={entity.id} 58 + token={{ id: permissionToken.id, permission_token_rights: [rights] }} 59 + initialFacts={[]} 60 + /> 61 </> 62 ); 63 }
+6
components/BlockOptions.tsx
··· 13 import { useState } from "react"; 14 import { Separator } from "./Layout"; 15 import { addLinkBlock } from "src/utils/addLinkBlock"; 16 17 type Props = { 18 parent: string; ··· 23 }; 24 export function BlockOptions(props: Props) { 25 let { rep } = useReplicache(); 26 27 let focusedElement = useUIState((s) => s.focusedBlock); 28 let focusedCardID = ··· 51 entity = crypto.randomUUID(); 52 await rep?.mutate.addBlock({ 53 parent: props.parent, 54 type: "text", 55 position: generateKeyBetween( 56 props.position, ··· 81 let entity = crypto.randomUUID(); 82 83 await rep?.mutate.addBlock({ 84 parent: props.parent, 85 type: "card", 86 position: generateKeyBetween( ··· 110 } 111 112 const BlockLinkButton = (props: Props) => { 113 let [linkOpen, setLinkOpen] = useState(false); 114 let [linkValue, setLinkValue] = useState(""); 115 let { rep } = useReplicache(); ··· 119 entity = crypto.randomUUID(); 120 121 await rep?.mutate.addBlock({ 122 parent: props.parent, 123 type: "card", 124 position: generateKeyBetween(props.position, props.nextPosition),
··· 13 import { useState } from "react"; 14 import { Separator } from "./Layout"; 15 import { addLinkBlock } from "src/utils/addLinkBlock"; 16 + import { useEntitySetContext } from "./EntitySetProvider"; 17 18 type Props = { 19 parent: string; ··· 24 }; 25 export function BlockOptions(props: Props) { 26 let { rep } = useReplicache(); 27 + let entity_set = useEntitySetContext(); 28 29 let focusedElement = useUIState((s) => s.focusedBlock); 30 let focusedCardID = ··· 53 entity = crypto.randomUUID(); 54 await rep?.mutate.addBlock({ 55 parent: props.parent, 56 + permission_set: entity_set.set, 57 type: "text", 58 position: generateKeyBetween( 59 props.position, ··· 84 let entity = crypto.randomUUID(); 85 86 await rep?.mutate.addBlock({ 87 + permission_set: entity_set.set, 88 parent: props.parent, 89 type: "card", 90 position: generateKeyBetween( ··· 114 } 115 116 const BlockLinkButton = (props: Props) => { 117 + let entity_set = useEntitySetContext(); 118 let [linkOpen, setLinkOpen] = useState(false); 119 let [linkValue, setLinkValue] = useState(""); 120 let { rep } = useReplicache(); ··· 124 entity = crypto.randomUUID(); 125 126 await rep?.mutate.addBlock({ 127 + permission_set: entity_set.set, 128 parent: props.parent, 129 type: "card", 130 position: generateKeyBetween(props.position, props.nextPosition),
+9
components/Blocks.tsx
··· 13 import { BlockOptions } from "./BlockOptions"; 14 import { useBlocks } from "src/hooks/queries/useBlocks"; 15 import { setEditorState, useEditorStates } from "src/state/useEditorState"; 16 17 export type Block = { 18 parent: string; ··· 22 }; 23 export function Blocks(props: { entityID: string }) { 24 let rep = useReplicache(); 25 let blocks = useBlocks(props.entityID); 26 27 let lastBlock = blocks[blocks.length - 1]; ··· 38 let newEntityID = crypto.randomUUID(); 39 await rep.rep?.mutate.addBlock({ 40 parent: props.entityID, 41 type: "text", 42 position: generateKeyBetween(lastBlock.position || null, null), 43 newEntityID, ··· 77 focusBlock({ ...lastBlock, type: "text" }, { type: "end" }); 78 } else { 79 rep?.rep?.mutate.addBlock({ 80 parent: props.entityID, 81 type: "text", 82 position: generateKeyBetween(lastBlock?.position || null, null), ··· 97 98 function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) { 99 let { rep } = useReplicache(); 100 let textContent = useEntity( 101 props.lastBlock?.type === "text" ? props.lastBlock.value : null, 102 "block/text", ··· 115 await rep?.mutate.addBlock({ 116 parent: props.entityID, 117 type: "text", 118 position: generateKeyBetween( 119 props.lastBlock?.position || null, 120 null, ··· 161 ); 162 let { rep } = useReplicache(); 163 164 useEffect(() => { 165 if (!selected || !rep) return; 166 let r = rep; ··· 198 if (e.key === "Enter") { 199 let newEntityID = crypto.randomUUID(); 200 r.mutate.addBlock({ 201 newEntityID, 202 parent: props.parent, 203 type: "text", ··· 215 window.addEventListener("keydown", listener); 216 return () => window.removeEventListener("keydown", listener); 217 }, [ 218 selected, 219 props.entityID, 220 props.nextBlock,
··· 13 import { BlockOptions } from "./BlockOptions"; 14 import { useBlocks } from "src/hooks/queries/useBlocks"; 15 import { setEditorState, useEditorStates } from "src/state/useEditorState"; 16 + import { useEntitySetContext } from "./EntitySetProvider"; 17 18 export type Block = { 19 parent: string; ··· 23 }; 24 export function Blocks(props: { entityID: string }) { 25 let rep = useReplicache(); 26 + let entity_set = useEntitySetContext(); 27 let blocks = useBlocks(props.entityID); 28 29 let lastBlock = blocks[blocks.length - 1]; ··· 40 let newEntityID = crypto.randomUUID(); 41 await rep.rep?.mutate.addBlock({ 42 parent: props.entityID, 43 + permission_set: entity_set.set, 44 type: "text", 45 position: generateKeyBetween(lastBlock.position || null, null), 46 newEntityID, ··· 80 focusBlock({ ...lastBlock, type: "text" }, { type: "end" }); 81 } else { 82 rep?.rep?.mutate.addBlock({ 83 + permission_set: entity_set.set, 84 parent: props.entityID, 85 type: "text", 86 position: generateKeyBetween(lastBlock?.position || null, null), ··· 101 102 function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) { 103 let { rep } = useReplicache(); 104 + let entity_set = useEntitySetContext(); 105 let textContent = useEntity( 106 props.lastBlock?.type === "text" ? props.lastBlock.value : null, 107 "block/text", ··· 120 await rep?.mutate.addBlock({ 121 parent: props.entityID, 122 type: "text", 123 + permission_set: entity_set.set, 124 position: generateKeyBetween( 125 props.lastBlock?.position || null, 126 null, ··· 167 ); 168 let { rep } = useReplicache(); 169 170 + let entity_set = useEntitySetContext(); 171 useEffect(() => { 172 if (!selected || !rep) return; 173 let r = rep; ··· 205 if (e.key === "Enter") { 206 let newEntityID = crypto.randomUUID(); 207 r.mutate.addBlock({ 208 + permission_set: entity_set.set, 209 newEntityID, 210 parent: props.parent, 211 type: "text", ··· 223 window.addEventListener("keydown", listener); 224 return () => window.removeEventListener("keydown", listener); 225 }, [ 226 + entity_set, 227 selected, 228 props.entityID, 229 props.nextBlock,
+28
components/EntitySetProvider.tsx
···
··· 1 + "use client"; 2 + import { createContext, useContext } from "react"; 3 + import { useReplicache } from "src/replicache"; 4 + 5 + export const EntitySetContext = createContext({ 6 + set: "", 7 + permissions: { read: false, write: false }, 8 + }); 9 + export const useEntitySetContext = () => useContext(EntitySetContext); 10 + 11 + export function EntitySetProvider(props: { 12 + set: string; 13 + children: React.ReactNode; 14 + }) { 15 + let { permission_token } = useReplicache(); 16 + return ( 17 + <EntitySetContext.Provider 18 + value={{ 19 + set: props.set, 20 + permissions: permission_token.permission_token_rights.find( 21 + (r) => r.entity_set === props.set, 22 + ) || { read: false, write: false }, 23 + }} 24 + > 25 + {props.children} 26 + </EntitySetContext.Provider> 27 + ); 28 + }
+16 -8
components/TextBlock/index.tsx
··· 37 import { useIsMobile } from "src/hooks/isMobile"; 38 import { setMark } from "src/utils/prosemirror/setMark"; 39 import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 40 41 export function TextBlock(props: BlockProps & { className: string }) { 42 let initialized = useInitialPageLoad(); 43 let first = props.previousBlock === null; 44 return ( 45 <> 46 - {!initialized && ( 47 <RenderedTextBlock 48 entityID={props.entityID} 49 className={props.className} ··· 51 first={first} 52 /> 53 )} 54 - <div className={`relative group/text ${!initialized ? "hidden" : ""}`}> 55 - <IOSBS {...props} /> 56 - <BaseTextBlock {...props} /> 57 - </div> 58 </> 59 ); 60 } ··· 139 140 let [value, factID] = useYJSValue(props.entityID); 141 let repRef = useRef<null | Replicache<ReplicacheMutators>>(null); 142 - let propsRef = useRef(props); 143 useEffect(() => { 144 - propsRef.current = props; 145 - }, [props]); 146 let rep = useReplicache(); 147 useEffect(() => { 148 repRef.current = rep.rep; ··· 244 propsRef.current.nextPosition, 245 ); 246 repRef.current?.mutate.addBlock({ 247 newEntityID: entityID, 248 parent: propsRef.current.parent, 249 type: type, ··· 297 propsRef.current.nextPosition, 298 ); 299 repRef.current?.mutate.addBlock({ 300 newEntityID, 301 parent: propsRef.current.parent, 302 type: "text", ··· 332 } else { 333 entity = crypto.randomUUID(); 334 rep.rep.mutate.addBlock({ 335 type: "image", 336 newEntityID: entity, 337 parent: props.parent,
··· 37 import { useIsMobile } from "src/hooks/isMobile"; 38 import { setMark } from "src/utils/prosemirror/setMark"; 39 import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 40 + import { useEntitySetContext } from "components/EntitySetProvider"; 41 42 export function TextBlock(props: BlockProps & { className: string }) { 43 let initialized = useInitialPageLoad(); 44 let first = props.previousBlock === null; 45 + let permission = useEntitySetContext().permissions.write; 46 return ( 47 <> 48 + {(!initialized || !permission) && ( 49 <RenderedTextBlock 50 entityID={props.entityID} 51 className={props.className} ··· 53 first={first} 54 /> 55 )} 56 + {permission && ( 57 + <div className={`relative group/text ${!initialized ? "hidden" : ""}`}> 58 + <IOSBS {...props} /> 59 + <BaseTextBlock {...props} /> 60 + </div> 61 + )} 62 </> 63 ); 64 } ··· 143 144 let [value, factID] = useYJSValue(props.entityID); 145 let repRef = useRef<null | Replicache<ReplicacheMutators>>(null); 146 + let entity_set = useEntitySetContext(); 147 + let propsRef = useRef({ ...props, entity_set }); 148 useEffect(() => { 149 + propsRef.current = { ...props, entity_set }; 150 + }, [props, entity_set]); 151 let rep = useReplicache(); 152 useEffect(() => { 153 repRef.current = rep.rep; ··· 249 propsRef.current.nextPosition, 250 ); 251 repRef.current?.mutate.addBlock({ 252 + permission_set: entity_set.set, 253 newEntityID: entityID, 254 parent: propsRef.current.parent, 255 type: type, ··· 303 propsRef.current.nextPosition, 304 ); 305 repRef.current?.mutate.addBlock({ 306 + permission_set: entity_set.set, 307 newEntityID, 308 parent: propsRef.current.parent, 309 type: "text", ··· 339 } else { 340 entity = crypto.randomUUID(); 341 rep.rep.mutate.addBlock({ 342 + permission_set: entity_set.set, 343 type: "image", 344 newEntityID: entity, 345 parent: props.parent,
+3 -2
components/TextBlock/keymap.ts
··· 13 import { focusCard } from "components/Cards"; 14 15 export const TextBlockKeymap = ( 16 - propsRef: MutableRefObject<BlockProps>, 17 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 18 ) => 19 keymap({ ··· 240 241 const enter = 242 ( 243 - propsRef: MutableRefObject<BlockProps>, 244 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 245 ) => 246 (state: EditorState, dispatch?: (tr: Transaction) => void) => { ··· 255 ); 256 repRef.current?.mutate.addBlock({ 257 newEntityID, 258 parent: propsRef.current.parent, 259 type: "text", 260 position,
··· 13 import { focusCard } from "components/Cards"; 14 15 export const TextBlockKeymap = ( 16 + propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 17 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 18 ) => 19 keymap({ ··· 240 241 const enter = 242 ( 243 + propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 244 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 245 ) => 246 (state: EditorState, dispatch?: (tr: Transaction) => void) => { ··· 255 ); 256 repRef.current?.mutate.addBlock({ 257 newEntityID, 258 + permission_set: propsRef.current.entity_set.set, 259 parent: propsRef.current.parent, 260 type: "text", 261 position,
+5 -3
components/UpdateURL.tsx
··· 1 "use client"; 2 import { useEffect } from "react"; 3 4 export function UpdateURL(props: { url: string }) { 5 useEffect(() => { 6 - window.history.replaceState(null, "", props.url); 7 - }, [props.url]); 8 - return null; 9 }
··· 1 "use client"; 2 + import { useRouter } from "next/navigation"; 3 import { useEffect } from "react"; 4 5 export function UpdateURL(props: { url: string }) { 6 + let router = useRouter(); 7 useEffect(() => { 8 + router.replace(props.url); 9 + }, [props.url, router]); 10 + return <></>; 11 }
+32 -3
drizzle/relations.ts
··· 1 import { relations } from "drizzle-orm/relations"; 2 - import { entities, facts } from "./schema"; 3 4 export const factsRelations = relations(facts, ({one}) => ({ 5 entity: one(entities, { ··· 8 }), 9 })); 10 11 - export const entitiesRelations = relations(entities, ({many}) => ({ 12 - facts: many(facts), 13 }));
··· 1 import { relations } from "drizzle-orm/relations"; 2 + import { entity_sets, entities, permission_tokens, facts, permission_token_rights } from "./schema"; 3 + 4 + export const entitiesRelations = relations(entities, ({one, many}) => ({ 5 + entity_set: one(entity_sets, { 6 + fields: [entities.set], 7 + references: [entity_sets.id] 8 + }), 9 + permission_tokens: many(permission_tokens), 10 + facts: many(facts), 11 + })); 12 + 13 + export const entity_setsRelations = relations(entity_sets, ({many}) => ({ 14 + entities: many(entities), 15 + permission_token_rights: many(permission_token_rights), 16 + })); 17 + 18 + export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 19 + entity: one(entities, { 20 + fields: [permission_tokens.root_entity], 21 + references: [entities.id] 22 + }), 23 + permission_token_rights: many(permission_token_rights), 24 + })); 25 26 export const factsRelations = relations(facts, ({one}) => ({ 27 entity: one(entities, { ··· 30 }), 31 })); 32 33 + export const permission_token_rightsRelations = relations(permission_token_rights, ({one}) => ({ 34 + entity_set: one(entity_sets, { 35 + fields: [permission_token_rights.entity_set], 36 + references: [entity_sets.id] 37 + }), 38 + permission_token: one(permission_tokens, { 39 + fields: [permission_token_rights.token], 40 + references: [permission_tokens.id] 41 + }), 42 }));
+54 -86
drizzle/schema.ts
··· 1 - import { 2 - pgTable, 3 - pgEnum, 4 - uuid, 5 - timestamp, 6 - text, 7 - bigint, 8 - foreignKey, 9 - jsonb, 10 - } from "drizzle-orm/pg-core"; 11 - import { Fact } from "src/replicache"; 12 - import { Attributes } from "src/replicache/attributes"; 13 14 - export const aal_level = pgEnum("aal_level", ["aal1", "aal2", "aal3"]); 15 - export const code_challenge_method = pgEnum("code_challenge_method", [ 16 - "s256", 17 - "plain", 18 - ]); 19 - export const factor_status = pgEnum("factor_status", [ 20 - "unverified", 21 - "verified", 22 - ]); 23 - export const factor_type = pgEnum("factor_type", ["totp", "webauthn"]); 24 - export const request_status = pgEnum("request_status", [ 25 - "PENDING", 26 - "SUCCESS", 27 - "ERROR", 28 - ]); 29 - export const key_status = pgEnum("key_status", [ 30 - "default", 31 - "valid", 32 - "invalid", 33 - "expired", 34 - ]); 35 - export const key_type = pgEnum("key_type", [ 36 - "aead-ietf", 37 - "aead-det", 38 - "hmacsha512", 39 - "hmacsha256", 40 - "auth", 41 - "shorthash", 42 - "generichash", 43 - "kdf", 44 - "secretbox", 45 - "secretstream", 46 - "stream_xchacha20", 47 - ]); 48 - export const action = pgEnum("action", [ 49 - "INSERT", 50 - "UPDATE", 51 - "DELETE", 52 - "TRUNCATE", 53 - "ERROR", 54 - ]); 55 - export const equality_op = pgEnum("equality_op", [ 56 - "eq", 57 - "neq", 58 - "lt", 59 - "lte", 60 - "gt", 61 - "gte", 62 - "in", 63 - ]); 64 65 export const entities = pgTable("entities", { 66 - id: uuid("id").defaultRandom().primaryKey().notNull(), 67 - created_at: timestamp("created_at", { withTimezone: true, mode: "string" }) 68 - .defaultNow() 69 - .notNull(), 70 }); 71 72 - export const replicache_clients = pgTable("replicache_clients", { 73 - client_id: text("client_id").primaryKey().notNull(), 74 - client_group: text("client_group").notNull(), 75 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 76 - last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 77 }); 78 79 export const facts = pgTable("facts", { 80 - id: uuid("id").defaultRandom().primaryKey().notNull(), 81 - entity: uuid("entity") 82 - .notNull() 83 - .references(() => entities.id, { 84 - onDelete: "cascade", 85 - onUpdate: "restrict", 86 - }), 87 - attribute: text("attribute").notNull().$type<keyof typeof Attributes>(), 88 - data: jsonb("data").notNull().$type<Fact<any>["data"]>(), 89 - created_at: timestamp("created_at", { mode: "string" }) 90 - .defaultNow() 91 - .notNull(), 92 - updated_at: timestamp("updated_at", { mode: "string" }), 93 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 94 - version: bigint("version", { mode: "number" }).default(0).notNull(), 95 });
··· 1 + import { pgTable, pgEnum, text, bigint, foreignKey, uuid, timestamp, jsonb, primaryKey, boolean } from "drizzle-orm/pg-core" 2 + import { sql } from "drizzle-orm" 3 4 + export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) 5 + export const code_challenge_method = pgEnum("code_challenge_method", ['s256', 'plain']) 6 + export const factor_status = pgEnum("factor_status", ['unverified', 'verified']) 7 + export const factor_type = pgEnum("factor_type", ['totp', 'webauthn']) 8 + export const one_time_token_type = pgEnum("one_time_token_type", ['confirmation_token', 'reauthentication_token', 'recovery_token', 'email_change_token_new', 'email_change_token_current', 'phone_change_token']) 9 + export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) 10 + export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) 11 + export const key_type = pgEnum("key_type", ['aead-ietf', 'aead-det', 'hmacsha512', 'hmacsha256', 'auth', 'shorthash', 'generichash', 'kdf', 'secretbox', 'secretstream', 'stream_xchacha20']) 12 + export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 13 + export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 14 + 15 + 16 + export const replicache_clients = pgTable("replicache_clients", { 17 + client_id: text("client_id").primaryKey().notNull(), 18 + client_group: text("client_group").notNull(), 19 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 20 + last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 21 + }); 22 23 export const entities = pgTable("entities", { 24 + id: uuid("id").defaultRandom().primaryKey().notNull(), 25 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 26 + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 27 + }); 28 + 29 + export const entity_sets = pgTable("entity_sets", { 30 + id: uuid("id").defaultRandom().primaryKey().notNull(), 31 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 32 }); 33 34 + export const permission_tokens = pgTable("permission_tokens", { 35 + id: uuid("id").defaultRandom().primaryKey().notNull(), 36 + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 37 }); 38 39 export const facts = pgTable("facts", { 40 + id: uuid("id").defaultRandom().primaryKey().notNull(), 41 + entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), 42 + attribute: text("attribute").notNull(), 43 + data: jsonb("data").notNull(), 44 + created_at: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(), 45 + updated_at: timestamp("updated_at", { mode: 'string' }), 46 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 47 + version: bigint("version", { mode: "number" }).default(0).notNull(), 48 }); 49 + 50 + export const permission_token_rights = pgTable("permission_token_rights", { 51 + token: uuid("token").notNull().references(() => permission_tokens.id, { onDelete: "cascade", onUpdate: "cascade" } ), 52 + entity_set: uuid("entity_set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 53 + read: boolean("read").default(false).notNull(), 54 + write: boolean("write").default(false).notNull(), 55 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 56 + create_token: boolean("create_token").default(false).notNull(), 57 + change_entity_set: boolean("change_entity_set").default(false).notNull(), 58 + }, 59 + (table) => { 60 + return { 61 + permission_token_rights_pkey: primaryKey({ columns: [table.token, table.entity_set], name: "permission_token_rights_pkey"}), 62 + } 63 + });
+2 -2
src/replicache/clientMutationContext.ts
··· 14 let supabase = supabaseBrowserClient(); 15 return cb({ supabase }); 16 }, 17 - async createEntity(_entityID) { 18 - tx.set(_entityID, true); 19 return true; 20 }, 21 scanIndex: {
··· 14 let supabase = supabaseBrowserClient(); 15 return cb({ supabase }); 16 }, 17 + async createEntity({ entityID }) { 18 + tx.set(entityID, true); 19 return true; 20 }, 21 scanIndex: {
+17 -2
src/replicache/index.tsx
··· 19 let ReplicacheContext = createContext({ 20 rep: null as null | Replicache<ReplicacheMutators>, 21 initialFacts: [] as Fact<keyof typeof Attributes>[], 22 }); 23 export function useReplicache() { 24 return useContext(ReplicacheContext); ··· 28 tx: WriteTransaction, 29 args: Parameters<(typeof mutations)[k]>[0], 30 ) => Promise<void>; 31 }; 32 export function ReplicacheProvider(props: { 33 initialFacts: Fact<keyof typeof Attributes>[]; 34 name: string; 35 children: React.ReactNode; 36 }) { ··· 55 licenseKey: "l381074b8d5224dabaef869802421225a", 56 pusher: async (pushRequest) => { 57 return { 58 - response: await Push(pushRequest, props.name), 59 httpRequestInfo: { errorMessage: "", httpStatusCode: 200 }, 60 }; 61 }, ··· 87 }, [props.name]); 88 return ( 89 <ReplicacheContext.Provider 90 - value={{ rep, initialFacts: props.initialFacts }} 91 > 92 {props.children} 93 </ReplicacheContext.Provider>
··· 19 let ReplicacheContext = createContext({ 20 rep: null as null | Replicache<ReplicacheMutators>, 21 initialFacts: [] as Fact<keyof typeof Attributes>[], 22 + permission_token: {} as PermissionToken, 23 }); 24 export function useReplicache() { 25 return useContext(ReplicacheContext); ··· 29 tx: WriteTransaction, 30 args: Parameters<(typeof mutations)[k]>[0], 31 ) => Promise<void>; 32 + }; 33 + 34 + export type PermissionToken = { 35 + id: string; 36 + permission_token_rights: { 37 + entity_set: string; 38 + read: boolean; 39 + write: boolean; 40 + }[]; 41 }; 42 export function ReplicacheProvider(props: { 43 initialFacts: Fact<keyof typeof Attributes>[]; 44 + token: PermissionToken; 45 name: string; 46 children: React.ReactNode; 47 }) { ··· 66 licenseKey: "l381074b8d5224dabaef869802421225a", 67 pusher: async (pushRequest) => { 68 return { 69 + response: await Push(pushRequest, props.name, props.token), 70 httpRequestInfo: { errorMessage: "", httpStatusCode: 200 }, 71 }; 72 }, ··· 98 }, [props.name]); 99 return ( 100 <ReplicacheContext.Provider 101 + value={{ 102 + rep, 103 + initialFacts: props.initialFacts, 104 + permission_token: props.token, 105 + }} 106 > 107 {props.children} 108 </ReplicacheContext.Provider>
+9 -2
src/replicache/mutations.ts
··· 5 import { Database } from "supabase/database.types"; 6 7 export type MutationContext = { 8 - createEntity: (entityID: string) => Promise<boolean>; 9 scanIndex: { 10 eav: <A extends keyof typeof Attributes>( 11 entity: string, ··· 29 30 const addBlock: Mutation<{ 31 parent: string; 32 type: Fact<"block/type">["data"]["value"]; 33 newEntityID: string; 34 position: string; 35 }> = async (args, ctx) => { 36 - await ctx.createEntity(args.newEntityID); 37 await ctx.assertFact({ 38 entity: args.parent, 39 data: {
··· 5 import { Database } from "supabase/database.types"; 6 7 export type MutationContext = { 8 + createEntity: (args: { 9 + entityID: string; 10 + permission_set: string; 11 + }) => Promise<boolean>; 12 scanIndex: { 13 eav: <A extends keyof typeof Attributes>( 14 entity: string, ··· 32 33 const addBlock: Mutation<{ 34 parent: string; 35 + permission_set: string; 36 type: Fact<"block/type">["data"]["value"]; 37 newEntityID: string; 38 position: string; 39 }> = async (args, ctx) => { 40 + await ctx.createEntity({ 41 + entityID: args.newEntityID, 42 + permission_set: args.permission_set, 43 + }); 44 await ctx.assertFact({ 45 entity: args.parent, 46 data: {
+11 -2
src/replicache/push.ts
··· 3 import { serverMutationContext } from "./serverMutationContext"; 4 import { mutations } from "./mutations"; 5 import { drizzle } from "drizzle-orm/postgres-js"; 6 import postgres from "postgres"; 7 - import { replicache_clients } from "drizzle/schema"; 8 import { getClientGroup } from "./utils"; 9 import { createClient } from "@supabase/supabase-js"; 10 import { Database } from "supabase/database.types"; ··· 18 export async function Push( 19 pushRequest: PushRequest, 20 rootEntity: string, 21 ): Promise<PushResponse | undefined> { 22 if (pushRequest.pushVersion !== 1) 23 return { error: "VersionNotSupported", versionType: "push" }; 24 let clientGroup = await getClientGroup(db, pushRequest.clientGroupID); 25 for (let mutation of pushRequest.mutations) { 26 let lastMutationID = clientGroup[mutation.clientID] || 0; 27 if (mutation.id <= lastMutationID) continue; ··· 32 } 33 await db.transaction(async (tx) => { 34 try { 35 - await mutations[name](mutation.args as any, serverMutationContext(tx)); 36 } catch (e) { 37 console.log( 38 `Error occured while running mutation: ${name}`,
··· 3 import { serverMutationContext } from "./serverMutationContext"; 4 import { mutations } from "./mutations"; 5 import { drizzle } from "drizzle-orm/postgres-js"; 6 + import { eq } from "drizzle-orm"; 7 import postgres from "postgres"; 8 + import { permission_token_rights, replicache_clients } from "drizzle/schema"; 9 import { getClientGroup } from "./utils"; 10 import { createClient } from "@supabase/supabase-js"; 11 import { Database } from "supabase/database.types"; ··· 19 export async function Push( 20 pushRequest: PushRequest, 21 rootEntity: string, 22 + token: { id: string }, 23 ): Promise<PushResponse | undefined> { 24 if (pushRequest.pushVersion !== 1) 25 return { error: "VersionNotSupported", versionType: "push" }; 26 let clientGroup = await getClientGroup(db, pushRequest.clientGroupID); 27 + let token_rights = await db 28 + .select() 29 + .from(permission_token_rights) 30 + .where(eq(permission_token_rights.token, token.id)); 31 for (let mutation of pushRequest.mutations) { 32 let lastMutationID = clientGroup[mutation.clientID] || 0; 33 if (mutation.id <= lastMutationID) continue; ··· 38 } 39 await db.transaction(async (tx) => { 40 try { 41 + await mutations[name]( 42 + mutation.args as any, 43 + serverMutationContext(tx, token_rights), 44 + ); 45 } catch (e) { 46 console.log( 47 `Error occured while running mutation: ${name}`,
+45 -5
src/replicache/serverMutationContext.ts
··· 5 import { MutationContext } from "./mutations"; 6 import { entities, facts } from "drizzle/schema"; 7 import { Attributes, FilterAttributes } from "./attributes"; 8 - import { Fact } from "."; 9 import { DeepReadonly } from "replicache"; 10 import { createClient } from "@supabase/supabase-js"; 11 import { Database } from "supabase/database.types"; 12 - export function serverMutationContext(tx: PgTransaction<any, any, any>) { 13 - let ctx: MutationContext = { 14 async runOnServer(cb) { 15 let supabase = createClient<Database>( 16 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 18 ); 19 return cb({ supabase }); 20 }, 21 async runOnClient(_cb) {}, 22 - async createEntity(entity) { 23 await tx.transaction( 24 async (tx2) => 25 await tx2 26 .insert(entities) 27 .values({ 28 - id: entity, 29 }) 30 .catch(console.log), 31 ); ··· 54 if (!attribute) return; 55 let id = f.id || crypto.randomUUID(); 56 let data = { ...f.data }; 57 if (attribute.cardinality === "one") { 58 let existingFact = await tx 59 .select({ id: facts.id, data: facts.data }) ··· 104 ); 105 }, 106 async retractFact(id) { 107 await tx.delete(facts).where(driz.eq(facts.id, id)); 108 }, 109 async deleteEntity(entity) { 110 await Promise.all([ 111 tx.delete(entities).where(driz.eq(entities.id, entity)), 112 tx
··· 5 import { MutationContext } from "./mutations"; 6 import { entities, facts } from "drizzle/schema"; 7 import { Attributes, FilterAttributes } from "./attributes"; 8 + import { Fact, PermissionToken } from "."; 9 import { DeepReadonly } from "replicache"; 10 import { createClient } from "@supabase/supabase-js"; 11 import { Database } from "supabase/database.types"; 12 + export function serverMutationContext( 13 + tx: PgTransaction<any, any, any>, 14 + token_rights: PermissionToken["permission_token_rights"], 15 + ) { 16 + let ctx: MutationContext & { 17 + checkPermission: (entity: string) => Promise<boolean>; 18 + } = { 19 async runOnServer(cb) { 20 let supabase = createClient<Database>( 21 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 23 ); 24 return cb({ supabase }); 25 }, 26 + async checkPermission(entity: string) { 27 + let [permission_set] = await tx 28 + .select({ entity_set: entities.set }) 29 + .from(entities) 30 + .where(driz.eq(entities.id, entity)); 31 + return ( 32 + !!permission_set && 33 + !!token_rights.find( 34 + (r) => r.entity_set === permission_set.entity_set && r.write == true, 35 + ) 36 + ); 37 + }, 38 async runOnClient(_cb) {}, 39 + async createEntity({ entityID, permission_set }) { 40 + if ( 41 + !token_rights.find( 42 + (r) => r.entity_set === permission_set && r.write === true, 43 + ) 44 + ) { 45 + console.log("NO RIGHT???"); 46 + console.log(token_rights); 47 + console.log(permission_set); 48 + return false; 49 + } 50 await tx.transaction( 51 async (tx2) => 52 await tx2 53 .insert(entities) 54 .values({ 55 + set: permission_set, 56 + id: entityID, 57 }) 58 .catch(console.log), 59 ); ··· 82 if (!attribute) return; 83 let id = f.id || crypto.randomUUID(); 84 let data = { ...f.data }; 85 + let [permission_set] = await tx 86 + .select({ entity_set: entities.set }) 87 + .from(entities) 88 + .where(driz.eq(entities.id, f.entity)); 89 + if (!this.checkPermission(f.entity)) return; 90 if (attribute.cardinality === "one") { 91 let existingFact = await tx 92 .select({ id: facts.id, data: facts.data }) ··· 137 ); 138 }, 139 async retractFact(id) { 140 + let [f] = await tx 141 + .select() 142 + .from(facts) 143 + .rightJoin(entities, driz.eq(entities.id, facts.entity)) 144 + .where(driz.eq(facts.id, id)); 145 + if (!f || !this.checkPermission(f.entities.id)) return; 146 await tx.delete(facts).where(driz.eq(facts.id, id)); 147 }, 148 async deleteEntity(entity) { 149 + if (!this.checkPermission(entity)) return; 150 await Promise.all([ 151 tx.delete(entities).where(driz.eq(entities.id, entity)), 152 tx
+94
supabase/database.types.ts
··· 38 Row: { 39 created_at: string 40 id: string 41 } 42 Insert: { 43 created_at?: string ··· 81 { 82 foreignKeyName: "facts_entity_fkey" 83 columns: ["entity"] 84 isOneToOne: false 85 referencedRelation: "entities" 86 referencedColumns: ["id"]
··· 38 Row: { 39 created_at: string 40 id: string 41 + set: string 42 + } 43 + Insert: { 44 + created_at?: string 45 + id?: string 46 + set: string 47 + } 48 + Update: { 49 + created_at?: string 50 + id?: string 51 + set?: string 52 + } 53 + Relationships: [ 54 + { 55 + foreignKeyName: "entities_set_fkey" 56 + columns: ["set"] 57 + isOneToOne: false 58 + referencedRelation: "entity_sets" 59 + referencedColumns: ["id"] 60 + }, 61 + ] 62 + } 63 + entity_sets: { 64 + Row: { 65 + created_at: string 66 + id: string 67 } 68 Insert: { 69 created_at?: string ··· 107 { 108 foreignKeyName: "facts_entity_fkey" 109 columns: ["entity"] 110 + isOneToOne: false 111 + referencedRelation: "entities" 112 + referencedColumns: ["id"] 113 + }, 114 + ] 115 + } 116 + permission_token_rights: { 117 + Row: { 118 + change_entity_set: boolean 119 + create_token: boolean 120 + created_at: string 121 + entity_set: string 122 + read: boolean 123 + token: string 124 + write: boolean 125 + } 126 + Insert: { 127 + change_entity_set?: boolean 128 + create_token?: boolean 129 + created_at?: string 130 + entity_set: string 131 + read?: boolean 132 + token: string 133 + write?: boolean 134 + } 135 + Update: { 136 + change_entity_set?: boolean 137 + create_token?: boolean 138 + created_at?: string 139 + entity_set?: string 140 + read?: boolean 141 + token?: string 142 + write?: boolean 143 + } 144 + Relationships: [ 145 + { 146 + foreignKeyName: "permission_token_rights_entity_set_fkey" 147 + columns: ["entity_set"] 148 + isOneToOne: false 149 + referencedRelation: "entity_sets" 150 + referencedColumns: ["id"] 151 + }, 152 + { 153 + foreignKeyName: "permission_token_rights_token_fkey" 154 + columns: ["token"] 155 + isOneToOne: false 156 + referencedRelation: "permission_tokens" 157 + referencedColumns: ["id"] 158 + }, 159 + ] 160 + } 161 + permission_tokens: { 162 + Row: { 163 + id: string 164 + root_entity: string 165 + } 166 + Insert: { 167 + id?: string 168 + root_entity: string 169 + } 170 + Update: { 171 + id?: string 172 + root_entity?: string 173 + } 174 + Relationships: [ 175 + { 176 + foreignKeyName: "permission_tokens_root_entity_fkey" 177 + columns: ["root_entity"] 178 isOneToOne: false 179 referencedRelation: "entities" 180 referencedColumns: ["id"]
+184
supabase/migrations/20240703183954_add_permission_system.sql
···
··· 1 + create table "public"."entity_sets" ( 2 + "id" uuid not null default gen_random_uuid(), 3 + "created_at" timestamp with time zone not null default now() 4 + ); 5 + 6 + 7 + alter table "public"."entity_sets" enable row level security; 8 + 9 + create table "public"."permission_token_rights" ( 10 + "token" uuid not null, 11 + "entity_set" uuid not null, 12 + "read" boolean not null default false, 13 + "write" boolean not null default false, 14 + "created_at" timestamp with time zone not null default now(), 15 + "create_token" boolean not null default false, 16 + "change_entity_set" boolean not null default false 17 + ); 18 + 19 + 20 + alter table "public"."permission_token_rights" enable row level security; 21 + 22 + create table "public"."permission_tokens" ( 23 + "id" uuid not null default gen_random_uuid(), 24 + "root_entity" uuid not null 25 + ); 26 + 27 + 28 + alter table "public"."permission_tokens" enable row level security; 29 + 30 + alter table "public"."entities" add column "set" uuid not null; 31 + 32 + CREATE UNIQUE INDEX entity_sets_pkey ON public.entity_sets USING btree (id); 33 + 34 + CREATE UNIQUE INDEX permission_token_rights_pkey ON public.permission_token_rights USING btree (token, entity_set); 35 + 36 + CREATE UNIQUE INDEX permission_tokens_pkey ON public.permission_tokens USING btree (id); 37 + 38 + alter table "public"."entity_sets" add constraint "entity_sets_pkey" PRIMARY KEY using index "entity_sets_pkey"; 39 + 40 + alter table "public"."permission_token_rights" add constraint "permission_token_rights_pkey" PRIMARY KEY using index "permission_token_rights_pkey"; 41 + 42 + alter table "public"."permission_tokens" add constraint "permission_tokens_pkey" PRIMARY KEY using index "permission_tokens_pkey"; 43 + 44 + alter table "public"."entities" add constraint "entities_set_fkey" FOREIGN KEY (set) REFERENCES entity_sets(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 45 + 46 + alter table "public"."entities" validate constraint "entities_set_fkey"; 47 + 48 + alter table "public"."permission_token_rights" add constraint "permission_token_rights_entity_set_fkey" FOREIGN KEY (entity_set) REFERENCES entity_sets(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 49 + 50 + alter table "public"."permission_token_rights" validate constraint "permission_token_rights_entity_set_fkey"; 51 + 52 + alter table "public"."permission_token_rights" add constraint "permission_token_rights_token_fkey" FOREIGN KEY (token) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 53 + 54 + alter table "public"."permission_token_rights" validate constraint "permission_token_rights_token_fkey"; 55 + 56 + alter table "public"."permission_tokens" add constraint "permission_tokens_root_entity_fkey" FOREIGN KEY (root_entity) REFERENCES entities(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 57 + 58 + alter table "public"."permission_tokens" validate constraint "permission_tokens_root_entity_fkey"; 59 + 60 + grant delete on table "public"."entity_sets" to "anon"; 61 + 62 + grant insert on table "public"."entity_sets" to "anon"; 63 + 64 + grant references on table "public"."entity_sets" to "anon"; 65 + 66 + grant select on table "public"."entity_sets" to "anon"; 67 + 68 + grant trigger on table "public"."entity_sets" to "anon"; 69 + 70 + grant truncate on table "public"."entity_sets" to "anon"; 71 + 72 + grant update on table "public"."entity_sets" to "anon"; 73 + 74 + grant delete on table "public"."entity_sets" to "authenticated"; 75 + 76 + grant insert on table "public"."entity_sets" to "authenticated"; 77 + 78 + grant references on table "public"."entity_sets" to "authenticated"; 79 + 80 + grant select on table "public"."entity_sets" to "authenticated"; 81 + 82 + grant trigger on table "public"."entity_sets" to "authenticated"; 83 + 84 + grant truncate on table "public"."entity_sets" to "authenticated"; 85 + 86 + grant update on table "public"."entity_sets" to "authenticated"; 87 + 88 + grant delete on table "public"."entity_sets" to "service_role"; 89 + 90 + grant insert on table "public"."entity_sets" to "service_role"; 91 + 92 + grant references on table "public"."entity_sets" to "service_role"; 93 + 94 + grant select on table "public"."entity_sets" to "service_role"; 95 + 96 + grant trigger on table "public"."entity_sets" to "service_role"; 97 + 98 + grant truncate on table "public"."entity_sets" to "service_role"; 99 + 100 + grant update on table "public"."entity_sets" to "service_role"; 101 + 102 + grant delete on table "public"."permission_token_rights" to "anon"; 103 + 104 + grant insert on table "public"."permission_token_rights" to "anon"; 105 + 106 + grant references on table "public"."permission_token_rights" to "anon"; 107 + 108 + grant select on table "public"."permission_token_rights" to "anon"; 109 + 110 + grant trigger on table "public"."permission_token_rights" to "anon"; 111 + 112 + grant truncate on table "public"."permission_token_rights" to "anon"; 113 + 114 + grant update on table "public"."permission_token_rights" to "anon"; 115 + 116 + grant delete on table "public"."permission_token_rights" to "authenticated"; 117 + 118 + grant insert on table "public"."permission_token_rights" to "authenticated"; 119 + 120 + grant references on table "public"."permission_token_rights" to "authenticated"; 121 + 122 + grant select on table "public"."permission_token_rights" to "authenticated"; 123 + 124 + grant trigger on table "public"."permission_token_rights" to "authenticated"; 125 + 126 + grant truncate on table "public"."permission_token_rights" to "authenticated"; 127 + 128 + grant update on table "public"."permission_token_rights" to "authenticated"; 129 + 130 + grant delete on table "public"."permission_token_rights" to "service_role"; 131 + 132 + grant insert on table "public"."permission_token_rights" to "service_role"; 133 + 134 + grant references on table "public"."permission_token_rights" to "service_role"; 135 + 136 + grant select on table "public"."permission_token_rights" to "service_role"; 137 + 138 + grant trigger on table "public"."permission_token_rights" to "service_role"; 139 + 140 + grant truncate on table "public"."permission_token_rights" to "service_role"; 141 + 142 + grant update on table "public"."permission_token_rights" to "service_role"; 143 + 144 + grant delete on table "public"."permission_tokens" to "anon"; 145 + 146 + grant insert on table "public"."permission_tokens" to "anon"; 147 + 148 + grant references on table "public"."permission_tokens" to "anon"; 149 + 150 + grant select on table "public"."permission_tokens" to "anon"; 151 + 152 + grant trigger on table "public"."permission_tokens" to "anon"; 153 + 154 + grant truncate on table "public"."permission_tokens" to "anon"; 155 + 156 + grant update on table "public"."permission_tokens" to "anon"; 157 + 158 + grant delete on table "public"."permission_tokens" to "authenticated"; 159 + 160 + grant insert on table "public"."permission_tokens" to "authenticated"; 161 + 162 + grant references on table "public"."permission_tokens" to "authenticated"; 163 + 164 + grant select on table "public"."permission_tokens" to "authenticated"; 165 + 166 + grant trigger on table "public"."permission_tokens" to "authenticated"; 167 + 168 + grant truncate on table "public"."permission_tokens" to "authenticated"; 169 + 170 + grant update on table "public"."permission_tokens" to "authenticated"; 171 + 172 + grant delete on table "public"."permission_tokens" to "service_role"; 173 + 174 + grant insert on table "public"."permission_tokens" to "service_role"; 175 + 176 + grant references on table "public"."permission_tokens" to "service_role"; 177 + 178 + grant select on table "public"."permission_tokens" to "service_role"; 179 + 180 + grant trigger on table "public"."permission_tokens" to "service_role"; 181 + 182 + grant truncate on table "public"."permission_tokens" to "service_role"; 183 + 184 + grant update on table "public"."permission_tokens" to "service_role";