@atproto/lex 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +287 -95
- package/bin/lex +18 -10
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @atproto/lex
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5006](https://github.com/bluesky-social/atproto/pull/5006) [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix README
|
|
8
|
+
|
|
9
|
+
- [#5006](https://github.com/bluesky-social/atproto/pull/5006) [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `yargs` depedency
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc), [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc)]:
|
|
12
|
+
- @atproto/lex-schema@0.1.2
|
|
13
|
+
- @atproto/lex-client@0.1.3
|
|
14
|
+
|
|
15
|
+
## 0.1.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#4984](https://github.com/bluesky-social/atproto/pull/4984) [`5bdb4ad`](https://github.com/bluesky-social/atproto/commit/5bdb4addd6a3798bf6c8c391c74044b3e251008a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` export of "main" schema in the namespace file
|
|
20
|
+
|
|
21
|
+
- [#4979](https://github.com/bluesky-social/atproto/pull/4979) [`314df62`](https://github.com/bluesky-social/atproto/commit/314df62537bb519231aa375dd3a38360afc79ce0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Always add `#__PURE__` annotations to function calls
|
|
22
|
+
|
|
23
|
+
- [#4983](https://github.com/bluesky-social/atproto/pull/4983) [`1259646`](https://github.com/bluesky-social/atproto/commit/125964673962a86282a05bf22f410fad8ad06b41) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make the generation of the `$defs` namespace optional by default
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [[`3aae4fe`](https://github.com/bluesky-social/atproto/commit/3aae4fe43448dca860fc6c4a24d18bfa64de084b), [`5bdb4ad`](https://github.com/bluesky-social/atproto/commit/5bdb4addd6a3798bf6c8c391c74044b3e251008a), [`3aae4fe`](https://github.com/bluesky-social/atproto/commit/3aae4fe43448dca860fc6c4a24d18bfa64de084b), [`314df62`](https://github.com/bluesky-social/atproto/commit/314df62537bb519231aa375dd3a38360afc79ce0), [`1259646`](https://github.com/bluesky-social/atproto/commit/125964673962a86282a05bf22f410fad8ad06b41), [`482767c`](https://github.com/bluesky-social/atproto/commit/482767c4bcce95aa390b2992b028fd8e27d162b2)]:
|
|
26
|
+
- @atproto/lex-builder@0.1.2
|
|
27
|
+
- @atproto/lex-client@0.1.2
|
|
28
|
+
|
|
3
29
|
## 0.1.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
> [!IMPORTANT]
|
|
2
|
-
>
|
|
3
|
-
> This package is currently in **preview**. The API and features are subject to change before the stable release. See the [Changelog](./CHANGELOG.md) for version history.
|
|
4
|
-
|
|
5
1
|
Type-safe Lexicon tooling for AT Protocol data.
|
|
6
2
|
|
|
7
3
|
- Fetch and manage Lexicon schemas, generate TypeScript validators
|
|
@@ -54,6 +50,16 @@ const posts = await client.list(app.bsky.feed.post, { limit: 10 })
|
|
|
54
50
|
- [Type definitions](#type-definitions)
|
|
55
51
|
- [Building data](#building-data)
|
|
56
52
|
- [Validation Helpers](#validation-helpers)
|
|
53
|
+
- [Record / typed-object helpers](#record--typed-object-helpers)
|
|
54
|
+
- [`$type` - Type Identifier](#type---type-identifier)
|
|
55
|
+
- [`$build(data)` - Build with Defaults](#builddata---build-with-defaults)
|
|
56
|
+
- [`$isTypeOf(data)` - Type Discriminator](#istypeofdata---type-discriminator)
|
|
57
|
+
- [Universal validation helpers](#universal-validation-helpers)
|
|
58
|
+
- [`$matches(data)` - Type Guard](#matchesdata---type-guard)
|
|
59
|
+
- [`$assert(data)` - Type-Narrowing Assertion](#assertdata---type-narrowing-assertion)
|
|
60
|
+
- [`$parse(data)` - Parse and Validate](#parsedata---parse-and-validate)
|
|
61
|
+
- [`$validate(data)` - Validate a value against the schema](#validatedata---validate-a-value-against-the-schema)
|
|
62
|
+
- [`$safeParse(data, options?)` - Parse a value against a schema and get the resulting value](#safeparsedata-options---parse-a-value-against-a-schema-and-get-the-resulting-value)
|
|
57
63
|
- [Data Model](#data-model)
|
|
58
64
|
- [Types](#types)
|
|
59
65
|
- [JSON Encoding](#json-encoding)
|
|
@@ -61,21 +67,58 @@ const posts = await client.list(app.bsky.feed.post, { limit: 10 })
|
|
|
61
67
|
- [Making simple XRPC Requests](#making-simple-xrpc-requests)
|
|
62
68
|
- [Client API](#client-api)
|
|
63
69
|
- [Creating a Client](#creating-a-client)
|
|
70
|
+
- [Unauthenticated Client](#unauthenticated-client)
|
|
71
|
+
- [Authenticated Client with OAuth](#authenticated-client-with-oauth)
|
|
72
|
+
- [Authenticated Client with Password](#authenticated-client-with-password)
|
|
73
|
+
- [Client with Service Proxy (authenticated only)](#client-with-service-proxy-authenticated-only)
|
|
74
|
+
- [Validation and Strictness Options](#validation-and-strictness-options)
|
|
64
75
|
- [Core Methods](#core-methods)
|
|
76
|
+
- [`client.call()`](#clientcall)
|
|
77
|
+
- [`client.create()`](#clientcreate)
|
|
78
|
+
- [`client.get()`](#clientget)
|
|
79
|
+
- [`client.put()`](#clientput)
|
|
80
|
+
- [`client.delete()`](#clientdelete)
|
|
81
|
+
- [`client.list()`](#clientlist)
|
|
82
|
+
- [`client.applyWrites()`](#clientapplywrites)
|
|
65
83
|
- [Error Handling](#error-handling)
|
|
84
|
+
- [Safe Methods](#safe-methods)
|
|
85
|
+
- [XrpcFailure Type](#xrpcfailure-type)
|
|
66
86
|
- [Authentication Methods](#authentication-methods)
|
|
87
|
+
- [`client.did`](#clientdid)
|
|
88
|
+
- [`client.assertAuthenticated()`](#clientassertauthenticated)
|
|
89
|
+
- [`client.assertDid`](#clientassertdid)
|
|
67
90
|
- [Labeler Configuration](#labeler-configuration)
|
|
68
91
|
- [Low-Level XRPC](#low-level-xrpc)
|
|
69
92
|
- [Utilities](#utilities)
|
|
70
93
|
- [Datetime Strings](#datetime-strings)
|
|
71
94
|
- [Advanced Usage](#advanced-usage)
|
|
72
95
|
- [Workflow Integration](#workflow-integration)
|
|
96
|
+
- [Development Workflow](#development-workflow)
|
|
73
97
|
- [Tree-Shaking](#tree-shaking)
|
|
98
|
+
- [Namespace notation](#namespace-notation)
|
|
99
|
+
- [Explicit `.main` reference](#explicit-main-reference)
|
|
100
|
+
- [Direct named import from the schema file](#direct-named-import-from-the-schema-file)
|
|
101
|
+
- [Default import (recommended)](#default-import-recommended)
|
|
102
|
+
- [Drawbacks of the default export](#drawbacks-of-the-default-export)
|
|
103
|
+
- [Summary](#summary)
|
|
74
104
|
- [Blob references](#blob-references)
|
|
105
|
+
- [TypedBlobRef: The Current Standard](#typedblobref-the-current-standard)
|
|
106
|
+
- [LegacyBlobRef: Historical Format](#legacyblobref-historical-format)
|
|
107
|
+
- [Working with Both Formats](#working-with-both-formats)
|
|
75
108
|
- [Actions](#actions)
|
|
109
|
+
- [What are Actions?](#what-are-actions)
|
|
110
|
+
- [Using Actions](#using-actions)
|
|
111
|
+
- [Composing Multiple Operations](#composing-multiple-operations)
|
|
112
|
+
- [Higher-Order Actions](#higher-order-actions)
|
|
76
113
|
- [Creating a Client from Another Client](#creating-a-client-from-another-client)
|
|
77
114
|
- [Building Library-Style APIs with Actions](#building-library-style-apis-with-actions)
|
|
115
|
+
- [Creating Posts](#creating-posts)
|
|
116
|
+
- [Following Users](#following-users)
|
|
117
|
+
- [Updating Profile with Retry Logic](#updating-profile-with-retry-logic)
|
|
118
|
+
- [Packaging Actions as a Library](#packaging-actions-as-a-library)
|
|
119
|
+
- [Best Practices for Actions](#best-practices-for-actions)
|
|
78
120
|
- [Standard Schema Compatibility](#standard-schema-compatibility)
|
|
121
|
+
- [Validating Generic Schemas with `$check`](#validating-generic-schemas-with-check)
|
|
79
122
|
- [License](#license)
|
|
80
123
|
|
|
81
124
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
@@ -189,14 +232,16 @@ Options:
|
|
|
189
232
|
- `--clear` - Clear output directory before generating
|
|
190
233
|
- `--override` - Override existing files (has no effect with --clear)
|
|
191
234
|
- `--no-pretty` - Don't run prettier on generated files (prettier is enabled by default)
|
|
192
|
-
- `--ignore-errors` -
|
|
193
|
-
- `--
|
|
235
|
+
- `--ignore-errors` - Skip files that fail to parse or compile instead of aborting the build
|
|
236
|
+
- `--ignore-invalid-lexicons` - Skip lexicon files that fail validation instead of exiting with an error
|
|
194
237
|
- `--exclude <patterns...>` - List of strings or regex patterns to exclude lexicon documents by their IDs
|
|
195
238
|
- `--include <patterns...>` - List of strings or regex patterns to include lexicon documents by their IDs
|
|
196
239
|
- `--lib <package>` - Package name of the library to import the lex schema utility "l" from (default: `@atproto/lex`)
|
|
197
|
-
- `--
|
|
198
|
-
- `--
|
|
199
|
-
- `--
|
|
240
|
+
- `--import-ext <ext>` - File extension to use for import statements in generated files (default: `.js`). Use `--import-ext ""` to generate extension-less imports
|
|
241
|
+
- `--file-ext <ext>` - File extension to use for generated files (default: `.ts`)
|
|
242
|
+
- `--index-file` - Generate an "index" file that re-exports all root-level namespaces (disabled by default)
|
|
243
|
+
- `--defs-export` - When some definitions conflict with child namespaces, export lexicon definitions under a separate `$defs` namespace (e.g. `com.example.foo.$defs`)
|
|
244
|
+
- `--no-default-export` - Disable generation of a `default` export of the `main` schema in each schema's namespace file (default exports are enabled by default; see [Tree-Shaking](#tree-shaking))
|
|
200
245
|
|
|
201
246
|
### Generated Schema Structure
|
|
202
247
|
|
|
@@ -241,11 +286,12 @@ app.bsky.feed.post.$validate(post)
|
|
|
241
286
|
|
|
242
287
|
### Validation Helpers
|
|
243
288
|
|
|
244
|
-
|
|
289
|
+
Generated namespaces expose a handful of `$`-prefixed helpers bound to the namespace's `main` schema. They come in two groups:
|
|
245
290
|
|
|
246
|
-
|
|
291
|
+
- [**Universal validation helpers**](#universal-validation-helpers) are available on every schema's `main`: `$matches`, `$assert`, `$check`, `$parse`, `$safeParse`, `$validate`, `$safeValidate` (and `$cast` / `$ifMatches`). These work for records, typed objects, queries, procedures, and subscriptions.
|
|
292
|
+
- [**Record / typed-object helpers**](#record--typed-object-helpers) are only emitted for record and typed-object schemas: `$type`, `$build`, `$isTypeOf`.
|
|
247
293
|
|
|
248
|
-
|
|
294
|
+
In addition, every generated namespace file exports a top-level `$nsid` constant containing the NSID of the lexicon document:
|
|
249
295
|
|
|
250
296
|
```typescript
|
|
251
297
|
import * as app from './lexicons/app.js'
|
|
@@ -253,9 +299,13 @@ import * as app from './lexicons/app.js'
|
|
|
253
299
|
console.log(app.bsky.feed.defs.$nsid) // 'app.bsky.feed.defs'
|
|
254
300
|
```
|
|
255
301
|
|
|
256
|
-
|
|
302
|
+
The Schema instance itself (for example `app.bsky.feed.post.main`) also exposes the underlying methods both with and without the `$` prefix (e.g. `main.parse()` and `main.$parse()`).
|
|
257
303
|
|
|
258
|
-
|
|
304
|
+
#### Record / typed-object helpers
|
|
305
|
+
|
|
306
|
+
##### `$type` - Type Identifier
|
|
307
|
+
|
|
308
|
+
Returns the `$type` string of the schema (only available on record and typed-object schemas):
|
|
259
309
|
|
|
260
310
|
```typescript
|
|
261
311
|
import * as app from './lexicons/app.js'
|
|
@@ -264,7 +314,52 @@ console.log(app.bsky.feed.post.$type) // 'app.bsky.feed.post'
|
|
|
264
314
|
console.log(app.bsky.actor.defs.profileViewBasic.$type) // 'app.bsky.actor.defs#profileViewBasic'
|
|
265
315
|
```
|
|
266
316
|
|
|
267
|
-
|
|
317
|
+
Prefer `$type` over hard-coding the equivalent string literal in your code. The constant is emitted exactly once per schema in the generated namespace file, so every reference reuses the same string instance. Inlining `'app.bsky.feed.post'` everywhere instead leaks the same string into every call site, increases bundle size, and creates a typo-prone source of drift between your code and the schema.
|
|
318
|
+
|
|
319
|
+
##### `$build(data)` - Build with Defaults
|
|
320
|
+
|
|
321
|
+
Builds data by adding the `$type` property and properly types the result. This also allows to declare a variable with the correct type without having to explicitly specify it.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import { l } from '@atproto/lex'
|
|
325
|
+
import * as app from './lexicons/app.js'
|
|
326
|
+
|
|
327
|
+
// The type of the "like" variable will be "app.bsky.feed.like.Main" (no need to explicitly specify the type)
|
|
328
|
+
const like = app.bsky.feed.like.$build({
|
|
329
|
+
subject: {
|
|
330
|
+
uri: 'at://did:plc:abc/app.bsky.feed.post/123',
|
|
331
|
+
cid: 'bafyrei...',
|
|
332
|
+
},
|
|
333
|
+
createdAt: l.currentDatetimeString(),
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> [!NOTE]
|
|
338
|
+
>
|
|
339
|
+
> `$build()` does not perform validation, and expects properly typed input data - use `$parse()` if you need validation.
|
|
340
|
+
|
|
341
|
+
##### `$isTypeOf(data)` - Type Discriminator
|
|
342
|
+
|
|
343
|
+
Discriminates (pre-validated) data based on its `$type` property, without re-validating. This is especially useful when working with union types:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { l } from '@atproto/lex'
|
|
347
|
+
import * as app from './lexicons/app.js'
|
|
348
|
+
|
|
349
|
+
declare const data:
|
|
350
|
+
| app.bsky.feed.post.Main
|
|
351
|
+
| app.bsky.feed.like.Main
|
|
352
|
+
| l.Unknown$TypedObject
|
|
353
|
+
|
|
354
|
+
// Discriminate by $type without re-validating
|
|
355
|
+
if (app.bsky.feed.post.$isTypeOf(data)) {
|
|
356
|
+
// data is a post
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Universal validation helpers
|
|
361
|
+
|
|
362
|
+
##### `$matches(data)` - Type Guard
|
|
268
363
|
|
|
269
364
|
Returns `true` if data matches the schema, `false` otherwise. Acts as a TypeScript type guard:
|
|
270
365
|
|
|
@@ -272,19 +367,45 @@ Returns `true` if data matches the schema, `false` otherwise. Acts as a TypeScri
|
|
|
272
367
|
import { l } from '@atproto/lex'
|
|
273
368
|
import * as app from './lexicons/app.js'
|
|
274
369
|
|
|
275
|
-
const data = {
|
|
370
|
+
const data: unknown = {
|
|
276
371
|
$type: 'app.bsky.feed.post',
|
|
277
372
|
text: 'Hello!',
|
|
278
373
|
createdAt: l.currentDatetimeString(),
|
|
279
374
|
}
|
|
280
375
|
|
|
281
|
-
if (app.bsky.feed.post.$
|
|
376
|
+
if (app.bsky.feed.post.$matches(data)) {
|
|
282
377
|
// TypeScript knows data is a Post here
|
|
283
378
|
console.log(data.text)
|
|
284
379
|
}
|
|
285
380
|
```
|
|
286
381
|
|
|
287
|
-
|
|
382
|
+
> [!NOTE]
|
|
383
|
+
>
|
|
384
|
+
> Performs validation so [`$isTypeOf`](#istypeofdata---type-discriminator) is preferred for pre-validated & properly typed data.
|
|
385
|
+
|
|
386
|
+
##### `$assert(data)` - Type-Narrowing Assertion
|
|
387
|
+
|
|
388
|
+
Throws if `data` does not match the schema. When the schema is statically known (e.g. `app.bsky.feed.post`), TypeScript narrows the type of `data` after the call:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { l } from '@atproto/lex'
|
|
392
|
+
import * as app from './lexicons/app.js'
|
|
393
|
+
|
|
394
|
+
const data: unknown = {
|
|
395
|
+
$type: 'app.bsky.feed.post',
|
|
396
|
+
text: 'Hello!',
|
|
397
|
+
createdAt: l.currentDatetimeString(),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
app.bsky.feed.post.$assert(data)
|
|
401
|
+
|
|
402
|
+
// TypeScript now knows data is app.bsky.feed.post.Main
|
|
403
|
+
console.log(data.text)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
For library code that operates on a schema parameter whose type cannot be fully expressed, see [Validating Generic Schemas with `$check`](#validating-generic-schemas-with-check).
|
|
407
|
+
|
|
408
|
+
##### `$parse(data)` - Parse and Validate
|
|
288
409
|
|
|
289
410
|
Validates and returns typed data, throwing an error if validation fails:
|
|
290
411
|
|
|
@@ -293,11 +414,12 @@ import { l } from '@atproto/lex'
|
|
|
293
414
|
import * as app from './lexicons/app.js'
|
|
294
415
|
|
|
295
416
|
try {
|
|
296
|
-
const post = app.bsky.feed.post.$
|
|
417
|
+
const post = app.bsky.feed.post.$parse({
|
|
297
418
|
$type: 'app.bsky.feed.post',
|
|
298
419
|
text: 'Hello!',
|
|
299
420
|
createdAt: l.currentDatetimeString(),
|
|
300
421
|
})
|
|
422
|
+
|
|
301
423
|
// post is now typed and validated
|
|
302
424
|
console.log(post.text)
|
|
303
425
|
} catch (error) {
|
|
@@ -309,7 +431,7 @@ try {
|
|
|
309
431
|
>
|
|
310
432
|
> The `$parse` method will apply defaults defined in the schema for optional fields, as well as data coercion (e.g., CID strings to Cid types). This means that the returned value might be different from the input data if defaults were applied. Use `$validate()` for value validation.
|
|
311
433
|
|
|
312
|
-
|
|
434
|
+
##### `$validate(data)` - Validate a value against the schema
|
|
313
435
|
|
|
314
436
|
Validates an existing value against a schema, returning the value itself if, and only if, it already matches the schema (ie. without applying defaults or coercion).
|
|
315
437
|
|
|
@@ -329,7 +451,7 @@ const result = app.bsky.feed.post.$validate(value)
|
|
|
329
451
|
value === result // true
|
|
330
452
|
```
|
|
331
453
|
|
|
332
|
-
|
|
454
|
+
##### `$safeParse(data, options?)` - Parse a value against a schema and get the resulting value
|
|
333
455
|
|
|
334
456
|
Returns a detailed validation result object without throwing:
|
|
335
457
|
|
|
@@ -360,43 +482,6 @@ app.bsky.feed.post.$safeParse(data) // { strict: true } is the default
|
|
|
360
482
|
app.bsky.feed.post.$safeParse(data, { strict: false })
|
|
361
483
|
```
|
|
362
484
|
|
|
363
|
-
#### `$build(data)` - Build with Defaults
|
|
364
|
-
|
|
365
|
-
Builds data by adding the `$type` property and properly types the result. Note that `$build()` does not perform validation - use `$parse()` if you need validation:
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
import { l } from '@atproto/lex'
|
|
369
|
-
import * as app from './lexicons/app.js'
|
|
370
|
-
|
|
371
|
-
// The type of the "like" variable will be "app.bsky.feed.like.Main"
|
|
372
|
-
const like = app.bsky.feed.like.$build({
|
|
373
|
-
subject: {
|
|
374
|
-
uri: 'at://did:plc:abc/app.bsky.feed.post/123',
|
|
375
|
-
cid: 'bafyrei...',
|
|
376
|
-
},
|
|
377
|
-
createdAt: l.currentDatetimeString(),
|
|
378
|
-
})
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### `$isTypeOf(data)` - Type Discriminator
|
|
382
|
-
|
|
383
|
-
Discriminates (pre-validated) data based on its `$type` property, without re-validating. This is especially useful when working with union types:
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
import { l } from '@atproto/lex'
|
|
387
|
-
import * as app from './lexicons/app.js'
|
|
388
|
-
|
|
389
|
-
declare const data:
|
|
390
|
-
| app.bsky.feed.post.Main
|
|
391
|
-
| app.bsky.feed.like.Main
|
|
392
|
-
| l.Unknown$TypedObject
|
|
393
|
-
|
|
394
|
-
// Discriminate by $type without re-validating
|
|
395
|
-
if (app.bsky.feed.post.$isTypeOf(data)) {
|
|
396
|
-
// data is a post
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
485
|
## Data Model
|
|
401
486
|
|
|
402
487
|
The AT Protocol uses a [data model](https://atproto.com/specs/data-model) that extends JSON with two additional data structures: **CIDs** (content-addressed links) and **bytes** (for raw data). This data model can be encoded either as JSON for XRPC (HTTP API) or as [CBOR](https://dasl.ing/drisl.html) for storage and authentication (see [`@atproto/lex-cbor`](../lex-cbor)).
|
|
@@ -665,7 +750,7 @@ console.log(result.cid)
|
|
|
665
750
|
Options:
|
|
666
751
|
|
|
667
752
|
- `rkey` - Custom record key (auto-generated if not provided)
|
|
668
|
-
- `validate` -
|
|
753
|
+
- `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
|
|
669
754
|
- `validateRequest` - Validate the record locally against schema before submitting the request
|
|
670
755
|
- `swapCommit` - CID for optimistic concurrency control
|
|
671
756
|
|
|
@@ -707,8 +792,8 @@ await client.put(app.bsky.actor.profile, {
|
|
|
707
792
|
Options:
|
|
708
793
|
|
|
709
794
|
- `rkey` - Record key (required for non-literal keys)
|
|
710
|
-
- `validate` -
|
|
711
|
-
- `validateRequest` -
|
|
795
|
+
- `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
|
|
796
|
+
- `validateRequest` - Validate the record locally against schema before submitting the request
|
|
712
797
|
- `swapCommit` - Expected repo commit CID
|
|
713
798
|
- `swapRecord` - Expected record CID
|
|
714
799
|
|
|
@@ -740,6 +825,11 @@ for (const record of result.records) {
|
|
|
740
825
|
console.log(record.uri, record.value.text)
|
|
741
826
|
}
|
|
742
827
|
|
|
828
|
+
// Records that failed local schema validation are returned separately
|
|
829
|
+
for (const invalid of result.invalid) {
|
|
830
|
+
console.warn('Invalid record:', invalid)
|
|
831
|
+
}
|
|
832
|
+
|
|
743
833
|
// Pagination
|
|
744
834
|
if (result.cursor) {
|
|
745
835
|
const nextPage = await client.list(app.bsky.feed.post, {
|
|
@@ -749,6 +839,12 @@ if (result.cursor) {
|
|
|
749
839
|
}
|
|
750
840
|
```
|
|
751
841
|
|
|
842
|
+
The result includes:
|
|
843
|
+
|
|
844
|
+
- `records` - Records that successfully validated against the schema
|
|
845
|
+
- `invalid` - Records returned by the server that failed local schema validation (raw `LexMap` values)
|
|
846
|
+
- `cursor` - Pagination cursor (if more results are available)
|
|
847
|
+
|
|
752
848
|
#### `client.applyWrites()`
|
|
753
849
|
|
|
754
850
|
Perform an atomic batch of create, update, and delete operations in a single request.
|
|
@@ -785,7 +881,7 @@ for (const result of response.body.results) {
|
|
|
785
881
|
Options:
|
|
786
882
|
|
|
787
883
|
- `repo` - Repository identifier (defaults to authenticated user's DID)
|
|
788
|
-
- `validate` -
|
|
884
|
+
- `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
|
|
789
885
|
- `swapCommit` - CID for optimistic concurrency control
|
|
790
886
|
|
|
791
887
|
> [!NOTE]
|
|
@@ -824,13 +920,23 @@ if (result.success) {
|
|
|
824
920
|
// Handle success
|
|
825
921
|
console.log(result.body)
|
|
826
922
|
} else {
|
|
827
|
-
// Handle failure - result is an XrpcFailure
|
|
923
|
+
// Handle failure - result is an XrpcFailure.
|
|
924
|
+
//
|
|
925
|
+
// All XrpcFailure subclasses inherit from XrpcError and share these members:
|
|
926
|
+
result.error // string error code (e.g. "HandleNotFound", "UpstreamFailure")
|
|
927
|
+
result.message // string
|
|
928
|
+
result.shouldRetry() // boolean - whether the error is transient
|
|
929
|
+
|
|
930
|
+
if (result.matchesSchemaErrors()) {
|
|
931
|
+
// Check if the error matches a declared error in the schema.
|
|
932
|
+
// TypeScript narrows `result.error` to one of the method's declared error codes.
|
|
933
|
+
result.error // "HandleNotFound"
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Branch on the specific error class to access additional members:
|
|
828
937
|
if (result instanceof XrpcResponseError) {
|
|
829
938
|
// The server responded with an error status code (4xx or 5xx).
|
|
830
939
|
// This is used for all error responses, whether or not they have a valid XRPC error payload.
|
|
831
|
-
|
|
832
|
-
result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", "UpstreamFailure", etc.)
|
|
833
|
-
result.message // string
|
|
834
940
|
result.response.status // number
|
|
835
941
|
result.response.headers // Headers
|
|
836
942
|
result.payload // undefined | { body: unknown; encoding: string }
|
|
@@ -840,25 +946,11 @@ if (result.success) {
|
|
|
840
946
|
} else if (result instanceof XrpcInvalidResponseError) {
|
|
841
947
|
// The response was truly invalid (3xx redirect, malformed JSON, schema mismatch, etc.).
|
|
842
948
|
// This is a more specific error for responses that are not processable.
|
|
843
|
-
|
|
844
|
-
result.error // "UpstreamFailure"
|
|
845
|
-
result.message // string
|
|
846
949
|
result.response.status // number
|
|
847
950
|
result.response.headers // Headers
|
|
848
951
|
result.payload // undefined | { body: unknown; encoding: string }
|
|
849
952
|
} else if (result instanceof XrpcInternalError) {
|
|
850
953
|
// Something went wrong on the client side (network error, etc.)
|
|
851
|
-
result.error // "InternalServerError"
|
|
852
|
-
result.message // string
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// All XrpcFailure types have these properties:
|
|
856
|
-
result.shouldRetry() // boolean - whether the error is transient
|
|
857
|
-
|
|
858
|
-
if (result.matchesSchemaErrors()) {
|
|
859
|
-
// Check if the error matches a declared error in the schema.
|
|
860
|
-
// TypeScript knows this is a declared error for the method.
|
|
861
|
-
result.error // "HandleNotFound"
|
|
862
954
|
}
|
|
863
955
|
}
|
|
864
956
|
```
|
|
@@ -996,9 +1088,9 @@ import {
|
|
|
996
1088
|
isLanguageString, // Validate language tags (e.g., 'en', 'pt-BR')
|
|
997
1089
|
|
|
998
1090
|
// Low-level JSON encoding helpers
|
|
999
|
-
parseLexLink, // { $link: string } → Cid
|
|
1091
|
+
parseLexLink, // { $link: string } → Cid | undefined
|
|
1000
1092
|
encodeLexLink, // Cid → { $link: string }
|
|
1001
|
-
parseLexBytes, // { $bytes: string } → Uint8Array
|
|
1093
|
+
parseLexBytes, // { $bytes: string } → Uint8Array | undefined
|
|
1002
1094
|
encodeLexBytes, // Uint8Array → { $bytes: string }
|
|
1003
1095
|
} from '@atproto/lex'
|
|
1004
1096
|
|
|
@@ -1064,24 +1156,104 @@ This ensures that:
|
|
|
1064
1156
|
|
|
1065
1157
|
### Tree-Shaking
|
|
1066
1158
|
|
|
1067
|
-
The generated TypeScript is
|
|
1159
|
+
The generated TypeScript code is structured to be tree-shakeable, but the way you reference schemas has a meaningful impact on the final bundle size. There are several ways to refer to a generated schema, and each comes with different trade-offs.
|
|
1160
|
+
|
|
1161
|
+
#### Namespace notation
|
|
1162
|
+
|
|
1163
|
+
The most ergonomic style is to use a namespace import and reference schemas through dotted paths:
|
|
1068
1164
|
|
|
1069
1165
|
```typescript
|
|
1070
|
-
|
|
1071
|
-
import { post } from './lexicons/app/bsky/feed/post.js'
|
|
1072
|
-
import { getProfile } from './lexicons/app/bsky/actor/getProfile.js'
|
|
1166
|
+
import * as com from './lexicons/com.js'
|
|
1073
1167
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1168
|
+
await client.call(com.atproto.repo.getRecord, {
|
|
1169
|
+
/* ... */
|
|
1170
|
+
})
|
|
1076
1171
|
```
|
|
1077
1172
|
|
|
1078
|
-
|
|
1173
|
+
This style is convenient and reads naturally as it mirrors the NSID of the schema. However, it produces the largest bundles. From the bundler's point of view, `com.atproto.repo.getRecord` is the whole schema namespace (which contains the `main` schema as well as helpers, and any other definitions). The bundler cannot know that `client.call()` only consumes the `main` schema, so it has to keep the rest of the namespace alive in the bundle.
|
|
1174
|
+
|
|
1175
|
+
#### Explicit `.main` reference
|
|
1176
|
+
|
|
1177
|
+
You can mitigate the bundle-size cost by explicitly naming the `main` definition:
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
import * as com from './lexicons/com.js'
|
|
1181
|
+
|
|
1182
|
+
await client.call(com.atproto.repo.getRecord.main, {
|
|
1183
|
+
/* ... */
|
|
1184
|
+
})
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
This lets the bundler drop the sibling definitions inside `getRecord` that aren't referenced. The drawback is that it leaks an implementation detail: the `main` segment of the path. In Lexicon, `main` is typically implicit:
|
|
1188
|
+
|
|
1189
|
+
- Records use a `$type` of `app.bsky.feed.post` (no `#main`)
|
|
1190
|
+
- XRPC endpoints are exposed as `/xrpc/com.atproto.repo.getRecord` (no `main`)
|
|
1191
|
+
|
|
1192
|
+
So writing `.main` in application code feels verbose compared to how Lexicons are normally referred to.
|
|
1193
|
+
|
|
1194
|
+
#### Direct named import from the schema file
|
|
1195
|
+
|
|
1196
|
+
You can also import the `main` schema directly from the file that defines it:
|
|
1197
|
+
|
|
1198
|
+
```typescript
|
|
1199
|
+
import { main as getRecord } from './lexicons/com/atproto/repo/getRecord.js'
|
|
1200
|
+
|
|
1201
|
+
await client.call(getRecord, {
|
|
1202
|
+
/* ... */
|
|
1203
|
+
})
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
This produces equally small bundles as the explicit `.main` reference, but it still surfaces the `main` identifier: you have to know to import `main` and likely rename it.
|
|
1207
|
+
|
|
1208
|
+
#### Default import (recommended)
|
|
1209
|
+
|
|
1210
|
+
To make the small-bundle path also the ergonomic path, every namespace file generated by `lex build` re-exports the `main` schema as its `default` export:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
// generated file: ./lexicons/com/atproto/repo/getRecord.js
|
|
1214
|
+
export * from './getRecord.defs.js'
|
|
1215
|
+
export { main as default } from './getRecord.defs.js'
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
This means you can write:
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
import getRecord from './lexicons/com/atproto/repo/getRecord.js'
|
|
1222
|
+
import post from './lexicons/app/bsky/feed/post.js'
|
|
1223
|
+
|
|
1224
|
+
await client.call(getRecord, {
|
|
1225
|
+
/* ... */
|
|
1226
|
+
})
|
|
1227
|
+
await client.create(post, {
|
|
1228
|
+
/* ... */
|
|
1229
|
+
})
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
This is the most bundle-friendly style: the bundler only pulls in the `main` schema, and the import name doesn't have to mention `main` at all. This helps keeping application code aligned with how Lexicons are usually identified.
|
|
1233
|
+
|
|
1234
|
+
#### Drawbacks of the default export
|
|
1235
|
+
|
|
1236
|
+
The `default` re-export is enabled by default but has two minor drawbacks:
|
|
1237
|
+
|
|
1238
|
+
1. It is one additional property on the namespace module, which can very slightly increase bundle size if you also use the namespace in some places.
|
|
1239
|
+
2. Any Lexicon document whose path segment is literally `default` (for example a hypothetical `com.example.records.default`) would conflict with the generated `default` export.
|
|
1240
|
+
|
|
1241
|
+
If either of these matters for your use case, you can disable the generation of `default` exports with the `--no-defaultExport` flag:
|
|
1079
1242
|
|
|
1080
1243
|
```bash
|
|
1081
|
-
lex build --
|
|
1244
|
+
lex build --no-defaultExport
|
|
1082
1245
|
```
|
|
1083
1246
|
|
|
1084
|
-
|
|
1247
|
+
#### Summary
|
|
1248
|
+
|
|
1249
|
+
| Style | Bundle size | Ergonomics |
|
|
1250
|
+
| ------------------------------------------------------ | ----------- | ---------------------------- |
|
|
1251
|
+
| `com.atproto.repo.getRecord` (namespace) | Largest | Best: matches the NSID |
|
|
1252
|
+
| `com.atproto.repo.getRecord.main` | Small | Leaks the `main` identifier |
|
|
1253
|
+
| `import { main as getRecord } from '.../getRecord.js'` | Small | Verbose, leaks `main` |
|
|
1254
|
+
| `import getRecord from '.../getRecord.js'` | Small | Concise, no `main` in source |
|
|
1255
|
+
|
|
1256
|
+
For libraries and applications where bundle size matters (typically anything shipped to a browser), prefer the default-import style. For scripts, tests, and server-side code where the bundle size of generated schemas is not a concern, the namespace style is perfectly fine.
|
|
1085
1257
|
|
|
1086
1258
|
### Blob references
|
|
1087
1259
|
|
|
@@ -1188,7 +1360,7 @@ An `Action` is a function with this signature:
|
|
|
1188
1360
|
type Action<Input, Output> = (
|
|
1189
1361
|
client: Client,
|
|
1190
1362
|
input: Input,
|
|
1191
|
-
options:
|
|
1363
|
+
options: ActionOptions,
|
|
1192
1364
|
) => Output | Promise<Output>
|
|
1193
1365
|
```
|
|
1194
1366
|
|
|
@@ -1196,7 +1368,7 @@ Actions receive:
|
|
|
1196
1368
|
|
|
1197
1369
|
- `client` - The Client instance (to make XRPC calls)
|
|
1198
1370
|
- `input` - The input data for the action
|
|
1199
|
-
- `options` -
|
|
1371
|
+
- `options` - `ActionOptions` (currently just `{ signal?: AbortSignal }`)
|
|
1200
1372
|
|
|
1201
1373
|
#### Using Actions
|
|
1202
1374
|
|
|
@@ -1511,10 +1683,10 @@ export const updateProfile: Action<ProfileUpdate, void> = async (
|
|
|
1511
1683
|
},
|
|
1512
1684
|
})
|
|
1513
1685
|
|
|
1514
|
-
const current = app.bsky.actor.profile
|
|
1686
|
+
const current = app.bsky.actor.profile.$safeValidate(res.body.record)
|
|
1515
1687
|
|
|
1516
1688
|
// Merge updates with current profile (if valid)
|
|
1517
|
-
const updated = app.bsky.actor.profile
|
|
1689
|
+
const updated = app.bsky.actor.profile.$build({
|
|
1518
1690
|
...(current.success ? current.value : undefined),
|
|
1519
1691
|
...updates,
|
|
1520
1692
|
})
|
|
@@ -1619,6 +1791,26 @@ if ('value' in result) {
|
|
|
1619
1791
|
|
|
1620
1792
|
When validated through the Standard Schema interface, schemas operate in "parse" mode, meaning transformations like defaults and coercions are applied to the output.
|
|
1621
1793
|
|
|
1794
|
+
### Validating Generic Schemas with `$check`
|
|
1795
|
+
|
|
1796
|
+
`$check(data)` is the non-narrowing counterpart to [`$assert(data)`](#assertdata---type-narrowing-assertion): both throw when `data` does not match the schema, but `$check` does not refine the static type of its argument.
|
|
1797
|
+
|
|
1798
|
+
`$check` is rarely needed in application code — prefer `$assert`. It is intended for library-style code that takes a schema as a generic parameter, where TypeScript cannot satisfy the assertion-signature requirement and `$assert` produces the following error:
|
|
1799
|
+
|
|
1800
|
+
> 'schema' needs an explicit type annotation.
|
|
1801
|
+
> Assertions require every name in the call target to be declared with an explicit type annotation. `ts(2775)`
|
|
1802
|
+
|
|
1803
|
+
In that situation, switch to `$check`:
|
|
1804
|
+
|
|
1805
|
+
```typescript
|
|
1806
|
+
import type { Schema } from '@atproto/lex'
|
|
1807
|
+
|
|
1808
|
+
function ensureMatches<S extends Schema>(schema: S, data: unknown) {
|
|
1809
|
+
// schema.$assert(data) // ❌ ts(2775): needs an explicit type annotation
|
|
1810
|
+
schema.$check(data) // ✅ throws on invalid, no type narrowing
|
|
1811
|
+
}
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1622
1814
|
## License
|
|
1623
1815
|
|
|
1624
1816
|
MIT or Apache2
|
package/bin/lex
CHANGED
|
@@ -51,12 +51,6 @@ yargs(hideBin(process.argv))
|
|
|
51
51
|
default: false,
|
|
52
52
|
describe: 'how to handle errors when processing input files',
|
|
53
53
|
},
|
|
54
|
-
'pure-annotations': {
|
|
55
|
-
type: 'boolean',
|
|
56
|
-
default: false,
|
|
57
|
-
describe:
|
|
58
|
-
'adds `/*#__PURE__*/` annotations for tree-shaking tools. Set this to true if you are using generated lexicons in a library.',
|
|
59
|
-
},
|
|
60
54
|
exclude: {
|
|
61
55
|
array: true,
|
|
62
56
|
type: 'string',
|
|
@@ -75,25 +69,37 @@ yargs(hideBin(process.argv))
|
|
|
75
69
|
describe:
|
|
76
70
|
'package name of the library to import the lex schema utility "l" from',
|
|
77
71
|
},
|
|
78
|
-
|
|
72
|
+
'import-ext': {
|
|
79
73
|
type: 'string',
|
|
80
74
|
default: '.js',
|
|
81
75
|
describe:
|
|
82
76
|
'file extension to use for import statements in generated files (e.g. ".ts", ".mts", ".cts"). Use --import-ext "" to generate extension-less imports.',
|
|
83
77
|
},
|
|
84
|
-
|
|
78
|
+
'file-ext': {
|
|
85
79
|
type: 'string',
|
|
86
80
|
default: '.ts',
|
|
87
81
|
describe:
|
|
88
82
|
'file extension to use for generated files (e.g. ".ts", ".mts", ".cts")',
|
|
89
83
|
},
|
|
90
|
-
|
|
84
|
+
'index-file': {
|
|
91
85
|
type: 'boolean',
|
|
92
86
|
default: false,
|
|
93
87
|
describe:
|
|
94
88
|
'generate an "index.<fileExt>" file that exports all root-level namespaces',
|
|
95
89
|
},
|
|
96
|
-
|
|
90
|
+
'defs-export': {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
default: false,
|
|
93
|
+
describe:
|
|
94
|
+
'when some definitions conflict with child namespaces, this option allows to export lexicon definitions under a separate $defs namespace (e.g. com.example.foo.$defs)',
|
|
95
|
+
},
|
|
96
|
+
'default-export': {
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
default: true,
|
|
99
|
+
describe:
|
|
100
|
+
'whether to generate a default export for the "main" lexicon definition schema in the parent namespace file',
|
|
101
|
+
},
|
|
102
|
+
'ignore-invalid-lexicons': {
|
|
97
103
|
type: 'boolean',
|
|
98
104
|
default: false,
|
|
99
105
|
describe:
|
|
@@ -153,4 +159,6 @@ yargs(hideBin(process.argv))
|
|
|
153
159
|
})
|
|
154
160
|
},
|
|
155
161
|
)
|
|
162
|
+
.strictCommands()
|
|
163
|
+
.demandCommand(1)
|
|
156
164
|
.parseAsync()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"tslib": "^2.8.1",
|
|
39
|
-
"yargs": "^
|
|
40
|
-
"@atproto/lex-builder": "^0.1.
|
|
41
|
-
"@atproto/lex-client": "^0.1.
|
|
39
|
+
"yargs": "^18.0.0",
|
|
40
|
+
"@atproto/lex-builder": "^0.1.2",
|
|
41
|
+
"@atproto/lex-client": "^0.1.3",
|
|
42
42
|
"@atproto/lex-data": "^0.1.1",
|
|
43
43
|
"@atproto/lex-json": "^0.1.0",
|
|
44
44
|
"@atproto/lex-installer": "^0.1.0",
|
|
45
|
-
"@atproto/lex-schema": "^0.1.
|
|
45
|
+
"@atproto/lex-schema": "^0.1.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/yargs": "^17.0.33",
|