@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.
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LexiconSchemaBuilder = void 0;
4
+ exports.memoize = memoize;
5
+ const lex_schema_1 = require("@atproto/lex-schema");
6
+ /**
7
+ * Builds a validator for a given lexicon "ref" from a lexicon indexer.
8
+ *
9
+ * @example
10
+ *
11
+ * ```ts
12
+ * import { LexiconSchemaBuilder } from '@atproto/lex/doc'
13
+ * import { LexiconStreamIndexer } from '@atproto/lex/doc'
14
+ *
15
+ * const indexer = new LexiconStreamIndexer(lexiconDocs)
16
+ * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.foo#bar')
17
+ * ```
18
+ */
19
+ class LexiconSchemaBuilder {
20
+ indexer;
21
+ static async build(indexer, fullRef) {
22
+ const ctx = new LexiconSchemaBuilder(indexer);
23
+ try {
24
+ const result = await ctx.buildFullRef(fullRef);
25
+ if (!(result instanceof lex_schema_1.l.Validator)) {
26
+ throw new Error(`Ref ${fullRef} is not a validator schema type`);
27
+ }
28
+ return result;
29
+ }
30
+ finally {
31
+ await ctx.done();
32
+ }
33
+ }
34
+ static async buildAll(indexer) {
35
+ const builder = new LexiconSchemaBuilder(indexer);
36
+ const schemas = new Map();
37
+ if (!isAsyncIterableObject(indexer)) {
38
+ throw new Error('An iterable indexer is required to build all schemas');
39
+ }
40
+ try {
41
+ for await (const doc of indexer) {
42
+ for (const hash of Object.keys(doc.defs)) {
43
+ const fullRef = `${doc.id}#${hash}`;
44
+ const schema = await builder.buildFullRef(fullRef);
45
+ schemas.set(fullRef, schema);
46
+ }
47
+ }
48
+ return schemas;
49
+ }
50
+ finally {
51
+ await builder.done();
52
+ }
53
+ }
54
+ #asyncTasks = new AsyncTasks();
55
+ constructor(indexer) {
56
+ this.indexer = indexer;
57
+ }
58
+ async done() {
59
+ await this.#asyncTasks.done();
60
+ }
61
+ buildFullRef = memoize(async (fullRef) => {
62
+ const { nsid, hash } = parseRef(fullRef);
63
+ const doc = await this.indexer.get(nsid);
64
+ if (!doc)
65
+ throw new Error(`No lexicon found for NSID: ${nsid}`);
66
+ return this.compileDef(doc, hash);
67
+ });
68
+ buildRefGetter(fullRef) {
69
+ let validator;
70
+ this.#asyncTasks.add(this.buildFullRef(fullRef).then((v) => {
71
+ if (!(v instanceof lex_schema_1.l.Validator)) {
72
+ throw new Error(`Only refs to validator schema types are allowed`);
73
+ }
74
+ validator = v;
75
+ }));
76
+ return () => {
77
+ if (validator)
78
+ return validator;
79
+ throw new Error('Validator not yet built. Did you await done()?');
80
+ };
81
+ }
82
+ buildTypedRefGetter(fullRef) {
83
+ let validator;
84
+ this.#asyncTasks.add(this.buildFullRef(fullRef).then((v) => {
85
+ if (v instanceof lex_schema_1.l.TypedObjectSchema || v instanceof lex_schema_1.l.RecordSchema) {
86
+ validator = v;
87
+ }
88
+ else {
89
+ throw new Error('Only refs to records and object definitions are allowed');
90
+ }
91
+ }));
92
+ return () => {
93
+ if (validator)
94
+ return validator;
95
+ throw new Error('Validator not yet built. Did you await done()?');
96
+ };
97
+ }
98
+ compileDef(doc, hash) {
99
+ const def = Object.hasOwn(doc.defs, hash) ? doc.defs[hash] : null;
100
+ if (!def) {
101
+ throw new Error(`No definition found for hash "${JSON.stringify(hash)}" in ${doc.id}`);
102
+ }
103
+ switch (def.type) {
104
+ case 'permission-set':
105
+ return lex_schema_1.l.permissionSet(doc.id, def.permissions.map(({ resource, type, ...p }) => lex_schema_1.l.permission(resource, p)), def);
106
+ case 'procedure':
107
+ return lex_schema_1.l.procedure(doc.id, this.compileParams(doc, def.parameters), this.compilePayload(doc, def.input), this.compilePayload(doc, def.output), this.compileErrors(doc, def.errors));
108
+ case 'query':
109
+ return lex_schema_1.l.query(doc.id, this.compileParams(doc, def.parameters), this.compilePayload(doc, def.output), this.compileErrors(doc, def.errors));
110
+ case 'subscription':
111
+ return lex_schema_1.l.subscription(doc.id, this.compileParams(doc, def.parameters), this.compilePayloadSchema(doc, def.message?.schema), this.compileErrors(doc, def.errors));
112
+ case 'token':
113
+ return lex_schema_1.l.token(doc.id, hash);
114
+ case 'record':
115
+ return lex_schema_1.l.record(def.key ? lex_schema_1.l.asRecordKey(def.key) : 'any', doc.id, this.compileObject(doc, def.record));
116
+ case 'object':
117
+ return lex_schema_1.l.typedObject(doc.id, hash, this.compileObject(doc, def));
118
+ default:
119
+ return this.compileLeaf(doc, def);
120
+ }
121
+ }
122
+ compileLeaf(doc, def) {
123
+ switch (def.type) {
124
+ case 'string':
125
+ return lex_schema_1.l.string(def);
126
+ case 'integer':
127
+ return lex_schema_1.l.integer(def);
128
+ case 'boolean':
129
+ return lex_schema_1.l.boolean(def);
130
+ case 'blob':
131
+ return lex_schema_1.l.blob(def);
132
+ case 'cid-link':
133
+ return lex_schema_1.l.cidLink();
134
+ case 'bytes':
135
+ return lex_schema_1.l.bytes(def);
136
+ case 'unknown':
137
+ return lex_schema_1.l.unknown();
138
+ case 'array':
139
+ return lex_schema_1.l.array(this.compileLeaf(doc, def.items), def);
140
+ default:
141
+ return this.compileRef(doc, def);
142
+ }
143
+ }
144
+ compileRef(doc, def) {
145
+ switch (def.type) {
146
+ case 'ref':
147
+ return lex_schema_1.l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)));
148
+ case 'union':
149
+ return lex_schema_1.l.typedUnion(def.refs.map((r) => lex_schema_1.l.typedRef(this.buildTypedRefGetter(buildFullRef(doc, r)))), def.closed ?? false);
150
+ default:
151
+ // @ts-expect-error
152
+ throw new Error(`Unknown lexicon type: ${def.type}`);
153
+ }
154
+ }
155
+ compileObject(doc, def) {
156
+ const props = {};
157
+ for (const [key, propDef] of Object.entries(def.properties)) {
158
+ if (propDef === undefined)
159
+ continue;
160
+ props[key] = this.compileLeaf(doc, propDef);
161
+ }
162
+ return lex_schema_1.l.object(props, def);
163
+ }
164
+ compilePayload(doc, def) {
165
+ return lex_schema_1.l.payload(def?.encoding, def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined);
166
+ }
167
+ compileErrors(_doc, errors) {
168
+ return errors?.map((e) => e.name);
169
+ }
170
+ compilePayloadSchema(doc, def) {
171
+ if (!def)
172
+ return undefined;
173
+ switch (def.type) {
174
+ case 'object':
175
+ return this.compileObject(doc, def);
176
+ default:
177
+ return this.compileRef(doc, def);
178
+ }
179
+ }
180
+ compileParams(doc, def) {
181
+ if (!def)
182
+ return lex_schema_1.l.params();
183
+ const props = {};
184
+ for (const [key, propDef] of Object.entries(def.properties)) {
185
+ if (propDef === undefined)
186
+ continue;
187
+ props[key] = this.compileLeaf(doc, propDef);
188
+ }
189
+ return lex_schema_1.l.params(props, def);
190
+ }
191
+ }
192
+ exports.LexiconSchemaBuilder = LexiconSchemaBuilder;
193
+ class AsyncTasks {
194
+ /**
195
+ * A set that, eventually, contains only rejected promises.
196
+ */
197
+ #promises = new Set();
198
+ async done() {
199
+ do {
200
+ // @NOTE this is going to throw on the first rejected promise (which is
201
+ // what we want)
202
+ for (const p of this.#promises)
203
+ await p;
204
+ // At this point, all settled promises should have been removed. If
205
+ // this.#promises is not empty, it means new promises were added during
206
+ // the awaiting process, so we loop again.
207
+ } while (this.#promises.size > 0);
208
+ }
209
+ add(p) {
210
+ const promise = Promise.resolve(p).then(() => {
211
+ // No need to keep the promise any longer
212
+ this.#promises.delete(promise);
213
+ });
214
+ void promise.catch((_err) => {
215
+ // ignore errors here, they should be caught though done()
216
+ });
217
+ this.#promises.add(promise);
218
+ }
219
+ }
220
+ function parseRef(fullRef) {
221
+ const { length, 0: nsid, 1: hash } = fullRef.split('#');
222
+ if (length !== 2)
223
+ throw new Error('Uri can only have one hash segment');
224
+ if (!nsid || !hash)
225
+ throw new Error('Invalid ref, missing hash');
226
+ return { nsid, hash };
227
+ }
228
+ function buildFullRef(from, ref) {
229
+ if (ref.startsWith('#'))
230
+ return `${from.id}${ref}`;
231
+ return ref;
232
+ }
233
+ function memoize(fn) {
234
+ const cache = new Map();
235
+ return ((arg) => {
236
+ if (cache.has(arg))
237
+ return cache.get(arg);
238
+ const result = fn(arg);
239
+ cache.set(arg, result);
240
+ return result;
241
+ });
242
+ }
243
+ function isAsyncIterableObject(obj) {
244
+ return (obj != null &&
245
+ typeof obj === 'object' &&
246
+ Symbol.asyncIterator in obj &&
247
+ typeof obj[Symbol.asyncIterator] === 'function');
248
+ }
249
+ //# sourceMappingURL=lexicon-schema-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexicon-schema-builder.js","sourceRoot":"","sources":["../src/lexicon-schema-builder.ts"],"names":[],"mappings":";;;AAqUA,0BAQC;AA7UD,oDAAuC;AAcvC;;;;;;;;;;;;GAYG;AACH,MAAa,oBAAoB;IA8CT;IA7CtB,MAAM,CAAC,KAAK,CAAC,KAAK,CAChB,OAAuB,EACvB,OAAe;QAEf,MAAM,GAAG,GAAG,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;YAC9C,IAAI,CAAC,CAAC,MAAM,YAAY,cAAC,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CAAC,OAAO,OAAO,iCAAiC,CAAC,CAAA;YAClE,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;gBAAS,CAAC;YACT,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAuB;QAC3C,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAA;QACjD,MAAM,OAAO,GAAG,IAAI,GAAG,EAOpB,CAAA;QACH,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;QACzE,CAAC;QACD,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;gBAChC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBACzC,MAAM,OAAO,GAAG,GAAG,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,CAAA;oBACnC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;oBAClD,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;gBAC9B,CAAC;YACH,CAAC;YACD,OAAO,OAAO,CAAA;QAChB,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,IAAI,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAED,WAAW,GAAG,IAAI,UAAU,EAAE,CAAA;IAE9B,YAAsB,OAAuB;QAAvB,YAAO,GAAP,OAAO,CAAgB;IAAG,CAAC;IAEjD,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IAC/B,CAAC;IAED,YAAY,GAAG,OAAO,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;QAC/C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;QAExC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACxC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAA;QAE/D,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;IAEQ,cAAc,CAAC,OAAe;QACtC,IAAI,SAA+B,CAAA;QAEnC,IAAI,CAAC,WAAW,CAAC,GAAG,CAClB,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,CAAC,YAAY,cAAC,CAAC,SAAS,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;YACpE,CAAC;YACD,SAAS,GAAG,CAAC,CAAA;QACf,CAAC,CAAC,CACH,CAAA;QAED,OAAO,GAAG,EAAE;YACV,IAAI,SAAS;gBAAE,OAAO,SAAS,CAAA;YAC/B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAA;QACnE,CAAC,CAAA;IACH,CAAC;IAES,mBAAmB,CAC3B,OAAe;QAEf,IAAI,SAA+C,CAAA;QAEnD,IAAI,CAAC,WAAW,CAAC,GAAG,CAClB,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,YAAY,cAAC,CAAC,iBAAiB,IAAI,CAAC,YAAY,cAAC,CAAC,YAAY,EAAE,CAAC;gBACpE,SAAS,GAAG,CAAC,CAAA;YACf,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CACb,yDAAyD,CAC1D,CAAA;YACH,CAAC;QACH,CAAC,CAAC,CACH,CAAA;QAED,OAAO,GAAG,EAAE;YACV,IAAI,SAAS;gBAAE,OAAO,SAAS,CAAA;YAC/B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAA;QACnE,CAAC,CAAA;IACH,CAAC;IAES,UAAU,CAAC,GAAoB,EAAE,IAAY;QACrD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACjE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,iCAAiC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE,EAAE,CACtE,CAAA;QACH,CAAC;QACD,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,gBAAgB;gBACnB,OAAO,cAAC,CAAC,aAAa,CACpB,GAAG,CAAC,EAAE,EACN,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAC/C,cAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAC1B,EACD,GAAG,CACJ,CAAA;YACH,KAAK,WAAW;gBACd,OAAO,cAAC,CAAC,SAAS,CAChB,GAAG,CAAC,EAAE,EACN,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,EACvC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EACnC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,EACpC,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CACpC,CAAA;YACH,KAAK,OAAO;gBACV,OAAO,cAAC,CAAC,KAAK,CACZ,GAAG,CAAC,EAAE,EACN,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,EACvC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,EACpC,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CACpC,CAAA;YACH,KAAK,cAAc;gBACjB,OAAO,cAAC,CAAC,YAAY,CACnB,GAAG,CAAC,EAAE,EACN,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,EACvC,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,EACnD,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CACpC,CAAA;YACH,KAAK,OAAO;gBACV,OAAO,cAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;YAC9B,KAAK,QAAQ;gBACX,OAAO,cAAC,CAAC,MAAM,CACb,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,cAAC,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,EACxC,GAAG,CAAC,EAAE,EACN,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CACpC,CAAA;YACH,KAAK,QAAQ;gBACX,OAAO,cAAC,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;YAClE;gBACE,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IAES,WAAW,CACnB,GAAoB,EACpB,GAAqC;QAErC,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,QAAQ;gBACX,OAAO,cAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACtB,KAAK,SAAS;gBACZ,OAAO,cAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YACvB,KAAK,SAAS;gBACZ,OAAO,cAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YACvB,KAAK,MAAM;gBACT,OAAO,cAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACpB,KAAK,UAAU;gBACb,OAAO,cAAC,CAAC,OAAO,EAAE,CAAA;YACpB,KAAK,OAAO;gBACV,OAAO,cAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACrB,KAAK,SAAS;gBACZ,OAAO,cAAC,CAAC,OAAO,EAAE,CAAA;YACpB,KAAK,OAAO;gBACV,OAAO,cAAC,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAA;YACvD;gBACE,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACpC,CAAC;IACH,CAAC;IAES,UAAU,CAClB,GAAoB,EACpB,GAAiC;QAEjC,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,KAAK;gBACR,OAAO,cAAC,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YAC/D,KAAK,OAAO;gBACV,OAAO,cAAC,CAAC,UAAU,CACjB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACjB,cAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAC3D,EACD,GAAG,CAAC,MAAM,IAAI,KAAK,CACpB,CAAA;YACH;gBACE,mBAAmB;gBACnB,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;QACxD,CAAC;IACH,CAAC;IAES,aAAa,CACrB,GAAoB,EACpB,GAAkB;QAElB,MAAM,KAAK,GAAgC,EAAE,CAAA;QAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,IAAI,OAAO,KAAK,SAAS;gBAAE,SAAQ;YACnC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,cAAC,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC7B,CAAC;IAES,cAAc,CACtB,GAAoB,EACpB,GAA+B;QAE/B,OAAO,cAAC,CAAC,OAAO,CACd,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CACrE,CAAA;IACH,CAAC;IAES,aAAa,CACrB,IAAqB,EACrB,MAAgC;QAEhC,OAAO,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACnC,CAAC;IAES,oBAAoB,CAC5B,GAAoB,EACpB,GAAkD;QAElD,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAA;QAC1B,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YACrC;gBACE,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACpC,CAAC;IACH,CAAC;IAES,aAAa,CAAC,GAAoB,EAAE,GAAuB;QACnE,IAAI,CAAC,GAAG;YAAE,OAAO,cAAC,CAAC,MAAM,EAAE,CAAA;QAE3B,MAAM,KAAK,GAAgC,EAAE,CAAA;QAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,IAAI,OAAO,KAAK,SAAS;gBAAE,SAAQ;YACnC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,cAAC,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC7B,CAAC;CACF;AA7PD,oDA6PC;AAED,MAAM,UAAU;IACd;;OAEG;IACH,SAAS,GAAG,IAAI,GAAG,EAAiB,CAAA;IAEpC,KAAK,CAAC,IAAI;QACR,GAAG,CAAC;YACF,uEAAuE;YACvE,gBAAgB;YAChB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS;gBAAE,MAAM,CAAC,CAAA;YACvC,mEAAmE;YACnE,uEAAuE;YACvE,0CAA0C;QAC5C,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,EAAC;IACnC,CAAC;IAED,GAAG,CAAC,CAAgB;QAClB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YAC3C,yCAAyC;YACzC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAChC,CAAC,CAAC,CAAA;QAEF,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE;YAC1B,0DAA0D;QAC5D,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC7B,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,OAAe;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvD,IAAI,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvE,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;IAChE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AACvB,CAAC;AAED,SAAS,YAAY,CAAC,IAAqB,EAAE,GAAW;IACtD,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,CAAA;IAClD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAgB,OAAO,CAAsC,EAAM;IACjE,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAA;IAC/C,OAAO,CAAC,CAAC,GAAW,EAAE,EAAE;QACtB,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAE,CAAA;QAC1C,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,CAAmB,CAAA;QACxC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QACtB,OAAO,MAAM,CAAA;IACf,CAAC,CAAO,CAAA;AACV,CAAC;AAED,SAAS,qBAAqB,CAC5B,GAAM;IAEN,OAAO,CACL,GAAG,IAAI,IAAI;QACX,OAAO,GAAG,KAAK,QAAQ;QACvB,MAAM,CAAC,aAAa,IAAI,GAAG;QAC3B,OAAO,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,UAAU,CAChD,CAAA;AACH,CAAC","sourcesContent":["import { l } from '@atproto/lex-schema'\nimport {\n LexiconArray,\n LexiconArrayItems,\n LexiconDocument,\n LexiconError,\n LexiconObject,\n LexiconParameters,\n LexiconPayload,\n LexiconRef,\n LexiconRefUnion,\n} from './lexicon-document.js'\nimport { LexiconIndexer } from './lexicon-indexer.js'\n\n/**\n * Builds a validator for a given lexicon \"ref\" from a lexicon indexer.\n *\n * @example\n *\n * ```ts\n * import { LexiconSchemaBuilder } from '@atproto/lex/doc'\n * import { LexiconStreamIndexer } from '@atproto/lex/doc'\n *\n * const indexer = new LexiconStreamIndexer(lexiconDocs)\n * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.foo#bar')\n * ```\n */\nexport class LexiconSchemaBuilder {\n static async build(\n indexer: LexiconIndexer,\n fullRef: string,\n ): Promise<l.Validator<unknown>> {\n const ctx = new LexiconSchemaBuilder(indexer)\n try {\n const result = await ctx.buildFullRef(fullRef)\n if (!(result instanceof l.Validator)) {\n throw new Error(`Ref ${fullRef} is not a validator schema type`)\n }\n return result\n } finally {\n await ctx.done()\n }\n }\n\n static async buildAll(indexer: LexiconIndexer) {\n const builder = new LexiconSchemaBuilder(indexer)\n const schemas = new Map<\n string,\n | l.Validator<unknown>\n | l.Query\n | l.Subscription\n | l.Procedure\n | l.PermissionSet\n >()\n if (!isAsyncIterableObject(indexer)) {\n throw new Error('An iterable indexer is required to build all schemas')\n }\n try {\n for await (const doc of indexer) {\n for (const hash of Object.keys(doc.defs)) {\n const fullRef = `${doc.id}#${hash}`\n const schema = await builder.buildFullRef(fullRef)\n schemas.set(fullRef, schema)\n }\n }\n return schemas\n } finally {\n await builder.done()\n }\n }\n\n #asyncTasks = new AsyncTasks()\n\n constructor(protected indexer: LexiconIndexer) {}\n\n async done(): Promise<void> {\n await this.#asyncTasks.done()\n }\n\n buildFullRef = memoize(async (fullRef: string) => {\n const { nsid, hash } = parseRef(fullRef)\n\n const doc = await this.indexer.get(nsid)\n if (!doc) throw new Error(`No lexicon found for NSID: ${nsid}`)\n\n return this.compileDef(doc, hash)\n })\n\n protected buildRefGetter(fullRef: string): () => l.Validator<unknown> {\n let validator: l.Validator<unknown>\n\n this.#asyncTasks.add(\n this.buildFullRef(fullRef).then((v) => {\n if (!(v instanceof l.Validator)) {\n throw new Error(`Only refs to validator schema types are allowed`)\n }\n validator = v\n }),\n )\n\n return () => {\n if (validator) return validator\n throw new Error('Validator not yet built. Did you await done()?')\n }\n }\n\n protected buildTypedRefGetter(\n fullRef: string,\n ): () => l.TypedObjectSchema | l.RecordSchema {\n let validator: l.TypedObjectSchema | l.RecordSchema\n\n this.#asyncTasks.add(\n this.buildFullRef(fullRef).then((v) => {\n if (v instanceof l.TypedObjectSchema || v instanceof l.RecordSchema) {\n validator = v\n } else {\n throw new Error(\n 'Only refs to records and object definitions are allowed',\n )\n }\n }),\n )\n\n return () => {\n if (validator) return validator\n throw new Error('Validator not yet built. Did you await done()?')\n }\n }\n\n protected compileDef(doc: LexiconDocument, hash: string) {\n const def = Object.hasOwn(doc.defs, hash) ? doc.defs[hash] : null\n if (!def) {\n throw new Error(\n `No definition found for hash \"${JSON.stringify(hash)}\" in ${doc.id}`,\n )\n }\n switch (def.type) {\n case 'permission-set':\n return l.permissionSet(\n doc.id,\n def.permissions.map(({ resource, type, ...p }) =>\n l.permission(resource, p),\n ),\n def,\n )\n case 'procedure':\n return l.procedure(\n doc.id,\n this.compileParams(doc, def.parameters),\n this.compilePayload(doc, def.input),\n this.compilePayload(doc, def.output),\n this.compileErrors(doc, def.errors),\n )\n case 'query':\n return l.query(\n doc.id,\n this.compileParams(doc, def.parameters),\n this.compilePayload(doc, def.output),\n this.compileErrors(doc, def.errors),\n )\n case 'subscription':\n return l.subscription(\n doc.id,\n this.compileParams(doc, def.parameters),\n this.compilePayloadSchema(doc, def.message?.schema),\n this.compileErrors(doc, def.errors),\n )\n case 'token':\n return l.token(doc.id, hash)\n case 'record':\n return l.record(\n def.key ? l.asRecordKey(def.key) : 'any',\n doc.id,\n this.compileObject(doc, def.record),\n )\n case 'object':\n return l.typedObject(doc.id, hash, this.compileObject(doc, def))\n default:\n return this.compileLeaf(doc, def)\n }\n }\n\n protected compileLeaf(\n doc: LexiconDocument,\n def: LexiconArray | LexiconArrayItems,\n ): l.Validator<unknown> {\n switch (def.type) {\n case 'string':\n return l.string(def)\n case 'integer':\n return l.integer(def)\n case 'boolean':\n return l.boolean(def)\n case 'blob':\n return l.blob(def)\n case 'cid-link':\n return l.cidLink()\n case 'bytes':\n return l.bytes(def)\n case 'unknown':\n return l.unknown()\n case 'array':\n return l.array(this.compileLeaf(doc, def.items), def)\n default:\n return this.compileRef(doc, def)\n }\n }\n\n protected compileRef(\n doc: LexiconDocument,\n def: LexiconRef | LexiconRefUnion,\n ) {\n switch (def.type) {\n case 'ref':\n return l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)))\n case 'union':\n return l.typedUnion(\n def.refs.map((r) =>\n l.typedRef(this.buildTypedRefGetter(buildFullRef(doc, r))),\n ),\n def.closed ?? false,\n )\n default:\n // @ts-expect-error\n throw new Error(`Unknown lexicon type: ${def.type}`)\n }\n }\n\n protected compileObject(\n doc: LexiconDocument,\n def: LexiconObject,\n ): l.ObjectSchema {\n const props: Record<string, l.Validator> = {}\n for (const [key, propDef] of Object.entries(def.properties)) {\n if (propDef === undefined) continue\n props[key] = this.compileLeaf(doc, propDef)\n }\n return l.object(props, def)\n }\n\n protected compilePayload(\n doc: LexiconDocument,\n def: LexiconPayload | undefined,\n ): l.Payload {\n return l.payload(\n def?.encoding,\n def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined,\n )\n }\n\n protected compileErrors(\n _doc: LexiconDocument,\n errors?: readonly LexiconError[],\n ): undefined | string[] {\n return errors?.map((e) => e.name)\n }\n\n protected compilePayloadSchema(\n doc: LexiconDocument,\n def?: LexiconObject | LexiconRef | LexiconRefUnion,\n ) {\n if (!def) return undefined\n switch (def.type) {\n case 'object':\n return this.compileObject(doc, def)\n default:\n return this.compileRef(doc, def)\n }\n }\n\n protected compileParams(doc: LexiconDocument, def?: LexiconParameters) {\n if (!def) return l.params()\n\n const props: Record<string, l.Validator> = {}\n for (const [key, propDef] of Object.entries(def.properties)) {\n if (propDef === undefined) continue\n props[key] = this.compileLeaf(doc, propDef)\n }\n return l.params(props, def)\n }\n}\n\nclass AsyncTasks {\n /**\n * A set that, eventually, contains only rejected promises.\n */\n #promises = new Set<Promise<void>>()\n\n async done(): Promise<void> {\n do {\n // @NOTE this is going to throw on the first rejected promise (which is\n // what we want)\n for (const p of this.#promises) await p\n // At this point, all settled promises should have been removed. If\n // this.#promises is not empty, it means new promises were added during\n // the awaiting process, so we loop again.\n } while (this.#promises.size > 0)\n }\n\n add(p: Promise<void>) {\n const promise = Promise.resolve(p).then(() => {\n // No need to keep the promise any longer\n this.#promises.delete(promise)\n })\n\n void promise.catch((_err) => {\n // ignore errors here, they should be caught though done()\n })\n\n this.#promises.add(promise)\n }\n}\n\nfunction parseRef(fullRef: string) {\n const { length, 0: nsid, 1: hash } = fullRef.split('#')\n if (length !== 2) throw new Error('Uri can only have one hash segment')\n if (!nsid || !hash) throw new Error('Invalid ref, missing hash')\n return { nsid, hash }\n}\n\nfunction buildFullRef(from: LexiconDocument, ref: string) {\n if (ref.startsWith('#')) return `${from.id}${ref}`\n return ref\n}\n\nexport function memoize<Fn extends (arg: string) => unknown>(fn: Fn): Fn {\n const cache = new Map<string, ReturnType<Fn>>()\n return ((arg: string) => {\n if (cache.has(arg)) return cache.get(arg)!\n const result = fn(arg) as ReturnType<Fn>\n cache.set(arg, result)\n return result\n }) as Fn\n}\n\nfunction isAsyncIterableObject<T>(\n obj: T,\n): obj is T & object & AsyncIterable<unknown> {\n return (\n obj != null &&\n typeof obj === 'object' &&\n Symbol.asyncIterator in obj &&\n typeof obj[Symbol.asyncIterator] === 'function'\n )\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@atproto/lex-document",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "description": "Lexicon document validation tools for AT",
6
+ "keywords": [
7
+ "atproto",
8
+ "lexicon",
9
+ "document",
10
+ "lex"
11
+ ],
12
+ "homepage": "https://atproto.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/bluesky-social/atproto",
16
+ "directory": "packages/lex/lex-document"
17
+ },
18
+ "files": [
19
+ "./src",
20
+ "./dist"
21
+ ],
22
+ "sideEffects": false,
23
+ "type": "commonjs",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "browser": "./dist/index.js",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@atproto/lex-schema": "workspace:*",
36
+ "core-js": "^3",
37
+ "tslib": "^2.8.1"
38
+ },
39
+ "devDependencies": {
40
+ "jest": "^28.1.2"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc --build tsconfig.build.json",
44
+ "test": "jest"
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import 'core-js/modules/esnext.symbol.async-dispose'
2
+ import 'core-js/modules/esnext.symbol.dispose'
3
+
4
+ export * from './lexicon-document.js'
5
+ export * from './lexicon-indexer.js'
6
+ export * from './lexicon-schema-builder.js'
7
+ export * from './lexicon-iterable-indexer.js'
@@ -0,0 +1,22 @@
1
+ import { lexiconDocumentSchema } from './lexicon-document.js'
2
+
3
+ describe('General validation', () => {
4
+ it('allows unknown fields to be present', () => {
5
+ const value = {
6
+ lexicon: 1,
7
+ id: 'com.example.unknownFields',
8
+ defs: {
9
+ test: {
10
+ type: 'object',
11
+ properties: {},
12
+ foo: 3,
13
+ },
14
+ },
15
+ }
16
+
17
+ expect(lexiconDocumentSchema.validate(value)).toStrictEqual({
18
+ success: true,
19
+ value,
20
+ })
21
+ })
22
+ })