[SDK] Remove @atproto/api (#11386)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user