[SDK] Remove @atproto/api (#11386)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-13 22:26:22 +03:00
committed by GitHub
parent 54da80dfb0
commit 9fe0084874
391 changed files with 3130 additions and 3589 deletions
-41
View File
@@ -1,5 +1,3 @@
import {AppBskyFeedPost} from '@atproto/api'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
@@ -167,42 +165,3 @@ describe('types/bsky lexicon schema helpers (#/lexicons)', () => {
})
})
})
describe('types/bsky validator helpers (@atproto/api)', () => {
it('dangerousIsType accepts the right $type without validating the body', () => {
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
// Right $type, invalid body - still passes the fast guard.
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
wrongType,
AppBskyFeedPost.isRecord,
),
).toBe(false)
})
it('validate fully validates the body', () => {
expect(
bsky.validate<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.validateRecord,
),
).toBe(true)
expect(
bsky.validate<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.validateRecord,
),
).toBe(false)
})
})
+5 -23
View File
@@ -1,8 +1,3 @@
import {
type $Typed as $TypedApi,
type AppBskyEmbedRecord,
type AppBskyFeedDefs,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {type app} from '#/lexicons'
@@ -109,38 +104,25 @@ const starterPackViewBasic = {
const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed']
/*
* Type-level assertions for the dual-world widening. These are compile-time
* only: each widened arm must accept both the `#/lexicons` view and the
* `@atproto/api` view, and `parseEmbed` must accept a `PostView.embed` from
* either world, because both worlds have live producers.
* Type-level assertions for the embed union. Compile-time only: each arm must
* accept the `#/lexicons` view of its def, and `parseEmbed` must accept a
* `PostView.embed`.
*/
type Assignable<From, To> = From extends To ? true : false
type Expect<T extends true> = T
type _PostArmAcceptsNewWorld = Expect<
type _PostArmAcceptsView = Expect<
Assignable<
{type: 'post'; view: $Typed<app.bsky.embed.record.ViewRecord>},
EmbedType<'post'>
>
>
type _PostArmAcceptsOldWorld = Expect<
Assignable<
{type: 'post'; view: $TypedApi<AppBskyEmbedRecord.ViewRecord>},
EmbedType<'post'>
>
>
type _ParseEmbedAcceptsNewWorld = Expect<
type _ParseEmbedAcceptsPostViewEmbed = Expect<
Assignable<
app.bsky.feed.defs.PostView['embed'],
Parameters<typeof parseEmbed>[0]
>
>
type _ParseEmbedAcceptsOldWorld = Expect<
Assignable<
AppBskyFeedDefs.PostView['embed'],
Parameters<typeof parseEmbed>[0]
>
>
type _ParseEmbedReturnsEmbed = Expect<
Assignable<ReturnType<typeof parseEmbed>, Embed>
>
+5 -13
View File
@@ -1,5 +1,3 @@
import {type AppBskyGraphDefs} from '@atproto/api'
import {type app} from '#/lexicons'
import {
type AnyStarterPackView,
@@ -30,25 +28,19 @@ const fullView = {
}
/*
* Type-level assertions for the dual-world widening: the alias must accept a
* starter pack view from either world, because both have live producers. These
* are compile-time only - a failure surfaces as a typecheck error.
* Type-level assertions for the view alias: it must accept both the basic and
* the full starter pack view. Compile-time only - a failure surfaces as a
* typecheck error.
*/
type Assignable<From, To> = From extends To ? true : false
type Expect<T extends true> = T
type _AcceptsNewBasicView = Expect<
type _AcceptsBasicView = Expect<
Assignable<app.bsky.graph.defs.StarterPackViewBasic, AnyStarterPackView>
>
type _AcceptsNewFullView = Expect<
type _AcceptsFullView = Expect<
Assignable<app.bsky.graph.defs.StarterPackView, AnyStarterPackView>
>
type _AcceptsOldBasicView = Expect<
Assignable<AppBskyGraphDefs.StarterPackViewBasic, AnyStarterPackView>
>
type _AcceptsOldFullView = Expect<
Assignable<AppBskyGraphDefs.StarterPackView, AnyStarterPackView>
>
describe('types/bsky/starterPack guards', () => {
describe('isBasicView', () => {
+11 -70
View File
@@ -10,71 +10,15 @@ import {
type ValidateOptions,
type ValidationResult,
} from '@atproto/lex'
import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon'
export * as post from '#/types/bsky/post'
export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack'
/**
* 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.
*
* For full validation of the object schema, use the `validate` export from
* this file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function dangerousIsType<R extends {$type?: string}>(
record: unknown,
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
): record is R {
return identity(record)
}
/**
* Fully validates the object schema, which has 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<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function validate<R extends {$type?: string}>(
record: unknown,
validator: (v: unknown) => LegacyValidationResult<R>,
): record is R {
return validator(record).success
}
/*
* Two families of helpers live in this file, distinguished by the validation
* surface they consume.
*
* `dangerousIsType` and `validate` (above) operate on the standalone
* `is*`/`validate*` functions exported by '@atproto/api' - the record and the
* validator are passed separately, e.g.
* `dangerousIsType(v, AppBskyFeedPost.isRecord)`.
*
* `isType`, `matches`, `parse`, and `safeParse` (below) operate on the
* generated lexicon schema objects from '#/lexicons'. That codegen attaches
* the validation surface directly to each schema, so these helpers take the
* schema object itself:
* These helpers operate on the generated lexicon schema objects from
* '#/lexicons'. The codegen attaches the validation surface directly to each
* schema, so these helpers take the schema object itself:
*
* ```ts
* import {app} from '#/lexicons'
@@ -104,9 +48,8 @@ type TypedSchema = RecordSchema | TypedObjectSchema
* string; it does NOT assert the rest of the object matches the schema. An
* invalid record with the right `$type` will pass.
*
* This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full
* validation of the object schema, use {@link matches}, {@link parse}, or
* {@link safeParse} from this same file.
* For full validation of the object schema, use {@link matches}, {@link parse},
* or {@link safeParse} from this same file.
*
* Usage:
* ```ts
@@ -126,10 +69,9 @@ export function isType<S extends TypedSchema>(
* Deliberately NOT delegating to the schema's `isTypeOf`: the generated
* `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match
* (maybe-typed semantics), which would let any plain object satisfy any
* def-schema check and break `$type`-discriminated unions. The old
* `dangerousIsType` required a present, matching `$type`, and so do we.
* The nullish/object guard also mirrors the old `is$typed` behavior of
* returning false (not throwing) for null/undefined input.
* def-schema check and break `$type`-discriminated unions. We require a
* present, matching `$type`. The nullish/object guard also returns false
* (not throws) for null/undefined input.
*/
return (
value != null &&
@@ -142,9 +84,8 @@ export function isType<S extends TypedSchema>(
* Fully validates the object against the schema (strict, no coercion), which
* has a performance cost, and narrows the value on success.
*
* This is the '#/lexicons' equivalent of {@link validate}. For faster checks
* with data we trust, like that from our app view, use {@link isType} from this
* same file.
* For faster checks with data we trust, like that from our app view, use
* {@link isType} from this same file.
*
* Usage:
* ```ts
@@ -166,7 +107,7 @@ export function matches<S extends Schema>(
/**
* Fully validates and parses the value against the schema, returning the typed
* value or throwing a `LexValidationError` on failure. Parsing may apply
* value or throwing an `LexValidationError` on failure. Parsing may apply
* schema transformations such as default values.
*
* Prefer {@link safeParse} where you want to branch on failure without a
+13 -75
View File
@@ -1,110 +1,56 @@
import {
type $Typed as $TypedApi,
type AppBskyEmbedExternal,
type AppBskyEmbedGallery,
type AppBskyEmbedImages,
type AppBskyEmbedRecord,
type AppBskyEmbedVideo,
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AppBskyLabelerDefs,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {app} from '#/lexicons'
import {isType} from '#/types/bsky'
/*
* Each `view` slot below accepts both the generated `#/lexicons` view and the
* `@atproto/api` view of the same def, because both worlds have live producers:
* `parseEmbed` narrows with the `#/lexicons` schemas and so returns new-world
* views, while call sites that build an `Embed` by hand still pass views
* produced through the `@atproto/api` agent.
*
* The two worlds share the same `$type` strings, so the guards in this file
* narrow a value from either producer; only the static type differs.
*
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
*/
export type Embed =
| {
type: 'post'
view:
| $Typed<app.bsky.embed.record.ViewRecord>
| $TypedApi<AppBskyEmbedRecord.ViewRecord>
view: $Typed<app.bsky.embed.record.ViewRecord>
}
| {
type: 'post_not_found'
view:
| $Typed<app.bsky.embed.record.ViewNotFound>
| $TypedApi<AppBskyEmbedRecord.ViewNotFound>
view: $Typed<app.bsky.embed.record.ViewNotFound>
}
| {
type: 'post_blocked'
view:
| $Typed<app.bsky.embed.record.ViewBlocked>
| $TypedApi<AppBskyEmbedRecord.ViewBlocked>
view: $Typed<app.bsky.embed.record.ViewBlocked>
}
| {
type: 'post_detached'
view:
| $Typed<app.bsky.embed.record.ViewDetached>
| $TypedApi<AppBskyEmbedRecord.ViewDetached>
view: $Typed<app.bsky.embed.record.ViewDetached>
}
| {
type: 'feed'
view:
| $Typed<app.bsky.feed.defs.GeneratorView>
| $TypedApi<AppBskyFeedDefs.GeneratorView>
view: $Typed<app.bsky.feed.defs.GeneratorView>
}
| {
type: 'list'
view:
| $Typed<app.bsky.graph.defs.ListView>
| $TypedApi<AppBskyGraphDefs.ListView>
view: $Typed<app.bsky.graph.defs.ListView>
}
| {
type: 'labeler'
view:
| $Typed<app.bsky.labeler.defs.LabelerView>
| $TypedApi<AppBskyLabelerDefs.LabelerView>
view: $Typed<app.bsky.labeler.defs.LabelerView>
}
| {
type: 'starter_pack'
view:
| $Typed<app.bsky.graph.defs.StarterPackViewBasic>
| $TypedApi<AppBskyGraphDefs.StarterPackViewBasic>
view: $Typed<app.bsky.graph.defs.StarterPackViewBasic>
}
| {
type: 'images'
/*
* Only the `@atproto/api` view, unlike the other arms: the ImageEmbed
* consumer reads `view.images` directly, and the `#/lexicons` view is
* assignable to this slot, so `parseEmbed`'s new-world value flows in
* while the consumer keeps a single structural shape to read from.
*/
view: $TypedApi<AppBskyEmbedImages.View>
view: $Typed<app.bsky.embed.images.View>
}
| {
type: 'gallery'
/*
* Old-world only for the same reason as the `images` arm above: the
* consumer narrows `view.items` with `AppBskyEmbedGallery.isViewImage`,
* which cannot narrow the new view's `Unknown$TypedObject` arm.
*/
view: $TypedApi<AppBskyEmbedGallery.View>
view: $Typed<app.bsky.embed.gallery.View>
}
| {
type: 'link'
view:
| $Typed<app.bsky.embed.external.View>
| $TypedApi<AppBskyEmbedExternal.View>
view: $Typed<app.bsky.embed.external.View>
}
| {
type: 'video'
view:
| $Typed<app.bsky.embed.video.View>
| $TypedApi<AppBskyEmbedVideo.View>
view: $Typed<app.bsky.embed.video.View>
}
| {
type: 'post_with_media'
@@ -169,15 +115,7 @@ export function parseEmbedRecordView({
}
}
export function parseEmbed(
/*
* Accepts a `PostView.embed` from either world; the `#/lexicons` guards below
* narrow on `$type`, which is world-independent.
*/
embed:
| app.bsky.feed.defs.PostView['embed']
| AppBskyFeedDefs.PostView['embed'],
): Embed {
export function parseEmbed(embed: app.bsky.feed.defs.PostView['embed']): Embed {
if (isType(app.bsky.embed.images.view, embed)) {
return {
type: 'images',
+1 -14
View File
@@ -1,23 +1,10 @@
import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
import {type app, type chat} from '#/lexicons'
/**
* Matches any profile view exported by our SDK, in either world.
*
* Both the generated `#/lexicons` views and the `@atproto/api` views are
* accepted because both are live producers: queries migrated to the lexicon
* client emit the former, unmigrated ones emit the latter, and consumers of
* this alias take profiles from both.
*
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
* Matches any profile view exported by our SDK.
*/
export type AnyProfileView =
| app.bsky.actor.defs.ProfileViewBasic
| app.bsky.actor.defs.ProfileView
| app.bsky.actor.defs.ProfileViewDetailed
| chat.bsky.actor.defs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewDetailed
| ChatBskyActorDefs.ProfileViewBasic
+7 -26
View File
@@ -1,23 +1,14 @@
import {type AppBskyGraphDefs} from '@atproto/api'
import {app} from '#/lexicons'
/*
* `$type`-only guards for starter pack views. They compare against the
* `#/lexicons` schema's `$type` string rather than delegating to the schema's
* `isTypeOf` (which treats a missing `$type` as a match), matching the
* present-and-equal semantics of the old `@atproto/api`
* `AppBskyGraphDefs.isStarterPackView*` helpers.
*
* The `$type` string is identical in both worlds, so a single check narrows a
* value from either producer; the narrowed type is the union of both worlds'
* views for the same reason {@link AnyStarterPackView} is.
* The generated `$type`-only guards. These match on a present, matching
* `$type` against the `#/lexicons` schema's `$type` string rather than
* delegating to the schema's `isTypeOf` (which treats a missing `$type` as a
* match).
*/
export function isBasicView(
v: unknown,
): v is
| app.bsky.graph.defs.StarterPackViewBasic
| AppBskyGraphDefs.StarterPackViewBasic {
): v is app.bsky.graph.defs.StarterPackViewBasic {
return (
v != null &&
typeof v === 'object' &&
@@ -26,9 +17,7 @@ export function isBasicView(
)
}
export function isView(
v: unknown,
): v is app.bsky.graph.defs.StarterPackView | AppBskyGraphDefs.StarterPackView {
export function isView(v: unknown): v is app.bsky.graph.defs.StarterPackView {
return (
v != null &&
typeof v === 'object' &&
@@ -37,16 +26,8 @@ export function isView(
}
/**
* Matches any starter pack view exported by our SDK, in either world.
*
* Both the generated `#/lexicons` views and the `@atproto/api` views are
* accepted because both are live producers: queries migrated to the lexicon
* client emit the former, unmigrated ones emit the latter.
*
* TODO: remove the @atproto/api arms once all producers emit #/lexicons views
* Matches any starter pack view exported by our SDK.
*/
export type AnyStarterPackView =
| app.bsky.graph.defs.StarterPackViewBasic
| app.bsky.graph.defs.StarterPackView
| AppBskyGraphDefs.StarterPackViewBasic
| AppBskyGraphDefs.StarterPackView