From 119f56bd0096929f37e76ef48cad47b426e86362 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 6 Feb 2025 10:43:09 -0600 Subject: [PATCH] Add validate util --- package.json | 1 + src/components/StarterPack/QrCodeDialog.tsx | 10 ++--- src/lib/api/feed-manip.ts | 8 ++-- src/screens/Settings/components/PwiOptOut.tsx | 8 ++-- src/screens/StarterPack/Wizard/State.tsx | 8 +--- src/state/queries/postgate/index.ts | 4 +- src/state/queries/starter-packs.ts | 3 +- src/state/queries/threadgate/index.ts | 4 +- src/state/queries/threadgate/util.ts | 5 ++- src/types/bsky/index.ts | 38 +++++++++++++++---- src/view/com/post/Post.tsx | 4 +- yarn.lock | 22 +++++------ 12 files changed, 68 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 501beaefa0..780917e370 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@atproto/api": "0.14.0-next.6", + "@atproto/lexicon": "0.4.6-next.5", "@bitdrift/react-native": "^0.6.2", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index 63d58a0f44..43d8b72dad 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -4,11 +4,7 @@ import type ViewShot from 'react-native-view-shot' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {createAssetAsync} from 'expo-media-library' import * as Sharing from 'expo-sharing' -import { - AppBskyGraphDefs, - AppBskyGraphStarterpack, - asPredicate, -} from '@atproto/api' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -22,6 +18,7 @@ import * as Dialog from '#/components/Dialog' import {DialogControlProps} from '#/components/Dialog' import {Loader} from '#/components/Loader' import {QrCode} from '#/components/StarterPack/QrCode' +import * as bsky from '#/types/bsky' export function QrCodeDialog({ starterPack, @@ -82,8 +79,9 @@ export function QrCodeDialog({ setIsProcessing(true) if ( - !asPredicate(AppBskyGraphStarterpack.validateRecord)( + !bsky.validate( starterPack.record, + AppBskyGraphStarterpack.validateRecord, ) ) { return diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index ed43f362ea..a1b2e2bc90 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -4,9 +4,9 @@ import { AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyFeedPost, - asPredicate, } from '@atproto/api' +import * as bsky from '#/types/bsky' import {isPostInLanguage} from '../../locale/helpers' import {FALLBACK_MARKER_POST} from './feed/home' import {ReasonFeedSource} from './feed/types' @@ -68,7 +68,7 @@ export class FeedViewPostsSlice { } if ( !AppBskyFeedPost.isRecord(post.record) || - !asPredicate(AppBskyFeedPost.validateRecord)(post.record) + !bsky.validate(post.record, AppBskyFeedPost.validateRecord) ) { return } @@ -100,7 +100,7 @@ export class FeedViewPostsSlice { if ( !AppBskyFeedDefs.isPostView(parent) || !AppBskyFeedPost.isRecord(parent.record) || - !asPredicate(AppBskyFeedPost.validateRecord)(parent.record) + !bsky.validate(parent.record, AppBskyFeedPost.validateRecord) ) { this.isOrphan = true return @@ -142,7 +142,7 @@ export class FeedViewPostsSlice { if ( !AppBskyFeedDefs.isPostView(root) || !AppBskyFeedPost.isRecord(root.record) || - !asPredicate(AppBskyFeedPost.validateRecord)(root.record) + !bsky.validate(root.record, AppBskyFeedPost.validateRecord) ) { this.isOrphan = true return diff --git a/src/screens/Settings/components/PwiOptOut.tsx b/src/screens/Settings/components/PwiOptOut.tsx index e0581da7fa..e585149763 100644 --- a/src/screens/Settings/components/PwiOptOut.tsx +++ b/src/screens/Settings/components/PwiOptOut.tsx @@ -1,6 +1,6 @@ import React from 'react' import {View} from 'react-native' -import {$Typed, asPredicate,ComAtprotoLabelDefs} from '@atproto/api' +import {$Typed, ComAtprotoLabelDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -12,6 +12,7 @@ import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import {Text} from '#/components/Typography' +import * as bsky from '#/types/bsky' export function PwiOptOut() { const t = useTheme() @@ -33,9 +34,10 @@ export function PwiOptOut() { profile, updates: existing => { // create labels attr if needed - const labels: $Typed = asPredicate( + const labels: $Typed = bsky.validate( + existing.labels, ComAtprotoLabelDefs.validateSelfLabels, - )(existing.labels) + ) ? existing.labels : { $type: 'com.atproto.label.defs#selfLabels', diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx index 6c64add055..baf0195d82 100644 --- a/src/screens/StarterPack/Wizard/State.tsx +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -1,9 +1,5 @@ import React from 'react' -import { - AppBskyGraphDefs, - AppBskyGraphStarterpack, - asPredicate, -} from '@atproto/api' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {msg} from '@lingui/macro' @@ -128,7 +124,7 @@ export function Provider({ const createInitialState = (): State => { if ( starterPack && - asPredicate(AppBskyGraphStarterpack.validateRecord)(starterPack.record) + bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) ) { return { canNext: true, diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index 6605ab8e68..346e7bfe29 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -4,7 +4,6 @@ import { AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyFeedPostgate, - asPredicate, AtUri, BskyAgent, } from '@atproto/api' @@ -22,6 +21,7 @@ import { POSTGATE_COLLECTION, } from '#/state/queries/postgate/util' import {useAgent} from '#/state/session' +import * as bsky from '#/types/bsky' export async function getPostgateRecord({ agent, @@ -63,7 +63,7 @@ export async function getPostgateRecord({ if ( data.value && - asPredicate(AppBskyFeedPostgate.validateRecord)(data.value) + bsky.validate(data.value, AppBskyFeedPostgate.validateRecord) ) { return data.value } else { diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index 02da3f5a1b..64ebbd0e5a 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -4,7 +4,6 @@ import { AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, AppBskyRichtextFacet, - asPredicate, AtUri, BskyAgent, RichText, @@ -369,7 +368,7 @@ export async function precacheStarterPack( starterPackView = starterPack } else if ( AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && - asPredicate(AppBskyGraphStarterpack.validateRecord)(starterPack.record) + bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) ) { const listView: AppBskyGraphDefs.ListViewBasic = { uri: starterPack.record.list, diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 0eb1e72979..478658fe88 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -2,7 +2,6 @@ import { AppBskyFeedDefs, AppBskyFeedGetPostThread, AppBskyFeedThreadgate, - asPredicate, AtUri, BskyAgent, } from '@atproto/api' @@ -21,6 +20,7 @@ import { } from '#/state/queries/threadgate/util' import {useAgent} from '#/state/session' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' +import * as bsky from '#/types/bsky' export * from '#/state/queries/threadgate/types' export * from '#/state/queries/threadgate/util' @@ -141,7 +141,7 @@ export async function getThreadgateRecord({ if ( data.value && - asPredicate(AppBskyFeedThreadgate.validateRecord)(data.value) + bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord) ) { return data.value } else { diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts index 452fc84c79..1e70e2e31f 100644 --- a/src/state/queries/threadgate/util.ts +++ b/src/state/queries/threadgate/util.ts @@ -1,6 +1,7 @@ -import {AppBskyFeedDefs, AppBskyFeedThreadgate, asPredicate} from '@atproto/api' +import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' +import * as bsky from '#/types/bsky' export function threadgateViewToAllowUISetting( threadgateView: AppBskyFeedDefs.ThreadgateView | undefined, @@ -8,7 +9,7 @@ export function threadgateViewToAllowUISetting( // Validate the record for clarity, since backwards compat code is a little confusing const threadgate = threadgateView && - asPredicate(AppBskyFeedThreadgate.validateRecord)(threadgateView.record) + bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord) ? threadgateView.record : undefined return threadgateRecordToAllowUISetting(threadgate) diff --git a/src/types/bsky/index.ts b/src/types/bsky/index.ts index 2b9ab5b45e..7354e5bcd8 100644 --- a/src/types/bsky/index.ts +++ b/src/types/bsky/index.ts @@ -1,19 +1,22 @@ +import {asPredicate} from '@atproto/api' +import {ValidationResult} from '@atproto/lexicon' + export * as profile from '#/types/bsky/profile' export * as starterPack from '#/types/bsky/starterPack' /** - * Use sparingly, and only when you know it's safe to do so. + * Fast type checking without full schema validation, for use with data we + * trust, or for non-critical path use cases. Why? Our SDK's `is*` identity + * utils do not assert the type of the entire object, only the `$type` string. * - * Our SDK's `is*` identity utils do not assert the type of the entire object, - * and although the `isValid*` utils do, they also fully validate the object - * shape, which has a performance cost. This util allows us to prescribe the - * type we expect, while only checking the `$type` value of the record. + * For full validation of the object schema, use the `validate` export from + * this file. * * Usage: * ```ts - * import * as atp from '#/types/atproto' + * import * as bsky from '#/types/bsky' * - * if (atp.dangerousIsType(item, AppBskyFeedPost.isRecord)) { + * if (bsky.dangerousIsType(item, AppBskyFeedPost.isRecord)) { * // `item` has type `$Typed` here * } * ``` @@ -24,3 +27,24 @@ export function dangerousIsType( ): record is R { return identity(record) } + +/** + * Fully validates the object schema, which as a performance cost. + * + * For faster checks with data we trust, like that from our app view, use the + * `dangerousIsType` export from this same file. + * + * Usage: + * ```ts + * import * as bsky from '#/types/bsky' + * + * if (bsky.validate(item, AppBskyFeedPost.validateRecord)) { + * // `item` has type `$Typed` here + * } + */ +export function validate( + record: unknown, + identity: (v: unknown) => ValidationResult, +): record is R { + return asPredicate(identity)(record) +} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 07e374692c..2645237ad3 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -3,7 +3,6 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import { AppBskyFeedDefs, AppBskyFeedPost, - asPredicate, AtUri, ModerationDecision, RichText as RichTextAPI, @@ -29,6 +28,7 @@ import {atoms as a} from '#/alf' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' import {SubtleWebHover} from '#/components/SubtleWebHover' +import * as bsky from '#/types/bsky' import {ContentHider} from '../../../components/moderation/ContentHider' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' @@ -54,7 +54,7 @@ export function Post({ const moderationOpts = useModerationOpts() const record = useMemo( () => - asPredicate(AppBskyFeedPost.validateRecord)(post.record) + bsky.validate(post.record, AppBskyFeedPost.validateRecord) ? post.record : undefined, [post], diff --git a/yarn.lock b/yarn.lock index c126332323..4eeb36bee7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,6 +282,17 @@ multiformats "^9.9.0" zod "^3.23.8" +"@atproto/lexicon@0.4.6-next.5", "@atproto/lexicon@^0.4.6-next.5": + version "0.4.6-next.5" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.6-next.5.tgz#433a941cf8902afec15135f70cf2cbed37c35406" + integrity sha512-Id1atQ1rZ3dUGAUyBcauD+zs5lGHSmoAlC1Kwb2DEvkaWMv3oTXUjtdQxyb0/Kz4YaFI+nxLLRH9NBO+BipG+w== + dependencies: + "@atproto/common-web" "^0.3.2" + "@atproto/syntax" "^0.3.1" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + zod "^3.23.8" + "@atproto/lexicon@^0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a" @@ -304,17 +315,6 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/lexicon@^0.4.6-next.5": - version "0.4.6-next.5" - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.6-next.5.tgz#433a941cf8902afec15135f70cf2cbed37c35406" - integrity sha512-Id1atQ1rZ3dUGAUyBcauD+zs5lGHSmoAlC1Kwb2DEvkaWMv3oTXUjtdQxyb0/Kz4YaFI+nxLLRH9NBO+BipG+w== - dependencies: - "@atproto/common-web" "^0.3.2" - "@atproto/syntax" "^0.3.1" - iso-datestring-validator "^2.2.2" - multiformats "^9.9.0" - zod "^3.23.8" - "@atproto/oauth-provider@^0.2.10": version "0.2.10" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901"