@atproto/lex-document 0.0.0
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/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lexicon-document.d.ts +22099 -0
- package/dist/lexicon-document.d.ts.map +1 -0
- package/dist/lexicon-document.js +266 -0
- package/dist/lexicon-document.js.map +1 -0
- package/dist/lexicon-indexer.d.ts +7 -0
- package/dist/lexicon-indexer.d.ts.map +1 -0
- package/dist/lexicon-indexer.js +3 -0
- package/dist/lexicon-indexer.js.map +1 -0
- package/dist/lexicon-iterable-indexer.d.ts +14 -0
- package/dist/lexicon-iterable-indexer.d.ts.map +1 -0
- package/dist/lexicon-iterable-indexer.js +68 -0
- package/dist/lexicon-iterable-indexer.js.map +1 -0
- package/dist/lexicon-schema-builder.d.ts +89 -0
- package/dist/lexicon-schema-builder.d.ts.map +1 -0
- package/dist/lexicon-schema-builder.js +249 -0
- package/dist/lexicon-schema-builder.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +7 -0
- package/src/lexicon-document.test.ts +22 -0
- package/src/lexicon-document.ts +427 -0
- package/src/lexicon-indexer.ts +8 -0
- package/src/lexicon-iterable-indexer.ts +82 -0
- package/src/lexicon-schema-builder.test.ts +206 -0
- package/src/lexicon-schema-builder.ts +345 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { l } from '@atproto/lex-schema'
|
|
2
|
+
import {
|
|
3
|
+
LexiconArray,
|
|
4
|
+
LexiconArrayItems,
|
|
5
|
+
LexiconDocument,
|
|
6
|
+
LexiconError,
|
|
7
|
+
LexiconObject,
|
|
8
|
+
LexiconParameters,
|
|
9
|
+
LexiconPayload,
|
|
10
|
+
LexiconRef,
|
|
11
|
+
LexiconRefUnion,
|
|
12
|
+
} from './lexicon-document.js'
|
|
13
|
+
import { LexiconIndexer } from './lexicon-indexer.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a validator for a given lexicon "ref" from a lexicon indexer.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { LexiconSchemaBuilder } from '@atproto/lex/doc'
|
|
22
|
+
* import { LexiconStreamIndexer } from '@atproto/lex/doc'
|
|
23
|
+
*
|
|
24
|
+
* const indexer = new LexiconStreamIndexer(lexiconDocs)
|
|
25
|
+
* const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.foo#bar')
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class LexiconSchemaBuilder {
|
|
29
|
+
static async build(
|
|
30
|
+
indexer: LexiconIndexer,
|
|
31
|
+
fullRef: string,
|
|
32
|
+
): Promise<l.Validator<unknown>> {
|
|
33
|
+
const ctx = new LexiconSchemaBuilder(indexer)
|
|
34
|
+
try {
|
|
35
|
+
const result = await ctx.buildFullRef(fullRef)
|
|
36
|
+
if (!(result instanceof l.Validator)) {
|
|
37
|
+
throw new Error(`Ref ${fullRef} is not a validator schema type`)
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
} finally {
|
|
41
|
+
await ctx.done()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async buildAll(indexer: LexiconIndexer) {
|
|
46
|
+
const builder = new LexiconSchemaBuilder(indexer)
|
|
47
|
+
const schemas = new Map<
|
|
48
|
+
string,
|
|
49
|
+
| l.Validator<unknown>
|
|
50
|
+
| l.Query
|
|
51
|
+
| l.Subscription
|
|
52
|
+
| l.Procedure
|
|
53
|
+
| l.PermissionSet
|
|
54
|
+
>()
|
|
55
|
+
if (!isAsyncIterableObject(indexer)) {
|
|
56
|
+
throw new Error('An iterable indexer is required to build all schemas')
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
for await (const doc of indexer) {
|
|
60
|
+
for (const hash of Object.keys(doc.defs)) {
|
|
61
|
+
const fullRef = `${doc.id}#${hash}`
|
|
62
|
+
const schema = await builder.buildFullRef(fullRef)
|
|
63
|
+
schemas.set(fullRef, schema)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return schemas
|
|
67
|
+
} finally {
|
|
68
|
+
await builder.done()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#asyncTasks = new AsyncTasks()
|
|
73
|
+
|
|
74
|
+
constructor(protected indexer: LexiconIndexer) {}
|
|
75
|
+
|
|
76
|
+
async done(): Promise<void> {
|
|
77
|
+
await this.#asyncTasks.done()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
buildFullRef = memoize(async (fullRef: string) => {
|
|
81
|
+
const { nsid, hash } = parseRef(fullRef)
|
|
82
|
+
|
|
83
|
+
const doc = await this.indexer.get(nsid)
|
|
84
|
+
if (!doc) throw new Error(`No lexicon found for NSID: ${nsid}`)
|
|
85
|
+
|
|
86
|
+
return this.compileDef(doc, hash)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
protected buildRefGetter(fullRef: string): () => l.Validator<unknown> {
|
|
90
|
+
let validator: l.Validator<unknown>
|
|
91
|
+
|
|
92
|
+
this.#asyncTasks.add(
|
|
93
|
+
this.buildFullRef(fullRef).then((v) => {
|
|
94
|
+
if (!(v instanceof l.Validator)) {
|
|
95
|
+
throw new Error(`Only refs to validator schema types are allowed`)
|
|
96
|
+
}
|
|
97
|
+
validator = v
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
if (validator) return validator
|
|
103
|
+
throw new Error('Validator not yet built. Did you await done()?')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected buildTypedRefGetter(
|
|
108
|
+
fullRef: string,
|
|
109
|
+
): () => l.TypedObjectSchema | l.RecordSchema {
|
|
110
|
+
let validator: l.TypedObjectSchema | l.RecordSchema
|
|
111
|
+
|
|
112
|
+
this.#asyncTasks.add(
|
|
113
|
+
this.buildFullRef(fullRef).then((v) => {
|
|
114
|
+
if (v instanceof l.TypedObjectSchema || v instanceof l.RecordSchema) {
|
|
115
|
+
validator = v
|
|
116
|
+
} else {
|
|
117
|
+
throw new Error(
|
|
118
|
+
'Only refs to records and object definitions are allowed',
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
if (validator) return validator
|
|
126
|
+
throw new Error('Validator not yet built. Did you await done()?')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected compileDef(doc: LexiconDocument, hash: string) {
|
|
131
|
+
const def = Object.hasOwn(doc.defs, hash) ? doc.defs[hash] : null
|
|
132
|
+
if (!def) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`No definition found for hash "${JSON.stringify(hash)}" in ${doc.id}`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
switch (def.type) {
|
|
138
|
+
case 'permission-set':
|
|
139
|
+
return l.permissionSet(
|
|
140
|
+
doc.id,
|
|
141
|
+
def.permissions.map(({ resource, type, ...p }) =>
|
|
142
|
+
l.permission(resource, p),
|
|
143
|
+
),
|
|
144
|
+
def,
|
|
145
|
+
)
|
|
146
|
+
case 'procedure':
|
|
147
|
+
return l.procedure(
|
|
148
|
+
doc.id,
|
|
149
|
+
this.compileParams(doc, def.parameters),
|
|
150
|
+
this.compilePayload(doc, def.input),
|
|
151
|
+
this.compilePayload(doc, def.output),
|
|
152
|
+
this.compileErrors(doc, def.errors),
|
|
153
|
+
)
|
|
154
|
+
case 'query':
|
|
155
|
+
return l.query(
|
|
156
|
+
doc.id,
|
|
157
|
+
this.compileParams(doc, def.parameters),
|
|
158
|
+
this.compilePayload(doc, def.output),
|
|
159
|
+
this.compileErrors(doc, def.errors),
|
|
160
|
+
)
|
|
161
|
+
case 'subscription':
|
|
162
|
+
return l.subscription(
|
|
163
|
+
doc.id,
|
|
164
|
+
this.compileParams(doc, def.parameters),
|
|
165
|
+
this.compilePayloadSchema(doc, def.message?.schema),
|
|
166
|
+
this.compileErrors(doc, def.errors),
|
|
167
|
+
)
|
|
168
|
+
case 'token':
|
|
169
|
+
return l.token(doc.id, hash)
|
|
170
|
+
case 'record':
|
|
171
|
+
return l.record(
|
|
172
|
+
def.key ? l.asRecordKey(def.key) : 'any',
|
|
173
|
+
doc.id,
|
|
174
|
+
this.compileObject(doc, def.record),
|
|
175
|
+
)
|
|
176
|
+
case 'object':
|
|
177
|
+
return l.typedObject(doc.id, hash, this.compileObject(doc, def))
|
|
178
|
+
default:
|
|
179
|
+
return this.compileLeaf(doc, def)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected compileLeaf(
|
|
184
|
+
doc: LexiconDocument,
|
|
185
|
+
def: LexiconArray | LexiconArrayItems,
|
|
186
|
+
): l.Validator<unknown> {
|
|
187
|
+
switch (def.type) {
|
|
188
|
+
case 'string':
|
|
189
|
+
return l.string(def)
|
|
190
|
+
case 'integer':
|
|
191
|
+
return l.integer(def)
|
|
192
|
+
case 'boolean':
|
|
193
|
+
return l.boolean(def)
|
|
194
|
+
case 'blob':
|
|
195
|
+
return l.blob(def)
|
|
196
|
+
case 'cid-link':
|
|
197
|
+
return l.cidLink()
|
|
198
|
+
case 'bytes':
|
|
199
|
+
return l.bytes(def)
|
|
200
|
+
case 'unknown':
|
|
201
|
+
return l.unknown()
|
|
202
|
+
case 'array':
|
|
203
|
+
return l.array(this.compileLeaf(doc, def.items), def)
|
|
204
|
+
default:
|
|
205
|
+
return this.compileRef(doc, def)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected compileRef(
|
|
210
|
+
doc: LexiconDocument,
|
|
211
|
+
def: LexiconRef | LexiconRefUnion,
|
|
212
|
+
) {
|
|
213
|
+
switch (def.type) {
|
|
214
|
+
case 'ref':
|
|
215
|
+
return l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)))
|
|
216
|
+
case 'union':
|
|
217
|
+
return l.typedUnion(
|
|
218
|
+
def.refs.map((r) =>
|
|
219
|
+
l.typedRef(this.buildTypedRefGetter(buildFullRef(doc, r))),
|
|
220
|
+
),
|
|
221
|
+
def.closed ?? false,
|
|
222
|
+
)
|
|
223
|
+
default:
|
|
224
|
+
// @ts-expect-error
|
|
225
|
+
throw new Error(`Unknown lexicon type: ${def.type}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected compileObject(
|
|
230
|
+
doc: LexiconDocument,
|
|
231
|
+
def: LexiconObject,
|
|
232
|
+
): l.ObjectSchema {
|
|
233
|
+
const props: Record<string, l.Validator> = {}
|
|
234
|
+
for (const [key, propDef] of Object.entries(def.properties)) {
|
|
235
|
+
if (propDef === undefined) continue
|
|
236
|
+
props[key] = this.compileLeaf(doc, propDef)
|
|
237
|
+
}
|
|
238
|
+
return l.object(props, def)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected compilePayload(
|
|
242
|
+
doc: LexiconDocument,
|
|
243
|
+
def: LexiconPayload | undefined,
|
|
244
|
+
): l.Payload {
|
|
245
|
+
return l.payload(
|
|
246
|
+
def?.encoding,
|
|
247
|
+
def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected compileErrors(
|
|
252
|
+
_doc: LexiconDocument,
|
|
253
|
+
errors?: readonly LexiconError[],
|
|
254
|
+
): undefined | string[] {
|
|
255
|
+
return errors?.map((e) => e.name)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected compilePayloadSchema(
|
|
259
|
+
doc: LexiconDocument,
|
|
260
|
+
def?: LexiconObject | LexiconRef | LexiconRefUnion,
|
|
261
|
+
) {
|
|
262
|
+
if (!def) return undefined
|
|
263
|
+
switch (def.type) {
|
|
264
|
+
case 'object':
|
|
265
|
+
return this.compileObject(doc, def)
|
|
266
|
+
default:
|
|
267
|
+
return this.compileRef(doc, def)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected compileParams(doc: LexiconDocument, def?: LexiconParameters) {
|
|
272
|
+
if (!def) return l.params()
|
|
273
|
+
|
|
274
|
+
const props: Record<string, l.Validator> = {}
|
|
275
|
+
for (const [key, propDef] of Object.entries(def.properties)) {
|
|
276
|
+
if (propDef === undefined) continue
|
|
277
|
+
props[key] = this.compileLeaf(doc, propDef)
|
|
278
|
+
}
|
|
279
|
+
return l.params(props, def)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class AsyncTasks {
|
|
284
|
+
/**
|
|
285
|
+
* A set that, eventually, contains only rejected promises.
|
|
286
|
+
*/
|
|
287
|
+
#promises = new Set<Promise<void>>()
|
|
288
|
+
|
|
289
|
+
async done(): Promise<void> {
|
|
290
|
+
do {
|
|
291
|
+
// @NOTE this is going to throw on the first rejected promise (which is
|
|
292
|
+
// what we want)
|
|
293
|
+
for (const p of this.#promises) await p
|
|
294
|
+
// At this point, all settled promises should have been removed. If
|
|
295
|
+
// this.#promises is not empty, it means new promises were added during
|
|
296
|
+
// the awaiting process, so we loop again.
|
|
297
|
+
} while (this.#promises.size > 0)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
add(p: Promise<void>) {
|
|
301
|
+
const promise = Promise.resolve(p).then(() => {
|
|
302
|
+
// No need to keep the promise any longer
|
|
303
|
+
this.#promises.delete(promise)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
void promise.catch((_err) => {
|
|
307
|
+
// ignore errors here, they should be caught though done()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
this.#promises.add(promise)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseRef(fullRef: string) {
|
|
315
|
+
const { length, 0: nsid, 1: hash } = fullRef.split('#')
|
|
316
|
+
if (length !== 2) throw new Error('Uri can only have one hash segment')
|
|
317
|
+
if (!nsid || !hash) throw new Error('Invalid ref, missing hash')
|
|
318
|
+
return { nsid, hash }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildFullRef(from: LexiconDocument, ref: string) {
|
|
322
|
+
if (ref.startsWith('#')) return `${from.id}${ref}`
|
|
323
|
+
return ref
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function memoize<Fn extends (arg: string) => unknown>(fn: Fn): Fn {
|
|
327
|
+
const cache = new Map<string, ReturnType<Fn>>()
|
|
328
|
+
return ((arg: string) => {
|
|
329
|
+
if (cache.has(arg)) return cache.get(arg)!
|
|
330
|
+
const result = fn(arg) as ReturnType<Fn>
|
|
331
|
+
cache.set(arg, result)
|
|
332
|
+
return result
|
|
333
|
+
}) as Fn
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isAsyncIterableObject<T>(
|
|
337
|
+
obj: T,
|
|
338
|
+
): obj is T & object & AsyncIterable<unknown> {
|
|
339
|
+
return (
|
|
340
|
+
obj != null &&
|
|
341
|
+
typeof obj === 'object' &&
|
|
342
|
+
Symbol.asyncIterator in obj &&
|
|
343
|
+
typeof obj[Symbol.asyncIterator] === 'function'
|
|
344
|
+
)
|
|
345
|
+
}
|