@atproto/lex-document 0.0.12 → 0.0.13

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.
@@ -1,8 +1,60 @@
1
1
  import { LexiconDocument } from './lexicon-document.js'
2
2
 
3
+ /**
4
+ * Interface for indexing and retrieving Lexicon documents by their NSID.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // Using a custom indexer implementation
9
+ * const networkIndexer: LexiconIndexer = {
10
+ * async get(nsid: string) {
11
+ * const doc = await resolveLexicon(nsid)
12
+ * return doc
13
+ * }
14
+ * }
15
+ *
16
+ * const validator = await LexiconSchemaBuilder.build(networkIndexer, 'com.example.post#main')
17
+ * ```
18
+ */
3
19
  export interface LexiconIndexer {
20
+ /**
21
+ * Retrieves a Lexicon document by its NSID.
22
+ *
23
+ * @param nsid - The Namespaced Identifier of the Lexicon document to retrieve
24
+ * @returns A promise that resolves to the Lexicon document
25
+ * @throws When the document with the given NSID cannot be found
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const doc = await indexer.get('com.atproto.repo.createRecord')
30
+ * console.log(doc.defs.main?.type) // 'procedure'
31
+ * ```
32
+ */
4
33
  get(nsid: string): Promise<LexiconDocument>
5
34
 
35
+ /**
36
+ * Optional async disposal method for cleanup.
37
+ *
38
+ * When implemented, allows the indexer to be used with `await using`
39
+ * syntax for automatic resource cleanup.
40
+ *
41
+ * @returns A promise that resolves when disposal is complete
42
+ */
6
43
  [Symbol.asyncDispose]?: () => Promise<void>
44
+
45
+ /**
46
+ * Optional async iterator for iterating over all available Lexicon documents.
47
+ *
48
+ * @returns An async iterator yielding Lexicon documents
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * if (Symbol.asyncIterator in indexer) {
53
+ * for await (const doc of indexer) {
54
+ * console.log(doc.id)
55
+ * }
56
+ * }
57
+ * ```
58
+ */
7
59
  [Symbol.asyncIterator]?: () => AsyncIterator<LexiconDocument, void, unknown>
8
60
  }
@@ -2,7 +2,34 @@ import { LexiconDocument } from './lexicon-document.js'
2
2
  import { LexiconIndexer } from './lexicon-indexer.js'
3
3
 
4
4
  /**
5
- * (Lazily) indexes lexicon documents from an iterable source.
5
+ * Lazily indexes Lexicon documents from an iterable source.
6
+ *
7
+ * This class implements `LexiconIndexer` by consuming documents from an
8
+ * iterable (sync or async) and caching them for efficient retrieval.
9
+ * Documents are indexed on-demand as they are requested or iterated over.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // From an array of documents
14
+ * const docs = [lexiconDoc1, lexiconDoc2, lexiconDoc3]
15
+ * const indexer = new LexiconIterableIndexer(docs)
16
+ *
17
+ * // Documents are indexed lazily as requested
18
+ * const doc = await indexer.get('com.example.post')
19
+ * ```
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // From an async generator (e.g., reading from files)
24
+ * async function* loadLexicons() {
25
+ * for (const file of lexiconFiles) {
26
+ * yield JSON.parse(await fs.readFile(file, 'utf8'))
27
+ * }
28
+ * }
29
+ *
30
+ * await using indexer = new LexiconIterableIndexer(loadLexicons())
31
+ * const schemas = await LexiconSchemaBuilder.buildAll(indexer)
32
+ * ```
6
33
  */
7
34
  export class LexiconIterableIndexer implements LexiconIndexer, AsyncDisposable {
8
35
  readonly #lexicons: Map<string, LexiconDocument> = new Map()
@@ -10,6 +37,21 @@ export class LexiconIterableIndexer implements LexiconIndexer, AsyncDisposable {
10
37
  | AsyncIterator<LexiconDocument, void, unknown>
11
38
  | Iterator<LexiconDocument, void, unknown>
12
39
 
40
+ /**
41
+ * Creates a new {@link LexiconIterableIndexer} from an iterable source.
42
+ *
43
+ * @param source - An iterable or async iterable of Lexicon documents.
44
+ * The iterator is consumed lazily as documents are requested.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // Sync iterable (array, Set, Map.values(), etc.)
49
+ * const indexer = new LexiconIterableIndexer(lexiconDocuments)
50
+ *
51
+ * // Async iterable (async generator, ReadableStream, etc.)
52
+ * const indexer = new LexiconIterableIndexer(asyncLexiconStream)
53
+ * ```
54
+ */
13
55
  constructor(
14
56
  readonly source: AsyncIterable<LexiconDocument> | Iterable<LexiconDocument>,
15
57
  ) {
@@ -19,6 +61,14 @@ export class LexiconIterableIndexer implements LexiconIndexer, AsyncDisposable {
19
61
  : source[Symbol.iterator]()
20
62
  }
21
63
 
64
+ /**
65
+ * Retrieves a Lexicon document by its NSID.
66
+ *
67
+ * If the document has already been indexed, it is returned from cache.
68
+ * Otherwise, the source iterator is consumed until the document is found.
69
+ *
70
+ * @see {@link LexiconIndexer.get}
71
+ */
22
72
  async get(id: string): Promise<LexiconDocument> {
23
73
  const cached = this.#lexicons.get(id)
24
74
  if (cached) return cached
@@ -1,4 +1,11 @@
1
- import { l } from '@atproto/lex-schema'
1
+ import {
2
+ ArraySchema,
3
+ BooleanSchema,
4
+ IntegerSchema,
5
+ StringSchema,
6
+ l,
7
+ } from '@atproto/lex-schema'
8
+ import { LexValue } from '../../lex-data/dist/lex.js'
2
9
  import {
3
10
  LexiconArray,
4
11
  LexiconArrayItems,
@@ -13,23 +20,63 @@ import {
13
20
  import { LexiconIndexer } from './lexicon-indexer.js'
14
21
 
15
22
  /**
16
- * Builds a validator for a given lexicon "ref" from a lexicon indexer.
23
+ * Builds validators for Lexicon documents.
24
+ *
25
+ * This class converts Lexicon type definitions into runtime validators
26
+ * that can validate data against the schema. It handles reference resolution,
27
+ * supporting both local (`#defName`) and cross-document (`nsid#defName`) refs.
17
28
  *
18
29
  * @example
30
+ * ```ts
31
+ * import { LexiconSchemaBuilder, LexiconIterableIndexer } from '@atproto/lex-document'
32
+ *
33
+ * // Build a single validator
34
+ * const indexer = new LexiconIterableIndexer(lexiconDocs)
35
+ * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.post#main')
36
+ *
37
+ * // Validate data
38
+ * const result = validator.safeParse(myPostData)
39
+ * if (result.success) {
40
+ * console.log('Valid:', result.value)
41
+ * } else {
42
+ * console.log('Invalid:', result.error)
43
+ * }
44
+ * ```
19
45
  *
46
+ * @example
20
47
  * ```ts
21
- * import { LexiconSchemaBuilder } from '@atproto/lex/doc'
22
- * import { LexiconStreamIndexer } from '@atproto/lex/doc'
48
+ * // Build all validators from an iterable indexer
49
+ * const indexer = new LexiconIterableIndexer(lexiconDocs)
50
+ * const allSchemas = await LexiconSchemaBuilder.buildAll(indexer)
23
51
  *
24
- * const indexer = new LexiconStreamIndexer(lexiconDocs)
25
- * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.foo#bar')
52
+ * for (const [ref, schema] of allSchemas) {
53
+ * console.log(`Built validator for ${ref}`)
54
+ * }
26
55
  * ```
27
56
  */
28
57
  export class LexiconSchemaBuilder {
58
+ /**
59
+ * Builds a validator for a single Lexicon definition reference.
60
+ *
61
+ * @param indexer - The Lexicon indexer to resolve documents from
62
+ * @param fullRef - The full reference to build, in format "nsid#defName"
63
+ * @returns A promise resolving to a validator for the referenced definition
64
+ * @throws Error if the reference does not point to a schema type
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const validator = await LexiconSchemaBuilder.build(
69
+ * indexer,
70
+ * 'app.bsky.feed.post#main'
71
+ * )
72
+ *
73
+ * validator.parse(postRecord) // Throws if invalid
74
+ * ```
75
+ */
29
76
  static async build(
30
77
  indexer: LexiconIndexer,
31
78
  fullRef: string,
32
- ): Promise<l.Validator<unknown>> {
79
+ ): Promise<l.Schema<LexValue>> {
33
80
  const ctx = new LexiconSchemaBuilder(indexer)
34
81
  try {
35
82
  const result = await ctx.buildFullRef(fullRef)
@@ -42,11 +89,36 @@ export class LexiconSchemaBuilder {
42
89
  }
43
90
  }
44
91
 
92
+ /**
93
+ * Builds validators for all definitions in all documents from an iterable indexer.
94
+ *
95
+ * This method iterates over all Lexicon documents available in the indexer
96
+ * and builds validators for every definition in each document.
97
+ *
98
+ * @param indexer - An iterable Lexicon indexer (must implement `Symbol.asyncIterator`)
99
+ * @returns A promise resolving to a Map of full references to their validators.
100
+ * The map values can be validators, Query, Subscription, Procedure, or PermissionSet.
101
+ * @throws Error if the indexer does not support iteration
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const indexer = new LexiconIterableIndexer(allLexiconDocs)
106
+ * const schemas = await LexiconSchemaBuilder.buildAll(indexer)
107
+ *
108
+ * // Access a specific schema
109
+ * const postSchema = schemas.get('app.bsky.feed.post#main')
110
+ *
111
+ * // Iterate all schemas
112
+ * for (const [ref, schema] of schemas) {
113
+ * console.log(ref, schema)
114
+ * }
115
+ * ```
116
+ */
45
117
  static async buildAll(indexer: LexiconIndexer) {
46
118
  const builder = new LexiconSchemaBuilder(indexer)
47
119
  const schemas = new Map<
48
120
  string,
49
- | l.Validator<unknown>
121
+ | l.Schema<LexValue>
50
122
  | l.Query
51
123
  | l.Subscription
52
124
  | l.Procedure
@@ -71,12 +143,39 @@ export class LexiconSchemaBuilder {
71
143
 
72
144
  #asyncTasks = new AsyncTasks()
73
145
 
146
+ /**
147
+ * Creates a new LexiconSchemaBuilder instance.
148
+ *
149
+ * Note: For most use cases, prefer using the static `build()` or `buildAll()`
150
+ * methods instead of instantiating directly.
151
+ *
152
+ * @param indexer - The Lexicon indexer to resolve documents from
153
+ */
74
154
  constructor(protected indexer: LexiconIndexer) {}
75
155
 
156
+ /**
157
+ * Waits for all pending reference resolution tasks to complete.
158
+ *
159
+ * When building schemas with cross-references, the builder schedules
160
+ * async tasks to resolve those references. This method must be called
161
+ * to ensure all references are fully resolved before using the validators.
162
+ *
163
+ * @returns A promise that resolves when all pending tasks are complete
164
+ * @throws Rethrows any errors from failed reference resolution
165
+ */
76
166
  async done(): Promise<void> {
77
167
  await this.#asyncTasks.done()
78
168
  }
79
169
 
170
+ /**
171
+ * Builds a validator for a full reference (memoized).
172
+ *
173
+ * Results are cached, so calling with the same reference returns
174
+ * the same promise/result.
175
+ *
176
+ * @param fullRef - The full reference in format "nsid#defName"
177
+ * @returns A promise resolving to the built schema or method definition
178
+ */
80
179
  buildFullRef = memoize(async (fullRef: string) => {
81
180
  const { nsid, hash } = parseRef(fullRef)
82
181
 
@@ -85,20 +184,20 @@ export class LexiconSchemaBuilder {
85
184
  return this.compileDef(doc, hash)
86
185
  })
87
186
 
88
- protected buildRefGetter(fullRef: string): () => l.Validator<unknown> {
89
- let validator: l.Validator<unknown>
187
+ protected buildRefGetter(fullRef: string): () => l.Schema<LexValue> {
188
+ let schema: l.Schema<LexValue>
90
189
 
91
190
  this.#asyncTasks.add(
92
191
  this.buildFullRef(fullRef).then((v) => {
93
192
  if (!(v instanceof l.Schema)) {
94
193
  throw new Error(`Only refs to schema types are allowed`)
95
194
  }
96
- validator = v
195
+ schema = v
97
196
  }),
98
197
  )
99
198
 
100
199
  return () => {
101
- if (validator) return validator
200
+ if (schema) return schema
102
201
  throw new Error('Validator not yet built. Did you await done()?')
103
202
  }
104
203
  }
@@ -178,7 +277,7 @@ export class LexiconSchemaBuilder {
178
277
  protected compileLeaf(
179
278
  doc: LexiconDocument,
180
279
  def: LexiconArray | LexiconArrayItems,
181
- ): l.Validator<unknown> {
280
+ ): l.Schema<LexValue> {
182
281
  if (
183
282
  'const' in def &&
184
283
  'enum' in def &&
@@ -232,7 +331,7 @@ export class LexiconSchemaBuilder {
232
331
  case 'bytes':
233
332
  return l.bytes(def)
234
333
  case 'unknown':
235
- return l.unknown()
334
+ return l.unknownObject()
236
335
  case 'array':
237
336
  return l.array(this.compileLeaf(doc, def.items), def)
238
337
  default:
@@ -243,7 +342,7 @@ export class LexiconSchemaBuilder {
243
342
  protected compileRef(
244
343
  doc: LexiconDocument,
245
344
  def: LexiconRef | LexiconRefUnion,
246
- ) {
345
+ ): l.Schema<LexValue> {
247
346
  switch (def.type) {
248
347
  case 'ref':
249
348
  return l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)))
@@ -260,18 +359,18 @@ export class LexiconSchemaBuilder {
260
359
  }
261
360
  }
262
361
 
263
- protected compileObject(
264
- doc: LexiconDocument,
265
- def: LexiconObject,
266
- ): l.ObjectSchema {
267
- const props: Record<string, l.Validator> = {}
362
+ protected compileObject(doc: LexiconDocument, def: LexiconObject) {
363
+ const props: Record<string, l.Schema<undefined | LexValue>> = {}
268
364
  for (const [key, propDef] of Object.entries(def.properties)) {
269
365
  if (propDef === undefined) continue
270
366
 
271
367
  const isNullable = def.nullable?.includes(key)
272
368
  const isRequired = def.required?.includes(key)
273
369
 
274
- let schema = this.compileLeaf(doc, propDef)
370
+ let schema: l.Schema<undefined | LexValue> = this.compileLeaf(
371
+ doc,
372
+ propDef,
373
+ )
275
374
 
276
375
  if (isNullable) {
277
376
  schema = l.nullable(schema)
@@ -289,7 +388,7 @@ export class LexiconSchemaBuilder {
289
388
  protected compilePayload(
290
389
  doc: LexiconDocument,
291
390
  def: LexiconPayload | undefined,
292
- ): l.Payload {
391
+ ) {
293
392
  return l.payload(
294
393
  def?.encoding,
295
394
  def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined,
@@ -306,7 +405,7 @@ export class LexiconSchemaBuilder {
306
405
  protected compilePayloadSchema(
307
406
  doc: LexiconDocument,
308
407
  def: LexiconObject | LexiconRef | LexiconRefUnion,
309
- ) {
408
+ ): l.Schema<LexValue, LexValue> {
310
409
  switch (def.type) {
311
410
  case 'object':
312
411
  return this.compileObject(doc, def)
@@ -324,7 +423,11 @@ export class LexiconSchemaBuilder {
324
423
 
325
424
  const isRequired = def.required?.includes(paramName)
326
425
 
327
- const propSchema = this.compileLeaf(doc, paramDef) as l.Validator<l.Param>
426
+ const propSchema = this.compileLeaf(doc, paramDef) as
427
+ | StringSchema
428
+ | BooleanSchema
429
+ | IntegerSchema
430
+ | ArraySchema<StringSchema | BooleanSchema | IntegerSchema>
328
431
 
329
432
  shape[paramName] = isRequired ? propSchema : l.optional(propSchema)
330
433
  }