A plain JavaScript validator for AT Protocol lexicon schemas
1// Test inputs for ref data validation
2
3// Lexicon with a string definition
4const stringRefLexicon = {
5 lexicon: 1,
6 id: 'app.test.stringref',
7 defs: {
8 main: {
9 type: 'record',
10 key: 'tid',
11 record: {
12 type: 'object',
13 properties: {
14 content: { type: 'ref', ref: '#post' },
15 },
16 },
17 },
18 post: {
19 type: 'string',
20 maxLength: 280,
21 },
22 },
23};
24
25// Lexicon with an object definition
26const objectRefLexicon = {
27 lexicon: 1,
28 id: 'app.test.objectref',
29 defs: {
30 main: {
31 type: 'record',
32 key: 'tid',
33 record: {
34 type: 'object',
35 properties: {
36 user: { type: 'ref', ref: '#userDef' },
37 },
38 },
39 },
40 userDef: {
41 type: 'object',
42 required: ['name'],
43 properties: {
44 name: { type: 'string' },
45 age: { type: 'integer' },
46 },
47 },
48 },
49};
50
51// Lexicon with nested reference chain: refA -> refB -> actualString
52const nestedRefLexicon = {
53 lexicon: 1,
54 id: 'app.test.nested',
55 defs: {
56 main: {
57 type: 'record',
58 key: 'tid',
59 record: {
60 type: 'object',
61 properties: {
62 data: { type: 'ref', ref: '#refA' },
63 },
64 },
65 },
66 refA: {
67 type: 'ref',
68 ref: '#refB',
69 },
70 refB: {
71 type: 'ref',
72 ref: '#actualString',
73 },
74 actualString: {
75 type: 'string',
76 },
77 },
78};
79
80// Lexicon with circular reference: refA -> refB -> refA
81const circularRefLexicon = {
82 lexicon: 1,
83 id: 'app.test.circular',
84 defs: {
85 main: {
86 type: 'record',
87 key: 'tid',
88 record: {
89 type: 'object',
90 properties: {
91 data: { type: 'ref', ref: '#refA' },
92 },
93 },
94 },
95 refA: {
96 type: 'ref',
97 ref: '#refB',
98 },
99 refB: {
100 type: 'ref',
101 ref: '#refA',
102 },
103 },
104};
105
106// Cross-lexicon reference
107const crossRefLexicon1 = {
108 lexicon: 1,
109 id: 'app.test.schema',
110 defs: {
111 main: {
112 type: 'record',
113 key: 'tid',
114 record: {
115 type: 'object',
116 properties: {
117 user: { type: 'ref', ref: 'app.test.types#user' },
118 },
119 },
120 },
121 },
122};
123
124const crossRefLexicon2 = {
125 lexicon: 1,
126 id: 'app.test.types',
127 defs: {
128 user: {
129 type: 'object',
130 required: ['id'],
131 properties: {
132 id: { type: 'string' },
133 },
134 },
135 },
136};
137
138// Lexicon with nonexistent ref
139const badRefLexicon = {
140 lexicon: 1,
141 id: 'app.test.badref',
142 defs: {
143 main: {
144 type: 'record',
145 key: 'tid',
146 record: {
147 type: 'object',
148 properties: {
149 data: { type: 'ref', ref: '#nonexistent' },
150 },
151 },
152 },
153 },
154};
155
156// Cross-lexicon reference with nested local refs
157// Tests: when A refs B#foo, and B#foo refs #bar, it should resolve to B#bar (not A#bar)
158// This mirrors real-world case: app.bsky.actor.profile -> com.atproto.label.defs#selfLabels -> #selfLabel
159const crossRefNestedLexicon1 = {
160 lexicon: 1,
161 id: 'app.test.profile',
162 defs: {
163 main: {
164 type: 'record',
165 key: 'tid',
166 record: {
167 type: 'object',
168 properties: {
169 labels: { type: 'ref', ref: 'app.test.labels#selfLabels' },
170 },
171 },
172 },
173 // This should NOT be used - the ref should resolve to app.test.labels#selfLabel
174 selfLabel: {
175 type: 'object',
176 required: ['wrongField'],
177 properties: {
178 wrongField: { type: 'string' },
179 },
180 },
181 },
182};
183
184const crossRefNestedLexicon2 = {
185 lexicon: 1,
186 id: 'app.test.labels',
187 defs: {
188 selfLabels: {
189 type: 'object',
190 properties: {
191 values: {
192 type: 'array',
193 items: { type: 'ref', ref: '#selfLabel' },
194 },
195 },
196 },
197 selfLabel: {
198 type: 'object',
199 required: ['val'],
200 properties: {
201 val: { type: 'string' },
202 },
203 },
204 },
205};
206
207export const refDataInputs = [
208 {
209 name: 'ref-data-valid-to-string',
210 lexicons: [stringRefLexicon],
211 collection: 'app.test.stringref',
212 record: {
213 content: 'Hello, world!',
214 },
215 },
216 {
217 name: 'ref-data-invalid-string-too-long',
218 lexicons: [stringRefLexicon],
219 collection: 'app.test.stringref',
220 record: {
221 content: 'a'.repeat(281),
222 },
223 },
224 {
225 name: 'ref-data-valid-to-object',
226 lexicons: [objectRefLexicon],
227 collection: 'app.test.objectref',
228 record: {
229 user: { name: 'Alice', age: 30 },
230 },
231 },
232 {
233 name: 'ref-data-invalid-object-missing-required',
234 lexicons: [objectRefLexicon],
235 collection: 'app.test.objectref',
236 record: {
237 user: { age: 30 },
238 },
239 },
240 {
241 name: 'ref-data-valid-nested-chain',
242 lexicons: [nestedRefLexicon],
243 collection: 'app.test.nested',
244 record: {
245 data: 'Hello!',
246 },
247 },
248 {
249 name: 'ref-data-invalid-circular-reference',
250 lexicons: [circularRefLexicon],
251 collection: 'app.test.circular',
252 record: {
253 data: 'test',
254 },
255 },
256 {
257 name: 'ref-data-valid-cross-lexicon',
258 lexicons: [crossRefLexicon1, crossRefLexicon2],
259 collection: 'app.test.schema',
260 record: {
261 user: { id: 'user123' },
262 },
263 },
264 {
265 name: 'ref-data-invalid-cross-lexicon-missing-required',
266 lexicons: [crossRefLexicon1, crossRefLexicon2],
267 collection: 'app.test.schema',
268 record: {
269 user: { name: 'Alice' },
270 },
271 },
272 {
273 name: 'ref-data-invalid-not-found',
274 lexicons: [badRefLexicon],
275 collection: 'app.test.badref',
276 record: {
277 data: 'test',
278 },
279 },
280 // Cross-lexicon ref with nested local ref - should resolve to target lexicon's def
281 {
282 name: 'ref-data-valid-cross-lexicon-nested-local-ref',
283 lexicons: [crossRefNestedLexicon1, crossRefNestedLexicon2],
284 collection: 'app.test.profile',
285 record: {
286 labels: { values: [{ val: 'self-label-value' }] },
287 },
288 },
289 // This should fail because it uses the wrong lexicon's selfLabel definition
290 {
291 name: 'ref-data-invalid-cross-lexicon-wrong-context',
292 lexicons: [crossRefNestedLexicon1, crossRefNestedLexicon2],
293 collection: 'app.test.profile',
294 record: {
295 labels: { values: [{ wrongField: 'this-uses-wrong-lexicon' }] },
296 },
297 },
298];