@atproto/lexicon 0.4.13 → 0.5.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/src/types.ts CHANGED
@@ -1,31 +1,36 @@
1
1
  import { z } from 'zod'
2
- import { NSID } from '@atproto/syntax'
2
+ import { validateLanguage } from '@atproto/common-web'
3
+ import { isValidNsid } from '@atproto/syntax'
3
4
  import { requiredPropertiesRefinement } from './util'
4
5
 
6
+ export const languageSchema = z
7
+ .string()
8
+ .refine(validateLanguage, 'Invalid BCP47 language tag')
9
+
10
+ export const lexLang = z.record(languageSchema, z.string().optional())
11
+
12
+ export type LexLang = z.infer<typeof lexLang>
13
+
5
14
  // primitives
6
15
  // =
7
16
 
8
- export const lexBoolean = z
9
- .object({
10
- type: z.literal('boolean'),
11
- description: z.string().optional(),
12
- default: z.boolean().optional(),
13
- const: z.boolean().optional(),
14
- })
15
- .strict()
17
+ export const lexBoolean = z.object({
18
+ type: z.literal('boolean'),
19
+ description: z.string().optional(),
20
+ default: z.boolean().optional(),
21
+ const: z.boolean().optional(),
22
+ })
16
23
  export type LexBoolean = z.infer<typeof lexBoolean>
17
24
 
18
- export const lexInteger = z
19
- .object({
20
- type: z.literal('integer'),
21
- description: z.string().optional(),
22
- default: z.number().int().optional(),
23
- minimum: z.number().int().optional(),
24
- maximum: z.number().int().optional(),
25
- enum: z.number().int().array().optional(),
26
- const: z.number().int().optional(),
27
- })
28
- .strict()
25
+ export const lexInteger = z.object({
26
+ type: z.literal('integer'),
27
+ description: z.string().optional(),
28
+ default: z.number().int().optional(),
29
+ minimum: z.number().int().optional(),
30
+ maximum: z.number().int().optional(),
31
+ enum: z.number().int().array().optional(),
32
+ const: z.number().int().optional(),
33
+ })
29
34
  export type LexInteger = z.infer<typeof lexInteger>
30
35
 
31
36
  export const lexStringFormat = z.enum([
@@ -43,29 +48,25 @@ export const lexStringFormat = z.enum([
43
48
  ])
44
49
  export type LexStringFormat = z.infer<typeof lexStringFormat>
45
50
 
46
- export const lexString = z
47
- .object({
48
- type: z.literal('string'),
49
- format: lexStringFormat.optional(),
50
- description: z.string().optional(),
51
- default: z.string().optional(),
52
- minLength: z.number().int().optional(),
53
- maxLength: z.number().int().optional(),
54
- minGraphemes: z.number().int().optional(),
55
- maxGraphemes: z.number().int().optional(),
56
- enum: z.string().array().optional(),
57
- const: z.string().optional(),
58
- knownValues: z.string().array().optional(),
59
- })
60
- .strict()
51
+ export const lexString = z.object({
52
+ type: z.literal('string'),
53
+ format: lexStringFormat.optional(),
54
+ description: z.string().optional(),
55
+ default: z.string().optional(),
56
+ minLength: z.number().int().optional(),
57
+ maxLength: z.number().int().optional(),
58
+ minGraphemes: z.number().int().optional(),
59
+ maxGraphemes: z.number().int().optional(),
60
+ enum: z.string().array().optional(),
61
+ const: z.string().optional(),
62
+ knownValues: z.string().array().optional(),
63
+ })
61
64
  export type LexString = z.infer<typeof lexString>
62
65
 
63
- export const lexUnknown = z
64
- .object({
65
- type: z.literal('unknown'),
66
- description: z.string().optional(),
67
- })
68
- .strict()
66
+ export const lexUnknown = z.object({
67
+ type: z.literal('unknown'),
68
+ description: z.string().optional(),
69
+ })
69
70
  export type LexUnknown = z.infer<typeof lexUnknown>
70
71
 
71
72
  export const lexPrimitive = z.discriminatedUnion('type', [
@@ -79,22 +80,18 @@ export type LexPrimitive = z.infer<typeof lexPrimitive>
79
80
  // ipld types
80
81
  // =
81
82
 
82
- export const lexBytes = z
83
- .object({
84
- type: z.literal('bytes'),
85
- description: z.string().optional(),
86
- maxLength: z.number().optional(),
87
- minLength: z.number().optional(),
88
- })
89
- .strict()
83
+ export const lexBytes = z.object({
84
+ type: z.literal('bytes'),
85
+ description: z.string().optional(),
86
+ maxLength: z.number().optional(),
87
+ minLength: z.number().optional(),
88
+ })
90
89
  export type LexBytes = z.infer<typeof lexBytes>
91
90
 
92
- export const lexCidLink = z
93
- .object({
94
- type: z.literal('cid-link'),
95
- description: z.string().optional(),
96
- })
97
- .strict()
91
+ export const lexCidLink = z.object({
92
+ type: z.literal('cid-link'),
93
+ description: z.string().optional(),
94
+ })
98
95
  export type LexCidLink = z.infer<typeof lexCidLink>
99
96
 
100
97
  export const lexIpldType = z.discriminatedUnion('type', [lexBytes, lexCidLink])
@@ -103,23 +100,19 @@ export type LexIpldType = z.infer<typeof lexIpldType>
103
100
  // references
104
101
  // =
105
102
 
106
- export const lexRef = z
107
- .object({
108
- type: z.literal('ref'),
109
- description: z.string().optional(),
110
- ref: z.string(),
111
- })
112
- .strict()
103
+ export const lexRef = z.object({
104
+ type: z.literal('ref'),
105
+ description: z.string().optional(),
106
+ ref: z.string(),
107
+ })
113
108
  export type LexRef = z.infer<typeof lexRef>
114
109
 
115
- export const lexRefUnion = z
116
- .object({
117
- type: z.literal('union'),
118
- description: z.string().optional(),
119
- refs: z.string().array(),
120
- closed: z.boolean().optional(),
121
- })
122
- .strict()
110
+ export const lexRefUnion = z.object({
111
+ type: z.literal('union'),
112
+ description: z.string().optional(),
113
+ refs: z.string().array(),
114
+ closed: z.boolean().optional(),
115
+ })
123
116
  export type LexRefUnion = z.infer<typeof lexRefUnion>
124
117
 
125
118
  export const lexRefVariant = z.discriminatedUnion('type', [lexRef, lexRefUnion])
@@ -128,59 +121,51 @@ export type LexRefVariant = z.infer<typeof lexRefVariant>
128
121
  // blobs
129
122
  // =
130
123
 
131
- export const lexBlob = z
132
- .object({
133
- type: z.literal('blob'),
134
- description: z.string().optional(),
135
- accept: z.string().array().optional(),
136
- maxSize: z.number().optional(),
137
- })
138
- .strict()
124
+ export const lexBlob = z.object({
125
+ type: z.literal('blob'),
126
+ description: z.string().optional(),
127
+ accept: z.string().array().optional(),
128
+ maxSize: z.number().optional(),
129
+ })
139
130
  export type LexBlob = z.infer<typeof lexBlob>
140
131
 
141
132
  // complex types
142
133
  // =
143
134
 
144
- export const lexArray = z
145
- .object({
146
- type: z.literal('array'),
147
- description: z.string().optional(),
148
- items: z.discriminatedUnion('type', [
149
- // lexPrimitive
150
- lexBoolean,
151
- lexInteger,
152
- lexString,
153
- lexUnknown,
154
- // lexIpldType
155
- lexBytes,
156
- lexCidLink,
157
- // lexRefVariant
158
- lexRef,
159
- lexRefUnion,
160
- // other
161
- lexBlob,
162
- ]),
163
- minLength: z.number().int().optional(),
164
- maxLength: z.number().int().optional(),
165
- })
166
- .strict()
135
+ export const lexArray = z.object({
136
+ type: z.literal('array'),
137
+ description: z.string().optional(),
138
+ items: z.discriminatedUnion('type', [
139
+ // lexPrimitive
140
+ lexBoolean,
141
+ lexInteger,
142
+ lexString,
143
+ lexUnknown,
144
+ // lexIpldType
145
+ lexBytes,
146
+ lexCidLink,
147
+ // lexRefVariant
148
+ lexRef,
149
+ lexRefUnion,
150
+ // other
151
+ lexBlob,
152
+ ]),
153
+ minLength: z.number().int().optional(),
154
+ maxLength: z.number().int().optional(),
155
+ })
167
156
  export type LexArray = z.infer<typeof lexArray>
168
157
 
169
158
  export const lexPrimitiveArray = lexArray.merge(
170
- z
171
- .object({
172
- items: lexPrimitive,
173
- })
174
- .strict(),
159
+ z.object({
160
+ items: lexPrimitive,
161
+ }),
175
162
  )
176
163
  export type LexPrimitiveArray = z.infer<typeof lexPrimitiveArray>
177
164
 
178
- export const lexToken = z
179
- .object({
180
- type: z.literal('token'),
181
- description: z.string().optional(),
182
- })
183
- .strict()
165
+ export const lexToken = z.object({
166
+ type: z.literal('token'),
167
+ description: z.string().optional(),
168
+ })
184
169
  export type LexToken = z.infer<typeof lexToken>
185
170
 
186
171
  export const lexObject = z
@@ -190,6 +175,7 @@ export const lexObject = z
190
175
  required: z.string().array().optional(),
191
176
  nullable: z.string().array().optional(),
192
177
  properties: z.record(
178
+ z.string(),
193
179
  z.discriminatedUnion('type', [
194
180
  lexArray,
195
181
 
@@ -209,10 +195,45 @@ export const lexObject = z
209
195
  ]),
210
196
  ),
211
197
  })
212
- .strict()
213
198
  .superRefine(requiredPropertiesRefinement)
214
199
  export type LexObject = z.infer<typeof lexObject>
215
200
 
201
+ // permissions
202
+ // =
203
+
204
+ const lexPermission = z.intersection(
205
+ z.object({
206
+ type: z.literal('permission'),
207
+ resource: z.string().nonempty(),
208
+ }),
209
+ z.record(
210
+ z.string(),
211
+ z
212
+ .union([
213
+ z.array(z.union([z.string(), z.number().int(), z.boolean()])),
214
+
215
+ z.boolean(),
216
+ z.number().int(),
217
+ z.string(),
218
+ ])
219
+ .optional(),
220
+ ),
221
+ )
222
+
223
+ export type LexPermission = z.infer<typeof lexPermission>
224
+
225
+ export const lexPermissionSet = z.object({
226
+ type: z.literal('permission-set'),
227
+ description: z.string().optional(),
228
+ title: z.string().optional(),
229
+ 'title:lang': lexLang.optional(),
230
+ detail: z.string().optional(),
231
+ 'detail:lang': lexLang.optional(),
232
+ permissions: z.array(lexPermission),
233
+ })
234
+
235
+ export type LexPermissionSet = z.infer<typeof lexPermissionSet>
236
+
216
237
  // xrpc
217
238
  // =
218
239
 
@@ -222,6 +243,7 @@ export const lexXrpcParameters = z
222
243
  description: z.string().optional(),
223
244
  required: z.string().array().optional(),
224
245
  properties: z.record(
246
+ z.string(),
225
247
  z.discriminatedUnion('type', [
226
248
  lexPrimitiveArray,
227
249
 
@@ -233,84 +255,69 @@ export const lexXrpcParameters = z
233
255
  ]),
234
256
  ),
235
257
  })
236
- .strict()
237
258
  .superRefine(requiredPropertiesRefinement)
238
259
  export type LexXrpcParameters = z.infer<typeof lexXrpcParameters>
239
260
 
240
- export const lexXrpcBody = z
241
- .object({
242
- description: z.string().optional(),
243
- encoding: z.string(),
244
- // @NOTE using discriminatedUnion with a refined schema requires zod >= 4
245
- schema: z.union([lexRefVariant, lexObject]).optional(),
246
- })
247
- .strict()
261
+ export const lexXrpcBody = z.object({
262
+ description: z.string().optional(),
263
+ encoding: z.string(),
264
+ // @NOTE using discriminatedUnion with a refined schema requires zod >= 4
265
+ schema: z.union([lexRefVariant, lexObject]).optional(),
266
+ })
248
267
  export type LexXrpcBody = z.infer<typeof lexXrpcBody>
249
268
 
250
- export const lexXrpcSubscriptionMessage = z
251
- .object({
252
- description: z.string().optional(),
253
- // @NOTE using discriminatedUnion with a refined schema requires zod >= 4
254
- schema: z.union([lexRefVariant, lexObject]).optional(),
255
- })
256
- .strict()
269
+ export const lexXrpcSubscriptionMessage = z.object({
270
+ description: z.string().optional(),
271
+ // @NOTE using discriminatedUnion with a refined schema requires zod >= 4
272
+ schema: z.union([lexRefVariant, lexObject]).optional(),
273
+ })
257
274
  export type LexXrpcSubscriptionMessage = z.infer<
258
275
  typeof lexXrpcSubscriptionMessage
259
276
  >
260
277
 
261
- export const lexXrpcError = z
262
- .object({
263
- name: z.string(),
264
- description: z.string().optional(),
265
- })
266
- .strict()
278
+ export const lexXrpcError = z.object({
279
+ name: z.string(),
280
+ description: z.string().optional(),
281
+ })
267
282
  export type LexXrpcError = z.infer<typeof lexXrpcError>
268
283
 
269
- export const lexXrpcQuery = z
270
- .object({
271
- type: z.literal('query'),
272
- description: z.string().optional(),
273
- parameters: lexXrpcParameters.optional(),
274
- output: lexXrpcBody.optional(),
275
- errors: lexXrpcError.array().optional(),
276
- })
277
- .strict()
284
+ export const lexXrpcQuery = z.object({
285
+ type: z.literal('query'),
286
+ description: z.string().optional(),
287
+ parameters: lexXrpcParameters.optional(),
288
+ output: lexXrpcBody.optional(),
289
+ errors: lexXrpcError.array().optional(),
290
+ })
278
291
  export type LexXrpcQuery = z.infer<typeof lexXrpcQuery>
279
292
 
280
- export const lexXrpcProcedure = z
281
- .object({
282
- type: z.literal('procedure'),
283
- description: z.string().optional(),
284
- parameters: lexXrpcParameters.optional(),
285
- input: lexXrpcBody.optional(),
286
- output: lexXrpcBody.optional(),
287
- errors: lexXrpcError.array().optional(),
288
- })
289
- .strict()
293
+ export const lexXrpcProcedure = z.object({
294
+ type: z.literal('procedure'),
295
+ description: z.string().optional(),
296
+ parameters: lexXrpcParameters.optional(),
297
+ input: lexXrpcBody.optional(),
298
+ output: lexXrpcBody.optional(),
299
+ errors: lexXrpcError.array().optional(),
300
+ })
290
301
  export type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure>
291
302
 
292
- export const lexXrpcSubscription = z
293
- .object({
294
- type: z.literal('subscription'),
295
- description: z.string().optional(),
296
- parameters: lexXrpcParameters.optional(),
297
- message: lexXrpcSubscriptionMessage.optional(),
298
- errors: lexXrpcError.array().optional(),
299
- })
300
- .strict()
303
+ export const lexXrpcSubscription = z.object({
304
+ type: z.literal('subscription'),
305
+ description: z.string().optional(),
306
+ parameters: lexXrpcParameters.optional(),
307
+ message: lexXrpcSubscriptionMessage.optional(),
308
+ errors: lexXrpcError.array().optional(),
309
+ })
301
310
  export type LexXrpcSubscription = z.infer<typeof lexXrpcSubscription>
302
311
 
303
312
  // database
304
313
  // =
305
314
 
306
- export const lexRecord = z
307
- .object({
308
- type: z.literal('record'),
309
- description: z.string().optional(),
310
- key: z.string().optional(),
311
- record: lexObject,
312
- })
313
- .strict()
315
+ export const lexRecord = z.object({
316
+ type: z.literal('record'),
317
+ description: z.string().optional(),
318
+ key: z.string().optional(),
319
+ record: lexObject,
320
+ })
314
321
  export type LexRecord = z.infer<typeof lexRecord>
315
322
 
316
323
  // core
@@ -322,6 +329,7 @@ export type LexRecord = z.infer<typeof lexRecord>
322
329
  // see #915 for details
323
330
  export const lexUserType = z.custom<
324
331
  | LexRecord
332
+ | LexPermissionSet
325
333
  | LexXrpcQuery
326
334
  | LexXrpcProcedure
327
335
  | LexXrpcSubscription
@@ -349,6 +357,9 @@ export const lexUserType = z.custom<
349
357
  case 'record':
350
358
  return lexRecord.parse(val)
351
359
 
360
+ case 'permission-set':
361
+ return lexPermissionSet.parse(val)
362
+
352
363
  case 'query':
353
364
  return lexXrpcQuery.parse(val)
354
365
  case 'procedure':
@@ -412,34 +423,34 @@ export type LexUserType = z.infer<typeof lexUserType>
412
423
 
413
424
  export const lexiconDoc = z
414
425
  .object({
415
- // Compatibility with lexicon publishing
416
- $type: z.literal('com.atproto.lexicon.schema').optional(),
417
426
  lexicon: z.literal(1),
418
- id: z.string().refine((v: string) => NSID.isValid(v), {
427
+ id: z.string().refine(isValidNsid, {
419
428
  message: 'Must be a valid NSID',
420
429
  }),
421
430
  revision: z.number().optional(),
422
431
  description: z.string().optional(),
423
- defs: z.record(lexUserType),
432
+ defs: z.record(z.string(), lexUserType),
424
433
  })
425
- .strict()
426
- .superRefine((doc, ctx) => {
427
- for (const defId in doc.defs) {
428
- const def = doc.defs[defId]
429
- if (
430
- defId !== 'main' &&
431
- (def.type === 'record' ||
432
- def.type === 'procedure' ||
433
- def.type === 'query' ||
434
- def.type === 'subscription')
435
- ) {
436
- ctx.addIssue({
437
- code: z.ZodIssueCode.custom,
438
- message: `Records, procedures, queries, and subscriptions must be the main definition.`,
439
- })
434
+ .refine(
435
+ (doc) => {
436
+ for (const [defId, def] of Object.entries(doc.defs)) {
437
+ if (
438
+ defId !== 'main' &&
439
+ (def.type === 'record' ||
440
+ def.type === 'permission-set' ||
441
+ def.type === 'procedure' ||
442
+ def.type === 'query' ||
443
+ def.type === 'subscription')
444
+ ) {
445
+ return false
446
+ }
440
447
  }
441
- }
442
- })
448
+ return true
449
+ },
450
+ {
451
+ message: `Records, permission sets, procedures, queries, and subscriptions must be the main definition.`,
452
+ },
453
+ )
443
454
  export type LexiconDoc = z.infer<typeof lexiconDoc>
444
455
 
445
456
  // helpers
@@ -5,8 +5,8 @@ import {
5
5
  ensureValidAtUri,
6
6
  ensureValidDid,
7
7
  ensureValidHandle,
8
- ensureValidNsid,
9
8
  ensureValidRecordKey,
9
+ isValidNsid,
10
10
  isValidTid,
11
11
  } from '@atproto/syntax'
12
12
  import { ValidationError, ValidationResult } from '../types'
@@ -94,15 +94,17 @@ export function atIdentifier(path: string, value: string): ValidationResult {
94
94
  }
95
95
 
96
96
  export function nsid(path: string, value: string): ValidationResult {
97
- try {
98
- ensureValidNsid(value)
99
- } catch {
97
+ if (isValidNsid(value)) {
98
+ return {
99
+ success: true,
100
+ value,
101
+ }
102
+ } else {
100
103
  return {
101
104
  success: false,
102
105
  error: new ValidationError(`${path} must be a valid nsid`),
103
106
  }
104
107
  }
105
- return { success: true, value }
106
108
  }
107
109
 
108
110
  export function cid(path: string, value: string): ValidationResult {
@@ -100,13 +100,14 @@ describe('General validation', () => {
100
100
  parseLexiconDoc(schema)
101
101
  }).toThrow('Required field \\"foo\\" not defined')
102
102
  })
103
- it('fails when unknown fields are present', () => {
103
+ it('allows unknown fields to be present', () => {
104
104
  const schema = {
105
105
  lexicon: 1,
106
106
  id: 'com.example.unknownFields',
107
107
  defs: {
108
108
  test: {
109
109
  type: 'object',
110
+ properties: {},
110
111
  foo: 3,
111
112
  },
112
113
  },
@@ -114,7 +115,7 @@ describe('General validation', () => {
114
115
 
115
116
  expect(() => {
116
117
  parseLexiconDoc(schema)
117
- }).toThrow("Unrecognized key(s) in object: 'foo'")
118
+ }).not.toThrow()
118
119
  })
119
120
  it('fails lexicon parsing when uri is invalid', () => {
120
121
  const schema: LexiconDoc = {