@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,427 @@
|
|
|
1
|
+
import { l } from '@atproto/lex-schema'
|
|
2
|
+
|
|
3
|
+
// https://atproto.com/specs/lexicon
|
|
4
|
+
|
|
5
|
+
// "Concrete" Types
|
|
6
|
+
|
|
7
|
+
export const lexiconBooleanSchema = l.object(
|
|
8
|
+
{
|
|
9
|
+
type: l.literal('boolean'),
|
|
10
|
+
description: l.string(),
|
|
11
|
+
default: l.boolean(),
|
|
12
|
+
const: l.boolean(),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
required: ['type'],
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
export type LexiconBoolean = l.Infer<typeof lexiconBooleanSchema>
|
|
19
|
+
|
|
20
|
+
export const lexiconIntegerSchema = l.object(
|
|
21
|
+
{
|
|
22
|
+
type: l.literal('integer'),
|
|
23
|
+
description: l.string(),
|
|
24
|
+
default: l.integer(),
|
|
25
|
+
minimum: l.integer(),
|
|
26
|
+
maximum: l.integer(),
|
|
27
|
+
enum: l.array(l.integer()),
|
|
28
|
+
const: l.integer(),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
required: ['type'],
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
export type LexiconInteger = l.Infer<typeof lexiconIntegerSchema>
|
|
35
|
+
|
|
36
|
+
export type LexiconStringFormat = l.StringFormat
|
|
37
|
+
export const lexiconStringFormatSchema = l.enum<LexiconStringFormat>(
|
|
38
|
+
l.STRING_FORMATS,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const lexiconStringSchema = l.object(
|
|
42
|
+
{
|
|
43
|
+
type: l.literal('string'),
|
|
44
|
+
format: lexiconStringFormatSchema,
|
|
45
|
+
description: l.string(),
|
|
46
|
+
default: l.string(),
|
|
47
|
+
minLength: l.integer(),
|
|
48
|
+
maxLength: l.integer(),
|
|
49
|
+
minGraphemes: l.integer(),
|
|
50
|
+
maxGraphemes: l.integer(),
|
|
51
|
+
enum: l.array(l.string()),
|
|
52
|
+
const: l.string(),
|
|
53
|
+
knownValues: l.array(l.string()),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
required: ['type'],
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
export type LexiconString = l.Infer<typeof lexiconStringSchema>
|
|
60
|
+
|
|
61
|
+
export const lexiconBytesSchema = l.object(
|
|
62
|
+
{
|
|
63
|
+
type: l.literal('bytes'),
|
|
64
|
+
description: l.string(),
|
|
65
|
+
maxLength: l.integer(),
|
|
66
|
+
minLength: l.integer(),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
required: ['type'],
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
export type LexiconBytes = l.Infer<typeof lexiconBytesSchema>
|
|
73
|
+
|
|
74
|
+
export const lexiconCidLinkSchema = l.object(
|
|
75
|
+
{
|
|
76
|
+
type: l.literal('cid-link'),
|
|
77
|
+
description: l.string(),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
required: ['type'],
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
export type LexiconCid = l.Infer<typeof lexiconCidLinkSchema>
|
|
84
|
+
|
|
85
|
+
export const lexiconBlobSchema = l.object(
|
|
86
|
+
{
|
|
87
|
+
type: l.literal('blob'),
|
|
88
|
+
description: l.string(),
|
|
89
|
+
accept: l.array(l.string()),
|
|
90
|
+
maxSize: l.integer(),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
required: ['type'],
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
export type LexiconBlob = l.Infer<typeof lexiconBlobSchema>
|
|
97
|
+
|
|
98
|
+
const CONCRETE_TYPES = [
|
|
99
|
+
lexiconBooleanSchema,
|
|
100
|
+
lexiconIntegerSchema,
|
|
101
|
+
lexiconStringSchema,
|
|
102
|
+
// Lexicon (DAG-CBOR)
|
|
103
|
+
lexiconBytesSchema,
|
|
104
|
+
lexiconCidLinkSchema,
|
|
105
|
+
// Lexicon Specific
|
|
106
|
+
lexiconBlobSchema,
|
|
107
|
+
] as const
|
|
108
|
+
|
|
109
|
+
// Meta types
|
|
110
|
+
|
|
111
|
+
export const lexiconUnknownSchema = l.object(
|
|
112
|
+
{
|
|
113
|
+
type: l.literal('unknown'),
|
|
114
|
+
description: l.string(),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
required: ['type'],
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
export type LexiconUnknown = l.Infer<typeof lexiconUnknownSchema>
|
|
121
|
+
|
|
122
|
+
export const lexiconTokenSchema = l.object(
|
|
123
|
+
{
|
|
124
|
+
type: l.literal('token'),
|
|
125
|
+
description: l.string(),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
required: ['type'],
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
export type LexiconToken = l.Infer<typeof lexiconTokenSchema>
|
|
132
|
+
|
|
133
|
+
export const lexiconRefSchema = l.object(
|
|
134
|
+
{
|
|
135
|
+
type: l.literal('ref'),
|
|
136
|
+
description: l.string(),
|
|
137
|
+
ref: l.string(),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
required: ['type', 'ref'],
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
export type LexiconRef = l.Infer<typeof lexiconRefSchema>
|
|
144
|
+
|
|
145
|
+
export const lexiconRefUnionSchema = l.object(
|
|
146
|
+
{
|
|
147
|
+
type: l.literal('union'),
|
|
148
|
+
description: l.string(),
|
|
149
|
+
refs: l.array(l.string()),
|
|
150
|
+
closed: l.boolean(),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
required: ['type', 'refs'],
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
export type LexiconRefUnion = l.Infer<typeof lexiconRefUnionSchema>
|
|
157
|
+
|
|
158
|
+
// Complex Types
|
|
159
|
+
|
|
160
|
+
const ARRAY_ITEMS_SCHEMAS = [
|
|
161
|
+
...CONCRETE_TYPES,
|
|
162
|
+
// Meta
|
|
163
|
+
lexiconUnknownSchema,
|
|
164
|
+
lexiconRefSchema,
|
|
165
|
+
lexiconRefUnionSchema,
|
|
166
|
+
] as const
|
|
167
|
+
|
|
168
|
+
export type LexiconArrayItems = l.Infer<(typeof ARRAY_ITEMS_SCHEMAS)[number]>
|
|
169
|
+
|
|
170
|
+
export const lexiconArraySchema = l.object(
|
|
171
|
+
{
|
|
172
|
+
type: l.literal('array'),
|
|
173
|
+
description: l.string(),
|
|
174
|
+
items: l.discriminatedUnion('type', ARRAY_ITEMS_SCHEMAS),
|
|
175
|
+
minLength: l.integer(),
|
|
176
|
+
maxLength: l.integer(),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
required: ['type', 'items'],
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
export type LexiconArray = l.Infer<typeof lexiconArraySchema>
|
|
183
|
+
|
|
184
|
+
export const lexiconObjectSchema = l.object(
|
|
185
|
+
{
|
|
186
|
+
type: l.literal('object'),
|
|
187
|
+
description: l.string(),
|
|
188
|
+
required: l.array(l.string()),
|
|
189
|
+
nullable: l.array(l.string()),
|
|
190
|
+
properties: l.dict(
|
|
191
|
+
l.string(),
|
|
192
|
+
l.discriminatedUnion('type', [
|
|
193
|
+
...ARRAY_ITEMS_SCHEMAS,
|
|
194
|
+
lexiconArraySchema,
|
|
195
|
+
]),
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
required: ['type', 'properties'],
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
export type LexiconObject = l.Infer<typeof lexiconObjectSchema>
|
|
203
|
+
|
|
204
|
+
// Records
|
|
205
|
+
|
|
206
|
+
export const lexiconRecordSchema = l.object(
|
|
207
|
+
{
|
|
208
|
+
type: l.literal('record'),
|
|
209
|
+
description: l.string(),
|
|
210
|
+
key: l.string(),
|
|
211
|
+
record: lexiconObjectSchema,
|
|
212
|
+
},
|
|
213
|
+
{ required: ['type', 'record'] },
|
|
214
|
+
)
|
|
215
|
+
export type LexiconRecord = l.Infer<typeof lexiconRecordSchema>
|
|
216
|
+
|
|
217
|
+
// XRPC Methods
|
|
218
|
+
|
|
219
|
+
export const lexiconParameters = l.object(
|
|
220
|
+
{
|
|
221
|
+
type: l.literal('params'),
|
|
222
|
+
description: l.string(),
|
|
223
|
+
required: l.array(l.string()),
|
|
224
|
+
properties: l.dict(
|
|
225
|
+
l.string(),
|
|
226
|
+
l.discriminatedUnion('type', [
|
|
227
|
+
lexiconBooleanSchema,
|
|
228
|
+
lexiconIntegerSchema,
|
|
229
|
+
lexiconStringSchema,
|
|
230
|
+
l.object(
|
|
231
|
+
{
|
|
232
|
+
type: l.literal('array'),
|
|
233
|
+
description: l.string(),
|
|
234
|
+
items: l.discriminatedUnion('type', [
|
|
235
|
+
lexiconBooleanSchema,
|
|
236
|
+
lexiconIntegerSchema,
|
|
237
|
+
lexiconStringSchema,
|
|
238
|
+
]),
|
|
239
|
+
minLength: l.integer(),
|
|
240
|
+
maxLength: l.integer(),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
required: ['type', 'items'],
|
|
244
|
+
},
|
|
245
|
+
),
|
|
246
|
+
]),
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
required: ['type', 'properties'],
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
export type LexiconParameters = l.Infer<typeof lexiconParameters>
|
|
254
|
+
|
|
255
|
+
export const lexiconPayload = l.object(
|
|
256
|
+
{
|
|
257
|
+
description: l.string(),
|
|
258
|
+
encoding: l.string(),
|
|
259
|
+
schema: l.discriminatedUnion('type', [
|
|
260
|
+
lexiconRefSchema,
|
|
261
|
+
lexiconRefUnionSchema,
|
|
262
|
+
lexiconObjectSchema,
|
|
263
|
+
]),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
required: ['encoding'],
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
export type LexiconPayload = l.Infer<typeof lexiconPayload>
|
|
270
|
+
|
|
271
|
+
export const lexiconSubscriptionMessage = l.object({
|
|
272
|
+
description: l.string(),
|
|
273
|
+
schema: l.discriminatedUnion('type', [
|
|
274
|
+
lexiconRefSchema,
|
|
275
|
+
lexiconRefUnionSchema,
|
|
276
|
+
lexiconObjectSchema,
|
|
277
|
+
]),
|
|
278
|
+
})
|
|
279
|
+
export type LexiconSubscriptionMessage = l.Infer<
|
|
280
|
+
typeof lexiconSubscriptionMessage
|
|
281
|
+
>
|
|
282
|
+
|
|
283
|
+
export const lexiconError = l.object(
|
|
284
|
+
{
|
|
285
|
+
name: l.string({ minLength: 1 }),
|
|
286
|
+
description: l.string(),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
required: ['name'],
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
export type LexiconError = l.Infer<typeof lexiconError>
|
|
293
|
+
|
|
294
|
+
export const lexiconQuerySchema = l.object(
|
|
295
|
+
{
|
|
296
|
+
type: l.literal('query'),
|
|
297
|
+
description: l.string(),
|
|
298
|
+
parameters: lexiconParameters,
|
|
299
|
+
output: lexiconPayload,
|
|
300
|
+
errors: l.array(lexiconError),
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
required: ['type'],
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
export type LexiconQuery = l.Infer<typeof lexiconQuerySchema>
|
|
307
|
+
|
|
308
|
+
export const lexiconProcedureSchema = l.object(
|
|
309
|
+
{
|
|
310
|
+
type: l.literal('procedure'),
|
|
311
|
+
description: l.string(),
|
|
312
|
+
parameters: lexiconParameters,
|
|
313
|
+
input: lexiconPayload,
|
|
314
|
+
output: lexiconPayload,
|
|
315
|
+
errors: l.array(lexiconError),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
required: ['type'],
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
export type LexiconProcedure = l.Infer<typeof lexiconProcedureSchema>
|
|
322
|
+
|
|
323
|
+
export const lexiconSubscriptionSchema = l.object(
|
|
324
|
+
{
|
|
325
|
+
type: l.literal('subscription'),
|
|
326
|
+
description: l.string(),
|
|
327
|
+
parameters: lexiconParameters,
|
|
328
|
+
message: lexiconSubscriptionMessage,
|
|
329
|
+
errors: l.array(lexiconError),
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
required: ['type'],
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
export type LexiconSubscription = l.Infer<typeof lexiconSubscriptionSchema>
|
|
337
|
+
|
|
338
|
+
// Permissions
|
|
339
|
+
|
|
340
|
+
const lexiconLanguageSchema = l.string({ format: 'language' })
|
|
341
|
+
|
|
342
|
+
export type LexiconLanguage = l.Infer<typeof lexiconLanguageSchema>
|
|
343
|
+
|
|
344
|
+
const lexiconLanguageDict = l.dict(lexiconLanguageSchema, l.string())
|
|
345
|
+
|
|
346
|
+
export type LexiconLanguageDict = l.Infer<typeof lexiconLanguageDict>
|
|
347
|
+
|
|
348
|
+
const lexiconPermissionSchema = l.object(
|
|
349
|
+
{
|
|
350
|
+
type: l.literal('permission'),
|
|
351
|
+
resource: l.string({ minLength: 1 }),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
required: ['type', 'resource'],
|
|
355
|
+
unknownProperties: l.paramsSchema,
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
export type LexiconPermission = l.Infer<typeof lexiconPermissionSchema>
|
|
360
|
+
|
|
361
|
+
const lexiconPermissionSetSchema = l.object(
|
|
362
|
+
{
|
|
363
|
+
type: l.literal('permission-set'),
|
|
364
|
+
description: l.string(),
|
|
365
|
+
title: l.string(),
|
|
366
|
+
'title:lang': lexiconLanguageDict,
|
|
367
|
+
detail: l.string(),
|
|
368
|
+
'detail:lang': lexiconLanguageDict,
|
|
369
|
+
permissions: l.array(lexiconPermissionSchema),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
required: ['type', 'permissions'],
|
|
373
|
+
},
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
export type LexiconPermissionSet = l.Infer<typeof lexiconPermissionSetSchema>
|
|
377
|
+
|
|
378
|
+
// Schemas that can appear anywhere in the defs
|
|
379
|
+
const NAMED_LEXICON_SCHEMAS = [
|
|
380
|
+
...CONCRETE_TYPES,
|
|
381
|
+
lexiconArraySchema,
|
|
382
|
+
lexiconObjectSchema,
|
|
383
|
+
lexiconTokenSchema,
|
|
384
|
+
] as const
|
|
385
|
+
|
|
386
|
+
export type NamedLexiconDefinition = l.Infer<
|
|
387
|
+
(typeof NAMED_LEXICON_SCHEMAS)[number]
|
|
388
|
+
>
|
|
389
|
+
|
|
390
|
+
// Schemas that can only appear as "main" def
|
|
391
|
+
const MAIN_LEXICON_SCHEMAS = [
|
|
392
|
+
lexiconPermissionSetSchema,
|
|
393
|
+
lexiconProcedureSchema,
|
|
394
|
+
lexiconQuerySchema,
|
|
395
|
+
lexiconRecordSchema,
|
|
396
|
+
lexiconSubscriptionSchema,
|
|
397
|
+
...NAMED_LEXICON_SCHEMAS,
|
|
398
|
+
] as const
|
|
399
|
+
|
|
400
|
+
export type MainLexiconDefinition = l.Infer<
|
|
401
|
+
(typeof MAIN_LEXICON_SCHEMAS)[number]
|
|
402
|
+
>
|
|
403
|
+
|
|
404
|
+
export const lexiconIdentifierSchema = l.string({ format: 'nsid' })
|
|
405
|
+
export type LexiconIdentifier = l.Infer<typeof lexiconIdentifierSchema>
|
|
406
|
+
|
|
407
|
+
export const lexiconDocumentSchema = l.object(
|
|
408
|
+
{
|
|
409
|
+
lexicon: l.literal(1),
|
|
410
|
+
id: lexiconIdentifierSchema,
|
|
411
|
+
revision: l.integer(),
|
|
412
|
+
description: l.string(),
|
|
413
|
+
defs: l.object(
|
|
414
|
+
{ main: l.discriminatedUnion('type', MAIN_LEXICON_SCHEMAS) },
|
|
415
|
+
{
|
|
416
|
+
unknownProperties: l.dict(
|
|
417
|
+
l.string({ minLength: 1 }),
|
|
418
|
+
l.discriminatedUnion('type', NAMED_LEXICON_SCHEMAS),
|
|
419
|
+
),
|
|
420
|
+
},
|
|
421
|
+
),
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
required: ['lexicon', 'id', 'defs'],
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
export type LexiconDocument = l.Infer<typeof lexiconDocumentSchema>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LexiconDocument } from './lexicon-document.js'
|
|
2
|
+
|
|
3
|
+
export interface LexiconIndexer {
|
|
4
|
+
get(nsid: string): Promise<LexiconDocument>
|
|
5
|
+
|
|
6
|
+
[Symbol.asyncDispose]?: () => Promise<void>
|
|
7
|
+
[Symbol.asyncIterator]?: () => AsyncIterator<LexiconDocument, void, unknown>
|
|
8
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { LexiconDocument } from './lexicon-document.js'
|
|
2
|
+
import { LexiconIndexer } from './lexicon-indexer.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* (Lazily) indexes lexicon documents from an iterable source.
|
|
6
|
+
*/
|
|
7
|
+
export class LexiconIterableIndexer implements LexiconIndexer, AsyncDisposable {
|
|
8
|
+
readonly #lexicons: Map<string, LexiconDocument> = new Map()
|
|
9
|
+
readonly #iterator:
|
|
10
|
+
| AsyncIterator<LexiconDocument, void, unknown>
|
|
11
|
+
| Iterator<LexiconDocument, void, unknown>
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
readonly source: AsyncIterable<LexiconDocument> | Iterable<LexiconDocument>,
|
|
15
|
+
) {
|
|
16
|
+
this.#iterator =
|
|
17
|
+
Symbol.asyncIterator in source
|
|
18
|
+
? source[Symbol.asyncIterator]()
|
|
19
|
+
: source[Symbol.iterator]()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(id: string): Promise<LexiconDocument> {
|
|
23
|
+
const cached = this.#lexicons.get(id)
|
|
24
|
+
if (cached) return cached
|
|
25
|
+
|
|
26
|
+
for await (const doc of this) {
|
|
27
|
+
if (doc.id === id) return doc
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw Object.assign(new Error(`Lexicon ${id} not found`), {
|
|
31
|
+
code: 'ENOENT',
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async *[Symbol.asyncIterator](): AsyncIterator<
|
|
36
|
+
LexiconDocument,
|
|
37
|
+
void,
|
|
38
|
+
undefined
|
|
39
|
+
> {
|
|
40
|
+
const returned = new Set<string>()
|
|
41
|
+
|
|
42
|
+
for (const doc of this.#lexicons.values()) {
|
|
43
|
+
returned.add(doc.id)
|
|
44
|
+
yield doc
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
do {
|
|
48
|
+
const { value, done } = await this.#iterator.next()
|
|
49
|
+
|
|
50
|
+
if (done) break
|
|
51
|
+
|
|
52
|
+
if (returned.has(value.id)) {
|
|
53
|
+
const err = new Error(`Duplicate lexicon document id: ${value.id}`)
|
|
54
|
+
this.#iterator.throw?.(err)
|
|
55
|
+
throw err // In case iterator.throw does not exist or does not throw
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#lexicons.set(value.id, value)
|
|
59
|
+
returned.add(value.id)
|
|
60
|
+
yield value
|
|
61
|
+
} while (true)
|
|
62
|
+
|
|
63
|
+
// At this point, the underlying iterator is done. However, there may have
|
|
64
|
+
// been requests (.get()) for documents that caused the iterator to yield
|
|
65
|
+
// those documents during concurrent execution of this loop. If that was the
|
|
66
|
+
// case, new documents may have been added to `#lexicons` that have not yet
|
|
67
|
+
// been yielded. We need to yield those as well. Since we yield control back
|
|
68
|
+
// to the caller, we need to repeat this process until no new documents
|
|
69
|
+
// appear sunce we don't know what happens.
|
|
70
|
+
|
|
71
|
+
for (const doc of this.#lexicons.values()) {
|
|
72
|
+
if (!returned.has(doc.id)) {
|
|
73
|
+
returned.add(doc.id)
|
|
74
|
+
yield doc
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
80
|
+
await this.#iterator.return?.()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { CID, l } from '@atproto/lex-schema'
|
|
2
|
+
import { LexiconDocument, lexiconDocumentSchema } from './lexicon-document.js'
|
|
3
|
+
import { LexiconIterableIndexer } from './lexicon-iterable-indexer.js'
|
|
4
|
+
import { LexiconSchemaBuilder } from './lexicon-schema-builder.js'
|
|
5
|
+
|
|
6
|
+
describe('LexiconSchemaBuilder', () => {
|
|
7
|
+
let schemas: Map<
|
|
8
|
+
string,
|
|
9
|
+
| l.Validator<unknown>
|
|
10
|
+
| l.Query
|
|
11
|
+
| l.Subscription
|
|
12
|
+
| l.Procedure
|
|
13
|
+
| l.PermissionSet
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
const getSchema = <T extends abstract new (...args: any) => any>(
|
|
17
|
+
ref: string,
|
|
18
|
+
type: T,
|
|
19
|
+
) => {
|
|
20
|
+
const schema = schemas.get(ref)
|
|
21
|
+
expect(schema).toBeDefined()
|
|
22
|
+
expect(schema).toBeInstanceOf(type)
|
|
23
|
+
return schema as InstanceType<T>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
const indexer = new LexiconIterableIndexer([
|
|
28
|
+
lexiconDocumentSchema.parse({
|
|
29
|
+
lexicon: 1,
|
|
30
|
+
id: 'com.example.kitchenSink',
|
|
31
|
+
defs: {
|
|
32
|
+
main: {
|
|
33
|
+
type: 'record',
|
|
34
|
+
description: 'A record',
|
|
35
|
+
key: 'tid',
|
|
36
|
+
record: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: [
|
|
39
|
+
'object',
|
|
40
|
+
'array',
|
|
41
|
+
'boolean',
|
|
42
|
+
'integer',
|
|
43
|
+
'string',
|
|
44
|
+
'bytes',
|
|
45
|
+
'cidLink',
|
|
46
|
+
],
|
|
47
|
+
properties: {
|
|
48
|
+
object: { type: 'ref', ref: '#object' },
|
|
49
|
+
array: { type: 'array', items: { type: 'string' } },
|
|
50
|
+
boolean: { type: 'boolean' },
|
|
51
|
+
integer: { type: 'integer' },
|
|
52
|
+
string: { type: 'string' },
|
|
53
|
+
bytes: { type: 'bytes' },
|
|
54
|
+
cidLink: { type: 'cid-link' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
object: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
required: ['object', 'array', 'boolean', 'integer', 'string'],
|
|
61
|
+
properties: {
|
|
62
|
+
object: { type: 'ref', ref: '#subobject' },
|
|
63
|
+
array: { type: 'array', items: { type: 'string' } },
|
|
64
|
+
boolean: { type: 'boolean' },
|
|
65
|
+
integer: { type: 'integer' },
|
|
66
|
+
string: { type: 'string' },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
subobject: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
required: ['boolean'],
|
|
72
|
+
properties: {
|
|
73
|
+
boolean: { type: 'boolean' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
])
|
|
79
|
+
schemas = await LexiconSchemaBuilder.buildAll(indexer)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Validates records correctly', () => {
|
|
83
|
+
const schema = getSchema('com.example.kitchenSink#main', l.RecordSchema)
|
|
84
|
+
|
|
85
|
+
const value = {
|
|
86
|
+
$type: 'com.example.kitchenSink',
|
|
87
|
+
object: {
|
|
88
|
+
object: { boolean: true },
|
|
89
|
+
array: ['one', 'two'],
|
|
90
|
+
boolean: true,
|
|
91
|
+
integer: 123,
|
|
92
|
+
string: 'string',
|
|
93
|
+
},
|
|
94
|
+
array: ['one', 'two'],
|
|
95
|
+
boolean: true,
|
|
96
|
+
integer: 123,
|
|
97
|
+
string: 'string',
|
|
98
|
+
datetime: new Date().toISOString(),
|
|
99
|
+
atUri: 'at://did:web:example.com/com.example.test/self',
|
|
100
|
+
did: 'did:web:example.com',
|
|
101
|
+
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
102
|
+
bytes: new Uint8Array([0, 1, 2, 3]),
|
|
103
|
+
cidLink: CID.parse(
|
|
104
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(schema.validate(value)).toStrictEqual({ success: true, value })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('Validates objects correctly', () => {
|
|
112
|
+
const schema = getSchema(
|
|
113
|
+
'com.example.kitchenSink#object',
|
|
114
|
+
l.TypedObjectSchema,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const value = {
|
|
118
|
+
object: { boolean: true },
|
|
119
|
+
array: ['one', 'two'],
|
|
120
|
+
boolean: true,
|
|
121
|
+
integer: 123,
|
|
122
|
+
string: 'string',
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
expect(schema.validate(value)).toStrictEqual({ success: true, value })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('Rejects missing required record fields', () => {
|
|
129
|
+
const schema = getSchema(
|
|
130
|
+
'com.example.kitchenSink#object',
|
|
131
|
+
l.TypedObjectSchema,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const value = {
|
|
135
|
+
object: { boolean: true },
|
|
136
|
+
// array: ['one', 'two'],
|
|
137
|
+
boolean: true,
|
|
138
|
+
integer: 123,
|
|
139
|
+
string: 'string',
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
expect(schema.validate(value)).toMatchObject({
|
|
143
|
+
success: false,
|
|
144
|
+
error: { issues: [{ code: 'required_key', key: 'array' }] },
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('fails validation when ref uri has multiple hash segments', async () => {
|
|
149
|
+
const schema: LexiconDocument = {
|
|
150
|
+
lexicon: 1,
|
|
151
|
+
id: 'com.example.invalid',
|
|
152
|
+
defs: {
|
|
153
|
+
main: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
test: { type: 'ref', ref: 'com.example.invalid#test#test' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await expect(async () => {
|
|
163
|
+
await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))
|
|
164
|
+
}).rejects.toThrow('Uri can only have one hash segment')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('fails lexicon parsing when uri is invalid', async () => {
|
|
168
|
+
const schema: LexiconDocument = {
|
|
169
|
+
lexicon: 1,
|
|
170
|
+
id: 'com.example.invalid',
|
|
171
|
+
defs: {
|
|
172
|
+
main: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
test: { type: 'ref', ref: 'com.example.missing#main' },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await expect(async () => {
|
|
182
|
+
await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))
|
|
183
|
+
}).rejects.toThrow('Lexicon com.example.missing not found')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('fails lexicon parsing when uri is invalid', async () => {
|
|
187
|
+
const schema: LexiconDocument = {
|
|
188
|
+
lexicon: 1,
|
|
189
|
+
id: 'com.example.invalid',
|
|
190
|
+
defs: {
|
|
191
|
+
main: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
test: { type: 'ref', ref: 'com.example.invalid#nonexistent' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await expect(async () => {
|
|
201
|
+
await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))
|
|
202
|
+
}).rejects.toThrow(
|
|
203
|
+
'No definition found for hash ""nonexistent"" in com.example.invalid',
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
})
|