@atproto/lex-document 0.0.12 → 0.0.14

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,3 +1,4 @@
1
+ import { LexValue } from '@atproto/lex-data'
1
2
  import { l } from '@atproto/lex-schema'
2
3
  import {
3
4
  LexiconArray,
@@ -13,23 +14,63 @@ import {
13
14
  import { LexiconIndexer } from './lexicon-indexer.js'
14
15
 
15
16
  /**
16
- * Builds a validator for a given lexicon "ref" from a lexicon indexer.
17
+ * Builds validators for Lexicon documents.
18
+ *
19
+ * This class converts Lexicon type definitions into runtime validators
20
+ * that can validate data against the schema. It handles reference resolution,
21
+ * supporting both local (`#defName`) and cross-document (`nsid#defName`) refs.
17
22
  *
18
23
  * @example
24
+ * ```ts
25
+ * import { LexiconSchemaBuilder, LexiconIterableIndexer } from '@atproto/lex-document'
26
+ *
27
+ * // Build a single validator
28
+ * const indexer = new LexiconIterableIndexer(lexiconDocs)
29
+ * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.post#main')
19
30
  *
31
+ * // Validate data
32
+ * const result = validator.safeParse(myPostData)
33
+ * if (result.success) {
34
+ * console.log('Valid:', result.value)
35
+ * } else {
36
+ * console.log('Invalid:', result.error)
37
+ * }
38
+ * ```
39
+ *
40
+ * @example
20
41
  * ```ts
21
- * import { LexiconSchemaBuilder } from '@atproto/lex/doc'
22
- * import { LexiconStreamIndexer } from '@atproto/lex/doc'
42
+ * // Build all validators from an iterable indexer
43
+ * const indexer = new LexiconIterableIndexer(lexiconDocs)
44
+ * const allSchemas = await LexiconSchemaBuilder.buildAll(indexer)
23
45
  *
24
- * const indexer = new LexiconStreamIndexer(lexiconDocs)
25
- * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.foo#bar')
46
+ * for (const [ref, schema] of allSchemas) {
47
+ * console.log(`Built validator for ${ref}`)
48
+ * }
26
49
  * ```
27
50
  */
28
51
  export class LexiconSchemaBuilder {
52
+ /**
53
+ * Builds a validator for a single Lexicon definition reference.
54
+ *
55
+ * @param indexer - The Lexicon indexer to resolve documents from
56
+ * @param fullRef - The full reference to build, in format "nsid#defName"
57
+ * @returns A promise resolving to a validator for the referenced definition
58
+ * @throws Error if the reference does not point to a schema type
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const validator = await LexiconSchemaBuilder.build(
63
+ * indexer,
64
+ * 'app.bsky.feed.post#main'
65
+ * )
66
+ *
67
+ * validator.parse(postRecord) // Throws if invalid
68
+ * ```
69
+ */
29
70
  static async build(
30
71
  indexer: LexiconIndexer,
31
72
  fullRef: string,
32
- ): Promise<l.Validator<unknown>> {
73
+ ): Promise<l.Schema<LexValue>> {
33
74
  const ctx = new LexiconSchemaBuilder(indexer)
34
75
  try {
35
76
  const result = await ctx.buildFullRef(fullRef)
@@ -42,11 +83,36 @@ export class LexiconSchemaBuilder {
42
83
  }
43
84
  }
44
85
 
86
+ /**
87
+ * Builds validators for all definitions in all documents from an iterable indexer.
88
+ *
89
+ * This method iterates over all Lexicon documents available in the indexer
90
+ * and builds validators for every definition in each document.
91
+ *
92
+ * @param indexer - An iterable Lexicon indexer (must implement `Symbol.asyncIterator`)
93
+ * @returns A promise resolving to a Map of full references to their validators.
94
+ * The map values can be validators, Query, Subscription, Procedure, or PermissionSet.
95
+ * @throws Error if the indexer does not support iteration
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const indexer = new LexiconIterableIndexer(allLexiconDocs)
100
+ * const schemas = await LexiconSchemaBuilder.buildAll(indexer)
101
+ *
102
+ * // Access a specific schema
103
+ * const postSchema = schemas.get('app.bsky.feed.post#main')
104
+ *
105
+ * // Iterate all schemas
106
+ * for (const [ref, schema] of schemas) {
107
+ * console.log(ref, schema)
108
+ * }
109
+ * ```
110
+ */
45
111
  static async buildAll(indexer: LexiconIndexer) {
46
112
  const builder = new LexiconSchemaBuilder(indexer)
47
113
  const schemas = new Map<
48
114
  string,
49
- | l.Validator<unknown>
115
+ | l.Schema<LexValue>
50
116
  | l.Query
51
117
  | l.Subscription
52
118
  | l.Procedure
@@ -71,12 +137,39 @@ export class LexiconSchemaBuilder {
71
137
 
72
138
  #asyncTasks = new AsyncTasks()
73
139
 
140
+ /**
141
+ * Creates a new LexiconSchemaBuilder instance.
142
+ *
143
+ * Note: For most use cases, prefer using the static `build()` or `buildAll()`
144
+ * methods instead of instantiating directly.
145
+ *
146
+ * @param indexer - The Lexicon indexer to resolve documents from
147
+ */
74
148
  constructor(protected indexer: LexiconIndexer) {}
75
149
 
150
+ /**
151
+ * Waits for all pending reference resolution tasks to complete.
152
+ *
153
+ * When building schemas with cross-references, the builder schedules
154
+ * async tasks to resolve those references. This method must be called
155
+ * to ensure all references are fully resolved before using the validators.
156
+ *
157
+ * @returns A promise that resolves when all pending tasks are complete
158
+ * @throws Rethrows any errors from failed reference resolution
159
+ */
76
160
  async done(): Promise<void> {
77
161
  await this.#asyncTasks.done()
78
162
  }
79
163
 
164
+ /**
165
+ * Builds a validator for a full reference (memoized).
166
+ *
167
+ * Results are cached, so calling with the same reference returns
168
+ * the same promise/result.
169
+ *
170
+ * @param fullRef - The full reference in format "nsid#defName"
171
+ * @returns A promise resolving to the built schema or method definition
172
+ */
80
173
  buildFullRef = memoize(async (fullRef: string) => {
81
174
  const { nsid, hash } = parseRef(fullRef)
82
175
 
@@ -85,20 +178,20 @@ export class LexiconSchemaBuilder {
85
178
  return this.compileDef(doc, hash)
86
179
  })
87
180
 
88
- protected buildRefGetter(fullRef: string): () => l.Validator<unknown> {
89
- let validator: l.Validator<unknown>
181
+ protected buildRefGetter(fullRef: string): () => l.Schema<LexValue> {
182
+ let schema: l.Schema<LexValue>
90
183
 
91
184
  this.#asyncTasks.add(
92
185
  this.buildFullRef(fullRef).then((v) => {
93
186
  if (!(v instanceof l.Schema)) {
94
187
  throw new Error(`Only refs to schema types are allowed`)
95
188
  }
96
- validator = v
189
+ schema = v
97
190
  }),
98
191
  )
99
192
 
100
193
  return () => {
101
- if (validator) return validator
194
+ if (schema) return schema
102
195
  throw new Error('Validator not yet built. Did you await done()?')
103
196
  }
104
197
  }
@@ -178,7 +271,7 @@ export class LexiconSchemaBuilder {
178
271
  protected compileLeaf(
179
272
  doc: LexiconDocument,
180
273
  def: LexiconArray | LexiconArrayItems,
181
- ): l.Validator<unknown> {
274
+ ): l.Schema<LexValue> {
182
275
  if (
183
276
  'const' in def &&
184
277
  'enum' in def &&
@@ -232,7 +325,7 @@ export class LexiconSchemaBuilder {
232
325
  case 'bytes':
233
326
  return l.bytes(def)
234
327
  case 'unknown':
235
- return l.unknown()
328
+ return l.lexMap()
236
329
  case 'array':
237
330
  return l.array(this.compileLeaf(doc, def.items), def)
238
331
  default:
@@ -243,7 +336,7 @@ export class LexiconSchemaBuilder {
243
336
  protected compileRef(
244
337
  doc: LexiconDocument,
245
338
  def: LexiconRef | LexiconRefUnion,
246
- ) {
339
+ ): l.Schema<LexValue> {
247
340
  switch (def.type) {
248
341
  case 'ref':
249
342
  return l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)))
@@ -260,18 +353,18 @@ export class LexiconSchemaBuilder {
260
353
  }
261
354
  }
262
355
 
263
- protected compileObject(
264
- doc: LexiconDocument,
265
- def: LexiconObject,
266
- ): l.ObjectSchema {
267
- const props: Record<string, l.Validator> = {}
356
+ protected compileObject(doc: LexiconDocument, def: LexiconObject) {
357
+ const props: Record<string, l.Schema<undefined | LexValue>> = {}
268
358
  for (const [key, propDef] of Object.entries(def.properties)) {
269
359
  if (propDef === undefined) continue
270
360
 
271
361
  const isNullable = def.nullable?.includes(key)
272
362
  const isRequired = def.required?.includes(key)
273
363
 
274
- let schema = this.compileLeaf(doc, propDef)
364
+ let schema: l.Schema<undefined | LexValue> = this.compileLeaf(
365
+ doc,
366
+ propDef,
367
+ )
275
368
 
276
369
  if (isNullable) {
277
370
  schema = l.nullable(schema)
@@ -289,7 +382,7 @@ export class LexiconSchemaBuilder {
289
382
  protected compilePayload(
290
383
  doc: LexiconDocument,
291
384
  def: LexiconPayload | undefined,
292
- ): l.Payload {
385
+ ) {
293
386
  return l.payload(
294
387
  def?.encoding,
295
388
  def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined,
@@ -306,7 +399,7 @@ export class LexiconSchemaBuilder {
306
399
  protected compilePayloadSchema(
307
400
  doc: LexiconDocument,
308
401
  def: LexiconObject | LexiconRef | LexiconRefUnion,
309
- ) {
402
+ ): l.Schema<LexValue, LexValue> {
310
403
  switch (def.type) {
311
404
  case 'object':
312
405
  return this.compileObject(doc, def)
@@ -318,13 +411,19 @@ export class LexiconSchemaBuilder {
318
411
  protected compileParams(doc: LexiconDocument, def?: LexiconParameters) {
319
412
  if (!def) return l.params()
320
413
 
321
- const shape: l.ParamsSchemaShape = {}
414
+ const shape: l.ParamsShape = {}
322
415
  for (const [paramName, paramDef] of Object.entries(def.properties)) {
323
416
  if (paramDef === undefined) continue
324
417
 
325
418
  const isRequired = def.required?.includes(paramName)
326
419
 
327
- const propSchema = this.compileLeaf(doc, paramDef) as l.Validator<l.Param>
420
+ const propSchema = this.compileLeaf(doc, paramDef) as
421
+ | l.StringSchema
422
+ | l.BooleanSchema
423
+ | l.IntegerSchema
424
+ | l.ArraySchema<l.StringSchema>
425
+ | l.ArraySchema<l.BooleanSchema>
426
+ | l.ArraySchema<l.IntegerSchema>
328
427
 
329
428
  shape[paramName] = isRequired ? propSchema : l.optional(propSchema)
330
429
  }