@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.
- package/CHANGELOG.md +15 -0
- package/dist/lexicon-document.d.ts +402 -4
- package/dist/lexicon-document.d.ts.map +1 -1
- package/dist/lexicon-document.js +202 -11
- 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 +110 -23
- 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 +427 -7
- package/src/lexicon-indexer.ts +52 -0
- package/src/lexicon-iterable-indexer.ts +51 -1
- package/src/lexicon-schema-builder.ts +127 -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,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
-
let
|
|
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
|
-
|
|
195
|
+
schema = v
|
|
97
196
|
}),
|
|
98
197
|
)
|
|
99
198
|
|
|
100
199
|
return () => {
|
|
101
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
)
|
|
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
|
|
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
|
}
|