Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add mechanism to exlude records from sync

+231 -42
+2 -2
api/.sqlx/query-112e2cbb7ee10d0ec1261e8beebda6c126bf23dea5a8f3fe9f7e9952807a478f.json api/.sqlx/query-a371b03a7a67b4e2683bf191d44b5eaae5d514096b5cdf48806083618d59ac64.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT DISTINCT json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n ORDER BY json->>'nsid'\n ", 3 + "query": "\n SELECT DISTINCT json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true')\n ORDER BY json->>'nsid'\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 18 18 null 19 19 ] 20 20 }, 21 - "hash": "112e2cbb7ee10d0ec1261e8beebda6c126bf23dea5a8f3fe9f7e9952807a478f" 21 + "hash": "a371b03a7a67b4e2683bf191d44b5eaae5d514096b5cdf48806083618d59ac64" 22 22 }
-34
api/.sqlx/query-5fb1c2c7b29bfd9614e24fd391e52ca09f805ecdee4b28a4891bc527ab0d915a.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n WITH slice_collections AS (\n SELECT DISTINCT\n json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n )\n SELECT\n r.collection,\n COUNT(*) as record_count,\n COUNT(DISTINCT r.did) as unique_actors\n FROM record r\n INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid\n WHERE r.slice_uri = $1\n GROUP BY r.collection\n ORDER BY r.collection\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "collection", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "record_count", 14 - "type_info": "Int8" 15 - }, 16 - { 17 - "ordinal": 2, 18 - "name": "unique_actors", 19 - "type_info": "Int8" 20 - } 21 - ], 22 - "parameters": { 23 - "Left": [ 24 - "Text" 25 - ] 26 - }, 27 - "nullable": [ 28 - false, 29 - null, 30 - null 31 - ] 32 - }, 33 - "hash": "5fb1c2c7b29bfd9614e24fd391e52ca09f805ecdee4b28a4891bc527ab0d915a" 34 - }
+34
api/.sqlx/query-d266507b7dff72cff7a6743f45db96bde47db23d96a1d280e17837e38169e64b.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n WITH slice_collections AS (\n SELECT DISTINCT\n json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true')\n )\n SELECT\n r.collection,\n COUNT(*) as record_count,\n COUNT(DISTINCT r.did) as unique_actors\n FROM record r\n INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid\n WHERE r.slice_uri = $1\n GROUP BY r.collection\n ORDER BY r.collection\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "collection", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "record_count", 14 + "type_info": "Int8" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "unique_actors", 19 + "type_info": "Int8" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + null, 30 + null 31 + ] 32 + }, 33 + "hash": "d266507b7dff72cff7a6743f45db96bde47db23d96a1d280e17837e38169e64b" 34 + }
+2 -2
api/.sqlx/query-e62f397eb38f35e17988429c36de83f0e299c7def29dd1452706563ee58f9e3f.json api/.sqlx/query-1bba58bc784972353c054d767a7337f0d472d8d3d9484ebb18237f4a094c4e58.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n WITH slice_collections AS (\n SELECT DISTINCT\n json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n )\n SELECT COUNT(*) as count\n FROM record r\n INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid\n WHERE r.slice_uri = $1\n ", 3 + "query": "\n WITH slice_collections AS (\n SELECT DISTINCT\n json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND json->>'nsid' IS NOT NULL\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true')\n )\n SELECT COUNT(*) as count\n FROM record r\n INNER JOIN slice_collections sc ON r.collection = sc.collection_nsid\n WHERE r.slice_uri = $1\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 18 18 null 19 19 ] 20 20 }, 21 - "hash": "e62f397eb38f35e17988429c36de83f0e299c7def29dd1452706563ee58f9e3f" 21 + "hash": "1bba58bc784972353c054d767a7337f0d472d8d3d9484ebb18237f4a094c4e58" 22 22 }
+3
api/src/database.rs
··· 537 537 AND json->>'slice' = $1 538 538 AND json->>'nsid' IS NOT NULL 539 539 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 540 + AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 540 541 ) 541 542 SELECT 542 543 r.collection, ··· 575 576 AND json->>'slice' = $1 576 577 AND json->>'nsid' IS NOT NULL 577 578 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 579 + AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 578 580 ORDER BY json->>'nsid' 579 581 "#, 580 582 slice_uri ··· 599 601 AND json->>'slice' = $1 600 602 AND json->>'nsid' IS NOT NULL 601 603 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 604 + AND (json->>'excludedFromSync' IS NULL OR json->>'excludedFromSync' != 'true') 602 605 ) 603 606 SELECT COUNT(*) as count 604 607 FROM record r
+3 -1
frontend/src/client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-19 15:56:11 UTC 2 + // Generated at: 2025-09-19 22:48:29 UTC 3 3 // Lexicons: 25 4 4 5 5 /** ··· 1201 1201 updatedAt?: string; 1202 1202 /** AT-URI reference to the slice this lexicon belongs to */ 1203 1203 slice: string; 1204 + /** Whether this lexicon should be excluded from sync operations */ 1205 + excludedFromSync?: boolean; 1204 1206 } 1205 1207 1206 1208 export type NetworkSlicesLexiconSortFields =
+80 -1
frontend/src/features/slices/lexicon/handlers.tsx
··· 2 2 import { renderHTML } from "../../../utils/render.tsx"; 3 3 import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 4 4 import { getSliceClient } from "../../../utils/client.ts"; 5 - import { buildSliceUri } from "../../../utils/at-uri.ts"; 5 + import { buildSliceUri, getRkeyFromUri } from "../../../utils/at-uri.ts"; 6 6 import { withSliceAccess } from "../../../routes/slice-middleware.ts"; 7 7 import { extractSliceParams } from "../../../utils/slice-params.ts"; 8 8 import { SliceLexiconPage } from "./templates/SliceLexiconPage.tsx"; ··· 11 11 import { LexiconErrorMessage } from "./templates/fragments/LexiconErrorMessage.tsx"; 12 12 import { LexiconsList } from "./templates/fragments/LexiconsList.tsx"; 13 13 import { LexiconFormModal } from "./templates/fragments/LexiconFormModal.tsx"; 14 + import { Badge } from "../../../shared/fragments/Badge.tsx"; 14 15 import { FileCode } from "lucide-preact"; 15 16 import { buildSliceUrl } from "../../../utils/slice-params.ts"; 16 17 import type { NetworkSlicesLexicon } from "../../../client.ts"; ··· 252 253 definitions: lexicon.value.definitions, 253 254 uri: lexicon.uri, 254 255 createdAt: lexicon.value.createdAt, 256 + excludedFromSync: lexicon.value.excludedFromSync === true, 255 257 currentUser: authContext.currentUser, 256 258 hasSliceAccess: context.sliceContext?.hasAccess, 257 259 }) ··· 511 513 return renderHTML(<LexiconFormModal sliceId={sliceId} />); 512 514 } 513 515 516 + async function handleUpdateLexiconExclusion( 517 + req: Request, 518 + params?: URLPatternResult 519 + ): Promise<Response> { 520 + const context = await withAuth(req); 521 + const authResponse = requireAuth(context); 522 + if (authResponse) return authResponse; 523 + 524 + const sliceId = params?.pathname.groups.id; 525 + if (!sliceId) { 526 + return new Response("Invalid slice ID", { status: 400 }); 527 + } 528 + 529 + // Get URI from query parameters 530 + const url = new URL(req.url); 531 + const uri = url.searchParams.get("uri"); 532 + if (!uri) { 533 + return new Response("URI parameter is required", { status: 400 }); 534 + } 535 + 536 + try { 537 + const formData = await req.formData(); 538 + const nsid = formData.get("nsid") as string; 539 + const excludedFromSync = formData.get("excludedFromSync") === "true"; 540 + 541 + if (!nsid) { 542 + return new Response("NSID is required", { status: 400 }); 543 + } 544 + 545 + const sliceClient = getSliceClient(context, sliceId); 546 + 547 + // Get the current lexicon record using the URI 548 + const currentRecord = await sliceClient.network.slices.lexicon.getRecord({ 549 + uri 550 + }); 551 + 552 + // Update the record with the new exclusion status 553 + const updatedRecord = { 554 + ...currentRecord.value, 555 + excludedFromSync, 556 + updatedAt: new Date().toISOString(), 557 + }; 558 + 559 + // Extract rkey from URI for updateRecord 560 + const rkey = getRkeyFromUri(uri); 561 + await sliceClient.network.slices.lexicon.updateRecord(rkey, updatedRecord); 562 + 563 + // Return the updated status badge 564 + return renderHTML( 565 + <div id="exclusion-status"> 566 + {excludedFromSync ? ( 567 + <Badge variant="warning" className="min-w-[100px] justify-center"> 568 + Sync excluded 569 + </Badge> 570 + ) : ( 571 + <Badge variant="success" className="min-w-[100px] justify-center"> 572 + Sync enabled 573 + </Badge> 574 + )} 575 + </div> 576 + ); 577 + } catch (error) { 578 + console.error("Failed to update lexicon exclusion:", error); 579 + return renderHTML( 580 + <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> 581 + <p>Failed to update exclusion status: {error}</p> 582 + </div>, 583 + { status: 500 } 584 + ); 585 + } 586 + } 587 + 514 588 export const lexiconRoutes: Route[] = [ 515 589 { 516 590 method: "GET", ··· 557 631 method: "DELETE", 558 632 pattern: new URLPattern({ pathname: "/api/slices/:id/lexicons/:rkey" }), 559 633 handler: handleDeleteLexicon, 634 + }, 635 + { 636 + method: "PUT", 637 + pattern: new URLPattern({ pathname: "/api/slices/:id/lexicons/exclusion" }), 638 + handler: handleUpdateLexiconExclusion, 560 639 }, 561 640 ];
+68
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
··· 2 2 import { Breadcrumb } from "../../../../shared/fragments/Breadcrumb.tsx"; 3 3 import { Button } from "../../../../shared/fragments/Button.tsx"; 4 4 import { Card } from "../../../../shared/fragments/Card.tsx"; 5 + import { Badge } from "../../../../shared/fragments/Badge.tsx"; 6 + import { Text } from "../../../../shared/fragments/Text.tsx"; 5 7 import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 6 8 import type { NetworkSlicesSliceDefsSliceView } from "../../../../client.ts"; 7 9 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 10 + import { getRkeyFromUri } from "../../../../utils/at-uri.ts"; 8 11 import { codeToHtml } from "jsr:@shikijs/shiki"; 9 12 10 13 interface LexiconDetailPageProps { ··· 14 17 definitions: string; 15 18 uri: string; 16 19 createdAt: string; 20 + excludedFromSync?: boolean; 17 21 currentUser?: AuthenticatedUser; 18 22 hasSliceAccess?: boolean; 19 23 } ··· 23 27 sliceId, 24 28 nsid, 25 29 definitions, 30 + uri, 31 + excludedFromSync, 26 32 currentUser, 27 33 hasSliceAccess, 28 34 }: LexiconDetailPageProps) { ··· 50 56 }, 51 57 }); 52 58 59 + // Extract rkey for updating the record 60 + const rkey = getRkeyFromUri(uri); 61 + 62 + // Check if this is a record type lexicon 63 + const isRecordType = parsedDefinitions?.main?.type === "record"; 64 + 53 65 return ( 54 66 <Layout title={`${nsid} - ${slice.name}`} currentUser={currentUser}> 55 67 <div className="max-w-6xl mx-auto px-4 py-8"> ··· 87 99 <div dangerouslySetInnerHTML={{ __html: highlightedCode }} /> 88 100 </Card.Content> 89 101 </Card> 102 + 103 + {/* Sync Settings - only show for authenticated users with slice access and record type lexicons */} 104 + {currentUser && hasSliceAccess && isRecordType && ( 105 + <div className="mt-6"> 106 + <Card> 107 + <Card.Header title="Sync Settings" /> 108 + <Card.Content className="p-6"> 109 + <div className="space-y-6"> 110 + <div className="flex items-start justify-between gap-6"> 111 + <div className="flex-1 min-w-0"> 112 + <Text as="h3" size="sm" weight="medium" className="mb-2"> 113 + Exclude from Sync 114 + </Text> 115 + <Text size="sm" variant="muted" className="leading-relaxed"> 116 + When enabled, records for this lexicon will not be synced from the AT Protocol firehose or during bulk sync operations. 117 + </Text> 118 + </div> 119 + <div className="flex-shrink-0"> 120 + <form 121 + hx-put={`/api/slices/${sliceId}/lexicons/exclusion?uri=${encodeURIComponent(uri)}`} 122 + hx-target="#exclusion-status" 123 + hx-swap="outerHTML" 124 + > 125 + <input type="hidden" name="nsid" value={nsid} /> 126 + <input type="hidden" name="excludedFromSync" value={excludedFromSync ? "false" : "true"} /> 127 + <label className="relative inline-flex items-center cursor-pointer"> 128 + <input 129 + type="checkbox" 130 + checked={excludedFromSync === true} 131 + _="on change trigger submit on closest <form/>" 132 + className="sr-only peer" 133 + /> 134 + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div> 135 + </label> 136 + </form> 137 + </div> 138 + </div> 139 + 140 + <div className="pt-4 border-t border-zinc-200 dark:border-zinc-700"> 141 + <div id="exclusion-status"> 142 + {excludedFromSync ? ( 143 + <Badge variant="warning" className="min-w-[100px] justify-center"> 144 + Sync excluded 145 + </Badge> 146 + ) : ( 147 + <Badge variant="success" className="min-w-[100px] justify-center"> 148 + Sync enabled 149 + </Badge> 150 + )} 151 + </div> 152 + </div> 153 + </div> 154 + </Card.Content> 155 + </Card> 156 + </div> 157 + )} 90 158 </div> 91 159 </Layout> 92 160 );
+24
frontend/src/features/slices/lexicon/templates/fragments/LexiconListItem.tsx
··· 8 8 export function LexiconListItem({ 9 9 nsid, 10 10 uri, 11 + definitions, 12 + excludedFromSync, 11 13 sliceId, 12 14 handle, 13 15 isPrimary, ··· 15 17 }: { 16 18 nsid: string; 17 19 uri: string; 20 + definitions: string; 21 + excludedFromSync?: boolean; 18 22 sliceId: string; 19 23 handle?: string; 20 24 isPrimary?: boolean; ··· 22 26 }) { 23 27 const rkey = getRkeyFromUri(uri); 24 28 29 + // Check if this is a record type lexicon 30 + let isRecordType = false; 31 + try { 32 + const parsedDefinitions = JSON.parse(definitions); 33 + isRecordType = parsedDefinitions?.main?.type === "record"; 34 + } catch { 35 + // If parsing fails, default to false 36 + isRecordType = false; 37 + } 38 + 25 39 return ( 26 40 <ListItem id={`lexicon-${rkey}`}> 27 41 <div className="flex items-center w-full"> ··· 51 65 {isPrimary !== undefined && ( 52 66 <Badge variant={isPrimary ? "success" : "primary"}> 53 67 {isPrimary ? "Primary" : "External"} 68 + </Badge> 69 + )} 70 + {isRecordType && ( 71 + <Badge variant="secondary"> 72 + Record 73 + </Badge> 74 + )} 75 + {excludedFromSync && ( 76 + <Badge variant="warning"> 77 + Sync excluded 54 78 </Badge> 55 79 )} 56 80 </div>
+4
frontend/src/features/slices/lexicon/templates/fragments/LexiconsList.tsx
··· 94 94 key={record.uri} 95 95 nsid={record.value.nsid} 96 96 uri={record.uri} 97 + definitions={record.value.definitions} 98 + excludedFromSync={record.value.excludedFromSync} 97 99 sliceId={sliceId} 98 100 handle={handle} 99 101 isPrimary ··· 109 111 key={record.uri} 110 112 nsid={record.value.nsid} 111 113 uri={record.uri} 114 + definitions={record.value.definitions} 115 + excludedFromSync={record.value.excludedFromSync} 112 116 sliceId={sliceId} 113 117 handle={handle} 114 118 isPrimary={false}
+6 -2
frontend/src/features/slices/sync/handlers.tsx
··· 174 174 const recordLexicons = lexiconsResponse.records.filter((lexicon) => { 175 175 try { 176 176 const definitions = JSON.parse(lexicon.value.definitions); 177 - return definitions.main.type === "record"; 177 + const isRecordType = definitions.main.type === "record"; 178 + const isNotExcluded = !lexicon.value.excludedFromSync; 179 + return isRecordType && isNotExcluded; 178 180 } catch { 179 181 return false; 180 182 } ··· 236 238 const recordLexicons = lexiconsResponse.records.filter((lexicon) => { 237 239 try { 238 240 const definitions = JSON.parse(lexicon.value.definitions); 239 - return definitions.main.type === "record"; 241 + const isRecordType = definitions.main.type === "record"; 242 + const isNotExcluded = !lexicon.value.excludedFromSync; 243 + return isRecordType && isNotExcluded; 240 244 } catch { 241 245 return false; 242 246 }
+5
lexicons/network/slices/lexicon/lexicon.json
··· 33 33 "type": "string", 34 34 "format": "at-uri", 35 35 "description": "AT-URI reference to the slice this lexicon belongs to" 36 + }, 37 + "excludedFromSync": { 38 + "type": "boolean", 39 + "description": "Whether this lexicon should be excluded from sync operations", 40 + "default": false 36 41 } 37 42 } 38 43 }