Highly ambitious ATProtocol AppView service and sdks

social.slices -> network.slices

+2 -2
api/.spectral.yaml
··· 1 1 # Spectral linting for OpenAPI specs 2 - # 2 + # 3 3 # Usage: 4 4 # spectral lint <path-to-openapi-spec> 5 5 # spectral lint openapi.json --ruleset .spectral.yaml 6 6 # 7 7 # To lint the generated OpenAPI spec from the API: 8 - # curl "http://localhost:3000/xrpc/social.slices.slice.openapi?slice=at://did:plc:example/social.slices.slice/example" | spectral lint - 8 + # curl "http://localhost:3000/xrpc/network.slices.openapi?slice=at://did:plc:example/network.slices.slice/example" | spectral lint - 9 9 # 10 10 extends: ["spectral:oas"]
+22
api/.sqlx/query-112e2cbb7ee10d0ec1261e8beebda6c126bf23dea5a8f3fe9f7e9952807a478f.json
··· 1 + { 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 ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "collection_nsid", 9 + "type_info": "Text" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + null 19 + ] 20 + }, 21 + "hash": "112e2cbb7ee10d0ec1261e8beebda6c126bf23dea5a8f3fe9f7e9952807a478f" 22 + }
-22
api/.sqlx/query-4895a6217c253c74fae4332b31dabe04d0840ee1966d6fdb4c3dee6d2c87ad1b.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT COUNT(*) as count\n FROM record\n WHERE collection = 'social.slices.lexicon'\n AND json->>'slice' = $1\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "count", 9 - "type_info": "Int8" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - null 19 - ] 20 - }, 21 - "hash": "4895a6217c253c74fae4332b31dabe04d0840ee1966d6fdb4c3dee6d2c87ad1b" 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 + }
+22
api/.sqlx/query-6fa4f18b67c4dc5b175f353c6c03c8a77e42c5013e3deb5ce32afdcba0d150ab.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT COUNT(*) as count\n FROM record\n WHERE collection = 'network.slices.lexicon'\n AND json->>'slice' = $1\n AND (json->>'definitions')::jsonb->'main'->>'type' = 'record'\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "count", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + null 19 + ] 20 + }, 21 + "hash": "6fa4f18b67c4dc5b175f353c6c03c8a77e42c5013e3deb5ce32afdcba0d150ab" 22 + }
-22
api/.sqlx/query-88080d24df604cfdcd827c8201a37414cf6ebedc5e021f67d684806bcb6b1146.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 = 'social.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 ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "count", 9 - "type_info": "Int8" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - null 19 - ] 20 - }, 21 - "hash": "88080d24df604cfdcd827c8201a37414cf6ebedc5e021f67d684806bcb6b1146" 22 - }
-34
api/.sqlx/query-940380fe9b34d64b3d700032ab87b9f0e31ecd04a40e033aee2be995b3c54b28.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 = 'social.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": "940380fe9b34d64b3d700032ab87b9f0e31ecd04a40e033aee2be995b3c54b28" 34 - }
-22
api/.sqlx/query-e583ef70eb5262bb062a4e1bab103e99fd2757e4086aef695098861151a9fb75.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "query": "\n SELECT DISTINCT json->>'nsid' as collection_nsid\n FROM record\n WHERE collection = 'social.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 ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "collection_nsid", 9 - "type_info": "Text" 10 - } 11 - ], 12 - "parameters": { 13 - "Left": [ 14 - "Text" 15 - ] 16 - }, 17 - "nullable": [ 18 - null 19 - ] 20 - }, 21 - "hash": "e583ef70eb5262bb062a4e1bab103e99fd2757e4086aef695098861151a9fb75" 22 - }
+22
api/.sqlx/query-e62f397eb38f35e17988429c36de83f0e299c7def29dd1452706563ee58f9e3f.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 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 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "count", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + null 19 + ] 20 + }, 21 + "hash": "e62f397eb38f35e17988429c36de83f0e299c7def29dd1452706563ee58f9e3f" 22 + }
+2 -2
api/.sqlx/query-fd7e15432f1c5e2b04ab38b6828bc3862e6e2b4e6d3b45a3e5aef024e815c358.json api/.sqlx/query-732081a59997cde317e535cf85e281f4502e4af3a4704a84a615a5044f48f169.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT json->>'domain' as domain\n FROM record\n WHERE collection = 'social.slices.slice'\n AND uri = $1\n ", 3 + "query": "\n SELECT json->>'domain' as domain\n FROM record\n WHERE collection = 'network.slices.slice'\n AND uri = $1\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 18 18 null 19 19 ] 20 20 }, 21 - "hash": "fd7e15432f1c5e2b04ab38b6828bc3862e6e2b4e6d3b45a3e5aef024e815c358" 21 + "hash": "732081a59997cde317e535cf85e281f4502e4af3a4704a84a615a5044f48f169" 22 22 }
+51 -44
api/scripts/generate_typescript.ts
··· 30 30 const lexiconsInput = Deno.args[0] || ""; 31 31 const sliceUri = 32 32 Deno.args[1] || 33 - "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lx5zq4t56s2q"; 33 + "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z"; 34 34 35 35 if (!lexiconsInput) { 36 36 console.error("No lexicon data provided"); ··· 46 46 47 47 // Generate usage example based on available lexicons 48 48 function generateUsageExample(): string { 49 - // Find the first non-social.slices lexicon that has a record type 49 + // Find the first non-network.slices lexicon that has a record type 50 50 const nonSlicesLexicon = lexicons.find( 51 51 (lex) => 52 52 lex.id && 53 - !lex.id.startsWith("social.slices.") && 53 + !lex.id.startsWith("network.slices.") && 54 54 lex.definitions && 55 55 Object.values(lex.definitions).some((def) => def.type === "record") 56 56 ); ··· 85 85 * }); 86 86 * 87 87 * // Use slice-level methods for cross-collection queries with type safety 88 - * const sliceRecords = await client.social.slices.slice.getSliceRecords<${nsidToPascalCase(nonSlicesLexicon.id)}>({ 88 + * const sliceRecords = await client.network.slices.slice.getSliceRecords<${nsidToPascalCase( 89 + nonSlicesLexicon.id 90 + )}>({ 89 91 * where: { 90 92 * collection: { eq: '${nonSlicesLexicon.id}' } 91 93 * } 92 94 * }); 93 95 * 94 96 * // Search across multiple collections using union types 95 - * const multiCollectionRecords = await client.social.slices.slice.getSliceRecords<${nsidToPascalCase(nonSlicesLexicon.id)} | AppBskyActorProfile>({ 97 + * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<${nsidToPascalCase( 98 + nonSlicesLexicon.id 99 + )} | AppBskyActorProfile>({ 96 100 * where: { 97 101 * collection: { in: ['${nonSlicesLexicon.id}', 'app.bsky.actor.profile'] }, 98 102 * text: { contains: 'example search term' }, ··· 106 110 * \`\`\` 107 111 */`; 108 112 } else { 109 - // Fallback: find any lexicon with a record type (including social.slices) 113 + // Fallback: find any lexicon with a record type (including network.slices) 110 114 const anyRecordLexicon = lexicons.find( 111 115 (lex) => 112 116 lex.definitions && ··· 141 145 * }); 142 146 * 143 147 * // Cross-collection operations using base client with type safety 144 - * const multiCollectionResults = await client.getSliceRecords<${nsidToPascalCase(anyRecordLexicon.id)}>({ 148 + * const multiCollectionResults = await client.getSliceRecords<${nsidToPascalCase( 149 + anyRecordLexicon.id 150 + )}>({ 145 151 * where: { 146 152 * collection: { eq: '${anyRecordLexicon.id}' } 147 153 * }, ··· 405 411 sourceFile.addInterface({ 406 412 name: "GetJobLogsResponse", 407 413 isExported: true, 408 - properties: [ 409 - { name: "logs", type: "LogEntry[]" }, 410 - ], 414 + properties: [{ name: "logs", type: "LogEntry[]" }], 411 415 }); 412 416 413 417 sourceFile.addInterface({ 414 418 name: "GetJetstreamLogsParams", 415 419 isExported: true, 416 - properties: [ 417 - { name: "limit", type: "number", hasQuestionToken: true }, 418 - ], 420 + properties: [{ name: "limit", type: "number", hasQuestionToken: true }], 419 421 }); 420 422 421 423 sourceFile.addInterface({ 422 424 name: "GetJetstreamLogsResponse", 423 425 isExported: true, 424 - properties: [ 425 - { name: "logs", type: "LogEntry[]" }, 426 - ], 426 + properties: [{ name: "logs", type: "LogEntry[]" }], 427 427 }); 428 428 429 429 sourceFile.addInterface({ ··· 438 438 { name: "sliceUri", type: "string", hasQuestionToken: true }, 439 439 { name: "level", type: "string" }, 440 440 { name: "message", type: "string" }, 441 - { name: "metadata", type: "Record<string, unknown>", hasQuestionToken: true }, 441 + { 442 + name: "metadata", 443 + type: "Record<string, unknown>", 444 + hasQuestionToken: true, 445 + }, 442 446 ], 443 447 }); 444 448 ··· 522 526 isExported: true, 523 527 type: "{ [K in T]?: WhereCondition }", 524 528 }); 525 - 526 529 527 530 // IndexedRecord fields that are always available for filtering 528 531 sourceFile.addTypeAlias({ ··· 707 710 sourceFile.addInterface({ 708 711 name: "ListOAuthClientsResponse", 709 712 isExported: true, 710 - properties: [ 711 - { name: "clients", type: "OAuthClientDetails[]" }, 712 - ], 713 + properties: [{ name: "clients", type: "OAuthClientDetails[]" }], 713 714 }); 714 715 715 716 sourceFile.addInterface({ ··· 1625 1626 } 1626 1627 } 1627 1628 1628 - // Add codegen method to the social.slices.slice class 1629 + // Add codegen method to the network.slices.slice class 1629 1630 if ( 1630 1631 currentPath.length === 3 && 1631 - currentPath[0] === "social" && 1632 + currentPath[0] === "network" && 1632 1633 currentPath[1] === "slices" && 1633 1634 currentPath[2] === "slice" 1634 1635 ) { ··· 1638 1639 returnType: "Promise<CodegenXrpcResponse>", 1639 1640 isAsync: true, 1640 1641 statements: [ 1641 - `return await this.makeRequest<CodegenXrpcResponse>('social.slices.slice.codegen', 'POST', request);`, 1642 + `return await this.makeRequest<CodegenXrpcResponse>('network.slices.slice.codegen', 'POST', request);`, 1642 1643 ], 1643 1644 }); 1644 1645 ··· 1648 1649 returnType: "Promise<SliceStatsOutput>", 1649 1650 isAsync: true, 1650 1651 statements: [ 1651 - `return await this.makeRequest<SliceStatsOutput>('social.slices.slice.stats', 'POST', params);`, 1652 + `return await this.makeRequest<SliceStatsOutput>('network.slices.slice.stats', 'POST', params);`, 1652 1653 ], 1653 1654 }); 1654 1655 ··· 1656 1657 name: "getSliceRecords", 1657 1658 typeParameters: [{ name: "T", default: "Record<string, unknown>" }], 1658 1659 parameters: [ 1659 - { name: "params", type: "Omit<SliceLevelRecordsParams<T>, 'slice'>" }, 1660 + { 1661 + name: "params", 1662 + type: "Omit<SliceLevelRecordsParams<T>, 'slice'>", 1663 + }, 1660 1664 ], 1661 1665 returnType: "Promise<SliceRecordsOutput<T>>", 1662 1666 isAsync: true, ··· 1672 1676 ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1673 1677 ` slice: this.sliceUri`, 1674 1678 `};`, 1675 - `return await this.makeRequest<SliceRecordsOutput<T>>('social.slices.slice.getSliceRecords', 'POST', requestParams);`, 1679 + `return await this.makeRequest<SliceRecordsOutput<T>>('network.slices.slice.getSliceRecords', 'POST', requestParams);`, 1676 1680 ], 1677 1681 }); 1678 1682 ··· 1685 1689 isAsync: true, 1686 1690 statements: [ 1687 1691 `const requestParams = { ...params, slice: this.sliceUri };`, 1688 - `return await this.makeRequest<GetActorsResponse>('social.slices.slice.getActors', 'POST', requestParams);`, 1692 + `return await this.makeRequest<GetActorsResponse>('network.slices.slice.getActors', 'POST', requestParams);`, 1689 1693 ], 1690 1694 }); 1691 1695 } 1692 1696 1693 - // Add sync methods to the social.slices.slice class 1697 + // Add sync methods to the network.slices.slice class 1694 1698 if ( 1695 1699 currentPath.length === 3 && 1696 - currentPath[0] === "social" && 1700 + currentPath[0] === "network" && 1697 1701 currentPath[1] === "slices" && 1698 1702 currentPath[2] === "slice" 1699 1703 ) { ··· 1704 1708 isAsync: true, 1705 1709 statements: [ 1706 1710 `const requestParams = { ...params, slice: this.sliceUri };`, 1707 - `return await this.makeRequest<SyncJobResponse>('social.slices.slice.startSync', 'POST', requestParams);`, 1711 + `return await this.makeRequest<SyncJobResponse>('network.slices.slice.startSync', 'POST', requestParams);`, 1708 1712 ], 1709 1713 }); 1710 1714 ··· 1714 1718 returnType: "Promise<JobStatus>", 1715 1719 isAsync: true, 1716 1720 statements: [ 1717 - `return await this.makeRequest<JobStatus>('social.slices.slice.getJobStatus', 'GET', params);`, 1721 + `return await this.makeRequest<JobStatus>('network.slices.slice.getJobStatus', 'GET', params);`, 1718 1722 ], 1719 1723 }); 1720 1724 ··· 1724 1728 returnType: "Promise<GetJobHistoryResponse>", 1725 1729 isAsync: true, 1726 1730 statements: [ 1727 - `return await this.makeRequest<GetJobHistoryResponse>('social.slices.slice.getJobHistory', 'GET', params);`, 1731 + `return await this.makeRequest<GetJobHistoryResponse>('network.slices.slice.getJobHistory', 'GET', params);`, 1728 1732 ], 1729 1733 }); 1730 1734 ··· 1734 1738 returnType: "Promise<GetJobLogsResponse>", 1735 1739 isAsync: true, 1736 1740 statements: [ 1737 - `return await this.makeRequest<GetJobLogsResponse>('social.slices.slice.getJobLogs', 'GET', params);`, 1741 + `return await this.makeRequest<GetJobLogsResponse>('network.slices.slice.getJobLogs', 'GET', params);`, 1738 1742 ], 1739 1743 }); 1740 1744 ··· 1743 1747 returnType: "Promise<JetstreamStatusResponse>", 1744 1748 isAsync: true, 1745 1749 statements: [ 1746 - `return await this.makeRequest<JetstreamStatusResponse>('social.slices.slice.getJetstreamStatus', 'GET');`, 1750 + `return await this.makeRequest<JetstreamStatusResponse>('network.slices.slice.getJetstreamStatus', 'GET');`, 1747 1751 ], 1748 1752 }); 1749 1753 ··· 1753 1757 returnType: "Promise<GetJetstreamLogsResponse>", 1754 1758 isAsync: true, 1755 1759 statements: [ 1756 - `return await this.makeRequest<GetJetstreamLogsResponse>('social.slices.slice.getJetstreamLogs', 'GET', params);`, 1760 + `return await this.makeRequest<GetJetstreamLogsResponse>('network.slices.slice.getJetstreamLogs', 'GET', params);`, 1757 1761 ], 1758 1762 }); 1759 1763 ··· 1770 1774 isAsync: true, 1771 1775 statements: [ 1772 1776 `const requestParams = { slice: this.sliceUri, ...params };`, 1773 - `return await this.makeRequest<SyncUserCollectionsResult>('social.slices.slice.syncUserCollections', 'POST', requestParams);`, 1777 + `return await this.makeRequest<SyncUserCollectionsResult>('network.slices.slice.syncUserCollections', 'POST', requestParams);`, 1774 1778 ], 1775 1779 }); 1776 1780 ··· 1782 1786 isAsync: true, 1783 1787 statements: [ 1784 1788 `const requestParams = { ...params, sliceUri: this.sliceUri };`, 1785 - `return await this.makeRequest<OAuthClientDetails>('social.slices.slice.createOAuthClient', 'POST', requestParams);`, 1789 + `return await this.makeRequest<OAuthClientDetails>('network.slices.slice.createOAuthClient', 'POST', requestParams);`, 1786 1790 ], 1787 1791 }); 1788 1792 ··· 1792 1796 isAsync: true, 1793 1797 statements: [ 1794 1798 `const requestParams = { slice: this.sliceUri };`, 1795 - `return await this.makeRequest<ListOAuthClientsResponse>('social.slices.slice.getOAuthClients', 'GET', requestParams);`, 1799 + `return await this.makeRequest<ListOAuthClientsResponse>('network.slices.slice.getOAuthClients', 'GET', requestParams);`, 1796 1800 ], 1797 1801 }); 1798 1802 ··· 1803 1807 isAsync: true, 1804 1808 statements: [ 1805 1809 `const requestParams = { ...params, sliceUri: this.sliceUri };`, 1806 - `return await this.makeRequest<OAuthClientDetails>('social.slices.slice.updateOAuthClient', 'POST', requestParams);`, 1810 + `return await this.makeRequest<OAuthClientDetails>('network.slices.slice.updateOAuthClient', 'POST', requestParams);`, 1807 1811 ], 1808 1812 }); 1809 1813 ··· 1813 1817 returnType: "Promise<DeleteOAuthClientResponse>", 1814 1818 isAsync: true, 1815 1819 statements: [ 1816 - `return await this.makeRequest<DeleteOAuthClientResponse>('social.slices.slice.deleteOAuthClient', 'POST', { clientId });`, 1820 + `return await this.makeRequest<DeleteOAuthClientResponse>('network.slices.slice.deleteOAuthClient', 'POST', { clientId });`, 1817 1821 ], 1818 1822 }); 1819 1823 } ··· 1827 1831 isAsync: true, 1828 1832 statements: [ 1829 1833 `const requestParams = { ...params, slice: this.sliceUri };`, 1830 - `return await this.makeRequest<GetActorsResponse>('social.slices.slice.getActors', 'POST', requestParams);`, 1834 + `return await this.makeRequest<GetActorsResponse>('network.slices.slice.getActors', 'POST', requestParams);`, 1831 1835 ], 1832 1836 }); 1833 1837 ··· 1835 1839 name: "getSliceRecords", 1836 1840 typeParameters: [{ name: "T", default: "Record<string, unknown>" }], 1837 1841 parameters: [ 1838 - { name: "params", type: "Omit<SliceLevelRecordsParams<T>, 'slice'>" }, 1842 + { 1843 + name: "params", 1844 + type: "Omit<SliceLevelRecordsParams<T>, 'slice'>", 1845 + }, 1839 1846 ], 1840 1847 returnType: "Promise<SliceRecordsOutput<T>>", 1841 1848 isAsync: true, ··· 1851 1858 ` orWhere: undefined, // Remove orWhere as it's now in where.$or`, 1852 1859 ` slice: this.sliceUri`, 1853 1860 `};`, 1854 - `return await this.makeRequest<SliceRecordsOutput<T>>('social.slices.slice.getSliceRecords', 'POST', requestParams);`, 1861 + `return await this.makeRequest<SliceRecordsOutput<T>>('network.slices.slice.getSliceRecords', 'POST', requestParams);`, 1855 1862 ], 1856 1863 }); 1857 1864
+5 -5
api/scripts/prod_sync.sh
··· 10 10 echo "🔄 Testing Production Sync Endpoint..." 11 11 12 12 echo "🎯 Syncing slice collections with specific repos" 13 - curl -s -X POST https://slices-api.fly.dev/xrpc/social.slices.slice.startSync \ 13 + curl -s -X POST https://slices-api.fly.dev/xrpc/network.slices.slice.startSync \ 14 14 -H "Content-Type: application/json" \ 15 15 -H "Authorization: Bearer $ACCESS_TOKEN" \ 16 16 -d '{ 17 - "slice": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lwzmbjpqxk2q", 17 + "slice": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z", 18 18 "collections": [ 19 - "social.slices.slice", 20 - "social.slices.lexicon", 21 - "social.slices.actor.profile" 19 + "network.slices.slice", 20 + "network.slices.lexicon", 21 + "network.slices.actor.profile" 22 22 ], 23 23 "externalCollections": [ 24 24 "app.bsky.actor.profile"
+5 -5
api/scripts/test_sync.sh
··· 10 10 echo "🔄 Testing Sync Endpoint..." 11 11 12 12 echo "🎯 Syncing specific collections with specific repos" 13 - curl -s -X POST http://localhost:3000/xrpc/social.slices.slice.startSync \ 13 + curl -s -X POST http://localhost:3000/xrpc/network.slices.slice.startSync \ 14 14 -H "Content-Type: application/json" \ 15 15 -H "Authorization: Bearer $TOKEN" \ 16 16 -d '{ 17 - "slice": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lwzmbjpqxk2q", 17 + "slice": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z", 18 18 "collections": [ 19 - "social.slices.actor.profile", 20 - "social.slices.slice", 21 - "social.slices.lexicon" 19 + "network.slices.actor.profile", 20 + "network.slices.slice", 21 + "network.slices.lexicon" 22 22 ], 23 23 "externalCollections": [ 24 24 "app.bsky.actor.profile"
+206 -127
api/src/database.rs
··· 1 - use sqlx::PgPool; 2 1 use base64::{Engine as _, engine::general_purpose}; 2 + use sqlx::PgPool; 3 3 4 4 use crate::errors::DatabaseError; 5 - use crate::models::{Actor, CollectionStats, IndexedRecord, Record, WhereCondition, WhereClause, SortField, OAuthClient}; 5 + use crate::models::{ 6 + Actor, CollectionStats, IndexedRecord, OAuthClient, Record, SortField, WhereClause, 7 + WhereCondition, 8 + }; 6 9 use std::collections::HashMap; 7 - 8 10 9 11 // Helper function to build ORDER BY clause from sortBy array 10 12 fn build_order_by_clause(sort_by: Option<&Vec<SortField>>) -> String { ··· 19 21 }; 20 22 21 23 // Validate field name to prevent SQL injection 22 - if field.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') { 23 - if field == "indexed_at" || field == "uri" || field == "cid" || field == "did" || field == "collection" { 24 + if field 25 + .chars() 26 + .all(|c| c.is_alphanumeric() || c == '_' || c == '.') 27 + { 28 + if field == "indexed_at" 29 + || field == "uri" 30 + || field == "cid" 31 + || field == "did" 32 + || field == "collection" 33 + { 24 34 order_clauses.push(format!("{field} {direction}")); 25 35 } else { 26 36 // For JSON fields, handle nested paths and NULLs properly ··· 43 53 } 44 54 if !order_clauses.is_empty() { 45 55 // Always add indexed_at as tie-breaker if not already included 46 - let has_indexed_at = order_clauses.iter().any(|clause| clause.contains("indexed_at")); 56 + let has_indexed_at = order_clauses 57 + .iter() 58 + .any(|clause| clause.contains("indexed_at")); 47 59 if !has_indexed_at { 48 60 order_clauses.push("indexed_at DESC".to_string()); 49 61 } ··· 56 68 } 57 69 } 58 70 59 - 60 - fn generate_cursor(sort_value: &str, indexed_at: chrono::DateTime<chrono::Utc>, cid: &str) -> String { 71 + fn generate_cursor( 72 + sort_value: &str, 73 + indexed_at: chrono::DateTime<chrono::Utc>, 74 + cid: &str, 75 + ) -> String { 61 76 let cursor_content = format!("{}::{}::{}", sort_value, indexed_at.to_rfc3339(), cid); 62 77 general_purpose::URL_SAFE_NO_PAD.encode(cursor_content) 63 78 } ··· 65 80 // Extract the primary sort field from sortBy array for cursor generation 66 81 fn get_primary_sort_field(sort_by: Option<&Vec<SortField>>) -> String { 67 82 match sort_by { 68 - Some(sort_fields) if !sort_fields.is_empty() => { 69 - sort_fields[0].field.clone() 70 - } 83 + Some(sort_fields) if !sort_fields.is_empty() => sort_fields[0].field.clone(), 71 84 _ => "indexed_at".to_string(), 72 85 } 73 86 } ··· 81 94 "indexed_at" => record.indexed_at.to_rfc3339(), 82 95 field => { 83 96 // Extract field value from JSON 84 - record.json.get(field) 97 + record 98 + .json 99 + .get(field) 85 100 .and_then(|v| match v { 86 101 serde_json::Value::String(s) if !s.is_empty() => Some(s.clone()), 87 102 serde_json::Value::Number(n) => Some(n.to_string()), ··· 103 118 ) -> (Vec<String>, Vec<String>) { 104 119 let mut where_clauses = Vec::new(); 105 120 let mut or_clauses = Vec::new(); 106 - 121 + 107 122 if let Some(clause) = where_clause { 108 123 // Process regular AND conditions 109 124 for (field, condition) in &clause.conditions { 110 125 let field_clause = build_single_condition(field, condition, param_count); 111 126 where_clauses.push(field_clause); 112 127 } 113 - 128 + 114 129 // Process OR conditions 115 130 if let Some(or_conditions) = &clause.or_conditions { 116 131 for (field, condition) in or_conditions { ··· 119 134 } 120 135 } 121 136 } 122 - 137 + 123 138 (where_clauses, or_clauses) 124 139 } 125 140 126 141 // Helper function to bind parameters from WhereClause 127 142 fn bind_where_parameters<'q>( 128 - mut query_builder: sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments>, 129 - where_clause: Option<&'q WhereClause> 143 + mut query_builder: sqlx::query::QueryAs< 144 + 'q, 145 + sqlx::Postgres, 146 + Record, 147 + sqlx::postgres::PgArguments, 148 + >, 149 + where_clause: Option<&'q WhereClause>, 130 150 ) -> sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments> { 131 151 if let Some(clause) = where_clause { 132 152 // Bind AND condition parameters 133 153 for (_, condition) in &clause.conditions { 134 154 query_builder = bind_single_condition(query_builder, condition); 135 155 } 136 - 137 - // Bind OR condition parameters 156 + 157 + // Bind OR condition parameters 138 158 if let Some(or_conditions) = &clause.or_conditions { 139 159 for (_, condition) in or_conditions { 140 160 query_builder = bind_single_condition(query_builder, condition); ··· 146 166 147 167 // Helper function to bind parameters for a single condition 148 168 fn bind_single_condition<'q>( 149 - mut query_builder: sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments>, 150 - condition: &'q WhereCondition 169 + mut query_builder: sqlx::query::QueryAs< 170 + 'q, 171 + sqlx::Postgres, 172 + Record, 173 + sqlx::postgres::PgArguments, 174 + >, 175 + condition: &'q WhereCondition, 151 176 ) -> sqlx::query::QueryAs<'q, sqlx::Postgres, Record, sqlx::postgres::PgArguments> { 152 177 if let Some(eq_value) = &condition.eq { 153 178 if let Some(str_val) = eq_value.as_str() { ··· 156 181 query_builder = query_builder.bind(eq_value); 157 182 } 158 183 } 159 - 184 + 160 185 if let Some(in_values) = &condition.in_values { 161 - let str_values: Vec<String> = in_values.iter() 186 + let str_values: Vec<String> = in_values 187 + .iter() 162 188 .filter_map(|v| v.as_str().map(|s| s.to_string())) 163 189 .collect(); 164 190 query_builder = query_builder.bind(str_values); 165 191 } 166 - 192 + 167 193 if let Some(contains_value) = &condition.contains { 168 194 query_builder = query_builder.bind(contains_value); 169 195 } 170 - 196 + 171 197 query_builder 172 198 } 173 199 174 200 // Helper function to build a single condition clause 175 - fn build_single_condition(field: &str, condition: &WhereCondition, param_count: &mut usize) -> String { 201 + fn build_single_condition( 202 + field: &str, 203 + condition: &WhereCondition, 204 + param_count: &mut usize, 205 + ) -> String { 176 206 if let Some(_eq_value) = &condition.eq { 177 207 let clause = match field { 178 208 "did" | "collection" | "uri" | "cid" => { ··· 309 339 310 340 // Build bulk INSERT with multiple VALUES 311 341 let mut query = String::from( 312 - r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexed_at", "slice_uri") VALUES "# 342 + r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexed_at", "slice_uri") VALUES "#, 313 343 ); 314 344 315 345 // Add placeholders for each record ··· 320 350 let base = i * 7 + 1; // 7 fields per record 321 351 query.push_str(&format!( 322 352 "(${}, ${}, ${}, ${}, ${}, ${}, ${})", 323 - base, base + 1, base + 2, base + 3, base + 4, base + 5, base + 6 353 + base, 354 + base + 1, 355 + base + 2, 356 + base + 3, 357 + base + 4, 358 + base + 5, 359 + base + 6 324 360 )); 325 361 } 326 362 327 - query.push_str(r#" 363 + query.push_str( 364 + r#" 328 365 ON CONFLICT ON CONSTRAINT record_pkey 329 366 DO UPDATE SET 330 367 "cid" = EXCLUDED."cid", 331 368 "json" = EXCLUDED."json", 332 369 "indexed_at" = EXCLUDED."indexed_at" 333 - "#); 370 + "#, 371 + ); 334 372 335 373 // Bind all parameters 336 374 let mut sqlx_query = sqlx::query(&query); ··· 351 389 Ok(()) 352 390 } 353 391 354 - pub async fn get_existing_record_cids_for_slice(&self, did: &str, collection: &str, slice_uri: &str) -> Result<std::collections::HashMap<String, String>, DatabaseError> { 392 + pub async fn get_existing_record_cids_for_slice( 393 + &self, 394 + did: &str, 395 + collection: &str, 396 + slice_uri: &str, 397 + ) -> Result<std::collections::HashMap<String, String>, DatabaseError> { 355 398 let records = sqlx::query!( 356 399 r#"SELECT "uri", "cid" 357 400 FROM "record" ··· 392 435 Ok(indexed_record) 393 436 } 394 437 395 - 396 - pub async fn get_lexicons_by_slice(&self, slice_uri: &str) -> Result<Vec<serde_json::Value>, DatabaseError> { 438 + pub async fn get_lexicons_by_slice( 439 + &self, 440 + slice_uri: &str, 441 + ) -> Result<Vec<serde_json::Value>, DatabaseError> { 397 442 let records = sqlx::query_as::<_, Record>( 398 443 r#"SELECT "uri", "cid", "did", "collection", "json", "indexed_at", "slice_uri" 399 444 FROM "record" 400 - WHERE "collection" = 'social.slices.lexicon' 445 + WHERE "collection" = 'network.slices.lexicon' 401 446 AND "json"->>'slice' = $1 402 447 ORDER BY "indexed_at" DESC"#, 403 448 ) ··· 421 466 422 467 Ok(lexicon_definitions) 423 468 } 424 - 425 - 426 - 427 469 428 470 pub async fn update_record(&self, record: &Record) -> Result<(), DatabaseError> { 429 471 let result = sqlx::query!( ··· 440 482 .await?; 441 483 442 484 if result.rows_affected() == 0 { 443 - return Err(DatabaseError::RecordNotFound { uri: record.uri.clone() }); 485 + return Err(DatabaseError::RecordNotFound { 486 + uri: record.uri.clone(), 487 + }); 444 488 } 445 489 446 490 Ok(()) 447 491 } 448 - 449 492 450 493 pub async fn batch_insert_actors(&self, actors: &[Actor]) -> Result<(), DatabaseError> { 451 494 if actors.is_empty() { ··· 456 499 457 500 // Process actors in chunks to avoid hitting parameter limits 458 501 const CHUNK_SIZE: usize = 1000; 459 - 502 + 460 503 for chunk in actors.chunks(CHUNK_SIZE) { 461 504 for actor in chunk { 462 505 sqlx::query!( ··· 480 523 Ok(()) 481 524 } 482 525 483 - pub async fn get_slice_collection_stats(&self, slice_uri: &str) -> Result<Vec<CollectionStats>, DatabaseError> { 526 + pub async fn get_slice_collection_stats( 527 + &self, 528 + slice_uri: &str, 529 + ) -> Result<Vec<CollectionStats>, DatabaseError> { 484 530 let stats = sqlx::query!( 485 531 r#" 486 532 WITH slice_collections AS ( 487 533 SELECT DISTINCT 488 534 json->>'nsid' as collection_nsid 489 535 FROM record 490 - WHERE collection = 'social.slices.lexicon' 536 + WHERE collection = 'network.slices.lexicon' 491 537 AND json->>'slice' = $1 492 538 AND json->>'nsid' IS NOT NULL 493 539 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' ··· 507 553 .fetch_all(&self.pool) 508 554 .await?; 509 555 510 - Ok(stats.into_iter().map(|row| CollectionStats { 511 - collection: row.collection, 512 - record_count: row.record_count.unwrap_or(0), 513 - unique_actors: row.unique_actors.unwrap_or(0), 514 - }).collect()) 556 + Ok(stats 557 + .into_iter() 558 + .map(|row| CollectionStats { 559 + collection: row.collection, 560 + record_count: row.record_count.unwrap_or(0), 561 + unique_actors: row.unique_actors.unwrap_or(0), 562 + }) 563 + .collect()) 515 564 } 516 565 517 - pub async fn get_slice_collections_list(&self, slice_uri: &str) -> Result<Vec<String>, DatabaseError> { 566 + pub async fn get_slice_collections_list( 567 + &self, 568 + slice_uri: &str, 569 + ) -> Result<Vec<String>, DatabaseError> { 518 570 let rows = sqlx::query!( 519 571 r#" 520 572 SELECT DISTINCT json->>'nsid' as collection_nsid 521 573 FROM record 522 - WHERE collection = 'social.slices.lexicon' 574 + WHERE collection = 'network.slices.lexicon' 523 575 AND json->>'slice' = $1 524 576 AND json->>'nsid' IS NOT NULL 525 577 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' ··· 530 582 .fetch_all(&self.pool) 531 583 .await?; 532 584 533 - Ok(rows.into_iter() 585 + Ok(rows 586 + .into_iter() 534 587 .filter_map(|row| row.collection_nsid) 535 588 .collect()) 536 589 } ··· 542 595 SELECT DISTINCT 543 596 json->>'nsid' as collection_nsid 544 597 FROM record 545 - WHERE collection = 'social.slices.lexicon' 598 + WHERE collection = 'network.slices.lexicon' 546 599 AND json->>'slice' = $1 547 600 AND json->>'nsid' IS NOT NULL 548 601 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' ··· 580 633 slice_uri: &str, 581 634 limit: Option<i32>, 582 635 cursor: Option<&str>, 583 - where_conditions: Option<&HashMap<String, WhereCondition>> 636 + where_conditions: Option<&HashMap<String, WhereCondition>>, 584 637 ) -> Result<(Vec<Actor>, Option<String>), DatabaseError> { 585 638 let limit = limit.unwrap_or(50).min(100); // Cap at 100 586 - 639 + 587 640 // Handle where conditions with specific cases 588 641 let records = if let Some(conditions) = where_conditions { 589 642 // Check for handle contains filter ··· 662 715 } 663 716 } else { 664 717 // Default case with basic filtering 665 - self.query_actors_with_cursor(slice_uri, cursor, limit).await? 718 + self.query_actors_with_cursor(slice_uri, cursor, limit) 719 + .await? 666 720 } 667 721 } else if let Some(did_condition) = conditions.get("did") { 668 722 if let Some(in_values) = &did_condition.in_values { 669 - let string_values: Vec<String> = in_values.iter() 723 + let string_values: Vec<String> = in_values 724 + .iter() 670 725 .filter_map(|v| v.as_str()) 671 726 .map(|s| s.to_string()) 672 727 .collect(); 673 - 728 + 674 729 sqlx::query_as!( 675 730 Actor, 676 731 r#" ··· 724 779 } 725 780 } else { 726 781 // Default case with basic filtering 727 - self.query_actors_with_cursor(slice_uri, cursor, limit).await? 782 + self.query_actors_with_cursor(slice_uri, cursor, limit) 783 + .await? 728 784 } 729 785 } else { 730 786 // Default case with basic filtering 731 - self.query_actors_with_cursor(slice_uri, cursor, limit).await? 787 + self.query_actors_with_cursor(slice_uri, cursor, limit) 788 + .await? 732 789 } 733 790 } else { 734 791 // No where conditions, just basic slice + cursor filtering 735 - self.query_actors_with_cursor(slice_uri, cursor, limit).await? 792 + self.query_actors_with_cursor(slice_uri, cursor, limit) 793 + .await? 736 794 }; 737 - 795 + 738 796 // Generate cursor from the last record if there are any records 739 797 let cursor = if records.is_empty() { 740 798 None ··· 744 802 745 803 Ok((records, cursor)) 746 804 } 747 - 748 - async fn query_actors_with_cursor(&self, slice_uri: &str, cursor: Option<&str>, limit: i32) -> Result<Vec<Actor>, DatabaseError> { 805 + 806 + async fn query_actors_with_cursor( 807 + &self, 808 + slice_uri: &str, 809 + cursor: Option<&str>, 810 + limit: i32, 811 + ) -> Result<Vec<Actor>, DatabaseError> { 749 812 match cursor { 750 - Some(cursor_did) => { 751 - sqlx::query_as!( 752 - Actor, 753 - r#" 813 + Some(cursor_did) => sqlx::query_as!( 814 + Actor, 815 + r#" 754 816 SELECT did, handle, slice_uri, indexed_at 755 817 FROM actor 756 818 WHERE slice_uri = $1 AND did > $2 757 819 ORDER BY did ASC 758 820 LIMIT $3 759 821 "#, 760 - slice_uri, 761 - cursor_did, 762 - limit as i64 763 - ) 764 - .fetch_all(&self.pool) 765 - .await 766 - .map_err(DatabaseError::from) 767 - }, 768 - None => { 769 - sqlx::query_as!( 770 - Actor, 771 - r#" 822 + slice_uri, 823 + cursor_did, 824 + limit as i64 825 + ) 826 + .fetch_all(&self.pool) 827 + .await 828 + .map_err(DatabaseError::from), 829 + None => sqlx::query_as!( 830 + Actor, 831 + r#" 772 832 SELECT did, handle, slice_uri, indexed_at 773 833 FROM actor 774 834 WHERE slice_uri = $1 775 835 ORDER BY did ASC 776 836 LIMIT $2 777 837 "#, 778 - slice_uri, 779 - limit as i64 780 - ) 781 - .fetch_all(&self.pool) 782 - .await 783 - .map_err(DatabaseError::from) 784 - } 838 + slice_uri, 839 + limit as i64 840 + ) 841 + .fetch_all(&self.pool) 842 + .await 843 + .map_err(DatabaseError::from), 785 844 } 786 845 } 787 846 ··· 790 849 r#" 791 850 SELECT COUNT(*) as count 792 851 FROM record 793 - WHERE collection = 'social.slices.lexicon' 852 + WHERE collection = 'network.slices.lexicon' 794 853 AND json->>'slice' = $1 795 854 AND (json->>'definitions')::jsonb->'main'->>'type' = 'record' 796 855 "#, ··· 802 861 Ok(count.count.unwrap_or(0)) 803 862 } 804 863 805 - 806 864 pub async fn get_slice_collections_records( 807 865 &self, 808 866 slice_uri: &str, 809 867 limit: Option<i32>, 810 868 cursor: Option<&str>, 811 869 sort_by: Option<&Vec<SortField>>, 812 - where_clause: Option<&WhereClause> 870 + where_clause: Option<&WhereClause>, 813 871 ) -> Result<(Vec<Record>, Option<String>), DatabaseError> { 814 872 let limit = limit.unwrap_or(50).min(100); // Cap at 100 815 873 let order_by = build_order_by_clause(sort_by); ··· 818 876 let mut where_clauses = Vec::new(); 819 877 let mut param_count = 1; 820 878 821 - // Always filter by slice_uri, except for social.slices.lexicon which uses json->>'slice' 822 - let is_lexicon = where_clause.as_ref() 879 + // Always filter by slice_uri, except for network.slices.lexicon which uses json->>'slice' 880 + let is_lexicon = where_clause 881 + .as_ref() 823 882 .and_then(|wc| wc.conditions.get("collection")) 824 883 .and_then(|c| c.eq.as_ref()) 825 884 .and_then(|v| v.as_str()) 826 - .map_or(false, |s| s == "social.slices.lexicon"); 827 - 828 - if is_lexicon 829 - { 885 + .map_or(false, |s| s == "network.slices.lexicon"); 886 + 887 + if is_lexicon { 830 888 where_clauses.push(format!("json->>'slice' = ${}", param_count)); 831 889 } else { 832 890 where_clauses.push(format!("slice_uri = ${}", param_count)); ··· 840 898 } 841 899 842 900 // Use helper function to build where conditions 843 - let (and_conditions, or_conditions) = build_where_conditions(where_clause, &mut param_count); 901 + let (and_conditions, or_conditions) = 902 + build_where_conditions(where_clause, &mut param_count); 844 903 where_clauses.extend(and_conditions); 845 - 904 + 846 905 // Add OR conditions with proper parentheses if present 847 906 if !or_conditions.is_empty() { 848 907 let or_clause = format!("({})", or_conditions.join(" OR ")); 849 908 where_clauses.push(or_clause); 850 909 } 851 - 852 910 853 911 // Build the final query 854 912 let where_sql = where_clauses.join(" AND "); ··· 869 927 870 928 // Bind cursor if present 871 929 if let Some(cursor_time) = cursor { 872 - let cursor_dt = cursor_time.parse::<chrono::DateTime<chrono::Utc>>() 930 + let cursor_dt = cursor_time 931 + .parse::<chrono::DateTime<chrono::Utc>>() 873 932 .unwrap_or_else(|_| chrono::Utc::now()); 874 933 query_builder = query_builder.bind(cursor_dt); 875 934 } ··· 887 946 let cursor = if records.is_empty() { 888 947 None 889 948 } else { 890 - records.last().map(|record| generate_cursor_from_record(record, sort_by)) 949 + records 950 + .last() 951 + .map(|record| generate_cursor_from_record(record, sort_by)) 891 952 }; 892 953 893 954 Ok((records, cursor)) ··· 896 957 pub async fn count_slice_collections_records( 897 958 &self, 898 959 slice_uri: &str, 899 - where_clause: Option<&WhereClause> 960 + where_clause: Option<&WhereClause>, 900 961 ) -> Result<i64, DatabaseError> { 901 962 // Build WHERE clause dynamically 902 963 let mut where_clauses = Vec::new(); 903 964 let mut param_count = 1; 904 965 905 - // Always filter by slice_uri, except for social.slices.lexicon which uses json->>'slice' 906 - let is_lexicon = where_clause.as_ref() 966 + // Always filter by slice_uri, except for network.slices.lexicon which uses json->>'slice' 967 + let is_lexicon = where_clause 968 + .as_ref() 907 969 .and_then(|wc| wc.conditions.get("collection")) 908 970 .and_then(|c| c.eq.as_ref()) 909 971 .and_then(|v| v.as_str()) 910 - .map_or(false, |s| s == "social.slices.lexicon"); 911 - 912 - if is_lexicon 913 - { 972 + .map_or(false, |s| s == "network.slices.lexicon"); 973 + 974 + if is_lexicon { 914 975 where_clauses.push(format!("json->>'slice' = ${}", param_count)); 915 976 } else { 916 977 where_clauses.push(format!("slice_uri = ${}", param_count)); ··· 918 979 param_count += 1; 919 980 920 981 // Use helper function to build where conditions 921 - let (and_conditions, or_conditions) = build_where_conditions(where_clause, &mut param_count); 982 + let (and_conditions, or_conditions) = 983 + build_where_conditions(where_clause, &mut param_count); 922 984 where_clauses.extend(and_conditions); 923 - 985 + 924 986 // Add OR conditions with proper parentheses if present 925 987 if !or_conditions.is_empty() { 926 988 let or_clause = format!("({})", or_conditions.join(" OR ")); 927 989 where_clauses.push(or_clause); 928 990 } 929 - 930 991 931 992 // Build the final query 932 993 let where_sql = if where_clauses.is_empty() { ··· 937 998 938 999 let query = format!("SELECT COUNT(*) as count FROM record{}", where_sql); 939 1000 940 - // Execute query with parameters 1001 + // Execute query with parameters 941 1002 let mut query_builder = sqlx::query_scalar::<_, i64>(&query); 942 1003 query_builder = query_builder.bind(slice_uri); 943 1004 ··· 953 1014 } 954 1015 } 955 1016 if let Some(in_values) = &condition.in_values { 956 - let str_values: Vec<String> = in_values.iter() 1017 + let str_values: Vec<String> = in_values 1018 + .iter() 957 1019 .filter_map(|v| v.as_str().map(|s| s.to_string())) 958 1020 .collect(); 959 1021 query_builder = query_builder.bind(str_values); ··· 962 1024 query_builder = query_builder.bind(contains_value); 963 1025 } 964 1026 } 965 - 1027 + 966 1028 // Bind OR condition parameters 967 1029 if let Some(or_conditions) = &clause.or_conditions { 968 1030 for (_, condition) in or_conditions { ··· 974 1036 } 975 1037 } 976 1038 if let Some(in_values) = &condition.in_values { 977 - let str_values: Vec<String> = in_values.iter() 1039 + let str_values: Vec<String> = in_values 1040 + .iter() 978 1041 .filter_map(|v| v.as_str().map(|s| s.to_string())) 979 1042 .collect(); 980 1043 query_builder = query_builder.bind(str_values); ··· 990 1053 Ok(count) 991 1054 } 992 1055 993 - pub async fn delete_record_by_uri(&self, uri: &str, slice_uri: Option<&str>) -> Result<u64, DatabaseError> { 1056 + pub async fn delete_record_by_uri( 1057 + &self, 1058 + uri: &str, 1059 + slice_uri: Option<&str>, 1060 + ) -> Result<u64, DatabaseError> { 994 1061 let result = if let Some(slice_uri) = slice_uri { 995 1062 sqlx::query("DELETE FROM record WHERE uri = $1 AND slice_uri = $2") 996 1063 .bind(uri) ··· 1007 1074 Ok(result.rows_affected()) 1008 1075 } 1009 1076 1010 - 1011 1077 pub async fn upsert_record(&self, record: &Record) -> Result<bool, DatabaseError> { 1012 1078 // Returns true if inserted, false if updated 1013 - let result = sqlx::query_scalar::<_, bool>(r#" 1079 + let result = sqlx::query_scalar::<_, bool>( 1080 + r#" 1014 1081 INSERT INTO record (uri, cid, did, collection, json, indexed_at, slice_uri) 1015 1082 VALUES ($1, $2, $3, $4, $5, $6, $7) 1016 1083 ON CONFLICT ON CONSTRAINT record_pkey DO UPDATE ··· 1018 1085 json = EXCLUDED.json, 1019 1086 indexed_at = EXCLUDED.indexed_at 1020 1087 RETURNING (xmax = 0) 1021 - "#) 1088 + "#, 1089 + ) 1022 1090 .bind(&record.uri) 1023 1091 .bind(&record.cid) 1024 1092 .bind(&record.did) ··· 1032 1100 } 1033 1101 1034 1102 pub async fn get_all_slices(&self) -> Result<Vec<String>, DatabaseError> { 1035 - let rows: Vec<(String,)> = sqlx::query_as(r#" 1103 + let rows: Vec<(String,)> = sqlx::query_as( 1104 + r#" 1036 1105 SELECT DISTINCT json->>'slice' as slice_uri 1037 1106 FROM record 1038 - WHERE collection = 'social.slices.lexicon' 1107 + WHERE collection = 'network.slices.lexicon' 1039 1108 AND json->>'slice' IS NOT NULL 1040 - "#) 1109 + "#, 1110 + ) 1041 1111 .fetch_all(&self.pool) 1042 1112 .await?; 1043 1113 1044 1114 Ok(rows.into_iter().map(|(uri,)| uri).collect()) 1045 1115 } 1046 - 1047 1116 1048 1117 pub async fn get_all_actors(&self) -> Result<Vec<(String, String)>, DatabaseError> { 1049 1118 let rows = sqlx::query!( ··· 1055 1124 .fetch_all(&self.pool) 1056 1125 .await?; 1057 1126 1058 - Ok(rows.into_iter().map(|row| (row.did, row.slice_uri)).collect()) 1127 + Ok(rows 1128 + .into_iter() 1129 + .map(|row| (row.did, row.slice_uri)) 1130 + .collect()) 1059 1131 } 1060 1132 1061 1133 pub async fn get_slice_domain(&self, slice_uri: &str) -> Result<Option<String>, DatabaseError> { ··· 1063 1135 r#" 1064 1136 SELECT json->>'domain' as domain 1065 1137 FROM record 1066 - WHERE collection = 'social.slices.slice' 1138 + WHERE collection = 'network.slices.slice' 1067 1139 AND uri = $1 1068 1140 "#, 1069 1141 slice_uri ··· 1099 1171 Ok(client) 1100 1172 } 1101 1173 1102 - pub async fn get_oauth_clients_for_slice(&self, slice_uri: &str) -> Result<Vec<OAuthClient>, DatabaseError> { 1174 + pub async fn get_oauth_clients_for_slice( 1175 + &self, 1176 + slice_uri: &str, 1177 + ) -> Result<Vec<OAuthClient>, DatabaseError> { 1103 1178 let clients = sqlx::query_as!( 1104 1179 OAuthClient, 1105 1180 r#" ··· 1116 1191 Ok(clients) 1117 1192 } 1118 1193 1119 - pub async fn get_oauth_client_by_id(&self, client_id: &str) -> Result<Option<OAuthClient>, DatabaseError> { 1194 + pub async fn get_oauth_client_by_id( 1195 + &self, 1196 + client_id: &str, 1197 + ) -> Result<Option<OAuthClient>, DatabaseError> { 1120 1198 let client = sqlx::query_as!( 1121 1199 OAuthClient, 1122 1200 r#" ··· 1144 1222 .await?; 1145 1223 1146 1224 if result.rows_affected() == 0 { 1147 - return Err(DatabaseError::RecordNotFound { uri: client_id.to_string() }); 1225 + return Err(DatabaseError::RecordNotFound { 1226 + uri: client_id.to_string(), 1227 + }); 1148 1228 } 1149 1229 1150 1230 Ok(()) 1151 1231 } 1152 - 1153 1232 }
+42 -29
api/src/handler_sync_user_collections.rs
··· 6 6 use serde::Deserialize; 7 7 use tracing::{info, warn}; 8 8 9 + use crate::AppState; 9 10 use crate::auth::{extract_bearer_token, verify_oauth_token}; 10 11 use crate::sync::{SyncService, SyncUserCollectionsResult}; 11 - use crate::AppState; 12 12 13 13 #[derive(Deserialize)] 14 14 #[serde(rename_all = "camelCase")] ··· 22 22 30 // 30 second default timeout for login scenarios 23 23 } 24 24 25 - /// Handler for social.slices.slice.syncUserCollections 25 + /// Handler for network.slices.slice.syncUserCollections 26 26 /// Synchronously syncs external collections for the authenticated user with timeout protection 27 27 /// Automatically discovers external collections based on the slice's domain configuration 28 28 pub async fn sync_user_collections( ··· 32 32 ) -> Result<Json<SyncUserCollectionsResult>, (StatusCode, Json<serde_json::Value>)> { 33 33 // Extract and verify OAuth token 34 34 let token = extract_bearer_token(&headers).map_err(|e| { 35 - (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ 36 - "error": "AuthenticationRequired", 37 - "message": format!("Bearer token required: {}", e) 38 - }))) 35 + ( 36 + StatusCode::UNAUTHORIZED, 37 + Json(serde_json::json!({ 38 + "error": "AuthenticationRequired", 39 + "message": format!("Bearer token required: {}", e) 40 + })), 41 + ) 39 42 })?; 40 43 41 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 44 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 45 + .await 42 46 .map_err(|e| { 43 - (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ 44 - "error": "InvalidToken", 45 - "message": format!("Token verification failed: {}", e) 46 - }))) 47 + ( 48 + StatusCode::UNAUTHORIZED, 49 + Json(serde_json::json!({ 50 + "error": "InvalidToken", 51 + "message": format!("Token verification failed: {}", e) 52 + })), 53 + ) 47 54 })?; 48 55 49 56 let user_did = user_info.did.unwrap_or(user_info.sub); 50 - 57 + 51 58 info!( 52 59 "🔄 Starting user collections sync for {} on slice {} (timeout: {}s)", 53 60 user_did, request.slice, request.timeout_seconds ··· 55 62 56 63 // Validate timeout (max 5 minutes for sync operations) 57 64 if request.timeout_seconds > 300 { 58 - return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({ 59 - "error": "InvalidTimeout", 60 - "message": "Maximum timeout is 300 seconds (5 minutes)" 61 - })))); 65 + return Err(( 66 + StatusCode::BAD_REQUEST, 67 + Json(serde_json::json!({ 68 + "error": "InvalidTimeout", 69 + "message": "Maximum timeout is 300 seconds (5 minutes)" 70 + })), 71 + )); 62 72 } 63 73 64 74 // Create sync service 65 - let sync_service = SyncService::new(state.database.clone(), state.config.relay_endpoint.clone()); 66 - 75 + let sync_service = 76 + SyncService::new(state.database.clone(), state.config.relay_endpoint.clone()); 77 + 67 78 // Perform timeout-protected sync with auto-discovered external collections 68 - match sync_service.sync_user_collections( 69 - &user_did, 70 - &request.slice, 71 - request.timeout_seconds, 72 - ).await { 79 + match sync_service 80 + .sync_user_collections(&user_did, &request.slice, request.timeout_seconds) 81 + .await 82 + { 73 83 Ok(result) => { 74 84 if result.timed_out { 75 85 info!( ··· 83 93 ); 84 94 } 85 95 Ok(Json(result)) 86 - }, 96 + } 87 97 Err(e) => { 88 98 warn!("❌ Sync failed for user {}: {}", user_did, e); 89 - Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ 90 - "error": "SyncFailed", 91 - "message": format!("Sync operation failed: {}", e) 92 - })))) 99 + Err(( 100 + StatusCode::INTERNAL_SERVER_ERROR, 101 + Json(serde_json::json!({ 102 + "error": "SyncFailed", 103 + "message": format!("Sync operation failed: {}", e) 104 + })), 105 + )) 93 106 } 94 107 } 95 - } 108 + }
+365 -232
api/src/handler_xrpc_dynamic.rs
··· 1 + use atproto_client::com::atproto::repo::{ 2 + CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest, PutRecordRequest, 3 + PutRecordResponse, create_record, delete_record, put_record, 4 + }; 1 5 use axum::{ 2 6 extract::{Path, Query, State}, 3 7 http::{HeaderMap, StatusCode}, 4 8 response::Json, 5 9 }; 6 - use serde::Deserialize; 7 10 use chrono::Utc; 8 - use atproto_client::com::atproto::repo::{CreateRecordRequest, PutRecordRequest, DeleteRecordRequest, create_record, put_record, delete_record, CreateRecordResponse, PutRecordResponse}; 11 + use serde::Deserialize; 9 12 10 - use crate::auth::{extract_bearer_token, verify_oauth_token, get_atproto_auth_for_user}; 13 + use crate::AppState; 14 + use crate::auth::{extract_bearer_token, get_atproto_auth_for_user, verify_oauth_token}; 11 15 use crate::lexicon::LexiconValidator; 12 - use crate::models::{Record, SliceRecordsParams, SliceRecordsOutput, IndexedRecord, WhereCondition, SortField}; 16 + use crate::models::{ 17 + IndexedRecord, Record, SliceRecordsOutput, SliceRecordsParams, SortField, WhereCondition, 18 + }; 13 19 use std::collections::HashMap; 14 - use crate::AppState; 15 20 16 21 // Helper function to convert StatusCode errors to JSON error responses 17 22 fn status_to_error_response(status: StatusCode) -> (StatusCode, Json<serde_json::Value>) { ··· 24 29 _ => "Request failed", 25 30 }; 26 31 27 - (status, Json(serde_json::json!({ 28 - "error": status.as_str(), 29 - "message": message 30 - }))) 32 + ( 33 + status, 34 + Json(serde_json::json!({ 35 + "error": status.as_str(), 36 + "message": message 37 + })), 38 + ) 31 39 } 32 - 33 - 34 40 35 41 #[derive(Deserialize)] 36 42 pub struct GetRecordParams { ··· 66 72 headers: HeaderMap, 67 73 Json(body): Json<serde_json::Value>, 68 74 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 69 - 70 75 // Handle dynamic collection methods (e.g., social.grain.gallery.createRecord) 71 76 if method.ends_with(".getRecords") { 72 77 let collection = method.trim_end_matches(".getRecords").to_string(); ··· 95 100 params: serde_json::Value, 96 101 ) -> Result<Json<serde_json::Value>, StatusCode> { 97 102 // Parse parameters into SliceRecordsParams format 98 - let slice = params.get("slice") 103 + let slice = params 104 + .get("slice") 99 105 .and_then(|v| v.as_str()) 100 106 .ok_or(StatusCode::BAD_REQUEST)? 101 107 .to_string(); ··· 108 114 } 109 115 }); 110 116 111 - let cursor = params.get("cursor") 117 + let cursor = params 118 + .get("cursor") 112 119 .and_then(|v| v.as_str()) 113 120 .map(|s| s.to_string()); 114 121 115 122 // Parse sortBy from params - convert legacy sort string to new array format if present 116 - let sort_by = params.get("sort") 117 - .and_then(|v| v.as_str()) 118 - .map(|sort_str| { 119 - // Convert legacy "field:direction" format to new array format 120 - let mut sort_fields = Vec::new(); 121 - for sort_item in sort_str.split(',') { 122 - let parts: Vec<&str> = sort_item.trim().split(':').collect(); 123 - if parts.len() == 2 { 124 - sort_fields.push(SortField { 125 - field: parts[0].trim().to_string(), 126 - direction: parts[1].trim().to_string(), 127 - }); 128 - } else if parts.len() == 1 && !parts[0].is_empty() { 129 - // Default to ascending if no direction specified 130 - sort_fields.push(SortField { 131 - field: parts[0].trim().to_string(), 132 - direction: "asc".to_string(), 133 - }); 134 - } 123 + let sort_by = params.get("sort").and_then(|v| v.as_str()).map(|sort_str| { 124 + // Convert legacy "field:direction" format to new array format 125 + let mut sort_fields = Vec::new(); 126 + for sort_item in sort_str.split(',') { 127 + let parts: Vec<&str> = sort_item.trim().split(':').collect(); 128 + if parts.len() == 2 { 129 + sort_fields.push(SortField { 130 + field: parts[0].trim().to_string(), 131 + direction: parts[1].trim().to_string(), 132 + }); 133 + } else if parts.len() == 1 && !parts[0].is_empty() { 134 + // Default to ascending if no direction specified 135 + sort_fields.push(SortField { 136 + field: parts[0].trim().to_string(), 137 + direction: "asc".to_string(), 138 + }); 135 139 } 136 - sort_fields 137 - }); 140 + } 141 + sort_fields 142 + }); 138 143 139 144 // Parse where conditions from query params if present 140 145 let mut where_conditions = HashMap::new(); 141 146 142 147 // Handle legacy author/authors params by converting to where clause 143 148 if let Some(author_str) = params.get("author").and_then(|v| v.as_str()) { 144 - where_conditions.insert("did".to_string(), WhereCondition { 145 - eq: Some(serde_json::Value::String(author_str.to_string())), 146 - in_values: None, 147 - contains: None, 148 - }); 149 + where_conditions.insert( 150 + "did".to_string(), 151 + WhereCondition { 152 + eq: Some(serde_json::Value::String(author_str.to_string())), 153 + in_values: None, 154 + contains: None, 155 + }, 156 + ); 149 157 } else if let Some(authors_str) = params.get("authors").and_then(|v| v.as_str()) { 150 158 let authors: Vec<serde_json::Value> = authors_str 151 159 .split(',') 152 160 .map(|s| serde_json::Value::String(s.trim().to_string())) 153 161 .collect(); 154 - where_conditions.insert("did".to_string(), WhereCondition { 155 - eq: None, 156 - in_values: Some(authors), 157 - contains: None, 158 - }); 162 + where_conditions.insert( 163 + "did".to_string(), 164 + WhereCondition { 165 + eq: None, 166 + in_values: Some(authors), 167 + contains: None, 168 + }, 169 + ); 159 170 } 160 171 161 172 // Handle legacy query param by converting to where clause with contains 162 173 if let Some(query_str) = params.get("query").and_then(|v| v.as_str()) { 163 - let field = params.get("field") 174 + let field = params 175 + .get("field") 164 176 .and_then(|v| v.as_str()) 165 177 .unwrap_or("text"); // Default to text field for search 166 - where_conditions.insert(field.to_string(), WhereCondition { 167 - eq: None, 168 - in_values: None, 169 - contains: Some(query_str.to_string()), 170 - }); 178 + where_conditions.insert( 179 + field.to_string(), 180 + WhereCondition { 181 + eq: None, 182 + in_values: None, 183 + contains: Some(query_str.to_string()), 184 + }, 185 + ); 171 186 } 172 187 173 188 let where_clause = if where_conditions.is_empty() { ··· 188 203 }; 189 204 190 205 // First verify the collection belongs to this slice 191 - let slice_collections = state.database.get_slice_collections_list(&records_params.slice).await 206 + let slice_collections = state 207 + .database 208 + .get_slice_collections_list(&records_params.slice) 209 + .await 192 210 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 193 211 194 - // Special handling: social.slices.lexicon is always allowed as it defines the schema 195 - if collection != "social.slices.lexicon" && !slice_collections.contains(&collection) { 212 + // Special handling: network.slices.lexicon is always allowed as it defines the schema 213 + if collection != "network.slices.lexicon" && !slice_collections.contains(&collection) { 196 214 return Err(StatusCode::NOT_FOUND); 197 215 } 198 216 199 217 // Use the unified database method 200 - match state.database.get_slice_collections_records( 201 - &records_params.slice, 202 - records_params.limit, 203 - records_params.cursor.as_deref(), 204 - sort_by.as_ref(), 205 - records_params.where_clause.as_ref(), 206 - ).await { 218 + match state 219 + .database 220 + .get_slice_collections_records( 221 + &records_params.slice, 222 + records_params.limit, 223 + records_params.cursor.as_deref(), 224 + sort_by.as_ref(), 225 + records_params.where_clause.as_ref(), 226 + ) 227 + .await 228 + { 207 229 Ok((mut records, cursor)) => { 208 230 // Filter records to only include the specific collection 209 231 records.retain(|record| record.collection == collection); 210 232 211 - let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|record| IndexedRecord { 212 - uri: record.uri, 213 - cid: record.cid, 214 - did: record.did, 215 - collection: record.collection, 216 - value: record.json, 217 - indexed_at: record.indexed_at.to_rfc3339(), 218 - }).collect(); 233 + let indexed_records: Vec<IndexedRecord> = records 234 + .into_iter() 235 + .map(|record| IndexedRecord { 236 + uri: record.uri, 237 + cid: record.cid, 238 + did: record.did, 239 + collection: record.collection, 240 + value: record.json, 241 + indexed_at: record.indexed_at.to_rfc3339(), 242 + }) 243 + .collect(); 219 244 220 245 let output = SliceRecordsOutput { 221 246 success: true, ··· 224 249 message: None, 225 250 }; 226 251 227 - Ok(Json(serde_json::to_value(output) 228 - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?)) 229 - }, 252 + Ok(Json( 253 + serde_json::to_value(output).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, 254 + )) 255 + } 230 256 Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 231 257 } 232 258 } 233 - 234 - 235 259 236 260 // Implementation for get record 237 261 async fn dynamic_get_record_impl( ··· 239 263 state: AppState, 240 264 params: serde_json::Value, 241 265 ) -> Result<Json<serde_json::Value>, StatusCode> { 242 - let get_params: GetRecordParams = serde_json::from_value(params) 243 - .map_err(|_| StatusCode::BAD_REQUEST)?; 266 + let get_params: GetRecordParams = 267 + serde_json::from_value(params).map_err(|_| StatusCode::BAD_REQUEST)?; 244 268 245 269 // First verify the collection belongs to this slice 246 - let slice_collections = state.database.get_slice_collections_list(&get_params.slice).await 270 + let slice_collections = state 271 + .database 272 + .get_slice_collections_list(&get_params.slice) 273 + .await 247 274 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 248 275 249 - // Special handling: social.slices.lexicon is always allowed as it defines the schema 250 - if collection != "social.slices.lexicon" && !slice_collections.contains(&collection) { 276 + // Special handling: network.slices.lexicon is always allowed as it defines the schema 277 + if collection != "network.slices.lexicon" && !slice_collections.contains(&collection) { 251 278 return Err(StatusCode::NOT_FOUND); 252 279 } 253 280 254 281 // Use direct database query by URI for efficiency 255 282 match state.database.get_record(&get_params.uri).await { 256 283 Ok(Some(record)) => { 257 - let json_value = serde_json::to_value(record) 258 - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 284 + let json_value = 285 + serde_json::to_value(record).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 259 286 Ok(Json(json_value)) 260 - }, 261 - Ok(None) => { 262 - Err(StatusCode::NOT_FOUND) 263 - }, 264 - Err(_e) => { 265 - Err(StatusCode::INTERNAL_SERVER_ERROR) 266 - }, 287 + } 288 + Ok(None) => Err(StatusCode::NOT_FOUND), 289 + Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR), 267 290 } 268 291 } 269 292 ··· 274 297 body: serde_json::Value, 275 298 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 276 299 // Parse the JSON body into SliceRecordsParams 277 - let mut records_params: SliceRecordsParams = serde_json::from_value(body) 278 - .map_err(|_| (StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": "Invalid request body"}))))?; 300 + let mut records_params: SliceRecordsParams = serde_json::from_value(body).map_err(|_| { 301 + ( 302 + StatusCode::BAD_REQUEST, 303 + Json(serde_json::json!({"error": "Invalid request body"})), 304 + ) 305 + })?; 279 306 280 307 // First verify the collection belongs to this slice 281 - let slice_collections = state.database.get_slice_collections_list(&records_params.slice).await 282 - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database error"}))))?; 308 + let slice_collections = state 309 + .database 310 + .get_slice_collections_list(&records_params.slice) 311 + .await 312 + .map_err(|_| { 313 + ( 314 + StatusCode::INTERNAL_SERVER_ERROR, 315 + Json(serde_json::json!({"error": "Database error"})), 316 + ) 317 + })?; 283 318 284 - // Special handling: social.slices.lexicon is always allowed as it defines the schema 285 - if collection != "social.slices.lexicon" && !slice_collections.contains(&collection) { 286 - return Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Collection not found"})))); 319 + // Special handling: network.slices.lexicon is always allowed as it defines the schema 320 + if collection != "network.slices.lexicon" && !slice_collections.contains(&collection) { 321 + return Err(( 322 + StatusCode::NOT_FOUND, 323 + Json(serde_json::json!({"error": "Collection not found"})), 324 + )); 287 325 } 288 326 289 327 // Add collection filter to where conditions 290 - let mut where_clause = records_params.where_clause.unwrap_or(crate::models::WhereClause { 291 - conditions: HashMap::new(), 292 - or_conditions: None, 293 - }); 294 - where_clause.conditions.insert("collection".to_string(), WhereCondition { 295 - eq: Some(serde_json::Value::String(collection.clone())), 296 - in_values: None, 297 - contains: None, 298 - }); 328 + let mut where_clause = records_params 329 + .where_clause 330 + .unwrap_or(crate::models::WhereClause { 331 + conditions: HashMap::new(), 332 + or_conditions: None, 333 + }); 334 + where_clause.conditions.insert( 335 + "collection".to_string(), 336 + WhereCondition { 337 + eq: Some(serde_json::Value::String(collection.clone())), 338 + in_values: None, 339 + contains: None, 340 + }, 341 + ); 299 342 records_params.where_clause = Some(where_clause); 300 343 301 344 // Use the unified database method 302 - match state.database.get_slice_collections_records( 303 - &records_params.slice, 304 - records_params.limit, 305 - records_params.cursor.as_deref(), 306 - records_params.sort_by.as_ref(), 307 - records_params.where_clause.as_ref(), 308 - ).await { 345 + match state 346 + .database 347 + .get_slice_collections_records( 348 + &records_params.slice, 349 + records_params.limit, 350 + records_params.cursor.as_deref(), 351 + records_params.sort_by.as_ref(), 352 + records_params.where_clause.as_ref(), 353 + ) 354 + .await 355 + { 309 356 Ok((records, cursor)) => { 310 357 // No need to filter - collection filter is in the SQL query now 311 358 312 359 // Transform Record to IndexedRecord for the response 313 - let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|record| IndexedRecord { 314 - uri: record.uri, 315 - cid: record.cid, 316 - did: record.did, 317 - collection: record.collection, 318 - value: record.json, 319 - indexed_at: record.indexed_at.to_rfc3339(), 320 - }).collect(); 360 + let indexed_records: Vec<IndexedRecord> = records 361 + .into_iter() 362 + .map(|record| IndexedRecord { 363 + uri: record.uri, 364 + cid: record.cid, 365 + did: record.did, 366 + collection: record.collection, 367 + value: record.json, 368 + indexed_at: record.indexed_at.to_rfc3339(), 369 + }) 370 + .collect(); 321 371 322 372 let output = SliceRecordsOutput { 323 373 success: true, ··· 326 376 message: None, 327 377 }; 328 378 329 - Ok(Json(serde_json::to_value(output) 330 - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Serialization error"}))))?)) 331 - }, 332 - Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database error"})))), 379 + Ok(Json(serde_json::to_value(output).map_err(|_| { 380 + ( 381 + StatusCode::INTERNAL_SERVER_ERROR, 382 + Json(serde_json::json!({"error": "Serialization error"})), 383 + ) 384 + })?)) 385 + } 386 + Err(_) => Err(( 387 + StatusCode::INTERNAL_SERVER_ERROR, 388 + Json(serde_json::json!({"error": "Database error"})), 389 + )), 333 390 } 334 391 } 335 392 ··· 340 397 params: serde_json::Value, 341 398 ) -> Result<Json<serde_json::Value>, StatusCode> { 342 399 // Convert query parameters to SliceRecordsParams 343 - let mut records_params: SliceRecordsParams = serde_json::from_value(params) 344 - .map_err(|_| StatusCode::BAD_REQUEST)?; 400 + let mut records_params: SliceRecordsParams = 401 + serde_json::from_value(params).map_err(|_| StatusCode::BAD_REQUEST)?; 345 402 346 403 // First verify the collection belongs to this slice 347 - let slice_collections = state.database.get_slice_collections_list(&records_params.slice).await 404 + let slice_collections = state 405 + .database 406 + .get_slice_collections_list(&records_params.slice) 407 + .await 348 408 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 349 409 350 - // Special handling: social.slices.lexicon is always allowed as it defines the schema 351 - if collection != "social.slices.lexicon" && !slice_collections.contains(&collection) { 410 + // Special handling: network.slices.lexicon is always allowed as it defines the schema 411 + if collection != "network.slices.lexicon" && !slice_collections.contains(&collection) { 352 412 return Err(StatusCode::NOT_FOUND); 353 413 } 354 414 355 415 // Add collection filter to where conditions 356 - let mut where_clause = records_params.where_clause.unwrap_or(crate::models::WhereClause { 357 - conditions: HashMap::new(), 358 - or_conditions: None, 359 - }); 360 - where_clause.conditions.insert("collection".to_string(), WhereCondition { 361 - eq: Some(collection.clone().into()), 362 - contains: None, 363 - in_values: None, 364 - }); 416 + let mut where_clause = records_params 417 + .where_clause 418 + .unwrap_or(crate::models::WhereClause { 419 + conditions: HashMap::new(), 420 + or_conditions: None, 421 + }); 422 + where_clause.conditions.insert( 423 + "collection".to_string(), 424 + WhereCondition { 425 + eq: Some(collection.clone().into()), 426 + contains: None, 427 + in_values: None, 428 + }, 429 + ); 365 430 records_params.where_clause = Some(where_clause); 366 431 367 - match state.database.count_slice_collections_records(&records_params.slice, records_params.where_clause.as_ref()).await { 432 + match state 433 + .database 434 + .count_slice_collections_records( 435 + &records_params.slice, 436 + records_params.where_clause.as_ref(), 437 + ) 438 + .await 439 + { 368 440 Ok(count) => Ok(Json(serde_json::json!({ 369 441 "success": true, 370 442 "count": count, ··· 385 457 body: serde_json::Value, 386 458 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 387 459 // Parse the JSON body into SliceRecordsParams 388 - let mut records_params: SliceRecordsParams = serde_json::from_value(body) 389 - .map_err(|_| (StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": "Invalid request body"}))))?; 460 + let mut records_params: SliceRecordsParams = serde_json::from_value(body).map_err(|_| { 461 + ( 462 + StatusCode::BAD_REQUEST, 463 + Json(serde_json::json!({"error": "Invalid request body"})), 464 + ) 465 + })?; 390 466 391 467 // First verify the collection belongs to this slice 392 - let slice_collections = state.database.get_slice_collections_list(&records_params.slice).await 393 - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Database error"}))))?; 468 + let slice_collections = state 469 + .database 470 + .get_slice_collections_list(&records_params.slice) 471 + .await 472 + .map_err(|_| { 473 + ( 474 + StatusCode::INTERNAL_SERVER_ERROR, 475 + Json(serde_json::json!({"error": "Database error"})), 476 + ) 477 + })?; 394 478 395 - // Special handling: social.slices.lexicon is always allowed as it defines the schema 396 - if collection != "social.slices.lexicon" && !slice_collections.contains(&collection) { 397 - return Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Collection not found"})))); 479 + // Special handling: network.slices.lexicon is always allowed as it defines the schema 480 + if collection != "network.slices.lexicon" && !slice_collections.contains(&collection) { 481 + return Err(( 482 + StatusCode::NOT_FOUND, 483 + Json(serde_json::json!({"error": "Collection not found"})), 484 + )); 398 485 } 399 486 400 487 // Add collection filter to where conditions 401 - let mut where_clause = records_params.where_clause.unwrap_or(crate::models::WhereClause { 402 - conditions: HashMap::new(), 403 - or_conditions: None, 404 - }); 405 - where_clause.conditions.insert("collection".to_string(), WhereCondition { 406 - eq: Some(collection.clone().into()), 407 - in_values: None, 408 - contains: None, 409 - }); 488 + let mut where_clause = records_params 489 + .where_clause 490 + .unwrap_or(crate::models::WhereClause { 491 + conditions: HashMap::new(), 492 + or_conditions: None, 493 + }); 494 + where_clause.conditions.insert( 495 + "collection".to_string(), 496 + WhereCondition { 497 + eq: Some(collection.clone().into()), 498 + in_values: None, 499 + contains: None, 500 + }, 501 + ); 410 502 records_params.where_clause = Some(where_clause); 411 503 412 - match state.database.count_slice_collections_records(&records_params.slice, records_params.where_clause.as_ref()).await { 504 + match state 505 + .database 506 + .count_slice_collections_records( 507 + &records_params.slice, 508 + records_params.where_clause.as_ref(), 509 + ) 510 + .await 511 + { 413 512 Ok(count) => Ok(Json(serde_json::json!({ 414 513 "success": true, 415 514 "count": count, ··· 432 531 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 433 532 // Extract and verify OAuth token 434 533 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 435 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 534 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 535 + .await 436 536 .map_err(status_to_error_response)?; 437 537 438 538 // Get AT Protocol DPoP auth and PDS URL 439 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 539 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 540 + .await 440 541 .map_err(status_to_error_response)?; 441 542 442 543 // Extract the repo DID from user info ··· 446 547 let http_client = reqwest::Client::new(); 447 548 448 549 // Extract slice URI, rkey, and record value from structured body 449 - let slice_uri = body.get("slice") 550 + let slice_uri = body 551 + .get("slice") 450 552 .and_then(|v| v.as_str()) 451 553 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 452 554 .to_string(); 453 555 454 - let record_key = body.get("rkey") 556 + let record_key = body 557 + .get("rkey") 455 558 .and_then(|v| v.as_str()) 456 559 .filter(|s| !s.is_empty()) // Filter out empty strings 457 560 .map(|s| s.to_string()); 458 561 459 - let record_data = body.get("record") 562 + let record_data = body 563 + .get("record") 460 564 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 461 565 .clone(); 462 566 463 - 464 567 // Validate the record against its lexicon 465 568 466 - // For social.slices.lexicon collection, validate against the system slice 467 - let validation_slice_uri = if collection == "social.slices.lexicon" { 468 - "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lwzmbjpqxk2q" 569 + // For network.slices.lexicon collection, validate against the system slice 570 + let validation_slice_uri = if collection == "network.slices.lexicon" { 571 + "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z" 469 572 } else { 470 573 &slice_uri 471 574 }; 472 - 473 575 474 576 match LexiconValidator::for_slice(&state.database, validation_slice_uri).await { 475 577 Ok(validator) => { 476 - 477 578 // Debug: Get lexicons from the system slice to see what's there 478 - if collection == "social.slices.lexicon" { 479 - } 579 + if collection == "network.slices.lexicon" {} 480 580 481 581 if let Err(e) = validator.validate_record(&collection, &record_data) { 482 - return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({ 483 - "error": "ValidationError", 484 - "message": format!("Lexicon validation failed: {}", e) 485 - })))); 582 + return Err(( 583 + StatusCode::BAD_REQUEST, 584 + Json(serde_json::json!({ 585 + "error": "ValidationError", 586 + "message": format!("Lexicon validation failed: {}", e) 587 + })), 588 + )); 486 589 } 487 - }, 590 + } 488 591 Err(e) => { 489 592 // If no lexicon found, continue without validation (backwards compatibility) 490 593 eprintln!("Could not load lexicon validator: {:?}", e); ··· 502 605 validate: false, 503 606 }; 504 607 505 - 506 608 let result = create_record(&http_client, &dpop_auth, &pds_url, create_request) 507 609 .await 508 - .map_err(|_e| { 509 - status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR) 510 - })?; 610 + .map_err(|_e| status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR))?; 511 611 512 612 // Extract URI and CID from the response enum 513 613 let (uri, cid) = match result { 514 - CreateRecordResponse::StrongRef { uri, cid, .. } => { 515 - (uri, cid) 516 - }, 614 + CreateRecordResponse::StrongRef { uri, cid, .. } => (uri, cid), 517 615 CreateRecordResponse::Error(_e) => { 518 616 return Err(status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR)); 519 - }, 617 + } 520 618 }; 521 619 522 620 // Also store in local database for indexing ··· 550 648 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 551 649 // Extract and verify OAuth token 552 650 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 553 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 651 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 652 + .await 554 653 .map_err(status_to_error_response)?; 555 654 556 655 // Get AT Protocol DPoP auth and PDS URL 557 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 656 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 657 + .await 558 658 .map_err(status_to_error_response)?; 559 659 560 660 // Extract slice URI, rkey, and record value from structured body 561 - let slice_uri = body.get("slice") 661 + let slice_uri = body 662 + .get("slice") 562 663 .and_then(|v| v.as_str()) 563 664 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 564 665 .to_string(); 565 666 566 - let rkey = body.get("rkey") 667 + let rkey = body 668 + .get("rkey") 567 669 .and_then(|v| v.as_str()) 568 670 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 569 671 .to_string(); 570 672 571 - let record_data = body.get("record") 673 + let record_data = body 674 + .get("record") 572 675 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 573 676 .clone(); 574 677 ··· 579 682 match LexiconValidator::for_slice(&state.database, &slice_uri).await { 580 683 Ok(validator) => { 581 684 if let Err(e) = validator.validate_record(&collection, &record_data) { 582 - return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({ 583 - "error": "ValidationError", 584 - "message": format!("Lexicon validation failed: {}", e) 585 - })))); 685 + return Err(( 686 + StatusCode::BAD_REQUEST, 687 + Json(serde_json::json!({ 688 + "error": "ValidationError", 689 + "message": format!("Lexicon validation failed: {}", e) 690 + })), 691 + )); 586 692 } 587 - }, 693 + } 588 694 Err(e) => { 589 695 // If no lexicon found, continue without validation (backwards compatibility) 590 696 eprintln!("Could not load lexicon validator: {:?}", e); ··· 612 718 // Extract URI and CID from the response enum 613 719 let (uri, cid) = match result { 614 720 PutRecordResponse::StrongRef { uri, cid, .. } => (uri, cid), 615 - PutRecordResponse::Error(_) => return Err(status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR)), 721 + PutRecordResponse::Error(_) => { 722 + return Err(status_to_error_response(StatusCode::INTERNAL_SERVER_ERROR)); 723 + } 616 724 }; 617 725 618 726 // Also update in local database for indexing ··· 644 752 ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { 645 753 // Extract and verify OAuth token 646 754 let token = extract_bearer_token(&headers).map_err(status_to_error_response)?; 647 - let user_info = verify_oauth_token(&token, &state.config.auth_base_url).await 755 + let user_info = verify_oauth_token(&token, &state.config.auth_base_url) 756 + .await 648 757 .map_err(status_to_error_response)?; 649 758 650 759 // Get AT Protocol DPoP auth and PDS URL 651 - let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url).await 760 + let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url) 761 + .await 652 762 .map_err(status_to_error_response)?; 653 763 654 764 // Extract repo and rkey from body 655 765 let repo = user_info.did.unwrap_or(user_info.sub); 656 - let rkey = body["rkey"].as_str() 766 + let rkey = body["rkey"] 767 + .as_str() 657 768 .ok_or_else(|| status_to_error_response(StatusCode::BAD_REQUEST))? 658 769 .to_string(); 659 770 ··· 680 791 Ok(Json(serde_json::json!({}))) 681 792 } 682 793 683 - 684 794 #[cfg(test)] 685 795 mod dynamic_validation_tests { 686 796 use crate::lexicon::LexiconValidator; 687 797 use serde_json::json; 688 798 689 799 fn create_test_lexicons() -> Vec<serde_json::Value> { 690 - vec![ 691 - json!({ 692 - "id": "social.slices.testRecord", 693 - "lexicon": 1, 694 - "defs": { 695 - "main": { 696 - "type": "record", 697 - "record": { 698 - "type": "object", 699 - "required": ["title", "aspectRatio"], 700 - "properties": { 701 - "title": { "type": "string", "maxLength": 100 }, 702 - "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } 703 - } 704 - } 705 - }, 706 - "aspectRatio": { 800 + vec![json!({ 801 + "id": "network.slices.testRecord", 802 + "lexicon": 1, 803 + "defs": { 804 + "main": { 805 + "type": "record", 806 + "record": { 707 807 "type": "object", 708 - "required": ["width", "height"], 808 + "required": ["title", "aspectRatio"], 709 809 "properties": { 710 - "width": { "type": "integer", "minimum": 1 }, 711 - "height": { "type": "integer", "minimum": 1 } 810 + "title": { "type": "string", "maxLength": 100 }, 811 + "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } 712 812 } 713 813 } 814 + }, 815 + "aspectRatio": { 816 + "type": "object", 817 + "required": ["width", "height"], 818 + "properties": { 819 + "width": { "type": "integer", "minimum": 1 }, 820 + "height": { "type": "integer", "minimum": 1 } 821 + } 714 822 } 715 - }) 716 - ] 823 + } 824 + })] 717 825 } 718 826 719 827 #[test] ··· 729 837 } 730 838 }); 731 839 732 - let result = validator.validate_record("social.slices.testRecord", &valid_record); 733 - assert!(result.is_ok(), "Valid record should pass validation: {:?}", result); 840 + let result = validator.validate_record("network.slices.testRecord", &valid_record); 841 + assert!( 842 + result.is_ok(), 843 + "Valid record should pass validation: {:?}", 844 + result 845 + ); 734 846 } 735 847 736 848 #[test] ··· 743 855 // Missing required aspectRatio field 744 856 }); 745 857 746 - let result = validator.validate_record("social.slices.testRecord", &invalid_record); 747 - assert!(result.is_err(), "Record missing required field should fail validation"); 858 + let result = validator.validate_record("network.slices.testRecord", &invalid_record); 859 + assert!( 860 + result.is_err(), 861 + "Record missing required field should fail validation" 862 + ); 748 863 } 749 864 750 865 #[test] ··· 761 876 } 762 877 }); 763 878 764 - let result = validator.validate_record("social.slices.testRecord", &valid_record); 765 - assert!(result.is_ok(), "Cross-lexicon reference should work: {:?}", result); 879 + let result = validator.validate_record("network.slices.testRecord", &valid_record); 880 + assert!( 881 + result.is_ok(), 882 + "Cross-lexicon reference should work: {:?}", 883 + result 884 + ); 766 885 } 767 886 768 887 #[test] ··· 776 895 // Missing required aspectRatio field 777 896 }); 778 897 779 - let result = validator.validate_record("social.slices.testRecord", &invalid_record); 898 + let result = validator.validate_record("network.slices.testRecord", &invalid_record); 780 899 assert!(result.is_err(), "Invalid record should fail validation"); 781 900 782 901 if let Err(e) = result { 783 902 let error_message = format!("{}", e); 784 - assert!(!error_message.is_empty(), "Error message should not be empty"); 903 + assert!( 904 + !error_message.is_empty(), 905 + "Error message should not be empty" 906 + ); 785 907 // Error message should be user-friendly and descriptive 786 - assert!(error_message.contains("aspectRatio") || error_message.contains("required"), 787 - "Error message should indicate what's wrong: {}", error_message); 908 + assert!( 909 + error_message.contains("aspectRatio") || error_message.contains("required"), 910 + "Error message should indicate what's wrong: {}", 911 + error_message 912 + ); 788 913 } 789 914 } 790 915 ··· 802 927 } 803 928 }); 804 929 805 - let result = validator.validate_record("social.slices.testRecord", &invalid_record); 806 - assert!(result.is_err(), "Constraint violation should fail validation"); 930 + let result = validator.validate_record("network.slices.testRecord", &invalid_record); 931 + assert!( 932 + result.is_err(), 933 + "Constraint violation should fail validation" 934 + ); 807 935 808 936 if let Err(e) = result { 809 937 let error_message = format!("{}", e); 810 938 // Should indicate the specific constraint that was violated 811 - assert!(error_message.contains("length") || error_message.contains("maximum") || error_message.contains("100"), 812 - "Error message should indicate length constraint: {}", error_message); 939 + assert!( 940 + error_message.contains("length") 941 + || error_message.contains("maximum") 942 + || error_message.contains("100"), 943 + "Error message should indicate length constraint: {}", 944 + error_message 945 + ); 813 946 } 814 947 } 815 948 }
+561 -297
api/src/lexicon/test.rs
··· 19 19 "required": [ 20 20 "object", 21 21 "array", 22 - "boolean", 22 + "boolean", 23 23 "integer", 24 24 "string", 25 25 "bytes", ··· 408 408 } 409 409 } 410 410 } 411 - }) 411 + }), 412 412 ] 413 413 } 414 414 415 415 #[test] 416 416 fn test_kitchen_sink_validation() { 417 417 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 418 - 418 + 419 419 // Valid kitchen sink record (mirroring TypeScript test) 420 420 let valid_record = json!({ 421 421 "object": { ··· 434 434 "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" 435 435 } 436 436 }); 437 - 438 - assert!(validator.validate_record("com.example.kitchenSink", &valid_record).is_ok()); 439 - 437 + 438 + assert!( 439 + validator 440 + .validate_record("com.example.kitchenSink", &valid_record) 441 + .is_ok() 442 + ); 443 + 440 444 // Missing required field (object) 441 445 let invalid_record = json!({ 442 446 "array": ["one", "two"], ··· 448 452 "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" 449 453 } 450 454 }); 451 - 452 - assert!(validator.validate_record("com.example.kitchenSink", &invalid_record).is_err()); 455 + 456 + assert!( 457 + validator 458 + .validate_record("com.example.kitchenSink", &invalid_record) 459 + .is_err() 460 + ); 453 461 } 454 - 462 + 455 463 #[test] 456 464 fn test_local_refs() { 457 465 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 458 - 466 + 459 467 // Valid with local ref (#object -> #subobject) 460 468 let valid_record = json!({ 461 469 "object": { "boolean": true }, // subobject ref ··· 464 472 "integer": 123, 465 473 "string": "string" 466 474 }); 467 - 468 - assert!(validator.validate_record("com.example.kitchenSink#object", &valid_record).is_ok()); 469 - 475 + 476 + assert!( 477 + validator 478 + .validate_record("com.example.kitchenSink#object", &valid_record) 479 + .is_ok() 480 + ); 481 + 470 482 // Invalid - missing boolean in subobject 471 483 let invalid_record = json!({ 472 484 "object": {}, // missing required boolean 473 - "array": ["one", "two"], 485 + "array": ["one", "two"], 474 486 "boolean": true, 475 487 "integer": 123, 476 488 "string": "string" 477 489 }); 478 - 479 - assert!(validator.validate_record("com.example.kitchenSink#object", &invalid_record).is_err()); 490 + 491 + assert!( 492 + validator 493 + .validate_record("com.example.kitchenSink#object", &invalid_record) 494 + .is_err() 495 + ); 480 496 } 481 - 497 + 482 498 #[test] 483 499 fn test_array_length_constraints() { 484 500 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 485 - 501 + 486 502 // Valid array length 487 503 let valid = json!({ "array": [1, 2, 3] }); 488 - assert!(validator.validate_record("com.example.arrayLength", &valid).is_ok()); 489 - 504 + assert!( 505 + validator 506 + .validate_record("com.example.arrayLength", &valid) 507 + .is_ok() 508 + ); 509 + 490 510 // Too short 491 511 let too_short = json!({ "array": [1] }); 492 - assert!(validator.validate_record("com.example.arrayLength", &too_short).is_err()); 493 - 512 + assert!( 513 + validator 514 + .validate_record("com.example.arrayLength", &too_short) 515 + .is_err() 516 + ); 517 + 494 518 // Too long 495 519 let too_long = json!({ "array": [1, 2, 3, 4, 5] }); 496 - assert!(validator.validate_record("com.example.arrayLength", &too_long).is_err()); 497 - 520 + assert!( 521 + validator 522 + .validate_record("com.example.arrayLength", &too_long) 523 + .is_err() 524 + ); 525 + 498 526 // Wrong item type 499 527 let wrong_type = json!({ "array": [1, "2", 3] }); 500 - assert!(validator.validate_record("com.example.arrayLength", &wrong_type).is_err()); 528 + assert!( 529 + validator 530 + .validate_record("com.example.arrayLength", &wrong_type) 531 + .is_err() 532 + ); 501 533 } 502 - 534 + 503 535 #[test] 504 536 fn test_boolean_const_constraint() { 505 537 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 506 - 538 + 507 539 // Valid const 508 540 let valid = json!({ "boolean": false }); 509 - assert!(validator.validate_record("com.example.boolConst", &valid).is_ok()); 510 - 541 + assert!( 542 + validator 543 + .validate_record("com.example.boolConst", &valid) 544 + .is_ok() 545 + ); 546 + 511 547 // Invalid const 512 548 let invalid = json!({ "boolean": true }); 513 - assert!(validator.validate_record("com.example.boolConst", &invalid).is_err()); 549 + assert!( 550 + validator 551 + .validate_record("com.example.boolConst", &invalid) 552 + .is_err() 553 + ); 514 554 } 515 - 555 + 516 556 #[test] 517 557 fn test_integer_constraints() { 518 558 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 519 - 559 + 520 560 // Range test - valid 521 561 let valid_range = json!({ "integer": 2 }); 522 - assert!(validator.validate_record("com.example.integerRange", &valid_range).is_ok()); 523 - 562 + assert!( 563 + validator 564 + .validate_record("com.example.integerRange", &valid_range) 565 + .is_ok() 566 + ); 567 + 524 568 // Range test - too low 525 569 let too_low = json!({ "integer": 1 }); 526 - assert!(validator.validate_record("com.example.integerRange", &too_low).is_err()); 527 - 528 - // Range test - too high 570 + assert!( 571 + validator 572 + .validate_record("com.example.integerRange", &too_low) 573 + .is_err() 574 + ); 575 + 576 + // Range test - too high 529 577 let too_high = json!({ "integer": 5 }); 530 - assert!(validator.validate_record("com.example.integerRange", &too_high).is_err()); 531 - 578 + assert!( 579 + validator 580 + .validate_record("com.example.integerRange", &too_high) 581 + .is_err() 582 + ); 583 + 532 584 // Enum test - valid 533 585 let valid_enum = json!({ "integer": 2 }); 534 - assert!(validator.validate_record("com.example.integerEnum", &valid_enum).is_ok()); 535 - 586 + assert!( 587 + validator 588 + .validate_record("com.example.integerEnum", &valid_enum) 589 + .is_ok() 590 + ); 591 + 536 592 // Enum test - invalid 537 593 let invalid_enum = json!({ "integer": 0 }); 538 - assert!(validator.validate_record("com.example.integerEnum", &invalid_enum).is_err()); 539 - 594 + assert!( 595 + validator 596 + .validate_record("com.example.integerEnum", &invalid_enum) 597 + .is_err() 598 + ); 599 + 540 600 // Const test - valid 541 601 let valid_const = json!({ "integer": 0 }); 542 - assert!(validator.validate_record("com.example.integerConst", &valid_const).is_ok()); 543 - 602 + assert!( 603 + validator 604 + .validate_record("com.example.integerConst", &valid_const) 605 + .is_ok() 606 + ); 607 + 544 608 // Const test - invalid 545 609 let invalid_const = json!({ "integer": 1 }); 546 - assert!(validator.validate_record("com.example.integerConst", &invalid_const).is_err()); 610 + assert!( 611 + validator 612 + .validate_record("com.example.integerConst", &invalid_const) 613 + .is_err() 614 + ); 547 615 } 548 - 616 + 549 617 #[test] 550 618 fn test_string_constraints() { 551 619 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 552 - 620 + 553 621 // Length test - valid 554 622 let valid_length = json!({ "string": "ab" }); 555 - assert!(validator.validate_record("com.example.stringLength", &valid_length).is_ok()); 556 - 623 + assert!( 624 + validator 625 + .validate_record("com.example.stringLength", &valid_length) 626 + .is_ok() 627 + ); 628 + 557 629 let valid_length_max = json!({ "string": "abcd" }); 558 - assert!(validator.validate_record("com.example.stringLength", &valid_length_max).is_ok()); 559 - 630 + assert!( 631 + validator 632 + .validate_record("com.example.stringLength", &valid_length_max) 633 + .is_ok() 634 + ); 635 + 560 636 // Length test - too short 561 637 let too_short = json!({ "string": "a" }); 562 - assert!(validator.validate_record("com.example.stringLength", &too_short).is_err()); 563 - 638 + assert!( 639 + validator 640 + .validate_record("com.example.stringLength", &too_short) 641 + .is_err() 642 + ); 643 + 564 644 // Length test - too long 565 645 let too_long = json!({ "string": "abcde" }); 566 - assert!(validator.validate_record("com.example.stringLength", &too_long).is_err()); 567 - 646 + assert!( 647 + validator 648 + .validate_record("com.example.stringLength", &too_long) 649 + .is_err() 650 + ); 651 + 568 652 // Enum test - valid 569 653 let valid_enum = json!({ "string": "a" }); 570 - assert!(validator.validate_record("com.example.stringEnum", &valid_enum).is_ok()); 571 - 654 + assert!( 655 + validator 656 + .validate_record("com.example.stringEnum", &valid_enum) 657 + .is_ok() 658 + ); 659 + 572 660 // Enum test - invalid 573 661 let invalid_enum = json!({ "string": "c" }); 574 - assert!(validator.validate_record("com.example.stringEnum", &invalid_enum).is_err()); 575 - 662 + assert!( 663 + validator 664 + .validate_record("com.example.stringEnum", &invalid_enum) 665 + .is_err() 666 + ); 667 + 576 668 // Const test - valid 577 669 let valid_const = json!({ "string": "a" }); 578 - assert!(validator.validate_record("com.example.stringConst", &valid_const).is_ok()); 579 - 670 + assert!( 671 + validator 672 + .validate_record("com.example.stringConst", &valid_const) 673 + .is_ok() 674 + ); 675 + 580 676 // Const test - invalid 581 677 let invalid_const = json!({ "string": "b" }); 582 - assert!(validator.validate_record("com.example.stringConst", &invalid_const).is_err()); 678 + assert!( 679 + validator 680 + .validate_record("com.example.stringConst", &invalid_const) 681 + .is_err() 682 + ); 583 683 } 584 - 684 + 585 685 #[test] 586 686 fn test_string_formats() { 587 687 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 588 - 688 + 589 689 // DateTime format tests (matching TypeScript test cases) 590 690 let valid_datetimes = vec![ 591 691 "2022-12-12T00:50:36.809Z", 592 - "2022-12-12T00:50:36Z", 692 + "2022-12-12T00:50:36Z", 593 693 "2022-12-12T00:50:36.8Z", 594 694 "2022-12-12T00:50:36.80Z", 595 695 "2022-12-12T00:50:36+00:00", ··· 597 697 "2022-12-11T19:50:36-05:00", 598 698 "2022-12-11T19:50:36.8-05:00", 599 699 "2022-12-11T19:50:36.80-05:00", 600 - "2022-12-11T19:50:36.809-05:00" 700 + "2022-12-11T19:50:36.809-05:00", 601 701 ]; 602 - 702 + 603 703 for datetime in valid_datetimes { 604 704 let record = json!({ "datetime": datetime }); 605 - assert!(validator.validate_record("com.example.datetime", &record).is_ok(), 606 - "Should accept datetime: {}", datetime); 705 + assert!( 706 + validator 707 + .validate_record("com.example.datetime", &record) 708 + .is_ok(), 709 + "Should accept datetime: {}", 710 + datetime 711 + ); 607 712 } 608 - 713 + 609 714 let invalid_datetime = json!({ "datetime": "bad date" }); 610 - assert!(validator.validate_record("com.example.datetime", &invalid_datetime).is_err()); 611 - 715 + assert!( 716 + validator 717 + .validate_record("com.example.datetime", &invalid_datetime) 718 + .is_err() 719 + ); 720 + 612 721 // URI format tests 613 722 let valid_uris = vec![ 614 723 "https://example.com", 615 724 "https://example.com/with/path", 616 725 "https://example.com/with/path?and=query", 617 - "at://bsky.social", 618 - "did:example:test" 726 + "at://bsky.social", 727 + "did:example:test", 619 728 ]; 620 - 729 + 621 730 for uri in valid_uris { 622 731 let record = json!({ "uri": uri }); 623 - assert!(validator.validate_record("com.example.uri", &record).is_ok(), 624 - "Should accept URI: {}", uri); 732 + assert!( 733 + validator 734 + .validate_record("com.example.uri", &record) 735 + .is_ok(), 736 + "Should accept URI: {}", 737 + uri 738 + ); 625 739 } 626 - 740 + 627 741 let invalid_uri = json!({ "uri": "not a uri" }); 628 - assert!(validator.validate_record("com.example.uri", &invalid_uri).is_err()); 629 - 742 + assert!( 743 + validator 744 + .validate_record("com.example.uri", &invalid_uri) 745 + .is_err() 746 + ); 747 + 630 748 // AT-URI format test 631 749 let valid_at_uri = json!({ "atUri": "at://did:web:example.com/com.example.test/self" }); 632 - assert!(validator.validate_record("com.example.atUri", &valid_at_uri).is_ok()); 633 - 750 + assert!( 751 + validator 752 + .validate_record("com.example.atUri", &valid_at_uri) 753 + .is_ok() 754 + ); 755 + 634 756 let invalid_at_uri = json!({ "atUri": "http://not-atproto.com" }); 635 - assert!(validator.validate_record("com.example.atUri", &invalid_at_uri).is_err()); 636 - 637 - // DID format tests 638 - let valid_dids = vec![ 639 - "did:web:example.com", 640 - "did:plc:12345678abcdefghijklmnop" 641 - ]; 642 - 757 + assert!( 758 + validator 759 + .validate_record("com.example.atUri", &invalid_at_uri) 760 + .is_err() 761 + ); 762 + 763 + // DID format tests 764 + let valid_dids = vec!["did:web:example.com", "did:plc:12345678abcdefghijklmnop"]; 765 + 643 766 for did in valid_dids { 644 767 let record = json!({ "did": did }); 645 - assert!(validator.validate_record("com.example.did", &record).is_ok(), 646 - "Should accept DID: {}", did); 768 + assert!( 769 + validator 770 + .validate_record("com.example.did", &record) 771 + .is_ok(), 772 + "Should accept DID: {}", 773 + did 774 + ); 647 775 } 648 - 776 + 649 777 let invalid_dids = vec!["bad did", "did:short"]; 650 - 778 + 651 779 for did in invalid_dids { 652 780 let record = json!({ "did": did }); 653 - assert!(validator.validate_record("com.example.did", &record).is_err(), 654 - "Should reject DID: {}", did); 781 + assert!( 782 + validator 783 + .validate_record("com.example.did", &record) 784 + .is_err(), 785 + "Should reject DID: {}", 786 + did 787 + ); 655 788 } 656 - 789 + 657 790 // Handle format tests 658 - let valid_handles = vec![ 659 - "test.bsky.social", 660 - "bsky.test" 661 - ]; 662 - 791 + let valid_handles = vec!["test.bsky.social", "bsky.test"]; 792 + 663 793 for handle in valid_handles { 664 794 let record = json!({ "handle": handle }); 665 - assert!(validator.validate_record("com.example.handle", &record).is_ok(), 666 - "Should accept handle: {}", handle); 795 + assert!( 796 + validator 797 + .validate_record("com.example.handle", &record) 798 + .is_ok(), 799 + "Should accept handle: {}", 800 + handle 801 + ); 667 802 } 668 - 803 + 669 804 let invalid_handles = vec!["bad handle", "-bad-.test"]; 670 - 805 + 671 806 for handle in invalid_handles { 672 807 let record = json!({ "handle": handle }); 673 - assert!(validator.validate_record("com.example.handle", &record).is_err(), 674 - "Should reject handle: {}", handle); 808 + assert!( 809 + validator 810 + .validate_record("com.example.handle", &record) 811 + .is_err(), 812 + "Should reject handle: {}", 813 + handle 814 + ); 675 815 } 676 - 816 + 677 817 // AT-identifier format tests 678 - let valid_at_identifiers = vec![ 679 - "bsky.test", 680 - "did:plc:12345678abcdefghijklmnop" 681 - ]; 682 - 818 + let valid_at_identifiers = vec!["bsky.test", "did:plc:12345678abcdefghijklmnop"]; 819 + 683 820 for at_id in valid_at_identifiers { 684 821 let record = json!({ "atIdentifier": at_id }); 685 - assert!(validator.validate_record("com.example.atIdentifier", &record).is_ok(), 686 - "Should accept at-identifier: {}", at_id); 822 + assert!( 823 + validator 824 + .validate_record("com.example.atIdentifier", &record) 825 + .is_ok(), 826 + "Should accept at-identifier: {}", 827 + at_id 828 + ); 687 829 } 688 - 830 + 689 831 let invalid_at_identifiers = vec!["bad id", "-bad-.test"]; 690 - 832 + 691 833 for at_id in invalid_at_identifiers { 692 834 let record = json!({ "atIdentifier": at_id }); 693 - assert!(validator.validate_record("com.example.atIdentifier", &record).is_err(), 694 - "Should reject at-identifier: {}", at_id); 835 + assert!( 836 + validator 837 + .validate_record("com.example.atIdentifier", &record) 838 + .is_err(), 839 + "Should reject at-identifier: {}", 840 + at_id 841 + ); 695 842 } 696 - 843 + 697 844 // NSID format tests 698 - let valid_nsids = vec![ 699 - "com.atproto.test", 700 - "app.bsky.nested.test" 701 - ]; 702 - 845 + let valid_nsids = vec!["com.atproto.test", "app.bsky.nested.test"]; 846 + 703 847 for nsid in valid_nsids { 704 848 let record = json!({ "nsid": nsid }); 705 - assert!(validator.validate_record("com.example.nsid", &record).is_ok(), 706 - "Should accept NSID: {}", nsid); 849 + assert!( 850 + validator 851 + .validate_record("com.example.nsid", &record) 852 + .is_ok(), 853 + "Should accept NSID: {}", 854 + nsid 855 + ); 707 856 } 708 - 857 + 709 858 let invalid_nsids = vec!["bad nsid", "com.bad-.foo"]; 710 - 859 + 711 860 for nsid in invalid_nsids { 712 861 let record = json!({ "nsid": nsid }); 713 - assert!(validator.validate_record("com.example.nsid", &record).is_err(), 714 - "Should reject NSID: {}", nsid); 862 + assert!( 863 + validator 864 + .validate_record("com.example.nsid", &record) 865 + .is_err(), 866 + "Should reject NSID: {}", 867 + nsid 868 + ); 715 869 } 716 - 870 + 717 871 // CID format test 718 - let valid_cid = json!({ "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" }); 719 - assert!(validator.validate_record("com.example.cid", &valid_cid).is_ok()); 720 - 872 + let valid_cid = 873 + json!({ "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" }); 874 + assert!( 875 + validator 876 + .validate_record("com.example.cid", &valid_cid) 877 + .is_ok() 878 + ); 879 + 721 880 let invalid_cid = json!({ "cid": "abapsdofiuwrpoiasdfuaspdfoiu" }); 722 - assert!(validator.validate_record("com.example.cid", &invalid_cid).is_err()); 723 - 881 + assert!( 882 + validator 883 + .validate_record("com.example.cid", &invalid_cid) 884 + .is_err() 885 + ); 886 + 724 887 // Language format test 725 888 let valid_language = json!({ "language": "en-US-boont" }); 726 - assert!(validator.validate_record("com.example.language", &valid_language).is_ok()); 727 - 889 + assert!( 890 + validator 891 + .validate_record("com.example.language", &valid_language) 892 + .is_ok() 893 + ); 894 + 728 895 let invalid_language = json!({ "language": "not-a-language-" }); 729 - assert!(validator.validate_record("com.example.language", &invalid_language).is_err()); 896 + assert!( 897 + validator 898 + .validate_record("com.example.language", &invalid_language) 899 + .is_err() 900 + ); 730 901 } 731 - 902 + 732 903 #[test] 733 904 fn test_bytes_validation() { 734 905 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 735 - 906 + 736 907 // Valid bytes (base64, 3 bytes when decoded) 737 908 let valid_bytes = json!({ "bytes": "SGVs" }); // 3 bytes when decoded 738 - assert!(validator.validate_record("com.example.byteLength", &valid_bytes).is_ok()); 739 - 909 + assert!( 910 + validator 911 + .validate_record("com.example.byteLength", &valid_bytes) 912 + .is_ok() 913 + ); 914 + 740 915 // Too short (1 byte when decoded) 741 916 let too_short = json!({ "bytes": "SA==" }); 742 - assert!(validator.validate_record("com.example.byteLength", &too_short).is_err()); 743 - 744 - // Too long (5+ bytes when decoded) 917 + assert!( 918 + validator 919 + .validate_record("com.example.byteLength", &too_short) 920 + .is_err() 921 + ); 922 + 923 + // Too long (5+ bytes when decoded) 745 924 let too_long = json!({ "bytes": "SGVsbG9Xb3JsZA==" }); // "HelloWorld" is 10 bytes 746 - assert!(validator.validate_record("com.example.byteLength", &too_long).is_err()); 925 + assert!( 926 + validator 927 + .validate_record("com.example.byteLength", &too_long) 928 + .is_err() 929 + ); 747 930 } 748 - 931 + 749 932 #[test] 750 933 fn test_union_validation() { 751 934 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 752 - 935 + 753 936 // Valid union - open union with object variant 754 937 let valid_union = json!({ 755 938 "unionOpen": { ··· 761 944 "string": "string" 762 945 }, 763 946 "unionClosed": { 764 - "$type": "com.example.kitchenSink#subobject", 947 + "$type": "com.example.kitchenSink#subobject", 765 948 "boolean": true 766 949 } 767 950 }); 768 - 769 - assert!(validator.validate_record("com.example.union", &valid_union).is_ok()); 770 - 951 + 952 + assert!( 953 + validator 954 + .validate_record("com.example.union", &valid_union) 955 + .is_ok() 956 + ); 957 + 771 958 // Valid union - open union with other type 772 959 let valid_open = json!({ 773 960 "unionOpen": { ··· 775 962 }, 776 963 "unionClosed": { 777 964 "$type": "com.example.kitchenSink#subobject", 778 - "boolean": true 965 + "boolean": true 779 966 } 780 967 }); 781 - 968 + 782 969 // This should work for open unions (they allow unknown types) 783 - assert!(validator.validate_record("com.example.union", &valid_open).is_ok()); 784 - 970 + assert!( 971 + validator 972 + .validate_record("com.example.union", &valid_open) 973 + .is_ok() 974 + ); 975 + 785 976 // Missing $type in union 786 977 let missing_type = json!({ 787 978 "unionOpen": {}, 788 979 "unionClosed": {} 789 980 }); 790 - 791 - assert!(validator.validate_record("com.example.union", &missing_type).is_err()); 981 + 982 + assert!( 983 + validator 984 + .validate_record("com.example.union", &missing_type) 985 + .is_err() 986 + ); 792 987 } 793 - 988 + 794 989 #[test] 795 990 fn test_unknown_type() { 796 991 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 797 - 992 + 798 993 // Valid unknown field 799 994 let valid_unknown = json!({ "unknown": { "foo": "bar" } }); 800 - assert!(validator.validate_record("com.example.unknown", &valid_unknown).is_ok()); 801 - 995 + assert!( 996 + validator 997 + .validate_record("com.example.unknown", &valid_unknown) 998 + .is_ok() 999 + ); 1000 + 802 1001 // Missing required unknown field 803 1002 let missing_unknown = json!({}); 804 - assert!(validator.validate_record("com.example.unknown", &missing_unknown).is_err()); 1003 + assert!( 1004 + validator 1005 + .validate_record("com.example.unknown", &missing_unknown) 1006 + .is_err() 1007 + ); 805 1008 } 806 - 807 - #[test] 1009 + 1010 + #[test] 808 1011 fn test_type_validation_errors() { 809 1012 let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 810 - 1013 + 811 1014 let base_record = json!({ 812 1015 "object": { 813 1016 "object": { "boolean": true }, ··· 825 1028 "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" 826 1029 } 827 1030 }); 828 - 1031 + 829 1032 // Wrong boolean type 830 1033 let mut bad_boolean = base_record.clone(); 831 1034 bad_boolean["object"]["object"]["boolean"] = json!("not boolean"); 832 - assert!(validator.validate_record("com.example.kitchenSink", &bad_boolean).is_err()); 833 - 834 - // Wrong object type 1035 + assert!( 1036 + validator 1037 + .validate_record("com.example.kitchenSink", &bad_boolean) 1038 + .is_err() 1039 + ); 1040 + 1041 + // Wrong object type 835 1042 let mut bad_object = base_record.clone(); 836 1043 bad_object["object"] = json!(true); 837 - assert!(validator.validate_record("com.example.kitchenSink", &bad_object).is_err()); 838 - 1044 + assert!( 1045 + validator 1046 + .validate_record("com.example.kitchenSink", &bad_object) 1047 + .is_err() 1048 + ); 1049 + 839 1050 // Wrong array type 840 1051 let mut bad_array = base_record.clone(); 841 1052 bad_array["array"] = json!(1234); 842 - assert!(validator.validate_record("com.example.kitchenSink", &bad_array).is_err()); 843 - 1053 + assert!( 1054 + validator 1055 + .validate_record("com.example.kitchenSink", &bad_array) 1056 + .is_err() 1057 + ); 1058 + 844 1059 // Wrong integer type 845 1060 let mut bad_integer = base_record.clone(); 846 1061 bad_integer["integer"] = json!(true); 847 - assert!(validator.validate_record("com.example.kitchenSink", &bad_integer).is_err()); 848 - 1062 + assert!( 1063 + validator 1064 + .validate_record("com.example.kitchenSink", &bad_integer) 1065 + .is_err() 1066 + ); 1067 + 849 1068 // Wrong string type 850 1069 let mut bad_string = base_record.clone(); 851 1070 bad_string["string"] = json!({}); 852 - assert!(validator.validate_record("com.example.kitchenSink", &bad_string).is_err()); 853 - 1071 + assert!( 1072 + validator 1073 + .validate_record("com.example.kitchenSink", &bad_string) 1074 + .is_err() 1075 + ); 1076 + 854 1077 // Wrong bytes type 855 1078 let mut bad_bytes = base_record.clone(); 856 1079 bad_bytes["bytes"] = json!(1234); 857 - assert!(validator.validate_record("com.example.kitchenSink", &bad_bytes).is_err()); 858 - 1080 + assert!( 1081 + validator 1082 + .validate_record("com.example.kitchenSink", &bad_bytes) 1083 + .is_err() 1084 + ); 1085 + 859 1086 // Wrong CID link type 860 1087 let mut bad_cid_link = base_record.clone(); 861 - bad_cid_link["cidLink"] = json!("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a"); 862 - assert!(validator.validate_record("com.example.kitchenSink", &bad_cid_link).is_err()); 1088 + bad_cid_link["cidLink"] = 1089 + json!("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a"); 1090 + assert!( 1091 + validator 1092 + .validate_record("com.example.kitchenSink", &bad_cid_link) 1093 + .is_err() 1094 + ); 863 1095 } 864 - 1096 + 865 1097 #[test] 866 1098 fn test_lexicon_definitions_validation() { 867 1099 let lexicons = vec![serde_json::json!({ 868 - "id": "social.slices.lexicon", 1100 + "id": "network.slices.lexicon", 869 1101 "defs": { 870 1102 "main": { 871 1103 "type": "record", ··· 880 1112 } 881 1113 } 882 1114 })]; 883 - 1115 + 884 1116 let validator = LexiconValidator::new(lexicons).unwrap(); 885 - 1117 + 886 1118 // Valid lexicon record with valid definitions JSON 887 1119 let valid_record = serde_json::json!({ 888 1120 "nsid": "com.example.test", 889 1121 "definitions": r#"{"main": {"type": "record", "record": {"type": "object", "properties": {"title": {"type": "string"}}}}}"# 890 1122 }); 891 - 892 - let result = validator.validate_record("social.slices.lexicon", &valid_record); 893 - assert!(result.is_ok(), "Valid lexicon definitions should pass: {:?}", result); 894 - 1123 + 1124 + let result = validator.validate_record("network.slices.lexicon", &valid_record); 1125 + assert!( 1126 + result.is_ok(), 1127 + "Valid lexicon definitions should pass: {:?}", 1128 + result 1129 + ); 1130 + 895 1131 // Invalid JSON in definitions field 896 1132 let invalid_json_record = serde_json::json!({ 897 - "nsid": "com.example.test", 1133 + "nsid": "com.example.test", 898 1134 "definitions": r#"{"main": invalid json}"# 899 1135 }); 900 - 901 - let result = validator.validate_record("social.slices.lexicon", &invalid_json_record); 1136 + 1137 + let result = validator.validate_record("network.slices.lexicon", &invalid_json_record); 902 1138 assert!(result.is_err()); 903 - assert!(result.unwrap_err().to_string().contains("Invalid JSON in definitions field")); 904 - 1139 + assert!( 1140 + result 1141 + .unwrap_err() 1142 + .to_string() 1143 + .contains("Invalid JSON in definitions field") 1144 + ); 1145 + 905 1146 // Invalid lexicon structure (missing type) 906 1147 let invalid_structure_record = serde_json::json!({ 907 1148 "nsid": "com.example.test", 908 1149 "definitions": r#"{"main": {"record": {"type": "object"}}}"# 909 1150 }); 910 - 911 - let result = validator.validate_record("social.slices.lexicon", &invalid_structure_record); 1151 + 1152 + let result = validator.validate_record("network.slices.lexicon", &invalid_structure_record); 912 1153 assert!(result.is_err()); 913 - assert!(result.unwrap_err().to_string().contains("missing required 'type' field")); 914 - 1154 + assert!( 1155 + result 1156 + .unwrap_err() 1157 + .to_string() 1158 + .contains("missing required 'type' field") 1159 + ); 1160 + 915 1161 // Invalid definition name (with hyphen - not camelCase) 916 1162 let invalid_name_record = serde_json::json!({ 917 1163 "nsid": "com.example.test", 918 1164 "definitions": r#"{"my-invalid-name": {"type": "object"}}"# 919 1165 }); 920 - 921 - let result = validator.validate_record("social.slices.lexicon", &invalid_name_record); 1166 + 1167 + let result = validator.validate_record("network.slices.lexicon", &invalid_name_record); 922 1168 assert!(result.is_err()); 923 - assert!(result.unwrap_err().to_string().contains("must be camelCase")); 924 - 1169 + assert!( 1170 + result 1171 + .unwrap_err() 1172 + .to_string() 1173 + .contains("must be camelCase") 1174 + ); 1175 + 925 1176 // Valid camelCase definition names should work 926 1177 let valid_camel_record = serde_json::json!({ 927 1178 "nsid": "com.example.test", 928 1179 "definitions": r#"{"listViewBasic": {"type": "object"}, "starterPackView": {"type": "object"}}"# 929 1180 }); 930 - 931 - let result = validator.validate_record("social.slices.lexicon", &valid_camel_record); 932 - assert!(result.is_ok(), "CamelCase definition names should be valid: {:?}", result); 1181 + 1182 + let result = validator.validate_record("network.slices.lexicon", &valid_camel_record); 1183 + assert!( 1184 + result.is_ok(), 1185 + "CamelCase definition names should be valid: {:?}", 1186 + result 1187 + ); 933 1188 } 934 - 1189 + 935 1190 #[test] 936 1191 fn test_lexicon_set_completeness_validation() { 937 1192 // Test with complete lexicon set (should pass) ··· 961 1216 } 962 1217 } 963 1218 } 964 - }) 1219 + }), 965 1220 ]; 966 - 1221 + 967 1222 let validator = LexiconValidator::new(complete_lexicons).unwrap(); 968 1223 let result = validator.validate_lexicon_set_completeness(); 969 - assert!(result.is_ok(), "Complete lexicon set should pass validation"); 970 - 971 - // Test with missing lexicon reference (should fail) 972 - let incomplete_lexicons = vec![ 973 - serde_json::json!({ 974 - "id": "com.example.posts", 975 - "defs": { 976 - "main": { 977 - "type": "record", 978 - "record": { 979 - "type": "object", 980 - "properties": { 981 - "author": { "type": "ref", "ref": "com.example.missing#user" }, 982 - "content": { "type": "string" } 983 - } 1224 + assert!( 1225 + result.is_ok(), 1226 + "Complete lexicon set should pass validation" 1227 + ); 1228 + 1229 + // Test with missing lexicon reference (should fail) 1230 + let incomplete_lexicons = vec![serde_json::json!({ 1231 + "id": "com.example.posts", 1232 + "defs": { 1233 + "main": { 1234 + "type": "record", 1235 + "record": { 1236 + "type": "object", 1237 + "properties": { 1238 + "author": { "type": "ref", "ref": "com.example.missing#user" }, 1239 + "content": { "type": "string" } 984 1240 } 985 1241 } 986 1242 } 987 - }) 988 - ]; 989 - 1243 + } 1244 + })]; 1245 + 990 1246 let validator = LexiconValidator::new(incomplete_lexicons).unwrap(); 991 1247 let result = validator.validate_lexicon_set_completeness(); 992 1248 assert!(result.is_err()); 993 - assert!(result.unwrap_err().contains("Missing lexicon 'com.example.missing'")); 994 - 1249 + assert!( 1250 + result 1251 + .unwrap_err() 1252 + .contains("Missing lexicon 'com.example.missing'") 1253 + ); 1254 + 995 1255 // Test with local references (should pass - local refs are valid) 996 - let local_ref_lexicons = vec![ 997 - serde_json::json!({ 998 - "id": "com.example.media", 999 - "defs": { 1000 - "main": { 1001 - "type": "record", 1002 - "record": { 1003 - "type": "object", 1004 - "properties": { 1005 - "image": { "type": "ref", "ref": "#imageObject" } 1006 - } 1007 - } 1008 - }, 1009 - "imageObject": { 1256 + let local_ref_lexicons = vec![serde_json::json!({ 1257 + "id": "com.example.media", 1258 + "defs": { 1259 + "main": { 1260 + "type": "record", 1261 + "record": { 1010 1262 "type": "object", 1011 1263 "properties": { 1012 - "url": { "type": "string" } 1264 + "image": { "type": "ref", "ref": "#imageObject" } 1013 1265 } 1014 1266 } 1267 + }, 1268 + "imageObject": { 1269 + "type": "object", 1270 + "properties": { 1271 + "url": { "type": "string" } 1272 + } 1015 1273 } 1016 - }) 1017 - ]; 1018 - 1274 + } 1275 + })]; 1276 + 1019 1277 let validator = LexiconValidator::new(local_ref_lexicons).unwrap(); 1020 1278 let result = validator.validate_lexicon_set_completeness(); 1021 - assert!(result.is_ok(), "Local references should not cause validation errors"); 1022 - 1279 + assert!( 1280 + result.is_ok(), 1281 + "Local references should not cause validation errors" 1282 + ); 1283 + 1023 1284 // Test with direct lexicon reference (no # fragment) 1024 - let direct_ref_lexicons = vec![ 1025 - serde_json::json!({ 1026 - "id": "com.example.gallery", 1027 - "defs": { 1028 - "main": { 1029 - "type": "record", 1030 - "record": { 1031 - "type": "object", 1032 - "properties": { 1033 - "facets": { 1034 - "type": "array", 1035 - "items": { 1036 - "type": "ref", 1037 - "ref": "app.bsky.richtext.facet" 1038 - } 1285 + let direct_ref_lexicons = vec![serde_json::json!({ 1286 + "id": "com.example.gallery", 1287 + "defs": { 1288 + "main": { 1289 + "type": "record", 1290 + "record": { 1291 + "type": "object", 1292 + "properties": { 1293 + "facets": { 1294 + "type": "array", 1295 + "items": { 1296 + "type": "ref", 1297 + "ref": "app.bsky.richtext.facet" 1039 1298 } 1040 1299 } 1041 1300 } 1042 1301 } 1043 1302 } 1044 - }) 1045 - ]; 1046 - 1303 + } 1304 + })]; 1305 + 1047 1306 let validator = LexiconValidator::new(direct_ref_lexicons).unwrap(); 1048 1307 let result = validator.validate_lexicon_set_completeness(); 1049 1308 assert!(result.is_err()); 1050 - assert!(result.unwrap_err().contains("Missing lexicon 'app.bsky.richtext.facet'")); 1051 - 1309 + assert!( 1310 + result 1311 + .unwrap_err() 1312 + .contains("Missing lexicon 'app.bsky.richtext.facet'") 1313 + ); 1314 + 1052 1315 // Test with union refs array 1053 - let union_refs_lexicons = vec![ 1054 - serde_json::json!({ 1055 - "id": "com.example.content", 1056 - "defs": { 1057 - "main": { 1058 - "type": "record", 1059 - "record": { 1060 - "type": "object", 1061 - "properties": { 1062 - "subject": { 1063 - "type": "union", 1064 - "refs": [ 1065 - "com.example.missing#post", 1066 - "app.bsky.feed.post" 1067 - ] 1068 - } 1316 + let union_refs_lexicons = vec![serde_json::json!({ 1317 + "id": "com.example.content", 1318 + "defs": { 1319 + "main": { 1320 + "type": "record", 1321 + "record": { 1322 + "type": "object", 1323 + "properties": { 1324 + "subject": { 1325 + "type": "union", 1326 + "refs": [ 1327 + "com.example.missing#post", 1328 + "app.bsky.feed.post" 1329 + ] 1069 1330 } 1070 1331 } 1071 1332 } 1072 1333 } 1073 - }) 1074 - ]; 1075 - 1334 + } 1335 + })]; 1336 + 1076 1337 let validator = LexiconValidator::new(union_refs_lexicons).unwrap(); 1077 1338 let result = validator.validate_lexicon_set_completeness(); 1078 1339 assert!(result.is_err()); 1079 1340 let error_msg = result.unwrap_err(); 1080 1341 assert!(error_msg.contains("Missing lexicon 'com.example.missing'")); 1081 1342 assert!(error_msg.contains("Missing lexicon 'app.bsky.feed.post'")); 1082 - 1343 + 1083 1344 // Test that validation passes when all references are present 1084 1345 let complete_with_direct_refs = vec![ 1085 1346 serde_json::json!({ ··· 1112 1373 } 1113 1374 } 1114 1375 } 1115 - }) 1376 + }), 1116 1377 ]; 1117 - 1378 + 1118 1379 let validator = LexiconValidator::new(complete_with_direct_refs).unwrap(); 1119 1380 let result = validator.validate_lexicon_set_completeness(); 1120 - assert!(result.is_ok(), "Should pass when all direct references are present"); 1381 + assert!( 1382 + result.is_ok(), 1383 + "Should pass when all direct references are present" 1384 + ); 1121 1385 } 1122 - } 1386 + }
+461 -266
api/src/lexicon/validator.rs
··· 1 - use std::collections::HashMap; 2 - use serde_json::Value; 3 1 use chrono::DateTime; 4 2 use regex::Regex; 3 + use serde_json::Value; 4 + use std::collections::HashMap; 5 5 6 - use crate::database::Database; 7 6 use super::errors::ValidationError; 8 7 use super::types::{LexiconDoc, StringFormat, ValidationContext}; 8 + use crate::database::Database; 9 9 10 10 #[derive(Clone)] 11 11 pub struct LexiconValidator { ··· 16 16 /// Create a new validator with the given lexicon documents 17 17 pub fn new(lexicons: Vec<Value>) -> Result<Self, ValidationError> { 18 18 let mut lexicon_map = HashMap::new(); 19 - 19 + 20 20 for lexicon_value in lexicons { 21 21 let id = lexicon_value["id"] 22 22 .as_str() 23 23 .ok_or_else(|| ValidationError::InvalidSchema("Missing lexicon id".to_string()))? 24 24 .to_string(); 25 - 25 + 26 26 let defs = lexicon_value["defs"].clone(); 27 27 if defs.is_null() { 28 - return Err(ValidationError::InvalidSchema(format!("Missing defs in lexicon {}", id))); 28 + return Err(ValidationError::InvalidSchema(format!( 29 + "Missing defs in lexicon {}", 30 + id 31 + ))); 29 32 } 30 - 33 + 31 34 lexicon_map.insert(id.clone(), LexiconDoc { id, defs }); 32 35 } 33 - 34 - Ok(Self { lexicons: lexicon_map }) 36 + 37 + Ok(Self { 38 + lexicons: lexicon_map, 39 + }) 35 40 } 36 - 41 + 37 42 /// Load lexicons for a specific slice from the database 38 43 pub async fn for_slice(db: &Database, slice_uri: &str) -> Result<Self, ValidationError> { 39 - let lexicon_records = db.get_lexicons_by_slice(slice_uri) 40 - .await 41 - .map_err(|e| ValidationError::Unknown { 42 - path: "database".to_string(), 43 - message: e.to_string(), 44 - })?; 45 - 44 + let lexicon_records = 45 + db.get_lexicons_by_slice(slice_uri) 46 + .await 47 + .map_err(|e| ValidationError::Unknown { 48 + path: "database".to_string(), 49 + message: e.to_string(), 50 + })?; 51 + 46 52 // lexicon_records already has the correct format from get_lexicons_by_slice 47 53 let lexicons: Vec<serde_json::Value> = lexicon_records 48 54 .into_iter() ··· 55 61 lexicon 56 62 }) 57 63 .collect(); 58 - 64 + 59 65 Self::new(lexicons) 60 66 } 61 - 67 + 62 68 /// Validate a record against its collection's lexicon 63 69 pub fn validate_record(&self, collection: &str, record: &Value) -> Result<(), ValidationError> { 64 70 // Parse collection string which might have fragment (#object, #main, etc) 65 71 let parts: Vec<&str> = collection.split('#').collect(); 66 72 let nsid = parts[0]; 67 73 let fragment = parts.get(1).map(|s| *s).unwrap_or("main"); 68 - 69 - let lexicon = self.lexicons.get(nsid) 74 + 75 + let lexicon = self 76 + .lexicons 77 + .get(nsid) 70 78 .ok_or_else(|| ValidationError::LexiconNotFound(nsid.to_string()))?; 71 - 79 + 72 80 // Get the definition schema 73 81 let def_schema = &lexicon.defs[fragment]; 74 82 if def_schema.is_null() { 75 - return Err(ValidationError::InvalidSchema( 76 - format!("No {} definition in lexicon {}", fragment, nsid) 77 - )); 83 + return Err(ValidationError::InvalidSchema(format!( 84 + "No {} definition in lexicon {}", 85 + fragment, nsid 86 + ))); 78 87 } 79 - 88 + 80 89 // For record types, validate against the record schema 81 90 let record_schema = if def_schema["type"] == "record" { 82 91 &def_schema["record"] ··· 84 93 // For other types like object, validate directly 85 94 def_schema 86 95 }; 87 - 96 + 88 97 let ctx = ValidationContext::new(); 89 - 90 - // Special validation for social.slices.lexicon records 91 - if nsid == "social.slices.lexicon" { 98 + 99 + // Special validation for network.slices.lexicon records 100 + if nsid == "network.slices.lexicon" { 92 101 self.validate_lexicon_record(record, record_schema, &ctx)?; 93 102 } else { 94 103 self.validate_value(record, record_schema, &ctx)?; 95 104 } 96 - 105 + 97 106 Ok(()) 98 107 } 99 - 100 - /// Special validation for social.slices.lexicon records 101 - fn validate_lexicon_record(&self, record: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 108 + 109 + /// Special validation for network.slices.lexicon records 110 + fn validate_lexicon_record( 111 + &self, 112 + record: &Value, 113 + schema: &Value, 114 + ctx: &ValidationContext, 115 + ) -> Result<(), ValidationError> { 102 116 // First, validate against the normal schema 103 117 self.validate_value(record, schema, ctx)?; 104 - 118 + 105 119 // Then, validate that the "definitions" field contains valid lexicon JSON 106 120 if let Some(definitions_str) = record.get("definitions").and_then(|v| v.as_str()) { 107 121 // Parse the definitions JSON 108 - let definitions_json: Value = serde_json::from_str(definitions_str) 109 - .map_err(|e| ValidationError::InvalidSchema( 110 - format!("Invalid JSON in definitions field: {}", e) 111 - ))?; 112 - 122 + let definitions_json: Value = serde_json::from_str(definitions_str).map_err(|e| { 123 + ValidationError::InvalidSchema(format!("Invalid JSON in definitions field: {}", e)) 124 + })?; 125 + 113 126 // Validate that it looks like a proper lexicon definition 114 127 self.validate_lexicon_definitions(&definitions_json)?; 115 128 } 116 - 129 + 117 130 Ok(()) 118 131 } 119 - 132 + 120 133 /// Validate that a JSON value contains valid lexicon definitions 121 134 fn validate_lexicon_definitions(&self, definitions: &Value) -> Result<(), ValidationError> { 122 - let definitions_obj = definitions.as_object() 123 - .ok_or_else(|| ValidationError::InvalidSchema( 124 - "Lexicon definitions must be a JSON object".to_string() 125 - ))?; 126 - 135 + let definitions_obj = definitions.as_object().ok_or_else(|| { 136 + ValidationError::InvalidSchema("Lexicon definitions must be a JSON object".to_string()) 137 + })?; 138 + 127 139 // Each key should be a valid definition name, each value should be a valid definition 128 140 for (def_name, def_value) in definitions_obj { 129 141 // Validate definition name (should be camelCase identifier - letters and numbers only) 130 142 if def_name.is_empty() || !def_name.chars().all(|c| c.is_ascii_alphanumeric()) { 131 - return Err(ValidationError::InvalidSchema( 132 - format!("Invalid definition name '{}': must be camelCase (letters and numbers only)", def_name) 133 - )); 143 + return Err(ValidationError::InvalidSchema(format!( 144 + "Invalid definition name '{}': must be camelCase (letters and numbers only)", 145 + def_name 146 + ))); 134 147 } 135 - 148 + 136 149 // Validate definition structure 137 - let def_type = def_value.get("type") 150 + let def_type = def_value 151 + .get("type") 138 152 .and_then(|v| v.as_str()) 139 - .ok_or_else(|| ValidationError::InvalidSchema( 140 - format!("Definition '{}' missing required 'type' field", def_name) 141 - ))?; 142 - 153 + .ok_or_else(|| { 154 + ValidationError::InvalidSchema(format!( 155 + "Definition '{}' missing required 'type' field", 156 + def_name 157 + )) 158 + })?; 159 + 143 160 // Validate based on definition type 144 161 match def_type { 145 162 "record" => self.validate_record_definition(def_name, def_value)?, 146 163 "object" => self.validate_object_definition(def_name, def_value)?, 147 - "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes" | "cid-link" | "unknown" => { 164 + "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes" 165 + | "cid-link" | "unknown" => { 148 166 // Basic types are valid, could add more specific validation here 149 - }, 150 - _ => return Err(ValidationError::InvalidSchema( 151 - format!("Definition '{}' has unknown type '{}'", def_name, def_type) 152 - )) 167 + } 168 + _ => { 169 + return Err(ValidationError::InvalidSchema(format!( 170 + "Definition '{}' has unknown type '{}'", 171 + def_name, def_type 172 + ))); 173 + } 153 174 } 154 175 } 155 - 176 + 156 177 Ok(()) 157 178 } 158 - 179 + 159 180 /// Validate a record definition structure 160 - fn validate_record_definition(&self, def_name: &str, def_value: &Value) -> Result<(), ValidationError> { 181 + fn validate_record_definition( 182 + &self, 183 + def_name: &str, 184 + def_value: &Value, 185 + ) -> Result<(), ValidationError> { 161 186 // Record definitions should have a "record" field 162 - let record_def = def_value.get("record") 163 - .ok_or_else(|| ValidationError::InvalidSchema( 164 - format!("Record definition '{}' missing 'record' field", def_name) 165 - ))?; 166 - 187 + let record_def = def_value.get("record").ok_or_else(|| { 188 + ValidationError::InvalidSchema(format!( 189 + "Record definition '{}' missing 'record' field", 190 + def_name 191 + )) 192 + })?; 193 + 167 194 // The record field should be an object type 168 195 if record_def.get("type").and_then(|v| v.as_str()) != Some("object") { 169 - return Err(ValidationError::InvalidSchema( 170 - format!("Record definition '{}' record field must be type 'object'", def_name) 171 - )); 196 + return Err(ValidationError::InvalidSchema(format!( 197 + "Record definition '{}' record field must be type 'object'", 198 + def_name 199 + ))); 172 200 } 173 - 201 + 174 202 // Validate properties if they exist 175 203 if let Some(properties) = record_def.get("properties") { 176 204 if !properties.is_object() { 177 - return Err(ValidationError::InvalidSchema( 178 - format!("Record definition '{}' properties must be an object", def_name) 179 - )); 205 + return Err(ValidationError::InvalidSchema(format!( 206 + "Record definition '{}' properties must be an object", 207 + def_name 208 + ))); 180 209 } 181 210 } 182 - 211 + 183 212 Ok(()) 184 213 } 185 - 186 - /// Validate an object definition structure 187 - fn validate_object_definition(&self, def_name: &str, def_value: &Value) -> Result<(), ValidationError> { 214 + 215 + /// Validate an object definition structure 216 + fn validate_object_definition( 217 + &self, 218 + def_name: &str, 219 + def_value: &Value, 220 + ) -> Result<(), ValidationError> { 188 221 // Object definitions should have properties 189 222 if let Some(properties) = def_value.get("properties") { 190 223 if !properties.is_object() { 191 - return Err(ValidationError::InvalidSchema( 192 - format!("Object definition '{}' properties must be an object", def_name) 193 - )); 224 + return Err(ValidationError::InvalidSchema(format!( 225 + "Object definition '{}' properties must be an object", 226 + def_name 227 + ))); 194 228 } 195 229 } 196 - 230 + 197 231 // Validate required field if it exists 198 232 if let Some(required) = def_value.get("required") { 199 233 if !required.is_array() { 200 - return Err(ValidationError::InvalidSchema( 201 - format!("Object definition '{}' required field must be an array", def_name) 202 - )); 234 + return Err(ValidationError::InvalidSchema(format!( 235 + "Object definition '{}' required field must be an array", 236 + def_name 237 + ))); 203 238 } 204 239 } 205 - 240 + 206 241 Ok(()) 207 242 } 208 - 243 + 209 244 /// Validate that all cross-lexicon references can be resolved within the current lexicon set 210 245 /// This is used before code generation to ensure no missing references 211 246 pub fn validate_lexicon_set_completeness(&self) -> Result<(), String> { 212 247 let mut missing_refs = Vec::new(); 213 - 248 + 214 249 for (lexicon_id, lexicon) in &self.lexicons { 215 250 self.collect_missing_references(lexicon_id, &lexicon.defs, &mut missing_refs)?; 216 251 } 217 - 252 + 218 253 if missing_refs.is_empty() { 219 254 Ok(()) 220 255 } else { 221 256 Err(missing_refs.join(", ")) 222 257 } 223 258 } 224 - 259 + 225 260 /// Recursively collect missing cross-lexicon references 226 - fn collect_missing_references(&self, current_lexicon: &str, value: &Value, missing_refs: &mut Vec<String>) -> Result<(), String> { 261 + fn collect_missing_references( 262 + &self, 263 + current_lexicon: &str, 264 + value: &Value, 265 + missing_refs: &mut Vec<String>, 266 + ) -> Result<(), String> { 227 267 match value { 228 268 Value::Object(obj) => { 229 269 // Check for ref field ··· 236 276 if parts.len() == 2 { 237 277 let target_lexicon = parts[0]; 238 278 if !self.lexicons.contains_key(target_lexicon) { 239 - let missing_ref = format!("Missing lexicon '{}' (referenced as '{}')", target_lexicon, ref_str); 279 + let missing_ref = format!( 280 + "Missing lexicon '{}' (referenced as '{}')", 281 + target_lexicon, ref_str 282 + ); 240 283 if !missing_refs.contains(&missing_ref) { 241 284 missing_refs.push(missing_ref); 242 285 } ··· 246 289 // Direct lexicon reference without fragment (e.g., app.bsky.richtext.facet) 247 290 // This references the main definition of another lexicon 248 291 if !self.lexicons.contains_key(ref_str) { 249 - let missing_ref = format!("Missing lexicon '{}' (referenced as '{}')", ref_str, ref_str); 292 + let missing_ref = format!( 293 + "Missing lexicon '{}' (referenced as '{}')", 294 + ref_str, ref_str 295 + ); 250 296 if !missing_refs.contains(&missing_ref) { 251 297 missing_refs.push(missing_ref); 252 298 } ··· 254 300 } 255 301 } 256 302 } 257 - 303 + 258 304 // Check for refs array (used in union types) 259 305 if let Some(refs_array) = obj.get("refs").and_then(|v| v.as_array()) { 260 306 for ref_value in refs_array { ··· 266 312 if parts.len() == 2 { 267 313 let target_lexicon = parts[0]; 268 314 if !self.lexicons.contains_key(target_lexicon) { 269 - let missing_ref = format!("Missing lexicon '{}' (referenced as '{}')", target_lexicon, ref_str); 315 + let missing_ref = format!( 316 + "Missing lexicon '{}' (referenced as '{}')", 317 + target_lexicon, ref_str 318 + ); 270 319 if !missing_refs.contains(&missing_ref) { 271 320 missing_refs.push(missing_ref); 272 321 } ··· 275 324 } else if ref_str.contains('.') { 276 325 // Direct lexicon reference 277 326 if !self.lexicons.contains_key(ref_str) { 278 - let missing_ref = format!("Missing lexicon '{}' (referenced as '{}')", ref_str, ref_str); 327 + let missing_ref = format!( 328 + "Missing lexicon '{}' (referenced as '{}')", 329 + ref_str, ref_str 330 + ); 279 331 if !missing_refs.contains(&missing_ref) { 280 332 missing_refs.push(missing_ref); 281 333 } ··· 285 337 } 286 338 } 287 339 } 288 - 340 + 289 341 // Recursively check nested objects 290 342 for (_, nested_value) in obj { 291 343 self.collect_missing_references(current_lexicon, nested_value, missing_refs)?; ··· 301 353 // Primitive values, nothing to check 302 354 } 303 355 } 304 - 356 + 305 357 Ok(()) 306 358 } 307 - 359 + 308 360 /// Validate a value against a schema definition 309 - fn validate_value(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 361 + fn validate_value( 362 + &self, 363 + value: &Value, 364 + schema: &Value, 365 + ctx: &ValidationContext, 366 + ) -> Result<(), ValidationError> { 310 367 let schema_type = schema["type"].as_str().unwrap_or(""); 311 - 368 + 312 369 match schema_type { 313 370 "object" => self.validate_object(value, schema, ctx), 314 371 "string" => self.validate_string(value, schema, ctx), ··· 322 379 "cid-link" => self.validate_cid_link(value, schema, ctx), 323 380 "unknown" => Ok(()), // Unknown type accepts anything 324 381 "null" => self.validate_null(value, schema, ctx), 325 - "" => Err(ValidationError::InvalidSchema(format!("Missing type in schema at {}", ctx.path_string()))), 326 - _ => Err(ValidationError::InvalidSchema(format!("Unknown schema type: {} at {}", schema_type, ctx.path_string()))), 382 + "" => Err(ValidationError::InvalidSchema(format!( 383 + "Missing type in schema at {}", 384 + ctx.path_string() 385 + ))), 386 + _ => Err(ValidationError::InvalidSchema(format!( 387 + "Unknown schema type: {} at {}", 388 + schema_type, 389 + ctx.path_string() 390 + ))), 327 391 } 328 392 } 329 - 393 + 330 394 /// Validate an object against an object schema 331 - fn validate_object(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 332 - let obj = value.as_object().ok_or_else(|| ValidationError::TypeMismatch { 333 - path: ctx.path_string(), 334 - expected: "object".to_string(), 335 - actual: format!("{:?}", value), 336 - })?; 337 - 395 + fn validate_object( 396 + &self, 397 + value: &Value, 398 + schema: &Value, 399 + ctx: &ValidationContext, 400 + ) -> Result<(), ValidationError> { 401 + let obj = value 402 + .as_object() 403 + .ok_or_else(|| ValidationError::TypeMismatch { 404 + path: ctx.path_string(), 405 + expected: "object".to_string(), 406 + actual: format!("{:?}", value), 407 + })?; 408 + 338 409 // Check required fields 339 410 if let Some(required) = schema["required"].as_array() { 340 411 for req_field in required { ··· 347 418 } 348 419 } 349 420 } 350 - 421 + 351 422 // Validate properties 352 423 if let Some(properties) = schema["properties"].as_object() { 353 424 for (prop_name, prop_schema) in properties { 354 425 if let Some(prop_value) = obj.get(prop_name) { 355 426 // Check if field can be null 356 427 let nullable = if let Some(nullable_fields) = schema["nullable"].as_array() { 357 - nullable_fields.iter().any(|f| f.as_str() == Some(prop_name)) 428 + nullable_fields 429 + .iter() 430 + .any(|f| f.as_str() == Some(prop_name)) 358 431 } else { 359 432 false 360 433 }; 361 - 434 + 362 435 if prop_value.is_null() && !nullable { 363 436 return Err(ValidationError::TypeMismatch { 364 437 path: ctx.with_field(prop_name).path_string(), ··· 366 439 actual: "null".to_string(), 367 440 }); 368 441 } 369 - 442 + 370 443 if !prop_value.is_null() { 371 444 self.validate_value(prop_value, prop_schema, &ctx.with_field(prop_name))?; 372 445 } 373 446 } 374 447 } 375 448 } 376 - 449 + 377 450 Ok(()) 378 451 } 379 - 452 + 380 453 /// Validate a string value 381 - fn validate_string(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 382 - let string_val = value.as_str().ok_or_else(|| ValidationError::TypeMismatch { 383 - path: ctx.path_string(), 384 - expected: "string".to_string(), 385 - actual: format!("{:?}", value), 386 - })?; 387 - 454 + fn validate_string( 455 + &self, 456 + value: &Value, 457 + schema: &Value, 458 + ctx: &ValidationContext, 459 + ) -> Result<(), ValidationError> { 460 + let string_val = value 461 + .as_str() 462 + .ok_or_else(|| ValidationError::TypeMismatch { 463 + path: ctx.path_string(), 464 + expected: "string".to_string(), 465 + actual: format!("{:?}", value), 466 + })?; 467 + 388 468 // Check min/max length (in UTF-8 bytes) 389 469 if let Some(min_length) = schema["minLength"].as_u64() { 390 470 if (string_val.len() as u64) < min_length { 391 471 return Err(ValidationError::StringValidationFailed { 392 472 path: ctx.path_string(), 393 - message: format!("String length {} is less than minimum {}", string_val.len(), min_length), 473 + message: format!( 474 + "String length {} is less than minimum {}", 475 + string_val.len(), 476 + min_length 477 + ), 394 478 }); 395 479 } 396 480 } 397 - 481 + 398 482 if let Some(max_length) = schema["maxLength"].as_u64() { 399 483 if (string_val.len() as u64) > max_length { 400 484 return Err(ValidationError::StringValidationFailed { 401 485 path: ctx.path_string(), 402 - message: format!("String length {} exceeds maximum {}", string_val.len(), max_length), 486 + message: format!( 487 + "String length {} exceeds maximum {}", 488 + string_val.len(), 489 + max_length 490 + ), 403 491 }); 404 492 } 405 493 } 406 - 494 + 407 495 // Check min/max graphemes (simplified grapheme counting) 408 496 if let Some(min_graphemes) = schema["minGraphemes"].as_u64() { 409 497 let grapheme_count = self.count_graphemes(string_val); 410 498 if (grapheme_count as u64) < min_graphemes { 411 499 return Err(ValidationError::StringValidationFailed { 412 500 path: ctx.path_string(), 413 - message: format!("String has {} graphemes, less than minimum {}", grapheme_count, min_graphemes), 501 + message: format!( 502 + "String has {} graphemes, less than minimum {}", 503 + grapheme_count, min_graphemes 504 + ), 414 505 }); 415 506 } 416 507 } 417 - 508 + 418 509 if let Some(max_graphemes) = schema["maxGraphemes"].as_u64() { 419 510 let grapheme_count = self.count_graphemes(string_val); 420 511 if (grapheme_count as u64) > max_graphemes { 421 512 return Err(ValidationError::StringValidationFailed { 422 513 path: ctx.path_string(), 423 - message: format!("String has {} graphemes, exceeds maximum {}", grapheme_count, max_graphemes), 514 + message: format!( 515 + "String has {} graphemes, exceeds maximum {}", 516 + grapheme_count, max_graphemes 517 + ), 424 518 }); 425 519 } 426 520 } 427 - 521 + 428 522 // Check const value 429 523 if let Some(const_val) = schema["const"].as_str() { 430 524 if string_val != const_val { ··· 435 529 }); 436 530 } 437 531 } 438 - 532 + 439 533 // Check enum values 440 534 if let Some(enum_values) = schema["enum"].as_array() { 441 535 let valid = enum_values.iter().any(|v| v.as_str() == Some(string_val)); ··· 445 539 }); 446 540 } 447 541 } 448 - 542 + 449 543 // Check format 450 544 if let Some(format_str) = schema["format"].as_str() { 451 545 if let Some(format) = StringFormat::from_str(format_str) { 452 546 self.validate_string_format(string_val, format, ctx)?; 453 547 } 454 548 } 455 - 549 + 456 550 Ok(()) 457 551 } 458 - 552 + 459 553 /// Validate string formats 460 - fn validate_string_format(&self, value: &str, format: StringFormat, ctx: &ValidationContext) -> Result<(), ValidationError> { 554 + fn validate_string_format( 555 + &self, 556 + value: &str, 557 + format: StringFormat, 558 + ctx: &ValidationContext, 559 + ) -> Result<(), ValidationError> { 461 560 match format { 462 561 StringFormat::DateTime => { 463 562 // Validate RFC3339/ISO8601 datetime 464 - DateTime::parse_from_rfc3339(value).map_err(|_| ValidationError::FormatValidationFailed { 465 - path: ctx.path_string(), 466 - format: "datetime".to_string(), 563 + DateTime::parse_from_rfc3339(value).map_err(|_| { 564 + ValidationError::FormatValidationFailed { 565 + path: ctx.path_string(), 566 + format: "datetime".to_string(), 567 + } 467 568 })?; 468 - }, 569 + } 469 570 StringFormat::Uri => { 470 571 // Basic URI validation - must have scheme followed by colon 471 572 // Valid schemes include http://, https://, urn:, did:, etc. ··· 475 576 format: "uri".to_string(), 476 577 }); 477 578 } 478 - 579 + 479 580 // Check that scheme contains only valid characters 480 581 let colon_pos = value.find(':').unwrap(); 481 582 let scheme = &value[..colon_pos]; 482 - if scheme.is_empty() || !scheme.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') { 583 + if scheme.is_empty() 584 + || !scheme 585 + .chars() 586 + .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') 587 + { 483 588 return Err(ValidationError::FormatValidationFailed { 484 589 path: ctx.path_string(), 485 590 format: "uri".to_string(), 486 591 }); 487 592 } 488 - }, 593 + } 489 594 StringFormat::AtUri => { 490 595 // AT-URI format: at://[authority]/[collection]/[rkey] 491 596 if !value.starts_with("at://") { ··· 494 599 format: "at-uri".to_string(), 495 600 }); 496 601 } 497 - }, 602 + } 498 603 StringFormat::Did => { 499 604 // DID format: did:method:identifier 500 605 if !value.starts_with("did:") { ··· 503 608 format: "did".to_string(), 504 609 }); 505 610 } 506 - 611 + 507 612 // Must have at least 3 parts: did:method:identifier 508 613 let parts: Vec<&str> = value.split(':').collect(); 509 - if parts.len() < 3 || parts[0] != "did" || parts[1].is_empty() || parts[2].is_empty() { 614 + if parts.len() < 3 615 + || parts[0] != "did" 616 + || parts[1].is_empty() 617 + || parts[2].is_empty() 618 + { 510 619 return Err(ValidationError::FormatValidationFailed { 511 620 path: ctx.path_string(), 512 621 format: "did".to_string(), 513 622 }); 514 623 } 515 - }, 624 + } 516 625 StringFormat::Handle => { 517 626 // Handle format: domain-like (e.g., user.bsky.social) 518 627 let handle_regex = Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap(); ··· 522 631 format: "handle".to_string(), 523 632 }); 524 633 } 525 - }, 634 + } 526 635 StringFormat::AtIdentifier => { 527 636 // Either a DID or a handle (per spec: at-identifier can be either) 528 637 let is_did = value.starts_with("did:"); 529 - 530 - // Handle format: domain-like (e.g., user.bsky.social) 638 + 639 + // Handle format: domain-like (e.g., user.bsky.social) 531 640 let handle_regex = Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap(); 532 641 let is_handle = handle_regex.is_match(value); 533 - 642 + 534 643 if !is_did && !is_handle { 535 644 return Err(ValidationError::FormatValidationFailed { 536 645 path: ctx.path_string(), 537 646 format: "at-identifier".to_string(), 538 647 }); 539 648 } 540 - }, 649 + } 541 650 StringFormat::Nsid => { 542 651 // NSID format: reversed domain with name (e.g., com.example.foo) 543 652 let nsid_regex = Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$").unwrap(); ··· 547 656 format: "nsid".to_string(), 548 657 }); 549 658 } 550 - }, 659 + } 551 660 StringFormat::Cid => { 552 661 // Basic CID validation (starts with correct multibase prefix) 553 - if !value.starts_with("bafy") && !value.starts_with("bafk") && !value.starts_with("b") { 662 + if !value.starts_with("bafy") 663 + && !value.starts_with("bafk") 664 + && !value.starts_with("b") 665 + { 554 666 return Err(ValidationError::FormatValidationFailed { 555 667 path: ctx.path_string(), 556 668 format: "cid".to_string(), 557 669 }); 558 670 } 559 - }, 671 + } 560 672 StringFormat::Tid => { 561 673 // TID format: timestamp-based identifier (13 chars, base32) 562 674 let tid_regex = Regex::new(r"^[234567abcdefghijklmnopqrstuvwxyz]{13}$").unwrap(); ··· 566 678 format: "tid".to_string(), 567 679 }); 568 680 } 569 - }, 681 + } 570 682 StringFormat::RecordKey => { 571 683 // Record key: alphanumeric, dash, underscore, colon, tilde, or TID 572 684 let rkey_regex = Regex::new(r"^[a-zA-Z0-9._:~-]+$").unwrap(); ··· 576 688 format: "record-key".to_string(), 577 689 }); 578 690 } 579 - }, 691 + } 580 692 StringFormat::Language => { 581 693 // BCP47 language tag (simplified validation) 582 694 // Allows for language-region-extension pattern like "en-US-boont" ··· 587 699 format: "language".to_string(), 588 700 }); 589 701 } 590 - }, 702 + } 591 703 } 592 - 704 + 593 705 Ok(()) 594 706 } 595 - 707 + 596 708 /// Validate an integer value 597 - fn validate_integer(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 598 - let int_val = value.as_i64().ok_or_else(|| ValidationError::TypeMismatch { 599 - path: ctx.path_string(), 600 - expected: "integer".to_string(), 601 - actual: format!("{:?}", value), 602 - })?; 603 - 709 + fn validate_integer( 710 + &self, 711 + value: &Value, 712 + schema: &Value, 713 + ctx: &ValidationContext, 714 + ) -> Result<(), ValidationError> { 715 + let int_val = value 716 + .as_i64() 717 + .ok_or_else(|| ValidationError::TypeMismatch { 718 + path: ctx.path_string(), 719 + expected: "integer".to_string(), 720 + actual: format!("{:?}", value), 721 + })?; 722 + 604 723 if let Some(minimum) = schema["minimum"].as_i64() { 605 724 if int_val < minimum { 606 725 return Err(ValidationError::IntegerValidationFailed { ··· 609 728 }); 610 729 } 611 730 } 612 - 731 + 613 732 if let Some(maximum) = schema["maximum"].as_i64() { 614 733 if int_val > maximum { 615 734 return Err(ValidationError::IntegerValidationFailed { ··· 618 737 }); 619 738 } 620 739 } 621 - 740 + 622 741 // Check const value 623 742 if let Some(const_val) = schema["const"].as_i64() { 624 743 if int_val != const_val { ··· 629 748 }); 630 749 } 631 750 } 632 - 751 + 633 752 if let Some(enum_values) = schema["enum"].as_array() { 634 753 let valid = enum_values.iter().any(|v| v.as_i64() == Some(int_val)); 635 754 if !valid { ··· 638 757 }); 639 758 } 640 759 } 641 - 760 + 642 761 Ok(()) 643 762 } 644 - 763 + 645 764 /// Validate a boolean value 646 - fn validate_boolean(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 647 - value.as_bool().ok_or_else(|| ValidationError::TypeMismatch { 648 - path: ctx.path_string(), 649 - expected: "boolean".to_string(), 650 - actual: format!("{:?}", value), 651 - })?; 652 - 765 + fn validate_boolean( 766 + &self, 767 + value: &Value, 768 + schema: &Value, 769 + ctx: &ValidationContext, 770 + ) -> Result<(), ValidationError> { 771 + value 772 + .as_bool() 773 + .ok_or_else(|| ValidationError::TypeMismatch { 774 + path: ctx.path_string(), 775 + expected: "boolean".to_string(), 776 + actual: format!("{:?}", value), 777 + })?; 778 + 653 779 // Check const value if specified 654 780 if let Some(const_val) = schema["const"].as_bool() { 655 781 if value.as_bool() != Some(const_val) { ··· 660 786 }); 661 787 } 662 788 } 663 - 789 + 664 790 Ok(()) 665 791 } 666 - 792 + 667 793 /// Validate an array 668 - fn validate_array(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 669 - let array = value.as_array().ok_or_else(|| ValidationError::TypeMismatch { 670 - path: ctx.path_string(), 671 - expected: "array".to_string(), 672 - actual: format!("{:?}", value), 673 - })?; 674 - 794 + fn validate_array( 795 + &self, 796 + value: &Value, 797 + schema: &Value, 798 + ctx: &ValidationContext, 799 + ) -> Result<(), ValidationError> { 800 + let array = value 801 + .as_array() 802 + .ok_or_else(|| ValidationError::TypeMismatch { 803 + path: ctx.path_string(), 804 + expected: "array".to_string(), 805 + actual: format!("{:?}", value), 806 + })?; 807 + 675 808 // Check min/max length 676 809 if let Some(min_length) = schema["minLength"].as_u64() { 677 810 if (array.len() as u64) < min_length { 678 811 return Err(ValidationError::ArrayValidationFailed { 679 812 path: ctx.path_string(), 680 - message: format!("Array length {} is less than minimum {}", array.len(), min_length), 813 + message: format!( 814 + "Array length {} is less than minimum {}", 815 + array.len(), 816 + min_length 817 + ), 681 818 }); 682 819 } 683 820 } 684 - 821 + 685 822 if let Some(max_length) = schema["maxLength"].as_u64() { 686 823 if (array.len() as u64) > max_length { 687 824 return Err(ValidationError::ArrayValidationFailed { 688 825 path: ctx.path_string(), 689 - message: format!("Array length {} exceeds maximum {}", array.len(), max_length), 826 + message: format!( 827 + "Array length {} exceeds maximum {}", 828 + array.len(), 829 + max_length 830 + ), 690 831 }); 691 832 } 692 833 } 693 - 834 + 694 835 // Validate items 695 836 if let Some(items_schema) = schema.get("items") { 696 837 for (i, item) in array.iter().enumerate() { 697 838 self.validate_value(item, items_schema, &ctx.with_index(i))?; 698 839 } 699 840 } 700 - 841 + 701 842 Ok(()) 702 843 } 703 - 844 + 704 845 /// Validate a reference 705 - fn validate_ref(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 846 + fn validate_ref( 847 + &self, 848 + value: &Value, 849 + schema: &Value, 850 + ctx: &ValidationContext, 851 + ) -> Result<(), ValidationError> { 706 852 let ref_path = schema["ref"].as_str().ok_or_else(|| { 707 853 ValidationError::InvalidSchema(format!("Missing ref path at {}", ctx.path_string())) 708 854 })?; 709 - 855 + 710 856 // Resolve the reference 711 857 let resolved_schema = self.resolve_ref(ref_path)?; 712 - 858 + 713 859 // Validate against the resolved schema 714 860 self.validate_value(value, &resolved_schema, ctx) 715 861 } 716 - 862 + 717 863 /// Resolve a reference to its schema 718 864 fn resolve_ref(&self, ref_path: &str) -> Result<Value, ValidationError> { 719 865 // Note: Local references need context of which lexicon we're in ··· 723 869 // Local reference - would need current lexicon context to resolve 724 870 // For comprehensive testing, we'll implement a basic version 725 871 let fragment = &ref_path[1..]; // Remove the # prefix 726 - 872 + 727 873 // Try to find this fragment in any loaded lexicon (simplified approach) 728 874 for lexicon in self.lexicons.values() { 729 875 let def = &lexicon.defs[fragment]; ··· 731 877 return Ok(def.clone()); 732 878 } 733 879 } 734 - 880 + 735 881 return Err(ValidationError::ReferenceNotFound(ref_path.to_string())); 736 882 } 737 - 883 + 738 884 // Parse NSID#fragment format 739 885 let parts: Vec<&str> = ref_path.split('#').collect(); 740 886 let nsid = parts[0]; 741 887 let fragment = parts.get(1).map(|s| *s).unwrap_or("main"); 742 - 743 - let lexicon = self.lexicons.get(nsid) 888 + 889 + let lexicon = self 890 + .lexicons 891 + .get(nsid) 744 892 .ok_or_else(|| ValidationError::ReferenceNotFound(nsid.to_string()))?; 745 - 893 + 746 894 let def = &lexicon.defs[fragment]; 747 895 if def.is_null() { 748 896 return Err(ValidationError::ReferenceNotFound(ref_path.to_string())); 749 897 } 750 - 898 + 751 899 Ok(def.clone()) 752 900 } 753 - 901 + 754 902 /// Validate a union type 755 - fn validate_union(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 903 + fn validate_union( 904 + &self, 905 + value: &Value, 906 + schema: &Value, 907 + ctx: &ValidationContext, 908 + ) -> Result<(), ValidationError> { 756 909 let refs = schema["refs"].as_array().ok_or_else(|| { 757 910 ValidationError::InvalidSchema(format!("Union missing refs at {}", ctx.path_string())) 758 911 })?; 759 - 912 + 760 913 let is_closed = schema["closed"].as_bool().unwrap_or(false); 761 - 914 + 762 915 // Check if value has $type field 763 916 if let Some(type_field) = value.get("$type").and_then(|v| v.as_str()) { 764 917 // Try to match against the specific type 765 918 for ref_value in refs { 766 919 if let Some(ref_str) = ref_value.as_str() { 767 920 // For exact type match, validate against the reference 768 - if ref_str == type_field || ref_str.split('#').next().unwrap_or(ref_str) == type_field { 921 + if ref_str == type_field 922 + || ref_str.split('#').next().unwrap_or(ref_str) == type_field 923 + { 769 924 return self.validate_ref(value, &serde_json::json!({"ref": ref_str}), ctx); 770 925 } 771 926 } 772 927 } 773 - 928 + 774 929 // If this is an open union and we have a $type field, allow unknown types 775 930 if !is_closed { 776 931 // For open unions, any object with a $type is valid as long as it's structured properly ··· 779 934 } 780 935 } 781 936 } 782 - 937 + 783 938 // Try each variant (fallback for objects without $type or when $type doesn't match) 784 939 for ref_value in refs { 785 940 if let Some(ref_str) = ref_value.as_str() { ··· 789 944 } 790 945 } 791 946 } 792 - 947 + 793 948 Err(ValidationError::UnionValidationFailed { 794 949 path: ctx.path_string(), 795 950 }) 796 951 } 797 - 952 + 798 953 /// Validate a blob reference 799 - fn validate_blob(&self, value: &Value, _schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 800 - let obj = value.as_object().ok_or_else(|| ValidationError::TypeMismatch { 801 - path: ctx.path_string(), 802 - expected: "blob object".to_string(), 803 - actual: format!("{:?}", value), 804 - })?; 805 - 954 + fn validate_blob( 955 + &self, 956 + value: &Value, 957 + _schema: &Value, 958 + ctx: &ValidationContext, 959 + ) -> Result<(), ValidationError> { 960 + let obj = value 961 + .as_object() 962 + .ok_or_else(|| ValidationError::TypeMismatch { 963 + path: ctx.path_string(), 964 + expected: "blob object".to_string(), 965 + actual: format!("{:?}", value), 966 + })?; 967 + 806 968 // Check required blob fields 807 969 if !obj.contains_key("$type") || obj["$type"] != "blob" { 808 970 return Err(ValidationError::TypeMismatch { ··· 811 973 actual: format!("{:?}", value), 812 974 }); 813 975 } 814 - 976 + 815 977 // Check for ref object with $link 816 978 let ref_obj = obj.get("ref").and_then(|v| v.as_object()).ok_or_else(|| { 817 979 ValidationError::TypeMismatch { ··· 820 982 actual: "missing or invalid".to_string(), 821 983 } 822 984 })?; 823 - 985 + 824 986 if !ref_obj.contains_key("$link") { 825 987 return Err(ValidationError::TypeMismatch { 826 988 path: ctx.with_field("ref.$link").path_string(), ··· 828 990 actual: "missing".to_string(), 829 991 }); 830 992 } 831 - 993 + 832 994 // Check other required fields 833 995 if !obj.contains_key("mimeType") { 834 996 return Err(ValidationError::RequiredFieldMissing { 835 997 path: ctx.with_field("mimeType").path_string(), 836 998 }); 837 999 } 838 - 1000 + 839 1001 if !obj.contains_key("size") { 840 1002 return Err(ValidationError::RequiredFieldMissing { 841 1003 path: ctx.with_field("size").path_string(), 842 1004 }); 843 1005 } 844 - 1006 + 845 1007 Ok(()) 846 1008 } 847 - 1009 + 848 1010 /// Validate bytes type 849 - fn validate_bytes(&self, value: &Value, schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 1011 + fn validate_bytes( 1012 + &self, 1013 + value: &Value, 1014 + schema: &Value, 1015 + ctx: &ValidationContext, 1016 + ) -> Result<(), ValidationError> { 850 1017 // Bytes in JSON are typically base64 encoded strings 851 - let string_val = value.as_str().ok_or_else(|| ValidationError::TypeMismatch { 852 - path: ctx.path_string(), 853 - expected: "bytes (base64 string)".to_string(), 854 - actual: format!("{:?}", value), 855 - })?; 856 - 1018 + let string_val = value 1019 + .as_str() 1020 + .ok_or_else(|| ValidationError::TypeMismatch { 1021 + path: ctx.path_string(), 1022 + expected: "bytes (base64 string)".to_string(), 1023 + actual: format!("{:?}", value), 1024 + })?; 1025 + 857 1026 // Basic base64 validation (simplified) 858 - if !string_val.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') { 1027 + if !string_val 1028 + .chars() 1029 + .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') 1030 + { 859 1031 return Err(ValidationError::TypeMismatch { 860 1032 path: ctx.path_string(), 861 1033 expected: "valid base64 string".to_string(), 862 1034 actual: "invalid base64".to_string(), 863 1035 }); 864 1036 } 865 - 1037 + 866 1038 // Check length constraints (bytes length, not string length) 867 - let decoded_len = (string_val.len() * 3 / 4) - string_val.chars().filter(|&c| c == '=').count(); 868 - 1039 + let decoded_len = 1040 + (string_val.len() * 3 / 4) - string_val.chars().filter(|&c| c == '=').count(); 1041 + 869 1042 if let Some(min_length) = schema["minLength"].as_u64() { 870 1043 if (decoded_len as u64) < min_length { 871 1044 return Err(ValidationError::StringValidationFailed { 872 1045 path: ctx.path_string(), 873 - message: format!("Bytes length {} is less than minimum {}", decoded_len, min_length), 1046 + message: format!( 1047 + "Bytes length {} is less than minimum {}", 1048 + decoded_len, min_length 1049 + ), 874 1050 }); 875 1051 } 876 1052 } 877 - 1053 + 878 1054 if let Some(max_length) = schema["maxLength"].as_u64() { 879 1055 if (decoded_len as u64) > max_length { 880 1056 return Err(ValidationError::StringValidationFailed { 881 1057 path: ctx.path_string(), 882 - message: format!("Bytes length {} exceeds maximum {}", decoded_len, max_length), 1058 + message: format!( 1059 + "Bytes length {} exceeds maximum {}", 1060 + decoded_len, max_length 1061 + ), 883 1062 }); 884 1063 } 885 1064 } 886 - 1065 + 887 1066 Ok(()) 888 1067 } 889 - 1068 + 890 1069 /// Validate cid-link type 891 - fn validate_cid_link(&self, value: &Value, _schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 892 - let obj = value.as_object().ok_or_else(|| ValidationError::TypeMismatch { 893 - path: ctx.path_string(), 894 - expected: "cid-link object".to_string(), 895 - actual: format!("{:?}", value), 896 - })?; 897 - 1070 + fn validate_cid_link( 1071 + &self, 1072 + value: &Value, 1073 + _schema: &Value, 1074 + ctx: &ValidationContext, 1075 + ) -> Result<(), ValidationError> { 1076 + let obj = value 1077 + .as_object() 1078 + .ok_or_else(|| ValidationError::TypeMismatch { 1079 + path: ctx.path_string(), 1080 + expected: "cid-link object".to_string(), 1081 + actual: format!("{:?}", value), 1082 + })?; 1083 + 898 1084 // CID-link must have $link field with a valid CID 899 1085 let link = obj.get("$link").and_then(|v| v.as_str()).ok_or_else(|| { 900 1086 ValidationError::RequiredFieldMissing { 901 1087 path: ctx.with_field("$link").path_string(), 902 1088 } 903 1089 })?; 904 - 1090 + 905 1091 // Basic CID validation (simplified - should start with appropriate multibase prefix) 906 - if !link.starts_with("bafy") && !link.starts_with("bafk") && !link.starts_with("b") && link.len() < 20 { 1092 + if !link.starts_with("bafy") 1093 + && !link.starts_with("bafk") 1094 + && !link.starts_with("b") 1095 + && link.len() < 20 1096 + { 907 1097 return Err(ValidationError::FormatValidationFailed { 908 1098 path: ctx.with_field("$link").path_string(), 909 1099 format: "cid".to_string(), 910 1100 }); 911 1101 } 912 - 1102 + 913 1103 Ok(()) 914 1104 } 915 - 1105 + 916 1106 /// Validate null type 917 - fn validate_null(&self, value: &Value, _schema: &Value, ctx: &ValidationContext) -> Result<(), ValidationError> { 1107 + fn validate_null( 1108 + &self, 1109 + value: &Value, 1110 + _schema: &Value, 1111 + ctx: &ValidationContext, 1112 + ) -> Result<(), ValidationError> { 918 1113 if !value.is_null() { 919 1114 return Err(ValidationError::TypeMismatch { 920 1115 path: ctx.path_string(), ··· 924 1119 } 925 1120 Ok(()) 926 1121 } 927 - 1122 + 928 1123 /// Simplified grapheme counting (approximation) 929 - /// This is a basic implementation - for full compliance with TS version, 1124 + /// This is a basic implementation - for full compliance with TS version, 930 1125 /// would need proper Unicode grapheme cluster segmentation 931 1126 fn count_graphemes(&self, s: &str) -> usize { 932 1127 // Very simplified approach: count Unicode scalar values, with basic combining character handling 933 1128 let mut count = 0; 934 1129 let mut chars = s.chars().peekable(); 935 - 1130 + 936 1131 while let Some(_ch) = chars.next() { 937 1132 count += 1; 938 - 1133 + 939 1134 // Skip combining characters that follow this base character 940 1135 while let Some(&next_ch) = chars.peek() { 941 1136 if self.is_combining_character(next_ch) { ··· 945 1140 } 946 1141 } 947 1142 } 948 - 1143 + 949 1144 count 950 1145 } 951 - 1146 + 952 1147 /// Check if a character is a combining character (very simplified) 953 1148 fn is_combining_character(&self, ch: char) -> bool { 954 1149 // This is a simplified check for common combining marks 955 1150 // In a full implementation, would need proper Unicode category checking 956 - matches!(ch as u32, 1151 + matches!(ch as u32, 957 1152 0x0300..=0x036F | // Combining Diacritical Marks 958 1153 0x1AB0..=0x1AFF | // Combining Diacritical Marks Extended 959 1154 0x1DC0..=0x1DFF | // Combining Diacritical Marks Supplement ··· 961 1156 0xFE20..=0xFE2F // Combining Half Marks 962 1157 ) 963 1158 } 964 - } 1159 + }
+31 -24
api/src/main.rs
··· 38 38 use crate::database::Database; 39 39 use crate::errors::AppError; 40 40 use crate::jetstream::JetstreamConsumer; 41 - use crate::logging::{Logger, LogLevel}; 41 + use crate::logging::{LogLevel, Logger}; 42 42 43 43 #[derive(Clone)] 44 44 pub struct Config { ··· 158 158 Logger::global().log_jetstream( 159 159 LogLevel::Info, 160 160 "Starting Jetstream consumer", 161 - Some(serde_json::json!({"action": "starting_consumer"})) 161 + Some(serde_json::json!({"action": "starting_consumer"})), 162 162 ); 163 163 164 164 // Use existing consumer or create new one ··· 173 173 { 174 174 Ok(consumer) => std::sync::Arc::new(consumer), 175 175 Err(e) => { 176 - let message = format!("Failed to create Jetstream consumer: {} - will retry in {:?}", e, retry_delay); 176 + let message = format!( 177 + "Failed to create Jetstream consumer: {} - will retry in {:?}", 178 + e, retry_delay 179 + ); 177 180 tracing::error!("{}", message); 178 181 Logger::global().log_jetstream( 179 182 LogLevel::Error, ··· 182 185 "error": e.to_string(), 183 186 "retry_delay_secs": retry_delay.as_secs(), 184 187 "action": "consumer_creation_failed" 185 - })) 188 + })), 186 189 ); 187 190 jetstream_connected_clone 188 191 .store(false, std::sync::atomic::Ordering::Relaxed); ··· 221 224 // No events for 60+ seconds - mark as disconnected 222 225 health_check_connected 223 226 .store(false, std::sync::atomic::Ordering::Relaxed); 224 - let message = format!("Jetstream marked as disconnected: no events processed in {} seconds", no_events_duration); 227 + let message = format!( 228 + "Jetstream marked as disconnected: no events processed in {} seconds", 229 + no_events_duration 230 + ); 225 231 tracing::warn!("{}", message); 226 232 Logger::global().log_jetstream( 227 233 LogLevel::Warn, ··· 229 235 Some(serde_json::json!({ 230 236 "no_events_duration_secs": no_events_duration, 231 237 "action": "health_check_disconnected" 232 - })) 238 + })), 233 239 ); 234 240 } 235 241 } else { ··· 238 244 health_check_connected 239 245 .store(true, std::sync::atomic::Ordering::Relaxed); 240 246 if last_count == 0 && current_count > 0 { 241 - let message = "Jetstream health check: events flowing, marked as connected"; 247 + let message = 248 + "Jetstream health check: events flowing, marked as connected"; 242 249 tracing::info!("{}", message); 243 250 Logger::global().log_jetstream( 244 251 LogLevel::Info, ··· 246 253 Some(serde_json::json!({ 247 254 "event_count": current_count, 248 255 "action": "health_check_connected" 249 - })) 256 + })), 250 257 ); 251 258 } 252 259 } ··· 313 320 ) 314 321 // XRPC endpoints 315 322 .route( 316 - "/xrpc/social.slices.slice.startSync", 323 + "/xrpc/network.slices.slice.startSync", 317 324 post(handler_sync::sync), 318 325 ) 319 326 .route( 320 - "/xrpc/social.slices.slice.syncUserCollections", 327 + "/xrpc/network.slices.slice.syncUserCollections", 321 328 post(handler_sync_user_collections::sync_user_collections), 322 329 ) 323 330 .route( 324 - "/xrpc/social.slices.slice.getJobStatus", 331 + "/xrpc/network.slices.slice.getJobStatus", 325 332 get(handler_jobs::get_job_status), 326 333 ) 327 334 .route( 328 - "/xrpc/social.slices.slice.getJobHistory", 335 + "/xrpc/network.slices.slice.getJobHistory", 329 336 get(handler_jobs::get_slice_job_history), 330 337 ) 331 338 .route( 332 - "/xrpc/social.slices.slice.getJobLogs", 339 + "/xrpc/network.slices.slice.getJobLogs", 333 340 get(handler_logs::get_sync_job_logs_handler), 334 341 ) 335 342 .route( 336 - "/xrpc/social.slices.slice.getJetstreamLogs", 343 + "/xrpc/network.slices.slice.getJetstreamLogs", 337 344 get(handler_logs::get_jetstream_logs_handler), 338 345 ) 339 346 .route( 340 - "/xrpc/social.slices.slice.stats", 347 + "/xrpc/network.slices.slice.stats", 341 348 post(handler_stats::stats), 342 349 ) 343 350 .route( 344 - "/xrpc/social.slices.slice.getSliceRecords", 351 + "/xrpc/network.slices.slice.getSliceRecords", 345 352 post(handler_get_records::get_records), 346 353 ) 347 354 .route( 348 - "/xrpc/social.slices.slice.codegen", 355 + "/xrpc/network.slices.slice.codegen", 349 356 post(handler_xrpc_codegen::generate_client_xrpc), 350 357 ) 351 358 .route( 352 - "/xrpc/social.slices.slice.openapi", 359 + "/xrpc/network.slices.slice.openapi", 353 360 get(handler_openapi_spec::get_openapi_spec), 354 361 ) 355 362 .route( 356 - "/xrpc/social.slices.slice.getJetstreamStatus", 363 + "/xrpc/network.slices.slice.getJetstreamStatus", 357 364 get(handler_jetstream_status::get_jetstream_status), 358 365 ) 359 366 .route( 360 - "/xrpc/social.slices.slice.getActors", 367 + "/xrpc/network.slices.slice.getActors", 361 368 post(handler_get_actors::get_actors), 362 369 ) 363 370 // OAuth client management endpoints 364 371 .route( 365 - "/xrpc/social.slices.slice.createOAuthClient", 372 + "/xrpc/network.slices.slice.createOAuthClient", 366 373 post(handler_oauth_clients::create_oauth_client), 367 374 ) 368 375 .route( 369 - "/xrpc/social.slices.slice.getOAuthClients", 376 + "/xrpc/network.slices.slice.getOAuthClients", 370 377 get(handler_oauth_clients::get_oauth_clients), 371 378 ) 372 379 .route( 373 - "/xrpc/social.slices.slice.updateOAuthClient", 380 + "/xrpc/network.slices.slice.updateOAuthClient", 374 381 post(handler_oauth_clients::update_oauth_client), 375 382 ) 376 383 .route( 377 - "/xrpc/social.slices.slice.deleteOAuthClient", 384 + "/xrpc/network.slices.slice.deleteOAuthClient", 378 385 post(handler_oauth_clients::delete_oauth_client), 379 386 ) 380 387 // Dynamic collection-specific XRPC endpoints (wildcard routes must come last)
+57 -24
docs/api-reference.md
··· 10 10 11 11 ## Authentication 12 12 13 - Most write operations require OAuth 2.0 authentication. Include the access token in the Authorization header: 13 + Most write operations require OAuth 2.0 authentication. Include the access token 14 + in the Authorization header: 14 15 15 16 ``` 16 17 Authorization: Bearer YOUR_ACCESS_TOKEN ··· 22 23 23 24 ### Slice Management 24 25 25 - #### `social.slices.slice.listRecords` 26 + #### `network.slices.slice.listRecords` 26 27 27 28 List all slices. 28 29 29 30 **Method**: GET 30 31 31 32 **Parameters**: 33 + 32 34 - `limit` (number, optional): Maximum records to return (default: 50) 33 35 - `cursor` (string, optional): Pagination cursor 34 36 - `sort` (string, optional): Sort field and order (e.g., `createdAt:desc`) ··· 36 38 - `authors` (string[], optional): Filter by multiple author DIDs 37 39 38 40 **Response**: 41 + 39 42 ```json 40 43 { 41 44 "records": [ 42 45 { 43 - "uri": "at://did:plc:abc/social.slices.slice/xyz", 46 + "uri": "at://did:plc:abc/network.slices.slice/xyz", 44 47 "cid": "bafyrei...", 45 48 "did": "did:plc:abc", 46 - "collection": "social.slices.slice", 49 + "collection": "network.slices.slice", 47 50 "value": { 48 51 "name": "My Slice", 49 52 "domain": "com.example", ··· 56 59 } 57 60 ``` 58 61 59 - #### `social.slices.slice.getRecord` 62 + #### `network.slices.slice.getRecord` 60 63 61 64 Get a specific slice by URI. 62 65 63 66 **Method**: GET 64 67 65 68 **Parameters**: 69 + 66 70 - `uri` (string, required): AT Protocol URI of the slice 67 71 68 72 **Response**: Single record object (same structure as listRecords item) 69 73 70 - #### `social.slices.slice.createRecord` 74 + #### `network.slices.slice.createRecord` 71 75 72 76 Create a new slice. 73 77 ··· 76 80 **Authentication**: Required 77 81 78 82 **Body**: 83 + 79 84 ```json 80 85 { 81 86 "slice": "at://your-slice-uri", 82 87 "record": { 83 - "$type": "social.slices.slice", 88 + "$type": "network.slices.slice", 84 89 "name": "My New Slice", 85 90 "domain": "com.example", 86 91 "createdAt": "2024-01-01T00:00:00Z" ··· 90 95 ``` 91 96 92 97 **Response**: 98 + 93 99 ```json 94 100 { 95 - "uri": "at://did:plc:abc/social.slices.slice/xyz", 101 + "uri": "at://did:plc:abc/network.slices.slice/xyz", 96 102 "cid": "bafyrei..." 97 103 } 98 104 ``` 99 105 100 106 ### Slice Operations 101 107 102 - #### `social.slices.slice.stats` 108 + #### `network.slices.slice.stats` 103 109 104 110 Get statistics for a slice. 105 111 106 112 **Method**: POST 107 113 108 114 **Body**: 115 + 109 116 ```json 110 117 { 111 118 "slice": "at://your-slice-uri" ··· 113 120 ``` 114 121 115 122 **Response**: 123 + 116 124 ```json 117 125 { 118 126 "success": true, ··· 131 139 } 132 140 ``` 133 141 134 - #### `social.slices.slice.listSliceRecords` 142 + #### `network.slices.slice.listSliceRecords` 135 143 136 144 List records across multiple collections in a slice. 137 145 138 146 **Method**: POST 139 147 140 148 **Body**: 149 + 141 150 ```json 142 151 { 143 152 "slice": "at://your-slice-uri", ··· 149 158 ``` 150 159 151 160 **Response**: 161 + 152 162 ```json 153 163 { 154 164 "success": true, ··· 158 168 "cid": "bafyrei...", 159 169 "did": "did:plc:abc", 160 170 "collection": "com.example.post", 161 - "value": { /* record data */ }, 171 + "value": {/* record data */}, 162 172 "indexedAt": "2024-01-01T00:00:00Z" 163 173 } 164 174 ], ··· 166 176 } 167 177 ``` 168 178 169 - #### `social.slices.slice.searchSliceRecords` 179 + #### `network.slices.slice.searchSliceRecords` 170 180 171 181 Search records across multiple collections in a slice by content. 172 182 173 183 **Method**: POST 174 184 175 185 **Body**: 186 + 176 187 ```json 177 188 { 178 189 "slice": "at://your-slice-uri", ··· 185 196 ``` 186 197 187 198 **Response**: 199 + 188 200 ```json 189 201 { 190 202 "success": true, ··· 194 206 "cid": "bafyrei...", 195 207 "did": "did:plc:abc", 196 208 "collection": "com.example.post", 197 - "value": { /* record data */ }, 209 + "value": {/* record data */}, 198 210 "indexedAt": "2024-01-01T00:00:00Z" 199 211 } 200 212 ], ··· 202 214 } 203 215 ``` 204 216 205 - #### `social.slices.slice.syncUserCollections` 217 + #### `network.slices.slice.syncUserCollections` 206 218 207 219 Synchronously sync collections for the authenticated user. 208 220 ··· 211 223 **Authentication**: Required 212 224 213 225 **Body**: 226 + 214 227 ```json 215 228 { 216 229 "slice": "at://your-slice-uri", ··· 219 232 ``` 220 233 221 234 **Response**: 235 + 222 236 ```json 223 237 { 224 238 "success": true, ··· 229 243 } 230 244 ``` 231 245 232 - #### `social.slices.slice.startSync` 246 + #### `network.slices.slice.startSync` 233 247 234 248 Start an asynchronous bulk sync job. 235 249 ··· 238 252 **Authentication**: Required 239 253 240 254 **Body**: 255 + 241 256 ```json 242 257 { 243 258 "slice": "at://your-slice-uri", ··· 249 264 ``` 250 265 251 266 **Response**: 267 + 252 268 ```json 253 269 { 254 270 "success": true, ··· 257 273 } 258 274 ``` 259 275 260 - #### `social.slices.slice.codegen` 276 + #### `network.slices.slice.codegen` 261 277 262 278 Generate TypeScript client code. 263 279 264 280 **Method**: POST 265 281 266 282 **Body**: 283 + 267 284 ```json 268 285 { 269 286 "target": "typescript", ··· 272 289 ``` 273 290 274 291 **Response**: 292 + 275 293 ```json 276 294 { 277 295 "success": true, ··· 281 299 282 300 ## Dynamic Collection Endpoints 283 301 284 - For each collection in your slice, the following endpoints are automatically generated: 302 + For each collection in your slice, the following endpoints are automatically 303 + generated: 285 304 286 305 ### `[collection].listRecords` 287 306 ··· 290 309 **Method**: GET 291 310 292 311 **Parameters**: 312 + 293 313 - `slice` (string, required): Slice URI 294 314 - `limit` (number, optional): Maximum records (default: 50) 295 315 - `cursor` (string, optional): Pagination cursor ··· 304 324 **Method**: GET 305 325 306 326 **Parameters**: 327 + 307 328 - `slice` (string, required): Slice URI 308 329 - `uri` (string, required): Record URI 309 330 ··· 314 335 **Method**: GET 315 336 316 337 **Parameters**: 338 + 317 339 - `slice` (string, required): Slice URI 318 340 - `query` (string, required): Search query 319 341 - `field` (string, optional): Specific field to search ··· 330 352 **Authentication**: Required 331 353 332 354 **Body**: 355 + 333 356 ```json 334 357 { 335 358 "slice": "at://your-slice-uri", 336 359 "record": { 337 - "$type": "collection.name", 360 + "$type": "collection.name" 338 361 /* record fields */ 339 362 }, 340 363 "rkey": "optional-key" ··· 350 373 **Authentication**: Required 351 374 352 375 **Body**: 376 + 353 377 ```json 354 378 { 355 379 "slice": "at://your-slice-uri", 356 380 "rkey": "record-key", 357 381 "record": { 358 - "$type": "collection.name", 382 + "$type": "collection.name" 359 383 /* updated fields */ 360 384 } 361 385 } ··· 370 394 **Authentication**: Required 371 395 372 396 **Body**: 397 + 373 398 ```json 374 399 { 375 400 "rkey": "record-key" ··· 378 403 379 404 ## Lexicon Management 380 405 381 - ### `social.slices.lexicon.listRecords` 406 + ### `network.slices.lexicon.listRecords` 382 407 383 408 List lexicons in a slice. 384 409 ··· 386 411 387 412 **Parameters**: Same as collection.listRecords 388 413 389 - ### `social.slices.lexicon.createRecord` 414 + ### `network.slices.lexicon.createRecord` 390 415 391 416 Add a lexicon to a slice. 392 417 ··· 395 420 **Authentication**: Required 396 421 397 422 **Body**: 423 + 398 424 ```json 399 425 { 400 426 "slice": "at://your-slice-uri", 401 427 "record": { 402 - "$type": "social.slices.lexicon", 428 + "$type": "network.slices.lexicon", 403 429 "nsid": "com.example.post", 404 430 "definitions": "{\"lexicon\": 1, ...}", 405 431 "createdAt": "2024-01-01T00:00:00Z", ··· 410 436 411 437 ## Actor Management 412 438 413 - ### `social.slices.slice.getActors` 439 + ### `network.slices.slice.getActors` 414 440 415 441 Get actors (users) in a slice. 416 442 417 443 **Method**: GET 418 444 419 445 **Parameters**: 446 + 420 447 - `slice` (string, required): Slice URI 421 448 - `search` (string, optional): Search query 422 449 - `dids` (string[], optional): Filter by DIDs ··· 424 451 - `cursor` (string, optional): Pagination cursor 425 452 426 453 **Response**: 454 + 427 455 ```json 428 456 { 429 457 "actors": [ ··· 449 477 **Authentication**: Required 450 478 451 479 **Headers**: 480 + 452 481 - `Content-Type`: MIME type of the blob 453 482 454 483 **Body**: Raw binary data 455 484 456 485 **Response**: 486 + 457 487 ```json 458 488 { 459 489 "blob": { ··· 477 507 ``` 478 508 479 509 Common HTTP status codes: 510 + 480 511 - `200`: Success 481 512 - `400`: Bad request 482 513 - `401`: Authentication required ··· 493 524 3. Continue until no cursor returned 494 525 495 526 Example: 527 + 496 528 ```javascript 497 529 let cursor = undefined; 498 530 do { ··· 508 540 Sort parameter format: `field:order` or `field1:order1,field2:order2` 509 541 510 542 Examples: 543 + 511 544 - `createdAt:desc` - Newest first 512 545 - `name:asc` - Alphabetical 513 546 - `createdAt:desc,name:asc` - Newest first, then alphabetical ··· 516 549 517 550 - [SDK Usage](./sdk-usage.md) - Using generated TypeScript clients 518 551 - [Getting Started](./getting-started.md) - Build your first application 519 - - [Concepts](./concepts.md) - Understand the architecture 552 + - [Concepts](./concepts.md) - Understand the architecture
+7 -7
docs/concepts.md
··· 10 10 ### Key Properties 11 11 12 12 - **URI**: Unique AT Protocol URI (e.g., 13 - `at://did:plc:abc123/social.slices.slice/3xyz`) 13 + `at://did:plc:abc123/network.slices.slice/3xyz`) 14 14 - **Name**: Human-readable identifier 15 15 - **Domain**: Namespace for lexicons (e.g., `com.example`, `social.grain`) 16 16 - **Creation Date**: When the slice was created ··· 65 65 Lexicons follow reverse domain naming: 66 66 67 67 - `com.example.post` - A post in the example.com namespace 68 - - `social.slices.slice` - Core slice record type 68 + - `network.slices.slice` - Core slice record type 69 69 - `app.bsky.actor.profile` - Bluesky profile (external) 70 70 71 71 ## Collections ··· 217 217 218 218 Built-in endpoints for slice management: 219 219 220 - - `social.slices.slice.stats` - Slice statistics 221 - - `social.slices.slice.records` - Browse records 222 - - `social.slices.slice.codegen` - Generate SDKs 223 - - `social.slices.slice.sync` - Trigger sync 220 + - `network.slices.slice.stats` - Slice statistics 221 + - `network.slices.slice.records` - Browse records 222 + - `network.slices.slice.codegen` - Generate SDKs 223 + - `network.slices.slice.sync` - Trigger sync 224 224 225 225 ### Handler Authentication 226 226 ··· 255 255 256 256 // Use nested structure matching lexicons 257 257 await client.com.example.post.listRecords(); 258 - await client.social.slices.slice.stats(); 258 + await client.network.slices.slice.stats(); 259 259 await client.app.bsky.actor.profile.getRecord({ uri }); 260 260 ``` 261 261
+22 -9
docs/getting-started.md
··· 38 38 Create `.env` files for both API and frontend: 39 39 40 40 **API (`/api/.env`)**: 41 + 41 42 ```bash 42 43 DATABASE_URL=postgres://user:password@localhost:5432/slices 43 44 AUTH_BASE_URL=https://aip.your-domain.com ··· 45 46 ``` 46 47 47 48 **Frontend (`/frontend/.env`)**: 49 + 48 50 ```bash 49 51 OAUTH_CLIENT_ID=your-client-id 50 52 OAUTH_CLIENT_SECRET=your-client-secret ··· 52 54 OAUTH_AIP_BASE_URL=https://aip.your-domain.com 53 55 SESSION_ENCRYPTION_KEY=your-32-char-key 54 56 API_URL=http://localhost:3000 55 - SLICE_URI=at://did:plc:your-did/social.slices.slice/your-slice-id 57 + SLICE_URI=at://did:plc:your-did/network.slices.slice/your-slice-id 56 58 DATABASE_URL=slices.db 57 59 ``` 58 60 ··· 70 72 ### 5. Start the Services 71 73 72 74 Start the API server: 75 + 73 76 ```bash 74 77 cd api 75 78 cargo run 76 79 ``` 77 80 78 81 Start the frontend: 82 + 79 83 ```bash 80 84 cd frontend 81 85 deno task dev ··· 92 96 ### 2. Create a Slice 93 97 94 98 Click "Create Slice" and provide: 99 + 95 100 - **Name**: A friendly name for your slice 96 101 - **Domain**: Your namespace (e.g., `com.example`) 97 102 98 103 ### 3. Define a Lexicon 99 104 100 - Navigate to your slice and go to the Lexicon tab. Create a lexicon for your first record type: 105 + Navigate to your slice and go to the Lexicon tab. Create a lexicon for your 106 + first record type: 101 107 102 108 ```json 103 109 { ··· 140 146 141 147 ### 4. Generate TypeScript Client 142 148 143 - Navigate to the Code Generation tab and click "Generate TypeScript Client". This creates a type-safe client library for your slice. 149 + Navigate to the Code Generation tab and click "Generate TypeScript Client". This 150 + creates a type-safe client library for your slice. 144 151 145 152 ### 5. Use the Generated Client 146 153 ··· 150 157 import { AtProtoClient } from "./generated-client.ts"; 151 158 152 159 const client = new AtProtoClient( 153 - 'http://localhost:3000', 154 - 'at://did:plc:your-did/social.slices.slice/your-slice-id' 160 + "http://localhost:3000", 161 + "at://did:plc:your-did/network.slices.slice/your-slice-id", 155 162 ); 156 163 157 164 // List posts ··· 162 169 title: "My First Post", 163 170 content: "Hello from Slices!", 164 171 createdAt: new Date().toISOString(), 165 - tags: ["introduction", "slices"] 172 + tags: ["introduction", "slices"], 166 173 }); 167 174 168 175 // Get a specific post 169 176 const post = await client.com.example.post.getRecord({ 170 - uri: newPost.uri 177 + uri: newPost.uri, 171 178 }); 172 179 ``` 173 180 ··· 182 189 ### 2. Configure Sync 183 190 184 191 Choose collections to sync: 192 + 185 193 - **Primary Collections**: Your slice's lexicons 186 194 - **External Collections**: Bluesky or other AT Protocol collections 187 195 188 196 ### 3. Start Sync 189 197 190 - Specify repositories (DIDs) to sync from, or leave empty to sync all available data. 198 + Specify repositories (DIDs) to sync from, or leave empty to sync all available 199 + data. 191 200 192 201 ### 4. Monitor Progress 193 202 ··· 203 212 ## Troubleshooting 204 213 205 214 ### Database Connection Issues 215 + 206 216 - Verify PostgreSQL is running: `docker ps` 207 217 - Check DATABASE_URL format 208 218 - Ensure database exists 209 219 210 220 ### OAuth Errors 221 + 211 222 - Verify client ID and secret 212 223 - Check redirect URI matches configuration 213 224 - Ensure AIP server is accessible 214 225 215 226 ### Sync Not Working 227 + 216 228 - Check user has necessary permissions 217 229 - Verify lexicons are valid 218 230 - Check API server logs for errors 219 231 220 232 ### Generated Client Issues 233 + 221 234 - Regenerate client after lexicon changes 222 235 - Ensure API server is running 223 - - Check for TypeScript compilation errors 236 + - Check for TypeScript compilation errors
+44 -39
docs/sdk-usage.md
··· 19 19 ```typescript 20 20 const client = new AtProtoClient( 21 21 "https://api.your-domain.com", 22 - "at://did:plc:abc/social.slices.slice/your-slice-id", 22 + "at://did:plc:abc/network.slices.slice/your-slice-id", 23 23 ); 24 24 25 25 // Read operations work without auth ··· 43 43 // Initialize API client with OAuth 44 44 const client = new AtProtoClient( 45 45 "https://api.your-domain.com", 46 - "at://did:plc:abc/social.slices.slice/your-slice-id", 46 + "at://did:plc:abc/network.slices.slice/your-slice-id", 47 47 oauthClient, 48 48 ); 49 49 ``` ··· 292 292 ### Get Slice Statistics 293 293 294 294 ```typescript 295 - const stats = await client.social.slices.slice.stats({ 295 + const stats = await client.network.slices.slice.stats({ 296 296 slice: "at://your-slice-uri", 297 297 }); 298 298 ··· 311 311 312 312 ```typescript 313 313 // Get all actors in the slice 314 - const actors = await client.social.slices.slice.getActors(); 314 + const actors = await client.network.slices.slice.getActors(); 315 315 316 316 // With pagination 317 - const page1 = await client.social.slices.slice.getActors({ 317 + const page1 = await client.network.slices.slice.getActors({ 318 318 limit: 20, 319 319 }); 320 - const page2 = await client.social.slices.slice.getActors({ 320 + const page2 = await client.network.slices.slice.getActors({ 321 321 limit: 20, 322 322 cursor: page1.cursor, 323 323 }); 324 324 325 325 // Filter by specific DIDs 326 - const specificActors = await client.social.slices.slice.getActors({ 326 + const specificActors = await client.network.slices.slice.getActors({ 327 327 where: { 328 328 did: { in: ["did:plc:user1", "did:plc:user2"] }, 329 329 }, 330 330 }); 331 331 332 332 // Search by handle 333 - const searchByHandle = await client.social.slices.slice.getActors({ 333 + const searchByHandle = await client.network.slices.slice.getActors({ 334 334 where: { 335 335 handle: { contains: "alice" }, 336 336 }, 337 337 }); 338 338 339 339 // Filter by exact handle 340 - const exactHandle = await client.social.slices.slice.getActors({ 340 + const exactHandle = await client.network.slices.slice.getActors({ 341 341 where: { 342 342 handle: { eq: "alice.bsky.social" }, 343 343 }, ··· 357 357 358 358 ```typescript 359 359 // Get records from specific collections 360 - const records = await client.social.slices.slice.getSliceRecords({ 360 + const records = await client.network.slices.slice.getSliceRecords({ 361 361 where: { 362 362 collection: { eq: "com.example.post" }, 363 363 did: { eq: "did:plc:specific-author" }, // optional ··· 370 370 }); 371 371 372 372 // Search across collections using specific fields 373 - const searchResults = await client.social.slices.slice.getSliceRecords({ 373 + const searchResults = await client.network.slices.slice.getSliceRecords({ 374 374 where: { 375 375 collection: { eq: "com.example.post" }, 376 376 title: { contains: "hello world" }, ··· 380 380 }); 381 381 382 382 // Global search across ALL fields in records 383 - const globalSearchResults = await client.social.slices.slice.getSliceRecords({ 383 + const globalSearchResults = await client.network.slices.slice.getSliceRecords({ 384 384 where: { 385 385 collection: { eq: "com.example.post" }, 386 386 json: { contains: "hello world" }, // Searches entire record content ··· 394 394 }); 395 395 396 396 // Get records from any collection with global text search 397 - const allCollectionSearch = await client.social.slices.slice.getSliceRecords({ 397 + const allCollectionSearch = await client.network.slices.slice.getSliceRecords({ 398 398 where: { 399 399 json: { contains: "important content" }, // Searches ALL fields in ALL collections 400 400 }, ··· 451 451 452 452 ```typescript 453 453 // Search for "tutorial" across all collections 454 - const crossCollectionSearch = await client.social.slices.slice.getSliceRecords({ 455 - where: { 456 - json: { contains: "tutorial" }, 454 + const crossCollectionSearch = await client.network.slices.slice.getSliceRecords( 455 + { 456 + where: { 457 + json: { contains: "tutorial" }, 458 + }, 457 459 }, 458 - }); 460 + ); 459 461 460 462 // Limit to specific collections 461 - const specificSearch = await client.social.slices.slice.getSliceRecords({ 463 + const specificSearch = await client.network.slices.slice.getSliceRecords({ 462 464 where: { 463 465 collection: { in: ["com.example.post", "com.example.article"] }, 464 466 json: { contains: "guide" }, ··· 468 470 469 471 ### OR Query Support 470 472 471 - You can use OR queries to find records that match any of multiple conditions using the separate `orWhere` parameter. This provides clean type safety and autocomplete for field names: 473 + You can use OR queries to find records that match any of multiple conditions 474 + using the separate `orWhere` parameter. This provides clean type safety and 475 + autocomplete for field names: 472 476 473 477 ```typescript 474 478 // Find posts by either user1 OR user2 475 479 const posts = await client.com.example.post.getRecords({ 476 480 orWhere: { 477 - did: { in: ["did:plc:user1", "did:plc:user2"] } 478 - } 481 + did: { in: ["did:plc:user1", "did:plc:user2"] }, 482 + }, 479 483 }); 480 484 481 485 // Find posts that either have "typescript" in title OR are by a specific user 482 486 const posts = await client.com.example.post.getRecords({ 483 487 orWhere: { 484 488 title: { contains: "typescript" }, 485 - did: { eq: "did:plc:alice" } 486 - } 489 + did: { eq: "did:plc:alice" }, 490 + }, 487 491 }); 488 492 489 493 // Combining OR with regular AND conditions 490 494 const posts = await client.com.example.post.getRecords({ 491 495 where: { 492 - createdAt: { eq: "2025-09-03" }, // AND conditions 496 + createdAt: { eq: "2025-09-03" }, // AND conditions 493 497 }, 494 - orWhere: { // OR conditions 498 + orWhere: { // OR conditions 495 499 title: { contains: "guide" }, 496 - did: { eq: "did:plc:user1" } 497 - } 500 + did: { eq: "did:plc:user1" }, 501 + }, 498 502 }); 499 503 // SQL: WHERE created_at = '2025-09-03' AND (title LIKE '%guide%' OR did = 'did:plc:user1') 500 504 501 505 // OR queries work with cross-collection searches too 502 - const crossCollectionOrSearch = await client.social.slices.slice.getSliceRecords({ 503 - where: { 504 - collection: { eq: "com.example.post" }, 505 - }, 506 - orWhere: { 507 - title: { contains: "javascript" }, 508 - tags: { contains: "tutorial" } 509 - } 510 - }); 506 + const crossCollectionOrSearch = await client.network.slices.slice 507 + .getSliceRecords({ 508 + where: { 509 + collection: { eq: "com.example.post" }, 510 + }, 511 + orWhere: { 512 + title: { contains: "javascript" }, 513 + tags: { contains: "tutorial" }, 514 + }, 515 + }); 511 516 512 517 // You get full autocomplete and type safety for field names in both where and orWhere 513 518 const typedSearch = await client.com.example.post.getRecords({ ··· 519 524 // And also provides autocomplete here 520 525 description: { contains: "tutorial" }, 521 526 tags: { contains: "guide" }, 522 - } 527 + }, 523 528 }); 524 529 ``` 525 530 ··· 527 532 528 533 ```typescript 529 534 // Sync current user's data (requires auth) 530 - const syncResult = await client.social.slices.slice.syncUserCollections({ 535 + const syncResult = await client.network.slices.slice.syncUserCollections({ 531 536 timeoutSeconds: 30, 532 537 }); 533 538 ··· 593 598 const client = new AtProtoClient(apiUrl, sliceUri, oauthClient); 594 599 595 600 // OAuth tokens are automatically managed 596 - const profile = await client.social.slices.actor.profile.createRecord({ 601 + const profile = await client.network.slices.actor.profile.createRecord({ 597 602 displayName: "New User", 598 603 description: "My profile", 599 604 }, true); // useSelfRkey for profile
+1 -1
frontend/.env.example
··· 3 3 OAUTH_REDIRECT_URI="http://localhost:8080/oauth/callback" 4 4 OAUTH_AIP_BASE_URL="https://your-domain.com" 5 5 API_URL="http://localhost:3000" 6 - SLICE_URI="at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lwzmbjpqxk2q" 6 + SLICE_URI="at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z"
+1 -1
frontend/fly.toml
··· 13 13 PORT = '8080' 14 14 API_URL = 'https://slices-api.fly.dev' 15 15 DATABASE_URL = '/data/slices.db' 16 - SLICE_URI = 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lwzmbjpqxk2q' 16 + SLICE_URI = 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z' 17 17 DENO_ENV = 'production' 18 18 19 19 [http_service]
+127 -101
frontend/src/client.test.ts
··· 75 75 76 76 // Mock fetch function that simulates API responses 77 77 function createMockFetch() { 78 - return async (url: string | URL | Request, init?: RequestInit): Promise<Response> => { 78 + return async ( 79 + url: string | URL | Request, 80 + init?: RequestInit 81 + ): Promise<Response> => { 79 82 const requestUrl = url.toString(); 80 83 const method = init?.method || "GET"; 81 - const headers = init?.headers as Record<string, string> || {}; 82 - 84 + const headers = (init?.headers as Record<string, string>) || {}; 85 + 83 86 console.log(`Mock fetch: ${method} ${requestUrl}`); 84 87 console.log(`Authorization header: ${headers["Authorization"] || "none"}`); 85 - 88 + 86 89 // Check if request has valid authorization 87 - const hasAuth = headers["Authorization"]?.startsWith("Bearer valid") || 88 - headers["Authorization"]?.startsWith("Bearer refreshed"); 89 - 90 + const hasAuth = 91 + headers["Authorization"]?.startsWith("Bearer valid") || 92 + headers["Authorization"]?.startsWith("Bearer refreshed"); 93 + 90 94 if (method === "GET") { 91 95 // GET requests always succeed (read-only operations) 92 - return new Response(JSON.stringify({ 93 - records: [ 94 - { uri: "test://uri", cid: "test-cid", value: { name: "Test Record" } } 95 - ] 96 - }), { 97 - status: 200, 98 - headers: { "Content-Type": "application/json" } 99 - }); 96 + return new Response( 97 + JSON.stringify({ 98 + records: [ 99 + { 100 + uri: "test://uri", 101 + cid: "test-cid", 102 + value: { name: "Test Record" }, 103 + }, 104 + ], 105 + }), 106 + { 107 + status: 200, 108 + headers: { "Content-Type": "application/json" }, 109 + } 110 + ); 100 111 } else { 101 112 // POST/PUT/DELETE require valid auth 102 113 if (hasAuth) { 103 - return new Response(JSON.stringify({ 104 - uri: "test://created-uri", 105 - cid: "test-created-cid" 106 - }), { 107 - status: 200, 108 - headers: { "Content-Type": "application/json" } 109 - }); 114 + return new Response( 115 + JSON.stringify({ 116 + uri: "test://created-uri", 117 + cid: "test-created-cid", 118 + }), 119 + { 120 + status: 200, 121 + headers: { "Content-Type": "application/json" }, 122 + } 123 + ); 110 124 } else { 111 - return new Response(JSON.stringify({ 112 - error: "Unauthorized" 113 - }), { 114 - status: 401, 115 - headers: { "Content-Type": "application/json" } 116 - }); 125 + return new Response( 126 + JSON.stringify({ 127 + error: "Unauthorized", 128 + }), 129 + { 130 + status: 401, 131 + headers: { "Content-Type": "application/json" }, 132 + } 133 + ); 117 134 } 118 135 } 119 136 }; ··· 125 142 const mockOAuth = new MockOAuthClient(tokenState); 126 143 return new AtProtoClient( 127 144 "https://test-api.example.com", 128 - "at://did:plc:test/social.slices.slice/test", 145 + "at://did:plc:test/network.slices.slice/test", 129 146 mockOAuth as any 130 147 ); 131 148 } ··· 133 150 Deno.test("Valid tokens - read operation should succeed", async () => { 134 151 // Setup mock fetch 135 152 globalThis.fetch = createMockFetch(); 136 - 153 + 137 154 try { 138 155 const client = createTestClient("valid"); 139 - await client.social.slices.slice.getRecords(); 156 + await client.network.slices.slice.getRecords(); 140 157 console.log("Read operation succeeded as expected"); 141 158 } catch (error) { 142 159 throw new Error(`Read operation should have succeeded: ${error}`); ··· 149 166 Deno.test("Valid tokens - write operation should succeed", async () => { 150 167 // Setup mock fetch 151 168 globalThis.fetch = createMockFetch(); 152 - 169 + 153 170 try { 154 171 const client = createTestClient("valid"); 155 - await client.social.slices.slice.updateRecord("test", { 172 + await client.network.slices.slice.updateRecord("test", { 156 173 name: "Test Slice", 157 174 createdAt: new Date().toISOString(), 158 175 }); ··· 170 187 async () => { 171 188 // Setup mock fetch 172 189 globalThis.fetch = createMockFetch(); 173 - 190 + 174 191 try { 175 192 const client = createTestClient("expired"); 176 - await client.social.slices.slice.getRecords(); 193 + await client.network.slices.slice.getRecords(); 177 194 console.log("Read operation succeeded after token refresh"); 178 195 } catch (error) { 179 196 throw new Error( ··· 191 208 async () => { 192 209 // Setup mock fetch 193 210 globalThis.fetch = createMockFetch(); 194 - 211 + 195 212 try { 196 213 const client = createTestClient("expired"); 197 - await client.social.slices.slice.updateRecord("test", { 214 + await client.network.slices.slice.updateRecord("test", { 198 215 name: "Test Slice", 199 216 createdAt: new Date().toISOString(), 200 217 }); ··· 213 230 Deno.test("Token refresh fails - read operation should succeed", async () => { 214 231 // Setup mock fetch 215 232 globalThis.fetch = createMockFetch(); 216 - 233 + 217 234 try { 218 235 const client = createTestClient("refresh_fails"); 219 - await client.social.slices.slice.getRecords(); 236 + await client.network.slices.slice.getRecords(); 220 237 console.log("Read operation succeeded without auth (as expected)"); 221 238 } catch (error) { 222 239 throw new Error( ··· 233 250 async () => { 234 251 // Setup mock fetch 235 252 globalThis.fetch = createMockFetch(); 236 - 253 + 237 254 try { 238 255 const client = createTestClient("refresh_fails"); 239 256 await assertRejects( 240 257 async () => { 241 - await client.social.slices.slice.updateRecord("test", { 258 + await client.network.slices.slice.updateRecord("test", { 242 259 name: "Test Slice", 243 260 createdAt: new Date().toISOString(), 244 261 }); ··· 257 274 Deno.test("No tokens - read operation should succeed", async () => { 258 275 // Setup mock fetch 259 276 globalThis.fetch = createMockFetch(); 260 - 277 + 261 278 try { 262 279 const client = createTestClient("no_tokens"); 263 - await client.social.slices.slice.getRecords(); 280 + await client.network.slices.slice.getRecords(); 264 281 console.log("Read operation succeeded without tokens (as expected)"); 265 282 } catch (error) { 266 283 throw new Error( ··· 277 294 async () => { 278 295 // Setup mock fetch 279 296 globalThis.fetch = createMockFetch(); 280 - 297 + 281 298 try { 282 299 const client = createTestClient("no_tokens"); 283 300 await assertRejects( 284 301 async () => { 285 - await client.social.slices.slice.updateRecord("test", { 302 + await client.network.slices.slice.updateRecord("test", { 286 303 name: "Test Slice", 287 304 createdAt: new Date().toISOString(), 288 305 }); ··· 298 315 } 299 316 ); 300 317 301 - Deno.test( 302 - "401 response triggers token refresh and retry", 303 - async () => { 304 - // Create a fetch that returns 401 first time, 200 second time 305 - let callCount = 0; 306 - const mockFetch = (url: string | URL | Request, init?: RequestInit): Promise<Response> => { 307 - callCount++; 308 - const requestUrl = url.toString(); 309 - const method = init?.method || "GET"; 310 - const headers = init?.headers as Record<string, string> || {}; 311 - 312 - console.log(`Mock fetch call ${callCount}: ${method} ${requestUrl}`); 313 - console.log(`Authorization header: ${headers["Authorization"] || "none"}`); 314 - 315 - if (method === "POST") { 316 - if (callCount === 1) { 317 - // First call returns 401 318 - console.log("First call - returning 401 Unauthorized"); 319 - return Promise.resolve(new Response(JSON.stringify({ error: "Unauthorized" }), { 318 + Deno.test("401 response triggers token refresh and retry", async () => { 319 + // Create a fetch that returns 401 first time, 200 second time 320 + let callCount = 0; 321 + const mockFetch = ( 322 + url: string | URL | Request, 323 + init?: RequestInit 324 + ): Promise<Response> => { 325 + callCount++; 326 + const requestUrl = url.toString(); 327 + const method = init?.method || "GET"; 328 + const headers = (init?.headers as Record<string, string>) || {}; 329 + 330 + console.log(`Mock fetch call ${callCount}: ${method} ${requestUrl}`); 331 + console.log(`Authorization header: ${headers["Authorization"] || "none"}`); 332 + 333 + if (method === "POST") { 334 + if (callCount === 1) { 335 + // First call returns 401 336 + console.log("First call - returning 401 Unauthorized"); 337 + return Promise.resolve( 338 + new Response(JSON.stringify({ error: "Unauthorized" }), { 320 339 status: 401, 321 - headers: { "Content-Type": "application/json" } 322 - })); 323 - } else { 324 - // Second call should succeed (with refreshed token) 325 - console.log("Second call - returning success"); 326 - return Promise.resolve(new Response(JSON.stringify({ 327 - uri: "test://created-uri", 328 - cid: "test-created-cid" 329 - }), { 330 - status: 200, 331 - headers: { "Content-Type": "application/json" } 332 - })); 333 - } 334 - } 335 - 336 - return Promise.resolve(new Response("Not found", { status: 404 })); 337 - }; 338 - 339 - globalThis.fetch = mockFetch; 340 - 341 - try { 342 - const client = createTestClient("valid"); 343 - await client.social.slices.slice.updateRecord("test", { 344 - name: "Test Slice", 345 - createdAt: new Date().toISOString(), 346 - }); 347 - 348 - // Should have made exactly 2 calls 349 - console.log(`Total fetch calls made: ${callCount}`); 350 - if (callCount !== 2) { 351 - throw new Error(`Expected 2 fetch calls but got ${callCount}`); 340 + headers: { "Content-Type": "application/json" }, 341 + }) 342 + ); 343 + } else { 344 + // Second call should succeed (with refreshed token) 345 + console.log("Second call - returning success"); 346 + return Promise.resolve( 347 + new Response( 348 + JSON.stringify({ 349 + uri: "test://created-uri", 350 + cid: "test-created-cid", 351 + }), 352 + { 353 + status: 200, 354 + headers: { "Content-Type": "application/json" }, 355 + } 356 + ) 357 + ); 352 358 } 353 - 354 - console.log("401 retry test passed - request was retried after token refresh"); 355 - } finally { 356 - // Restore original fetch 357 - globalThis.fetch = originalFetch; 359 + } 360 + 361 + return Promise.resolve(new Response("Not found", { status: 404 })); 362 + }; 363 + 364 + globalThis.fetch = mockFetch; 365 + 366 + try { 367 + const client = createTestClient("valid"); 368 + await client.network.slices.slice.updateRecord("test", { 369 + name: "Test Slice", 370 + createdAt: new Date().toISOString(), 371 + }); 372 + 373 + // Should have made exactly 2 calls 374 + console.log(`Total fetch calls made: ${callCount}`); 375 + if (callCount !== 2) { 376 + throw new Error(`Expected 2 fetch calls but got ${callCount}`); 358 377 } 378 + 379 + console.log( 380 + "401 retry test passed - request was retried after token refresh" 381 + ); 382 + } finally { 383 + // Restore original fetch 384 + globalThis.fetch = originalFetch; 359 385 } 360 - ); 386 + });
+113 -105
frontend/src/client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-07 23:17:29 UTC 2 + // Generated at: 2025-09-12 05:27:35 UTC 3 3 // Lexicons: 6 4 4 5 5 /** ··· 9 9 * 10 10 * const client = new AtProtoClient( 11 11 * 'https://slices-api.fly.dev', 12 - * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lx5zq4t56s2q' 12 + * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z' 13 13 * ); 14 14 * 15 15 * // Get records from the app.bsky.actor.profile collection ··· 28 28 * }); 29 29 * 30 30 * // Use slice-level methods for cross-collection queries with type safety 31 - * const sliceRecords = await client.social.slices.slice.getSliceRecords<AppBskyActorProfile>({ 31 + * const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile>({ 32 32 * where: { 33 33 * collection: { eq: 'app.bsky.actor.profile' } 34 34 * } 35 35 * }); 36 36 * 37 37 * // Search across multiple collections using union types 38 - * const multiCollectionRecords = await client.social.slices.slice.getSliceRecords<AppBskyActorProfile | AppBskyActorProfile>({ 38 + * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile | AppBskyActorProfile>({ 39 39 * where: { 40 40 * collection: { in: ['app.bsky.actor.profile', 'app.bsky.actor.profile'] }, 41 41 * text: { contains: 'example search term' }, ··· 379 379 | "description" 380 380 | "displayName"; 381 381 382 - export interface SocialSlicesSlice { 382 + export interface NetworkSlicesSlice { 383 383 /** Name of the slice */ 384 384 name: string; 385 385 /** Primary domain namespace for this slice (e.g. social.grain) */ ··· 388 388 createdAt: string; 389 389 } 390 390 391 - export type SocialSlicesSliceSortFields = "name" | "domain" | "createdAt"; 391 + export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt"; 392 392 393 - export interface SocialSlicesLexicon { 393 + export interface NetworkSlicesLexicon { 394 394 /** Namespaced identifier for the lexicon */ 395 395 nsid: string; 396 396 /** The lexicon schema definitions as JSON */ ··· 403 403 slice: string; 404 404 } 405 405 406 - export type SocialSlicesLexiconSortFields = 406 + export type NetworkSlicesLexiconSortFields = 407 407 | "nsid" 408 408 | "definitions" 409 409 | "createdAt" 410 410 | "updatedAt" 411 411 | "slice"; 412 412 413 - export interface SocialSlicesActorProfile { 413 + export interface NetworkSlicesActorProfile { 414 414 displayName?: string; 415 415 /** Free-form profile description text. */ 416 416 description?: string; ··· 419 419 createdAt?: string; 420 420 } 421 421 422 - export type SocialSlicesActorProfileSortFields = 422 + export type NetworkSlicesActorProfileSortFields = 423 423 | "displayName" 424 424 | "description" 425 425 | "createdAt"; ··· 791 791 } 792 792 } 793 793 794 - class SliceSlicesSocialClient extends BaseClient { 794 + class SliceSlicesNetworkClient extends BaseClient { 795 795 private readonly sliceUri: string; 796 796 797 797 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { ··· 803 803 limit?: number; 804 804 cursor?: string; 805 805 where?: { 806 - [K in SocialSlicesSliceSortFields | IndexedRecordFields]?: WhereCondition; 806 + [K in 807 + | NetworkSlicesSliceSortFields 808 + | IndexedRecordFields]?: WhereCondition; 807 809 }; 808 810 orWhere?: { 809 - [K in SocialSlicesSliceSortFields | IndexedRecordFields]?: WhereCondition; 811 + [K in 812 + | NetworkSlicesSliceSortFields 813 + | IndexedRecordFields]?: WhereCondition; 810 814 }; 811 - sortBy?: SortField<SocialSlicesSliceSortFields>[]; 812 - }): Promise<GetRecordsResponse<SocialSlicesSlice>> { 815 + sortBy?: SortField<NetworkSlicesSliceSortFields>[]; 816 + }): Promise<GetRecordsResponse<NetworkSlicesSlice>> { 813 817 // Combine where and orWhere into the expected backend format 814 818 const whereClause: any = params?.where ? { ...params.where } : {}; 815 819 if (params?.orWhere) { ··· 823 827 slice: this.sliceUri, 824 828 }; 825 829 const result = await this.makeRequest<SliceRecordsOutput>( 826 - "social.slices.slice.getRecords", 830 + "network.slices.slice.getRecords", 827 831 "POST", 828 832 requestParams 829 833 ); ··· 833 837 cid: record.cid, 834 838 did: record.did, 835 839 collection: record.collection, 836 - value: record.value as unknown as SocialSlicesSlice, 840 + value: record.value as unknown as NetworkSlicesSlice, 837 841 indexedAt: record.indexedAt, 838 842 })), 839 843 cursor: result.cursor, ··· 842 846 843 847 async getRecord( 844 848 params: GetRecordParams 845 - ): Promise<RecordResponse<SocialSlicesSlice>> { 849 + ): Promise<RecordResponse<NetworkSlicesSlice>> { 846 850 const requestParams = { ...params, slice: this.sliceUri }; 847 - return await this.makeRequest<RecordResponse<SocialSlicesSlice>>( 848 - "social.slices.slice.getRecord", 851 + return await this.makeRequest<RecordResponse<NetworkSlicesSlice>>( 852 + "network.slices.slice.getRecord", 849 853 "GET", 850 854 requestParams 851 855 ); ··· 855 859 limit?: number; 856 860 cursor?: string; 857 861 where?: { 858 - [K in SocialSlicesSliceSortFields | IndexedRecordFields]?: WhereCondition; 862 + [K in 863 + | NetworkSlicesSliceSortFields 864 + | IndexedRecordFields]?: WhereCondition; 859 865 }; 860 866 orWhere?: { 861 - [K in SocialSlicesSliceSortFields | IndexedRecordFields]?: WhereCondition; 867 + [K in 868 + | NetworkSlicesSliceSortFields 869 + | IndexedRecordFields]?: WhereCondition; 862 870 }; 863 - sortBy?: SortField<SocialSlicesSliceSortFields>[]; 871 + sortBy?: SortField<NetworkSlicesSliceSortFields>[]; 864 872 }): Promise<CountRecordsResponse> { 865 873 // Combine where and orWhere into the expected backend format 866 874 const whereClause: any = params?.where ? { ...params.where } : {}; ··· 875 883 slice: this.sliceUri, 876 884 }; 877 885 return await this.makeRequest<CountRecordsResponse>( 878 - "social.slices.slice.countRecords", 886 + "network.slices.slice.countRecords", 879 887 "POST", 880 888 requestParams 881 889 ); 882 890 } 883 891 884 892 async createRecord( 885 - record: SocialSlicesSlice, 893 + record: NetworkSlicesSlice, 886 894 useSelfRkey?: boolean 887 895 ): Promise<{ uri: string; cid: string }> { 888 - const recordValue = { $type: "social.slices.slice", ...record }; 896 + const recordValue = { $type: "network.slices.slice", ...record }; 889 897 const payload = { 890 898 slice: this.sliceUri, 891 899 ...(useSelfRkey ? { rkey: "self" } : {}), 892 900 record: recordValue, 893 901 }; 894 902 return await this.makeRequest<{ uri: string; cid: string }>( 895 - "social.slices.slice.createRecord", 903 + "network.slices.slice.createRecord", 896 904 "POST", 897 905 payload 898 906 ); ··· 900 908 901 909 async updateRecord( 902 910 rkey: string, 903 - record: SocialSlicesSlice 911 + record: NetworkSlicesSlice 904 912 ): Promise<{ uri: string; cid: string }> { 905 - const recordValue = { $type: "social.slices.slice", ...record }; 913 + const recordValue = { $type: "network.slices.slice", ...record }; 906 914 const payload = { 907 915 slice: this.sliceUri, 908 916 rkey, 909 917 record: recordValue, 910 918 }; 911 919 return await this.makeRequest<{ uri: string; cid: string }>( 912 - "social.slices.slice.updateRecord", 920 + "network.slices.slice.updateRecord", 913 921 "POST", 914 922 payload 915 923 ); ··· 917 925 918 926 async deleteRecord(rkey: string): Promise<void> { 919 927 return await this.makeRequest<void>( 920 - "social.slices.slice.deleteRecord", 928 + "network.slices.slice.deleteRecord", 921 929 "POST", 922 930 { rkey } 923 931 ); ··· 925 933 926 934 async codegen(request: CodegenXrpcRequest): Promise<CodegenXrpcResponse> { 927 935 return await this.makeRequest<CodegenXrpcResponse>( 928 - "social.slices.slice.codegen", 936 + "network.slices.slice.codegen", 929 937 "POST", 930 938 request 931 939 ); ··· 933 941 934 942 async stats(params: SliceStatsParams): Promise<SliceStatsOutput> { 935 943 return await this.makeRequest<SliceStatsOutput>( 936 - "social.slices.slice.stats", 944 + "network.slices.slice.stats", 937 945 "POST", 938 946 params 939 947 ); ··· 955 963 slice: this.sliceUri, 956 964 }; 957 965 return await this.makeRequest<SliceRecordsOutput<T>>( 958 - "social.slices.slice.getSliceRecords", 966 + "network.slices.slice.getSliceRecords", 959 967 "POST", 960 968 requestParams 961 969 ); ··· 964 972 async getActors(params?: GetActorsParams): Promise<GetActorsResponse> { 965 973 const requestParams = { ...params, slice: this.sliceUri }; 966 974 return await this.makeRequest<GetActorsResponse>( 967 - "social.slices.slice.getActors", 975 + "network.slices.slice.getActors", 968 976 "POST", 969 977 requestParams 970 978 ); ··· 973 981 async startSync(params: BulkSyncParams): Promise<SyncJobResponse> { 974 982 const requestParams = { ...params, slice: this.sliceUri }; 975 983 return await this.makeRequest<SyncJobResponse>( 976 - "social.slices.slice.startSync", 984 + "network.slices.slice.startSync", 977 985 "POST", 978 986 requestParams 979 987 ); ··· 981 989 982 990 async getJobStatus(params: GetJobStatusParams): Promise<JobStatus> { 983 991 return await this.makeRequest<JobStatus>( 984 - "social.slices.slice.getJobStatus", 992 + "network.slices.slice.getJobStatus", 985 993 "GET", 986 994 params 987 995 ); ··· 991 999 params: GetJobHistoryParams 992 1000 ): Promise<GetJobHistoryResponse> { 993 1001 return await this.makeRequest<GetJobHistoryResponse>( 994 - "social.slices.slice.getJobHistory", 1002 + "network.slices.slice.getJobHistory", 995 1003 "GET", 996 1004 params 997 1005 ); ··· 999 1007 1000 1008 async getJobLogs(params: GetJobLogsParams): Promise<GetJobLogsResponse> { 1001 1009 return await this.makeRequest<GetJobLogsResponse>( 1002 - "social.slices.slice.getJobLogs", 1010 + "network.slices.slice.getJobLogs", 1003 1011 "GET", 1004 1012 params 1005 1013 ); ··· 1007 1015 1008 1016 async getJetstreamStatus(): Promise<JetstreamStatusResponse> { 1009 1017 return await this.makeRequest<JetstreamStatusResponse>( 1010 - "social.slices.slice.getJetstreamStatus", 1018 + "network.slices.slice.getJetstreamStatus", 1011 1019 "GET" 1012 1020 ); 1013 1021 } ··· 1016 1024 params: GetJetstreamLogsParams 1017 1025 ): Promise<GetJetstreamLogsResponse> { 1018 1026 return await this.makeRequest<GetJetstreamLogsResponse>( 1019 - "social.slices.slice.getJetstreamLogs", 1027 + "network.slices.slice.getJetstreamLogs", 1020 1028 "GET", 1021 1029 params 1022 1030 ); ··· 1027 1035 ): Promise<SyncUserCollectionsResult> { 1028 1036 const requestParams = { slice: this.sliceUri, ...params }; 1029 1037 return await this.makeRequest<SyncUserCollectionsResult>( 1030 - "social.slices.slice.syncUserCollections", 1038 + "network.slices.slice.syncUserCollections", 1031 1039 "POST", 1032 1040 requestParams 1033 1041 ); ··· 1038 1046 ): Promise<OAuthClientDetails> { 1039 1047 const requestParams = { ...params, sliceUri: this.sliceUri }; 1040 1048 return await this.makeRequest<OAuthClientDetails>( 1041 - "social.slices.slice.createOAuthClient", 1049 + "network.slices.slice.createOAuthClient", 1042 1050 "POST", 1043 1051 requestParams 1044 1052 ); ··· 1047 1055 async getOAuthClients(): Promise<ListOAuthClientsResponse> { 1048 1056 const requestParams = { slice: this.sliceUri }; 1049 1057 return await this.makeRequest<ListOAuthClientsResponse>( 1050 - "social.slices.slice.getOAuthClients", 1058 + "network.slices.slice.getOAuthClients", 1051 1059 "GET", 1052 1060 requestParams 1053 1061 ); ··· 1058 1066 ): Promise<OAuthClientDetails> { 1059 1067 const requestParams = { ...params, sliceUri: this.sliceUri }; 1060 1068 return await this.makeRequest<OAuthClientDetails>( 1061 - "social.slices.slice.updateOAuthClient", 1069 + "network.slices.slice.updateOAuthClient", 1062 1070 "POST", 1063 1071 requestParams 1064 1072 ); ··· 1068 1076 clientId: string 1069 1077 ): Promise<DeleteOAuthClientResponse> { 1070 1078 return await this.makeRequest<DeleteOAuthClientResponse>( 1071 - "social.slices.slice.deleteOAuthClient", 1079 + "network.slices.slice.deleteOAuthClient", 1072 1080 "POST", 1073 1081 { clientId } 1074 1082 ); 1075 1083 } 1076 1084 } 1077 1085 1078 - class LexiconSlicesSocialClient extends BaseClient { 1086 + class LexiconSlicesNetworkClient extends BaseClient { 1079 1087 private readonly sliceUri: string; 1080 1088 1081 1089 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { ··· 1088 1096 cursor?: string; 1089 1097 where?: { 1090 1098 [K in 1091 - | SocialSlicesLexiconSortFields 1099 + | NetworkSlicesLexiconSortFields 1092 1100 | IndexedRecordFields]?: WhereCondition; 1093 1101 }; 1094 1102 orWhere?: { 1095 1103 [K in 1096 - | SocialSlicesLexiconSortFields 1104 + | NetworkSlicesLexiconSortFields 1097 1105 | IndexedRecordFields]?: WhereCondition; 1098 1106 }; 1099 - sortBy?: SortField<SocialSlicesLexiconSortFields>[]; 1100 - }): Promise<GetRecordsResponse<SocialSlicesLexicon>> { 1107 + sortBy?: SortField<NetworkSlicesLexiconSortFields>[]; 1108 + }): Promise<GetRecordsResponse<NetworkSlicesLexicon>> { 1101 1109 // Combine where and orWhere into the expected backend format 1102 1110 const whereClause: any = params?.where ? { ...params.where } : {}; 1103 1111 if (params?.orWhere) { ··· 1111 1119 slice: this.sliceUri, 1112 1120 }; 1113 1121 const result = await this.makeRequest<SliceRecordsOutput>( 1114 - "social.slices.lexicon.getRecords", 1122 + "network.slices.lexicon.getRecords", 1115 1123 "POST", 1116 1124 requestParams 1117 1125 ); ··· 1121 1129 cid: record.cid, 1122 1130 did: record.did, 1123 1131 collection: record.collection, 1124 - value: record.value as unknown as SocialSlicesLexicon, 1132 + value: record.value as unknown as NetworkSlicesLexicon, 1125 1133 indexedAt: record.indexedAt, 1126 1134 })), 1127 1135 cursor: result.cursor, ··· 1130 1138 1131 1139 async getRecord( 1132 1140 params: GetRecordParams 1133 - ): Promise<RecordResponse<SocialSlicesLexicon>> { 1141 + ): Promise<RecordResponse<NetworkSlicesLexicon>> { 1134 1142 const requestParams = { ...params, slice: this.sliceUri }; 1135 - return await this.makeRequest<RecordResponse<SocialSlicesLexicon>>( 1136 - "social.slices.lexicon.getRecord", 1143 + return await this.makeRequest<RecordResponse<NetworkSlicesLexicon>>( 1144 + "network.slices.lexicon.getRecord", 1137 1145 "GET", 1138 1146 requestParams 1139 1147 ); ··· 1144 1152 cursor?: string; 1145 1153 where?: { 1146 1154 [K in 1147 - | SocialSlicesLexiconSortFields 1155 + | NetworkSlicesLexiconSortFields 1148 1156 | IndexedRecordFields]?: WhereCondition; 1149 1157 }; 1150 1158 orWhere?: { 1151 1159 [K in 1152 - | SocialSlicesLexiconSortFields 1160 + | NetworkSlicesLexiconSortFields 1153 1161 | IndexedRecordFields]?: WhereCondition; 1154 1162 }; 1155 - sortBy?: SortField<SocialSlicesLexiconSortFields>[]; 1163 + sortBy?: SortField<NetworkSlicesLexiconSortFields>[]; 1156 1164 }): Promise<CountRecordsResponse> { 1157 1165 // Combine where and orWhere into the expected backend format 1158 1166 const whereClause: any = params?.where ? { ...params.where } : {}; ··· 1167 1175 slice: this.sliceUri, 1168 1176 }; 1169 1177 return await this.makeRequest<CountRecordsResponse>( 1170 - "social.slices.lexicon.countRecords", 1178 + "network.slices.lexicon.countRecords", 1171 1179 "POST", 1172 1180 requestParams 1173 1181 ); 1174 1182 } 1175 1183 1176 1184 async createRecord( 1177 - record: SocialSlicesLexicon, 1185 + record: NetworkSlicesLexicon, 1178 1186 useSelfRkey?: boolean 1179 1187 ): Promise<{ uri: string; cid: string }> { 1180 - const recordValue = { $type: "social.slices.lexicon", ...record }; 1188 + const recordValue = { $type: "network.slices.lexicon", ...record }; 1181 1189 const payload = { 1182 1190 slice: this.sliceUri, 1183 1191 ...(useSelfRkey ? { rkey: "self" } : {}), 1184 1192 record: recordValue, 1185 1193 }; 1186 1194 return await this.makeRequest<{ uri: string; cid: string }>( 1187 - "social.slices.lexicon.createRecord", 1195 + "network.slices.lexicon.createRecord", 1188 1196 "POST", 1189 1197 payload 1190 1198 ); ··· 1192 1200 1193 1201 async updateRecord( 1194 1202 rkey: string, 1195 - record: SocialSlicesLexicon 1203 + record: NetworkSlicesLexicon 1196 1204 ): Promise<{ uri: string; cid: string }> { 1197 - const recordValue = { $type: "social.slices.lexicon", ...record }; 1205 + const recordValue = { $type: "network.slices.lexicon", ...record }; 1198 1206 const payload = { 1199 1207 slice: this.sliceUri, 1200 1208 rkey, 1201 1209 record: recordValue, 1202 1210 }; 1203 1211 return await this.makeRequest<{ uri: string; cid: string }>( 1204 - "social.slices.lexicon.updateRecord", 1212 + "network.slices.lexicon.updateRecord", 1205 1213 "POST", 1206 1214 payload 1207 1215 ); ··· 1209 1217 1210 1218 async deleteRecord(rkey: string): Promise<void> { 1211 1219 return await this.makeRequest<void>( 1212 - "social.slices.lexicon.deleteRecord", 1220 + "network.slices.lexicon.deleteRecord", 1213 1221 "POST", 1214 1222 { rkey } 1215 1223 ); 1216 1224 } 1217 1225 } 1218 1226 1219 - class ProfileActorSlicesSocialClient extends BaseClient { 1227 + class ProfileActorSlicesNetworkClient extends BaseClient { 1220 1228 private readonly sliceUri: string; 1221 1229 1222 1230 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { ··· 1229 1237 cursor?: string; 1230 1238 where?: { 1231 1239 [K in 1232 - | SocialSlicesActorProfileSortFields 1240 + | NetworkSlicesActorProfileSortFields 1233 1241 | IndexedRecordFields]?: WhereCondition; 1234 1242 }; 1235 1243 orWhere?: { 1236 1244 [K in 1237 - | SocialSlicesActorProfileSortFields 1245 + | NetworkSlicesActorProfileSortFields 1238 1246 | IndexedRecordFields]?: WhereCondition; 1239 1247 }; 1240 - sortBy?: SortField<SocialSlicesActorProfileSortFields>[]; 1241 - }): Promise<GetRecordsResponse<SocialSlicesActorProfile>> { 1248 + sortBy?: SortField<NetworkSlicesActorProfileSortFields>[]; 1249 + }): Promise<GetRecordsResponse<NetworkSlicesActorProfile>> { 1242 1250 // Combine where and orWhere into the expected backend format 1243 1251 const whereClause: any = params?.where ? { ...params.where } : {}; 1244 1252 if (params?.orWhere) { ··· 1252 1260 slice: this.sliceUri, 1253 1261 }; 1254 1262 const result = await this.makeRequest<SliceRecordsOutput>( 1255 - "social.slices.actor.profile.getRecords", 1263 + "network.slices.actor.profile.getRecords", 1256 1264 "POST", 1257 1265 requestParams 1258 1266 ); ··· 1262 1270 cid: record.cid, 1263 1271 did: record.did, 1264 1272 collection: record.collection, 1265 - value: record.value as unknown as SocialSlicesActorProfile, 1273 + value: record.value as unknown as NetworkSlicesActorProfile, 1266 1274 indexedAt: record.indexedAt, 1267 1275 })), 1268 1276 cursor: result.cursor, ··· 1271 1279 1272 1280 async getRecord( 1273 1281 params: GetRecordParams 1274 - ): Promise<RecordResponse<SocialSlicesActorProfile>> { 1282 + ): Promise<RecordResponse<NetworkSlicesActorProfile>> { 1275 1283 const requestParams = { ...params, slice: this.sliceUri }; 1276 - return await this.makeRequest<RecordResponse<SocialSlicesActorProfile>>( 1277 - "social.slices.actor.profile.getRecord", 1284 + return await this.makeRequest<RecordResponse<NetworkSlicesActorProfile>>( 1285 + "network.slices.actor.profile.getRecord", 1278 1286 "GET", 1279 1287 requestParams 1280 1288 ); ··· 1285 1293 cursor?: string; 1286 1294 where?: { 1287 1295 [K in 1288 - | SocialSlicesActorProfileSortFields 1296 + | NetworkSlicesActorProfileSortFields 1289 1297 | IndexedRecordFields]?: WhereCondition; 1290 1298 }; 1291 1299 orWhere?: { 1292 1300 [K in 1293 - | SocialSlicesActorProfileSortFields 1301 + | NetworkSlicesActorProfileSortFields 1294 1302 | IndexedRecordFields]?: WhereCondition; 1295 1303 }; 1296 - sortBy?: SortField<SocialSlicesActorProfileSortFields>[]; 1304 + sortBy?: SortField<NetworkSlicesActorProfileSortFields>[]; 1297 1305 }): Promise<CountRecordsResponse> { 1298 1306 // Combine where and orWhere into the expected backend format 1299 1307 const whereClause: any = params?.where ? { ...params.where } : {}; ··· 1308 1316 slice: this.sliceUri, 1309 1317 }; 1310 1318 return await this.makeRequest<CountRecordsResponse>( 1311 - "social.slices.actor.profile.countRecords", 1319 + "network.slices.actor.profile.countRecords", 1312 1320 "POST", 1313 1321 requestParams 1314 1322 ); 1315 1323 } 1316 1324 1317 1325 async createRecord( 1318 - record: SocialSlicesActorProfile, 1326 + record: NetworkSlicesActorProfile, 1319 1327 useSelfRkey?: boolean 1320 1328 ): Promise<{ uri: string; cid: string }> { 1321 - const recordValue = { $type: "social.slices.actor.profile", ...record }; 1329 + const recordValue = { $type: "network.slices.actor.profile", ...record }; 1322 1330 const payload = { 1323 1331 slice: this.sliceUri, 1324 1332 ...(useSelfRkey ? { rkey: "self" } : {}), 1325 1333 record: recordValue, 1326 1334 }; 1327 1335 return await this.makeRequest<{ uri: string; cid: string }>( 1328 - "social.slices.actor.profile.createRecord", 1336 + "network.slices.actor.profile.createRecord", 1329 1337 "POST", 1330 1338 payload 1331 1339 ); ··· 1333 1341 1334 1342 async updateRecord( 1335 1343 rkey: string, 1336 - record: SocialSlicesActorProfile 1344 + record: NetworkSlicesActorProfile 1337 1345 ): Promise<{ uri: string; cid: string }> { 1338 - const recordValue = { $type: "social.slices.actor.profile", ...record }; 1346 + const recordValue = { $type: "network.slices.actor.profile", ...record }; 1339 1347 const payload = { 1340 1348 slice: this.sliceUri, 1341 1349 rkey, 1342 1350 record: recordValue, 1343 1351 }; 1344 1352 return await this.makeRequest<{ uri: string; cid: string }>( 1345 - "social.slices.actor.profile.updateRecord", 1353 + "network.slices.actor.profile.updateRecord", 1346 1354 "POST", 1347 1355 payload 1348 1356 ); ··· 1350 1358 1351 1359 async deleteRecord(rkey: string): Promise<void> { 1352 1360 return await this.makeRequest<void>( 1353 - "social.slices.actor.profile.deleteRecord", 1361 + "network.slices.actor.profile.deleteRecord", 1354 1362 "POST", 1355 1363 { rkey } 1356 1364 ); 1357 1365 } 1358 1366 } 1359 1367 1360 - class ActorSlicesSocialClient extends BaseClient { 1361 - readonly profile: ProfileActorSlicesSocialClient; 1368 + class ActorSlicesNetworkClient extends BaseClient { 1369 + readonly profile: ProfileActorSlicesNetworkClient; 1362 1370 private readonly sliceUri: string; 1363 1371 1364 1372 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 1365 1373 super(baseUrl, oauthClient); 1366 1374 this.sliceUri = sliceUri; 1367 - this.profile = new ProfileActorSlicesSocialClient( 1375 + this.profile = new ProfileActorSlicesNetworkClient( 1368 1376 baseUrl, 1369 1377 sliceUri, 1370 1378 oauthClient ··· 1372 1380 } 1373 1381 } 1374 1382 1375 - class SlicesSocialClient extends BaseClient { 1376 - readonly slice: SliceSlicesSocialClient; 1377 - readonly lexicon: LexiconSlicesSocialClient; 1378 - readonly actor: ActorSlicesSocialClient; 1383 + class SlicesNetworkClient extends BaseClient { 1384 + readonly slice: SliceSlicesNetworkClient; 1385 + readonly lexicon: LexiconSlicesNetworkClient; 1386 + readonly actor: ActorSlicesNetworkClient; 1379 1387 private readonly sliceUri: string; 1380 1388 1381 1389 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 1382 1390 super(baseUrl, oauthClient); 1383 1391 this.sliceUri = sliceUri; 1384 - this.slice = new SliceSlicesSocialClient(baseUrl, sliceUri, oauthClient); 1385 - this.lexicon = new LexiconSlicesSocialClient( 1392 + this.slice = new SliceSlicesNetworkClient(baseUrl, sliceUri, oauthClient); 1393 + this.lexicon = new LexiconSlicesNetworkClient( 1386 1394 baseUrl, 1387 1395 sliceUri, 1388 1396 oauthClient 1389 1397 ); 1390 - this.actor = new ActorSlicesSocialClient(baseUrl, sliceUri, oauthClient); 1398 + this.actor = new ActorSlicesNetworkClient(baseUrl, sliceUri, oauthClient); 1391 1399 } 1392 1400 } 1393 1401 1394 - class SocialClient extends BaseClient { 1395 - readonly slices: SlicesSocialClient; 1402 + class NetworkClient extends BaseClient { 1403 + readonly slices: SlicesNetworkClient; 1396 1404 private readonly sliceUri: string; 1397 1405 1398 1406 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 1399 1407 super(baseUrl, oauthClient); 1400 1408 this.sliceUri = sliceUri; 1401 - this.slices = new SlicesSocialClient(baseUrl, sliceUri, oauthClient); 1409 + this.slices = new SlicesNetworkClient(baseUrl, sliceUri, oauthClient); 1402 1410 } 1403 1411 } 1404 1412 1405 1413 export class AtProtoClient extends BaseClient { 1406 1414 readonly app: AppClient; 1407 - readonly social: SocialClient; 1415 + readonly network: NetworkClient; 1408 1416 readonly oauth?: OAuthClient; 1409 1417 private readonly sliceUri: string; 1410 1418 ··· 1412 1420 super(baseUrl, oauthClient); 1413 1421 this.sliceUri = sliceUri; 1414 1422 this.app = new AppClient(baseUrl, sliceUri, oauthClient); 1415 - this.social = new SocialClient(baseUrl, sliceUri, oauthClient); 1423 + this.network = new NetworkClient(baseUrl, sliceUri, oauthClient); 1416 1424 this.oauth = this.oauthClient; 1417 1425 } 1418 1426 1419 1427 async getActors(params: GetActorsParams): Promise<GetActorsResponse> { 1420 1428 const requestParams = { ...params, slice: this.sliceUri }; 1421 1429 return await this.makeRequest<GetActorsResponse>( 1422 - "social.slices.slice.getActors", 1430 + "network.slices.slice.getActors", 1423 1431 "POST", 1424 1432 requestParams 1425 1433 ); ··· 1441 1449 slice: this.sliceUri, 1442 1450 }; 1443 1451 return await this.makeRequest<SliceRecordsOutput<T>>( 1444 - "social.slices.slice.getSliceRecords", 1452 + "network.slices.slice.getSliceRecords", 1445 1453 "POST", 1446 1454 requestParams 1447 1455 );
+1 -1
frontend/src/features/auth/handlers.tsx
··· 132 132 // If we can't find existing records, sync them 133 133 if (!profileCheck.records || profileCheck.records.length === 0) { 134 134 console.log("No existing external collections found, syncing..."); 135 - await atprotoClient.social.slices.slice.syncUserCollections(); 135 + await atprotoClient.network.slices.slice.syncUserCollections(); 136 136 } else { 137 137 console.log("External collections already synced, skipping sync"); 138 138 }
+15 -13
frontend/src/features/dashboard/handlers.tsx
··· 11 11 id: string; 12 12 } 13 13 14 - async function handleProfilePage(req: Request, params?: URLPatternResult): Promise<Response> { 14 + async function handleProfilePage( 15 + req: Request, 16 + params?: URLPatternResult 17 + ): Promise<Response> { 15 18 const context = await withAuth(req); 16 19 const authResponse = requireAuth(context); 17 20 if (authResponse) return authResponse; ··· 22 25 let profileDid: string; 23 26 try { 24 27 const actors = await atprotoClient.getActors({ 25 - where: { handle: { eq: handle } } 28 + where: { handle: { eq: handle } }, 26 29 }); 27 - 30 + 28 31 if (actors.actors.length === 0) { 29 32 return new Response("Profile not found", { status: 404 }); 30 33 } 31 - 34 + 32 35 profileDid = actors.actors[0].did; 33 36 } catch (error) { 34 37 console.error("Failed to get actor:", error); ··· 38 41 // Fetch profile record using the DID 39 42 let _profileRecord; 40 43 try { 41 - const profileResponse = await atprotoClient.app.bsky.actor.profile.getRecords({ 42 - where: { did: { eq: profileDid } } 43 - }); 44 + const profileResponse = 45 + await atprotoClient.app.bsky.actor.profile.getRecords({ 46 + where: { did: { eq: profileDid } }, 47 + }); 44 48 _profileRecord = profileResponse.records[0]; 45 49 } catch (error) { 46 50 console.error("Failed to fetch profile:", error); ··· 50 54 51 55 try { 52 56 // Fetch slices for this DID 53 - const sliceRecords = await atprotoClient.social.slices.slice.getRecords({ 57 + const sliceRecords = await atprotoClient.network.slices.slice.getRecords({ 54 58 where: { did: { eq: profileDid } }, 55 59 sortBy: [{ field: "createdAt", direction: "desc" }], 56 60 }); ··· 117 121 createdAt: new Date().toISOString(), 118 122 }; 119 123 120 - const result = await atprotoClient.social.slices.slice.createRecord( 124 + const result = await atprotoClient.network.slices.slice.createRecord( 121 125 recordData 122 126 ); 123 127 ··· 135 139 ); 136 140 } 137 141 } catch (_error) { 138 - return renderHTML( 139 - <CreateSliceDialog error="Failed to create slice" /> 140 - ); 142 + return renderHTML(<CreateSliceDialog error="Failed to create slice" />); 141 143 } 142 144 } 143 145 ··· 165 167 pattern: new URLPattern({ pathname: "/dialogs/create-slice" }), 166 168 handler: handleCreateSliceDialog, 167 169 }, 168 - ]; 170 + ];
+9 -9
frontend/src/features/settings/handlers.tsx
··· 28 28 29 29 try { 30 30 const profileRecord = 31 - await atprotoClient.social.slices.actor.profile.getRecord({ 31 + await atprotoClient.network.slices.actor.profile.getRecord({ 32 32 uri: buildAtUri({ 33 33 did: context.currentUser.sub!, 34 - collection: "social.slices.actor.profile", 34 + collection: "network.slices.actor.profile", 35 35 rkey: "self", 36 36 }), 37 37 }); ··· 47 47 } 48 48 49 49 return renderHTML( 50 - <SettingsPage 51 - profile={profile} 50 + <SettingsPage 51 + profile={profile} 52 52 currentUser={context.currentUser} 53 53 updated={updated === "true"} 54 54 error={error} ··· 94 94 } 95 95 96 96 const existingProfile = 97 - await atprotoClient.social.slices.actor.profile.getRecord({ 97 + await atprotoClient.network.slices.actor.profile.getRecord({ 98 98 uri: buildAtUri({ 99 99 did: context.currentUser.sub, 100 - collection: "social.slices.actor.profile", 100 + collection: "network.slices.actor.profile", 101 101 rkey: "self", 102 102 }), 103 103 }); 104 104 105 105 if (existingProfile) { 106 - await atprotoClient.social.slices.actor.profile.updateRecord("self", { 106 + await atprotoClient.network.slices.actor.profile.updateRecord("self", { 107 107 ...profileData, 108 108 createdAt: existingProfile.value.createdAt, 109 109 }); 110 110 } else { 111 - await atprotoClient.social.slices.actor.profile.createRecord( 111 + await atprotoClient.network.slices.actor.profile.createRecord( 112 112 profileData, 113 113 true 114 114 ); ··· 137 137 pattern: new URLPattern({ pathname: "/api/profile" }), 138 138 handler: handleUpdateProfile, 139 139 }, 140 - ]; 140 + ];
+3 -3
frontend/src/features/slices/api-docs/handlers.tsx
··· 37 37 try { 38 38 const sliceUri = buildAtUri({ 39 39 did: context.currentUser.sub!, 40 - collection: "social.slices.slice", 40 + collection: "network.slices.slice", 41 41 rkey: sliceId, 42 42 }); 43 43 44 - const sliceRecord = await atprotoClient.social.slices.slice.getRecord({ 44 + const sliceRecord = await atprotoClient.network.slices.slice.getRecord({ 45 45 uri: sliceUri, 46 46 }); 47 47 ··· 67 67 pattern: new URLPattern({ pathname: "/slices/:id/api-docs" }), 68 68 handler: handleSliceApiDocsPage, 69 69 }, 70 - ]; 70 + ];
+51 -45
frontend/src/features/slices/api-docs/templates/SliceApiDocsPage.tsx
··· 14 14 const baseUrl = Deno.env.get("API_URL") || "http://localhost:3000"; 15 15 16 16 // Build the slice URI 17 - const sliceUri = `at://${currentUser.sub}/social.slices.slice/${sliceId}`; 18 - const openApiUrl = `${baseUrl}/xrpc/social.slices.slice.openapi?slice=${encodeURIComponent( 17 + const sliceUri = `at://${currentUser.sub}/network.slices.slice/${sliceId}`; 18 + const openApiUrl = `${baseUrl}/xrpc/network.slices.slice.openapi?slice=${encodeURIComponent( 19 19 sliceUri 20 20 )}`; 21 21 ··· 28 28 <script src="https://cdn.tailwindcss.com"></script> 29 29 </head> 30 30 <body class="bg-gray-50 min-h-screen"> 31 - {/* Header with back button */} 32 - <div class="bg-white border-b border-gray-200 px-4 py-4"> 33 - <div class="max-w-7xl mx-auto flex items-center justify-between"> 34 - <div class="flex items-center"> 35 - <a 36 - href={`/slices/${sliceId}`} 37 - class="text-blue-600 hover:text-blue-800 mr-4 flex items-center" 38 - > 39 - <svg 40 - class="w-4 h-4 mr-1" 41 - fill="none" 42 - stroke="currentColor" 43 - viewBox="0 0 24 24" 31 + {/* Header with back button */} 32 + <div class="bg-white border-b border-gray-200 px-4 py-4"> 33 + <div class="max-w-7xl mx-auto flex items-center justify-between"> 34 + <div class="flex items-center"> 35 + <a 36 + href={`/slices/${sliceId}`} 37 + class="text-blue-600 hover:text-blue-800 mr-4 flex items-center" 44 38 > 45 - <path 46 - stroke-linecap="round" 47 - stroke-linejoin="round" 48 - stroke-width="2" 49 - d="M15 19l-7-7 7-7" 50 - /> 51 - </svg> 52 - Back to {sliceName} 53 - </a> 54 - </div> 55 - <div class="text-right"> 56 - <h1 class="text-xl font-semibold text-gray-900">API Documentation</h1> 57 - <p class="text-gray-600 text-sm"> 58 - Interactive OpenAPI docs for your slice 59 - </p> 39 + <svg 40 + class="w-4 h-4 mr-1" 41 + fill="none" 42 + stroke="currentColor" 43 + viewBox="0 0 24 24" 44 + > 45 + <path 46 + stroke-linecap="round" 47 + stroke-linejoin="round" 48 + stroke-width="2" 49 + d="M15 19l-7-7 7-7" 50 + /> 51 + </svg> 52 + Back to {sliceName} 53 + </a> 54 + </div> 55 + <div class="text-right"> 56 + <h1 class="text-xl font-semibold text-gray-900"> 57 + API Documentation 58 + </h1> 59 + <p class="text-gray-600 text-sm"> 60 + Interactive OpenAPI docs for your slice 61 + </p> 62 + </div> 60 63 </div> 61 64 </div> 62 - </div> 63 65 64 66 {/* Info bar */} 65 67 <div class="bg-blue-50 border-b border-blue-200 px-4 py-3"> ··· 85 87 </div> 86 88 </div> 87 89 88 - {/* Load Scalar API Reference */} 89 - <script 90 - src="https://cdn.jsdelivr.net/npm/@scalar/api-reference" 91 - async 92 - ></script> 90 + {/* Load Scalar API Reference */} 91 + <script 92 + src="https://cdn.jsdelivr.net/npm/@scalar/api-reference" 93 + async 94 + ></script> 93 95 94 - {/* Initialize Scalar when the script loads */} 95 - <script 96 - dangerouslySetInnerHTML={{ 97 - __html: ` 96 + {/* Initialize Scalar when the script loads */} 97 + <script 98 + dangerouslySetInnerHTML={{ 99 + __html: ` 98 100 document.addEventListener('DOMContentLoaded', function() { 99 101 // Wait for Scalar to be available 100 102 const initScalar = () => { ··· 119 121 // Alternative approach for parameter defaults 120 122 defaultParameters: { 121 123 slice: '${sliceUri}' 122 - },${accessToken ? ` 124 + },${ 125 + accessToken 126 + ? ` 123 127 authentication: { 124 128 preferredSecurityScheme: 'bearerAuth', 125 129 http: { ··· 127 131 token: '${accessToken}' 128 132 } 129 133 } 130 - },` : ''} 134 + },` 135 + : "" 136 + } 131 137 customCss: \` 132 138 .scalar-api-reference { 133 139 width: 100% !important; ··· 181 187 initScalar(); 182 188 }); 183 189 `, 184 - }} 185 - /> 190 + }} 191 + /> 186 192 </body> 187 193 </html> 188 194 ); 189 - } 195 + }
+2 -2
frontend/src/features/slices/codegen/handlers.tsx
··· 29 29 30 30 try { 31 31 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 32 - const slice = await atprotoClient.social.slices.slice.getRecord({ 32 + const slice = await atprotoClient.network.slices.slice.getRecord({ 33 33 uri: sliceUri, 34 34 }); 35 35 ··· 77 77 const sliceClient = getSliceClient(context, sliceId); 78 78 79 79 // Call the codegen XRPC endpoint 80 - const result = await sliceClient.social.slices.slice.codegen({ 80 + const result = await sliceClient.network.slices.slice.codegen({ 81 81 target: target as string, 82 82 slice: sliceUri, 83 83 });
+4 -4
frontend/src/features/slices/jetstream/handlers.tsx
··· 30 30 const sliceClient = getSliceClient(context, sliceId); 31 31 32 32 // Get Jetstream logs 33 - const result = await sliceClient.social.slices.slice.getJetstreamLogs({ 33 + const result = await sliceClient.network.slices.slice.getJetstreamLogs({ 34 34 limit: 100, 35 35 }); 36 36 ··· 82 82 const isCompact = url.searchParams.get("compact") === "true"; 83 83 84 84 // Fetch jetstream status using the atproto client 85 - const data = await atprotoClient.social.slices.slice.getJetstreamStatus(); 85 + const data = await atprotoClient.network.slices.slice.getJetstreamStatus(); 86 86 87 87 // Render compact version for logs page 88 88 if (isCompact) { ··· 162 162 try { 163 163 const sliceClient = getSliceClient(context, sliceId); 164 164 165 - const logsResult = await sliceClient.social.slices.slice.getJetstreamLogs({ 165 + const logsResult = await sliceClient.network.slices.slice.getJetstreamLogs({ 166 166 limit: 100, 167 167 }); 168 168 logs = logsResult.logs.sort( ··· 198 198 pattern: new URLPattern({ pathname: "/api/slices/:id/jetstream/logs" }), 199 199 handler: handleJetstreamLogs, 200 200 }, 201 - ]; 201 + ];
+10 -8
frontend/src/features/slices/lexicon/handlers.tsx
··· 26 26 27 27 try { 28 28 const sliceClient = getSliceClient(context, sliceId); 29 - const lexiconRecords = await sliceClient.social.slices.lexicon.getRecords(); 29 + const lexiconRecords = 30 + await sliceClient.network.slices.lexicon.getRecords(); 30 31 31 32 if (lexiconRecords.records.length === 0) { 32 33 return renderHTML(<EmptyLexiconState />); ··· 118 119 }; 119 120 120 121 const sliceClient = getSliceClient(context, sliceId); 121 - const result = await sliceClient.social.slices.lexicon.createRecord( 122 + const result = await sliceClient.network.slices.lexicon.createRecord( 122 123 lexiconRecord 123 124 ); 124 125 ··· 184 185 185 186 try { 186 187 const sliceClient = getSliceClient(context, sliceId); 187 - const lexiconRecords = await sliceClient.social.slices.lexicon.getRecords(); 188 + const lexiconRecords = 189 + await sliceClient.network.slices.lexicon.getRecords(); 188 190 189 191 const lexicon = lexiconRecords.records.find((record) => 190 192 record.uri.endsWith(`/${rkey}`) ··· 224 226 225 227 try { 226 228 const sliceClient = getSliceClient(context, sliceId); 227 - await sliceClient.social.slices.lexicon.deleteRecord(rkey); 229 + await sliceClient.network.slices.lexicon.deleteRecord(rkey); 228 230 229 231 const remainingLexicons = 230 - await sliceClient.social.slices.lexicon.getRecords(); 232 + await sliceClient.network.slices.lexicon.getRecords(); 231 233 232 234 if (remainingLexicons.records.length === 0) { 233 235 return renderHTML(<EmptyLexiconState withPadding />, { ··· 271 273 272 274 const sliceUri = buildAtUri({ 273 275 did: context.currentUser.sub!, 274 - collection: "social.slices.slice", 276 + collection: "network.slices.slice", 275 277 rkey: sliceId, 276 278 }); 277 279 278 280 let slice; 279 281 try { 280 - slice = await atprotoClient.social.slices.slice.getRecord({ 282 + slice = await atprotoClient.network.slices.slice.getRecord({ 281 283 uri: sliceUri, 282 284 }); 283 285 } catch (error) { ··· 322 324 pattern: new URLPattern({ pathname: "/api/slices/:id/lexicons/:rkey" }), 323 325 handler: handleDeleteLexicon, 324 326 }, 325 - ]; 327 + ];
+1 -1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
··· 53 53 class="font-mono text-sm" 54 54 placeholder={`{ 55 55 "lexicon": 1, 56 - "id": "social.slices.example", 56 + "id": "network.slices.example", 57 57 "description": "Example record type", 58 58 "defs": { 59 59 "main": {
+9 -9
frontend/src/features/slices/oauth/handlers.tsx
··· 76 76 77 77 // Register new OAuth client via backend API 78 78 const sliceClient = getSliceClient(context, sliceId); 79 - const newClient = await sliceClient.social.slices.slice.createOAuthClient({ 79 + const newClient = await sliceClient.network.slices.slice.createOAuthClient({ 80 80 clientName, 81 81 redirectUris, 82 82 scope: scope || undefined, ··· 119 119 try { 120 120 // Delete OAuth client via backend API 121 121 const sliceClient = getSliceClient(context, sliceId); 122 - await sliceClient.social.slices.slice.deleteOAuthClient(clientId); 122 + await sliceClient.network.slices.slice.deleteOAuthClient(clientId); 123 123 124 124 return renderHTML(<OAuthDeleteResult success />); 125 125 } catch (error) { ··· 148 148 // Fetch OAuth client details via backend API 149 149 const sliceClient = getSliceClient(context, sliceId); 150 150 const clientsResponse = 151 - await sliceClient.social.slices.slice.getOAuthClients(); 151 + await sliceClient.network.slices.slice.getOAuthClients(); 152 152 const clientData = clientsResponse.clients.find( 153 153 (c) => c.clientId === clientId 154 154 ); 155 155 156 156 const sliceUri = buildAtUri({ 157 157 did: context.currentUser.sub!, 158 - collection: "social.slices.slice", 158 + collection: "network.slices.slice", 159 159 rkey: sliceId, 160 160 }); 161 161 ··· 218 218 // Update OAuth client via backend API 219 219 const sliceClient = getSliceClient(context, sliceId); 220 220 const updatedClient = 221 - await sliceClient.social.slices.slice.updateOAuthClient({ 221 + await sliceClient.network.slices.slice.updateOAuthClient({ 222 222 clientId, 223 223 clientName: clientName || undefined, 224 224 redirectUris: redirectUris.length > 0 ? redirectUris : undefined, ··· 269 269 270 270 const sliceUri = buildAtUri({ 271 271 did: context.currentUser.sub!, 272 - collection: "social.slices.slice", 272 + collection: "network.slices.slice", 273 273 rkey: sliceId, 274 274 }); 275 275 ··· 277 277 278 278 let slice; 279 279 try { 280 - slice = await atprotoClient.social.slices.slice.getRecord({ 280 + slice = await atprotoClient.network.slices.slice.getRecord({ 281 281 uri: sliceUri, 282 282 }); 283 283 } catch (error) { ··· 296 296 297 297 try { 298 298 const oauthClientsResponse = 299 - await sliceClient.social.slices.slice.getOAuthClients(); 299 + await sliceClient.network.slices.slice.getOAuthClients(); 300 300 console.log("Fetched OAuth clients:", oauthClientsResponse.clients); 301 301 clientsWithDetails = oauthClientsResponse.clients.map((client) => ({ 302 302 clientId: client.clientId, ··· 351 351 pattern: new URLPattern({ pathname: "/api/slices/:id/oauth/:uri" }), 352 352 handler: handleOAuthClientDelete, 353 353 }, 354 - ]; 354 + ];
+7 -5
frontend/src/features/slices/overview/handlers.tsx
··· 28 28 if (context.currentUser.isAuthenticated) { 29 29 try { 30 30 const sliceUri = buildAtUri({ 31 - did: context.currentUser.sub || "unknown", 32 - collection: "social.slices.slice", 31 + did: context.currentUser.sub!, 32 + collection: "network.slices.slice", 33 33 rkey: sliceId, 34 34 }); 35 35 36 + console.log(sliceUri); 37 + 36 38 const [sliceRecord, stats] = await Promise.all([ 37 - atprotoClient.social.slices.slice.getRecord({ uri: sliceUri }), 38 - atprotoClient.social.slices.slice.stats({ slice: sliceUri }), 39 + atprotoClient.network.slices.slice.getRecord({ uri: sliceUri }), 40 + atprotoClient.network.slices.slice.stats({ slice: sliceUri }), 39 41 ]); 40 42 41 43 const collections = stats.success ··· 74 76 pattern: new URLPattern({ pathname: "/slices/:id" }), 75 77 handler: handleSliceOverview, 76 78 }, 77 - ]; 79 + ];
+6 -9
frontend/src/features/slices/records/handlers.tsx
··· 31 31 try { 32 32 const sliceUri = buildAtUri({ 33 33 did: context.currentUser.sub ?? "unknown", 34 - collection: "social.slices.slice", 34 + collection: "network.slices.slice", 35 35 rkey: sliceId, 36 36 }); 37 37 38 38 const [sliceRecord, stats] = await Promise.all([ 39 - atprotoClient.social.slices.slice.getRecord({ uri: sliceUri }), 40 - atprotoClient.social.slices.slice.stats({ slice: sliceUri }), 39 + atprotoClient.network.slices.slice.getRecord({ uri: sliceUri }), 40 + atprotoClient.network.slices.slice.stats({ slice: sliceUri }), 41 41 ]); 42 42 43 43 const collections = stats.success ··· 68 68 // Fetch real records if a collection is selected 69 69 let records: Array<IndexedRecord & { pretty_value: string }> = []; 70 70 71 - if ( 72 - (selectedCollection || searchQuery) && 73 - sliceData.collections.length > 0 74 - ) { 71 + if ((selectedCollection || searchQuery) && sliceData.collections.length > 0) { 75 72 try { 76 73 const sliceClient = getSliceClient(context, sliceId); 77 74 const recordsResult = 78 - await sliceClient.social.slices.slice.getSliceRecords({ 75 + await sliceClient.network.slices.slice.getSliceRecords({ 79 76 where: { 80 77 ...(selectedCollection && { 81 78 collection: { eq: selectedCollection }, ··· 122 119 pattern: new URLPattern({ pathname: "/slices/:id/records" }), 123 120 handler: handleSliceRecordsPage, 124 121 }, 125 - ]; 122 + ];
+4 -4
frontend/src/features/slices/settings/handlers.tsx
··· 34 34 35 35 try { 36 36 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 37 - const slice = await atprotoClient.social.slices.slice.getRecord({ 37 + const slice = await atprotoClient.network.slices.slice.getRecord({ 38 38 uri: sliceUri, 39 39 }); 40 40 ··· 91 91 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 92 92 93 93 // Get the current record first 94 - const currentRecord = await atprotoClient.social.slices.slice.getRecord({ 94 + const currentRecord = await atprotoClient.network.slices.slice.getRecord({ 95 95 uri: sliceUri, 96 96 }); 97 97 98 98 // Update the record with new name and domain 99 - await atprotoClient.social.slices.slice.updateRecord(sliceId, { 99 + await atprotoClient.network.slices.slice.updateRecord(sliceId, { 100 100 ...currentRecord.value, 101 101 name: name.trim(), 102 102 domain: domain.trim(), ··· 125 125 126 126 try { 127 127 // Delete the slice record from AT Protocol 128 - await atprotoClient.social.slices.slice.deleteRecord(sliceId); 128 + await atprotoClient.network.slices.slice.deleteRecord(sliceId); 129 129 130 130 return hxRedirect("/"); 131 131 } catch (_error) {
+4 -4
frontend/src/features/slices/sync-logs/handlers.tsx
··· 27 27 let slice: { name: string } = { name: "Unknown Slice" }; 28 28 try { 29 29 const sliceClient = getSliceClient(context, sliceId); 30 - const sliceRecord = await sliceClient.social.slices.slice.getRecord({ 30 + const sliceRecord = await sliceClient.network.slices.slice.getRecord({ 31 31 uri: buildAtUri({ 32 32 did: context.currentUser.sub!, 33 - collection: "social.slices.slice", 33 + collection: "network.slices.slice", 34 34 rkey: sliceId, 35 35 }), 36 36 }); ··· 73 73 74 74 try { 75 75 const sliceClient = getSliceClient(context, sliceId); 76 - const logsResponse = await sliceClient.social.slices.slice.getJobLogs({ 76 + const logsResponse = await sliceClient.network.slices.slice.getJobLogs({ 77 77 jobId, 78 78 }); 79 79 ··· 106 106 pattern: new URLPattern({ pathname: "/api/slices/:id/sync/logs/:jobId" }), 107 107 handler: handleSyncJobLogs, 108 108 }, 109 - ]; 109 + ];
+7 -9
frontend/src/features/slices/sync/handlers.tsx
··· 19 19 const sliceId = params?.pathname.groups.id; 20 20 21 21 if (!sliceId) { 22 - return renderHTML( 23 - <SyncResult success={false} error="Invalid slice ID" /> 24 - ); 22 + return renderHTML(<SyncResult success={false} error="Invalid slice ID" />); 25 23 } 26 24 27 25 try { ··· 56 54 } 57 55 58 56 const sliceClient = getSliceClient(context, sliceId); 59 - const syncJobResponse = await sliceClient.social.slices.slice.startSync({ 57 + const syncJobResponse = await sliceClient.network.slices.slice.startSync({ 60 58 collections: collections.length > 0 ? collections : undefined, 61 59 externalCollections: 62 60 externalCollections.length > 0 ? externalCollections : undefined, ··· 103 101 try { 104 102 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 105 103 const sliceClient = getSliceClient(context, sliceId); 106 - const jobsResponse = await sliceClient.social.slices.slice.getJobHistory({ 104 + const jobsResponse = await sliceClient.network.slices.slice.getJobHistory({ 107 105 userDid: context.currentUser.sub!, 108 106 sliceUri: sliceUri, 109 107 limit: 10, ··· 142 140 // Get the slice record 143 141 const sliceUri = buildAtUri({ 144 142 did: context.currentUser.sub!, 145 - collection: "social.slices.slice", 143 + collection: "network.slices.slice", 146 144 rkey: sliceId, 147 145 }); 148 146 ··· 153 151 const externalCollections: string[] = []; 154 152 155 153 try { 156 - slice = await atprotoClient.social.slices.slice.getRecord({ 154 + slice = await atprotoClient.network.slices.slice.getRecord({ 157 155 uri: sliceUri, 158 156 }); 159 157 160 158 // Get all lexicons and filter by record types 161 159 try { 162 160 const lexiconsResponse = 163 - await sliceClient.social.slices.lexicon.getRecords(); 161 + await sliceClient.network.slices.lexicon.getRecords(); 164 162 const recordLexicons = lexiconsResponse.records.filter((lexicon) => { 165 163 try { 166 164 const definitions = JSON.parse(lexicon.value.definitions); ··· 215 213 pattern: new URLPattern({ pathname: "/api/slices/:id/job-history" }), 216 214 handler: handleJobHistory, 217 215 }, 218 - ]; 216 + ];
+10 -10
frontend/src/utils/at-uri.ts
··· 14 14 * Format: at://did:method:identifier/collection/rkey#fragment 15 15 */ 16 16 export function parseAtUri(uri: string): AtUri { 17 - if (!uri.startsWith('at://')) { 17 + if (!uri.startsWith("at://")) { 18 18 throw new Error(`Invalid AT-URI: must start with at:// - got ${uri}`); 19 19 } 20 20 21 21 const withoutProtocol = uri.slice(5); // Remove 'at://' 22 - const [didAndPath, fragment] = withoutProtocol.split('#'); 23 - const parts = didAndPath.split('/'); 22 + const [didAndPath, fragment] = withoutProtocol.split("#"); 23 + const parts = didAndPath.split("/"); 24 24 25 25 if (parts.length < 3) { 26 26 throw new Error(`Invalid AT-URI: missing required parts - got ${uri}`); ··· 33 33 } 34 34 35 35 return { 36 - protocol: 'at', 36 + protocol: "at", 37 37 did, 38 38 collection, 39 39 rkey, 40 - fragment 40 + fragment, 41 41 }; 42 42 } 43 43 44 44 /** 45 45 * Build an AT-URI from components 46 46 */ 47 - export function buildAtUri(components: Omit<AtUri, 'protocol'>): string { 47 + export function buildAtUri(components: Omit<AtUri, "protocol">): string { 48 48 const { did, collection, rkey, fragment } = components; 49 49 let uri = `at://${did}/${collection}/${rkey}`; 50 - 50 + 51 51 if (fragment) { 52 52 uri += `#${fragment}`; 53 53 } 54 - 54 + 55 55 return uri; 56 56 } 57 57 ··· 80 80 * Build a slice URI from DID and slice ID 81 81 */ 82 82 export function buildSliceUri(did: string, sliceId: string): string { 83 - return buildAtUri({ did, collection: "social.slices.slice", rkey: sliceId }); 84 - } 83 + return buildAtUri({ did, collection: "network.slices.slice", rkey: sliceId }); 84 + }
+8 -5
frontend/src/utils/client.ts
··· 9 9 } 10 10 11 11 // Helper function to get a slice-specific AT Protocol client 12 - export function getSliceClient(context: AuthContext, sliceId: string): AtProtoClient { 12 + export function getSliceClient( 13 + context: AuthContext, 14 + sliceId: string 15 + ): AtProtoClient { 13 16 const API_URL = Deno.env.get("API_URL")!; 14 - 17 + 15 18 if (!context.currentUser.sub) { 16 19 throw new Error("User DID is required to create slice client"); 17 20 } 18 - 21 + 19 22 const sliceUri = buildAtUri({ 20 23 did: context.currentUser.sub, 21 - collection: "social.slices.slice", 24 + collection: "network.slices.slice", 22 25 rkey: sliceId, 23 26 }); 24 27 return new AtProtoClient(API_URL, sliceUri, atprotoClient.oauth); 25 - } 28 + }
+1 -1
lexicons/social/slices/lexicon.json lexicons/network/slices/lexicon.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "social.slices.lexicon", 3 + "id": "network.slices.lexicon", 4 4 "description": "Lexicon definition record type for AT Protocol schema storage", 5 5 "defs": { 6 6 "main": {
+1 -1
lexicons/social/slices/profile.json lexicons/network/slices/profile.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "social.slices.actor.profile", 3 + "id": "network.slices.actor.profile", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record",
+1 -1
lexicons/social/slices/slice.json lexicons/network/slices/slice.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "social.slices.slice", 3 + "id": "network.slices.slice", 4 4 "description": "Slice application record type", 5 5 "defs": { 6 6 "main": {