@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.
- package/CHANGELOG.md +24 -0
- package/dist/lexicon-document.d.ts +406 -8
- package/dist/lexicon-document.d.ts.map +1 -1
- package/dist/lexicon-document.js +206 -12
- package/dist/lexicon-document.js.map +1 -1
- package/dist/lexicon-indexer.d.ts +51 -0
- package/dist/lexicon-indexer.d.ts.map +1 -1
- package/dist/lexicon-indexer.js.map +1 -1
- package/dist/lexicon-iterable-indexer.d.ts +51 -1
- package/dist/lexicon-iterable-indexer.d.ts.map +1 -1
- package/dist/lexicon-iterable-indexer.js +51 -1
- package/dist/lexicon-iterable-indexer.js.map +1 -1
- package/dist/lexicon-schema-builder.d.ts +112 -25
- package/dist/lexicon-schema-builder.d.ts.map +1 -1
- package/dist/lexicon-schema-builder.js +102 -10
- package/dist/lexicon-schema-builder.js.map +1 -1
- package/package.json +4 -4
- package/src/lexicon-document.ts +432 -8
- package/src/lexicon-indexer.ts +52 -0
- package/src/lexicon-iterable-indexer.ts +51 -1
- package/src/lexicon-schema-builder.ts +123 -24
package/src/lexicon-indexer.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
-
let
|
|
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
|
-
|
|
189
|
+
schema = v
|
|
97
190
|
}),
|
|
98
191
|
)
|
|
99
192
|
|
|
100
193
|
return () => {
|
|
101
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
)
|
|
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.
|
|
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
|
|
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
|
}
|