@atproto/lexicon 0.0.4 → 0.2.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.
Files changed (47) hide show
  1. package/dist/blob-refs.d.ts +67 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +12944 -680
  4. package/dist/index.js.map +4 -4
  5. package/dist/lexicons.d.ts +6 -4
  6. package/dist/serialize.d.ts +12 -0
  7. package/dist/src/index.d.ts +2 -0
  8. package/dist/src/lexicons.d.ts +15 -0
  9. package/dist/src/record/index.d.ts +4 -0
  10. package/dist/src/record/schema.d.ts +9 -0
  11. package/dist/src/record/schemas.d.ts +10 -0
  12. package/dist/src/record/util.d.ts +1 -0
  13. package/dist/src/record/validation.d.ts +24 -0
  14. package/dist/src/record/validator.d.ts +17 -0
  15. package/dist/src/types.d.ts +30268 -0
  16. package/dist/src/util.d.ts +6 -0
  17. package/dist/src/validation.d.ts +6 -0
  18. package/dist/src/validators/blob.d.ts +6 -0
  19. package/dist/src/validators/complex.d.ts +5 -0
  20. package/dist/src/validators/primitives.d.ts +9 -0
  21. package/dist/src/validators/xrpc.d.ts +3 -0
  22. package/dist/tsconfig.build.tsbuildinfo +1 -0
  23. package/dist/types.d.ts +14999 -23510
  24. package/dist/util.d.ts +6 -1
  25. package/dist/validation.d.ts +6 -5
  26. package/dist/validators/blob.d.ts +0 -3
  27. package/dist/validators/complex.d.ts +2 -2
  28. package/dist/validators/formats.d.ts +10 -0
  29. package/dist/validators/primitives.d.ts +2 -2
  30. package/dist/validators/xrpc.d.ts +1 -1
  31. package/package.json +13 -4
  32. package/src/blob-refs.ts +70 -0
  33. package/src/index.ts +2 -0
  34. package/src/lexicons.ts +36 -5
  35. package/src/serialize.ts +93 -0
  36. package/src/types.ts +306 -180
  37. package/src/util.ts +64 -5
  38. package/src/validation.ts +28 -4
  39. package/src/validators/blob.ts +5 -43
  40. package/src/validators/complex.ts +31 -32
  41. package/src/validators/formats.ts +121 -0
  42. package/src/validators/primitives.ts +114 -67
  43. package/src/validators/xrpc.ts +29 -29
  44. package/tests/_scaffolds/lexicons.ts +178 -51
  45. package/tests/general.test.ts +496 -177
  46. package/tsconfig.build.tsbuildinfo +1 -1
  47. package/tsconfig.json +3 -1
package/src/validation.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Lexicons } from './lexicons'
2
- import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types'
2
+ import {
3
+ LexRecord,
4
+ LexXrpcProcedure,
5
+ LexXrpcQuery,
6
+ LexXrpcSubscription,
7
+ } from './types'
3
8
  import { assertValidOneOf } from './util'
4
9
 
5
10
  import * as ComplexValidators from './validators/complex'
@@ -12,16 +17,18 @@ export function assertValidRecord(
12
17
  ) {
13
18
  const res = ComplexValidators.object(lexicons, 'Record', def.record, value)
14
19
  if (!res.success) throw res.error
20
+ return res.value
15
21
  }
16
22
 
17
23
  export function assertValidXrpcParams(
18
24
  lexicons: Lexicons,
19
- def: LexXrpcProcedure | LexXrpcQuery,
25
+ def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription,
20
26
  value: unknown,
21
27
  ) {
22
28
  if (def.parameters) {
23
29
  const res = XrpcValidators.params(lexicons, 'Params', def.parameters, value)
24
30
  if (!res.success) throw res.error
31
+ return res.value
25
32
  }
26
33
  }
27
34
 
@@ -32,7 +39,7 @@ export function assertValidXrpcInput(
32
39
  ) {
33
40
  if (def.input?.schema) {
34
41
  // loop: all input schema definitions
35
- assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
42
+ return assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
36
43
  }
37
44
  }
38
45
 
@@ -43,6 +50,23 @@ export function assertValidXrpcOutput(
43
50
  ) {
44
51
  if (def.output?.schema) {
45
52
  // loop: all output schema definitions
46
- assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
53
+ return assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
54
+ }
55
+ }
56
+
57
+ export function assertValidXrpcMessage(
58
+ lexicons: Lexicons,
59
+ def: LexXrpcSubscription,
60
+ value: unknown,
61
+ ) {
62
+ if (def.message?.schema) {
63
+ // loop: all output schema definitions
64
+ return assertValidOneOf(
65
+ lexicons,
66
+ 'Message',
67
+ def.message.schema,
68
+ value,
69
+ true,
70
+ )
47
71
  }
48
72
  }
@@ -1,6 +1,6 @@
1
+ import { BlobRef } from '../blob-refs'
1
2
  import { Lexicons } from '../lexicons'
2
3
  import { LexUserType, ValidationResult, ValidationError } from '../types'
3
- import { isObj, hasProp } from '../types'
4
4
 
5
5
  export function blob(
6
6
  lexicons: Lexicons,
@@ -8,50 +8,12 @@ export function blob(
8
8
  def: LexUserType,
9
9
  value: unknown,
10
10
  ): ValidationResult {
11
- if (!isObj(value)) {
11
+ // check
12
+ if (!value || !(value instanceof BlobRef)) {
12
13
  return {
13
14
  success: false,
14
- error: new ValidationError(`${path} should be an object`),
15
+ error: new ValidationError(`${path} should be a blob ref`),
15
16
  }
16
17
  }
17
- if (!hasProp(value, 'cid') || typeof value.cid !== 'string') {
18
- return {
19
- success: false,
20
- error: new ValidationError(`${path}/cid should be a string`),
21
- }
22
- }
23
- if (!hasProp(value, 'mimeType') || typeof value.mimeType !== 'string') {
24
- return {
25
- success: false,
26
- error: new ValidationError(`${path}/mimeType should be a string`),
27
- }
28
- }
29
- return { success: true }
30
- }
31
-
32
- export function image(
33
- lexicons: Lexicons,
34
- path: string,
35
- def: LexUserType,
36
- value: unknown,
37
- ): ValidationResult {
38
- return blob(lexicons, path, def, value)
39
- }
40
-
41
- export function video(
42
- lexicons: Lexicons,
43
- path: string,
44
- def: LexUserType,
45
- value: unknown,
46
- ): ValidationResult {
47
- return blob(lexicons, path, def, value)
48
- }
49
-
50
- export function audio(
51
- lexicons: Lexicons,
52
- path: string,
53
- def: LexUserType,
54
- value: unknown,
55
- ): ValidationResult {
56
- return blob(lexicons, path, def, value)
18
+ return { success: true, value }
57
19
  }
@@ -20,14 +20,14 @@ export function validate(
20
20
  switch (def.type) {
21
21
  case 'boolean':
22
22
  return Primitives.boolean(lexicons, path, def, value)
23
- case 'number':
24
- return Primitives.number(lexicons, path, def, value)
25
23
  case 'integer':
26
24
  return Primitives.integer(lexicons, path, def, value)
27
25
  case 'string':
28
26
  return Primitives.string(lexicons, path, def, value)
29
- case 'datetime':
30
- return Primitives.datetime(lexicons, path, def, value)
27
+ case 'bytes':
28
+ return Primitives.bytes(lexicons, path, def, value)
29
+ case 'cid-link':
30
+ return Primitives.cidLink(lexicons, path, def, value)
31
31
  case 'unknown':
32
32
  return Primitives.unknown(lexicons, path, def, value)
33
33
  case 'object':
@@ -36,12 +36,6 @@ export function validate(
36
36
  return array(lexicons, path, def, value)
37
37
  case 'blob':
38
38
  return Blob.blob(lexicons, path, def, value)
39
- case 'image':
40
- return Blob.image(lexicons, path, def, value)
41
- case 'video':
42
- return Blob.video(lexicons, path, def, value)
43
- case 'audio':
44
- return Blob.audio(lexicons, path, def, value)
45
39
  default:
46
40
  return {
47
41
  success: false,
@@ -53,11 +47,9 @@ export function validate(
53
47
  export function array(
54
48
  lexicons: Lexicons,
55
49
  path: string,
56
- def: LexUserType,
50
+ def: LexArray,
57
51
  value: unknown,
58
52
  ): ValidationResult {
59
- def = def as LexArray
60
-
61
53
  // type
62
54
  if (!Array.isArray(value)) {
63
55
  return {
@@ -101,7 +93,7 @@ export function array(
101
93
  }
102
94
  }
103
95
 
104
- return { success: true }
96
+ return { success: true, value }
105
97
  }
106
98
 
107
99
  export function object(
@@ -120,33 +112,40 @@ export function object(
120
112
  }
121
113
  }
122
114
 
123
- // required
124
- if (Array.isArray(def.required)) {
125
- for (const key of def.required) {
126
- if (!(key in value)) {
127
- return {
128
- success: false,
129
- error: new ValidationError(`${path} must have the property "${key}"`),
130
- }
131
- }
132
- }
133
- }
115
+ const requiredProps = new Set(def.required)
116
+ const nullableProps = new Set(def.nullable)
134
117
 
135
118
  // properties
119
+ let resultValue = value
136
120
  if (typeof def.properties === 'object') {
137
121
  for (const key in def.properties) {
138
- const propValue = value[key]
139
- if (typeof propValue === 'undefined') {
140
- continue // skip- if required, will have already failed
122
+ if (value[key] === null && nullableProps.has(key)) {
123
+ continue
141
124
  }
142
125
  const propDef = def.properties[key]
143
126
  const propPath = `${path}/${key}`
144
- const res = validateOneOf(lexicons, propPath, propDef, propValue)
145
- if (!res.success) {
146
- return res
127
+ const validated = validateOneOf(lexicons, propPath, propDef, value[key])
128
+ const propValue = validated.success ? validated.value : value[key]
129
+ const propIsUndefined = typeof propValue === 'undefined'
130
+ // Return error for bad validation, giving required rule precedence
131
+ if (propIsUndefined && requiredProps.has(key)) {
132
+ return {
133
+ success: false,
134
+ error: new ValidationError(`${path} must have the property "${key}"`),
135
+ }
136
+ } else if (!propIsUndefined && !validated.success) {
137
+ return validated
138
+ }
139
+ // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value
140
+ if (propValue !== value[key]) {
141
+ if (resultValue === value) {
142
+ // Lazy shallow clone
143
+ resultValue = { ...value }
144
+ }
145
+ resultValue[key] = propValue
147
146
  }
148
147
  }
149
148
  }
150
149
 
151
- return { success: true }
150
+ return { success: true, value: resultValue }
152
151
  }
@@ -0,0 +1,121 @@
1
+ import { ensureValidAtUri } from '@atproto/uri'
2
+ import { isValidISODateString } from 'iso-datestring-validator'
3
+ import { CID } from 'multiformats/cid'
4
+ import { ValidationResult, ValidationError } from '../types'
5
+ import { ensureValidDid, ensureValidHandle } from '@atproto/identifier'
6
+ import { ensureValidNsid } from '@atproto/nsid'
7
+ import { validateLanguage } from '@atproto/common-web'
8
+
9
+ export function datetime(path: string, value: string): ValidationResult {
10
+ try {
11
+ if (!isValidISODateString(value)) {
12
+ throw new Error()
13
+ }
14
+ } catch {
15
+ return {
16
+ success: false,
17
+ error: new ValidationError(
18
+ `${path} must be an iso8601 formatted datetime`,
19
+ ),
20
+ }
21
+ }
22
+ return { success: true, value }
23
+ }
24
+
25
+ export function uri(path: string, value: string): ValidationResult {
26
+ const isUri = value.match(/^\w+:(?:\/\/)?[^\s/][^\s]*$/) !== null
27
+ if (!isUri) {
28
+ return {
29
+ success: false,
30
+ error: new ValidationError(`${path} must be a uri`),
31
+ }
32
+ }
33
+ return { success: true, value }
34
+ }
35
+
36
+ export function atUri(path: string, value: string): ValidationResult {
37
+ try {
38
+ ensureValidAtUri(value)
39
+ } catch {
40
+ return {
41
+ success: false,
42
+ error: new ValidationError(`${path} must be a valid at-uri`),
43
+ }
44
+ }
45
+ return { success: true, value }
46
+ }
47
+
48
+ export function did(path: string, value: string): ValidationResult {
49
+ try {
50
+ ensureValidDid(value)
51
+ } catch {
52
+ return {
53
+ success: false,
54
+ error: new ValidationError(`${path} must be a valid did`),
55
+ }
56
+ }
57
+ return { success: true, value }
58
+ }
59
+
60
+ export function handle(path: string, value: string): ValidationResult {
61
+ try {
62
+ ensureValidHandle(value)
63
+ } catch {
64
+ return {
65
+ success: false,
66
+ error: new ValidationError(`${path} must be a valid handle`),
67
+ }
68
+ }
69
+ return { success: true, value }
70
+ }
71
+
72
+ export function atIdentifier(path: string, value: string): ValidationResult {
73
+ const isDid = did(path, value)
74
+ if (!isDid.success) {
75
+ const isHandle = handle(path, value)
76
+ if (!isHandle.success) {
77
+ return {
78
+ success: false,
79
+ error: new ValidationError(`${path} must be a valid did or a handle`),
80
+ }
81
+ }
82
+ }
83
+ return { success: true, value }
84
+ }
85
+
86
+ export function nsid(path: string, value: string): ValidationResult {
87
+ try {
88
+ ensureValidNsid(value)
89
+ } catch {
90
+ return {
91
+ success: false,
92
+ error: new ValidationError(`${path} must be a valid nsid`),
93
+ }
94
+ }
95
+ return { success: true, value }
96
+ }
97
+
98
+ export function cid(path: string, value: string): ValidationResult {
99
+ try {
100
+ CID.parse(value)
101
+ } catch {
102
+ return {
103
+ success: false,
104
+ error: new ValidationError(`${path} must be a cid string`),
105
+ }
106
+ }
107
+ return { success: true, value }
108
+ }
109
+
110
+ // The language format validates well-formed BCP 47 language tags: https://www.rfc-editor.org/info/bcp47
111
+ export function language(path: string, value: string): ValidationResult {
112
+ if (validateLanguage(value)) {
113
+ return { success: true, value }
114
+ }
115
+ return {
116
+ success: false,
117
+ error: new ValidationError(
118
+ `${path} must be a well-formed BCP 47 language tag`,
119
+ ),
120
+ }
121
+ }
@@ -1,14 +1,15 @@
1
- import { isValidISODateString } from 'iso-datestring-validator'
1
+ import { utf8Len, graphemeLen } from '@atproto/common-web'
2
+ import { CID } from 'multiformats/cid'
2
3
  import { Lexicons } from '../lexicons'
4
+ import * as formats from './formats'
3
5
  import {
4
6
  LexUserType,
5
7
  LexBoolean,
6
- LexNumber,
7
8
  LexInteger,
8
9
  LexString,
9
- LexDatetime,
10
10
  ValidationResult,
11
11
  ValidationError,
12
+ LexBytes,
12
13
  } from '../types'
13
14
 
14
15
  export function validate(
@@ -20,14 +21,14 @@ export function validate(
20
21
  switch (def.type) {
21
22
  case 'boolean':
22
23
  return boolean(lexicons, path, def, value)
23
- case 'number':
24
- return number(lexicons, path, def, value)
25
24
  case 'integer':
26
25
  return integer(lexicons, path, def, value)
27
26
  case 'string':
28
27
  return string(lexicons, path, def, value)
29
- case 'datetime':
30
- return datetime(lexicons, path, def, value)
28
+ case 'bytes':
29
+ return bytes(lexicons, path, def, value)
30
+ case 'cid-link':
31
+ return cidLink(lexicons, path, def, value)
31
32
  case 'unknown':
32
33
  return unknown(lexicons, path, def, value)
33
34
  default:
@@ -48,9 +49,9 @@ export function boolean(
48
49
 
49
50
  // type
50
51
  const type = typeof value
51
- if (type == 'undefined') {
52
+ if (type === 'undefined') {
52
53
  if (typeof def.default === 'boolean') {
53
- return { success: true }
54
+ return { success: true, value: def.default }
54
55
  }
55
56
  return {
56
57
  success: false,
@@ -73,31 +74,31 @@ export function boolean(
73
74
  }
74
75
  }
75
76
 
76
- return { success: true }
77
+ return { success: true, value }
77
78
  }
78
79
 
79
- export function number(
80
+ export function integer(
80
81
  lexicons: Lexicons,
81
82
  path: string,
82
83
  def: LexUserType,
83
84
  value: unknown,
84
85
  ): ValidationResult {
85
- def = def as LexNumber
86
+ def = def as LexInteger
86
87
 
87
88
  // type
88
89
  const type = typeof value
89
- if (type == 'undefined') {
90
+ if (type === 'undefined') {
90
91
  if (typeof def.default === 'number') {
91
- return { success: true }
92
+ return { success: true, value: def.default }
92
93
  }
93
94
  return {
94
95
  success: false,
95
- error: new ValidationError(`${path} must be a number`),
96
+ error: new ValidationError(`${path} must be an integer`),
96
97
  }
97
- } else if (type !== 'number') {
98
+ } else if (!Number.isInteger(value)) {
98
99
  return {
99
100
  success: false,
100
- error: new ValidationError(`${path} must be a number`),
101
+ error: new ValidationError(`${path} must be an integer`),
101
102
  }
102
103
  }
103
104
 
@@ -147,32 +148,7 @@ export function number(
147
148
  }
148
149
  }
149
150
 
150
- return { success: true }
151
- }
152
-
153
- export function integer(
154
- lexicons: Lexicons,
155
- path: string,
156
- def: LexUserType,
157
- value: unknown,
158
- ): ValidationResult {
159
- def = def as LexInteger
160
-
161
- // run number validation
162
- const numRes = number(lexicons, path, def, value)
163
- if (!numRes.success) {
164
- return numRes
165
- }
166
-
167
- // whole numbers only
168
- if (!Number.isInteger(value)) {
169
- return {
170
- success: false,
171
- error: new ValidationError(`${path} must be an integer`),
172
- }
173
- }
174
-
175
- return { success: true }
151
+ return { success: true, value }
176
152
  }
177
153
 
178
154
  export function string(
@@ -184,16 +160,15 @@ export function string(
184
160
  def = def as LexString
185
161
 
186
162
  // type
187
- const type = typeof value
188
- if (type == 'undefined') {
163
+ if (typeof value === 'undefined') {
189
164
  if (typeof def.default === 'string') {
190
- return { success: true }
165
+ return { success: true, value: def.default }
191
166
  }
192
167
  return {
193
168
  success: false,
194
169
  error: new ValidationError(`${path} must be a string`),
195
170
  }
196
- } else if (type !== 'string') {
171
+ } else if (typeof value !== 'string') {
197
172
  return {
198
173
  success: false,
199
174
  error: new ValidationError(`${path} must be a string`),
@@ -224,7 +199,7 @@ export function string(
224
199
 
225
200
  // maxLength
226
201
  if (typeof def.maxLength === 'number') {
227
- if ((value as string).length > def.maxLength) {
202
+ if (utf8Len(value) > def.maxLength) {
228
203
  return {
229
204
  success: false,
230
205
  error: new ValidationError(
@@ -236,7 +211,7 @@ export function string(
236
211
 
237
212
  // minLength
238
213
  if (typeof def.minLength === 'number') {
239
- if ((value as string).length < def.minLength) {
214
+ if (utf8Len(value) < def.minLength) {
240
215
  return {
241
216
  success: false,
242
217
  error: new ValidationError(
@@ -246,40 +221,112 @@ export function string(
246
221
  }
247
222
  }
248
223
 
249
- return { success: true }
224
+ // maxGraphemes
225
+ if (typeof def.maxGraphemes === 'number') {
226
+ if (graphemeLen(value) > def.maxGraphemes) {
227
+ return {
228
+ success: false,
229
+ error: new ValidationError(
230
+ `${path} must not be longer than ${def.maxGraphemes} graphemes`,
231
+ ),
232
+ }
233
+ }
234
+ }
235
+
236
+ // minGraphemes
237
+ if (typeof def.minGraphemes === 'number') {
238
+ if (graphemeLen(value) < def.minGraphemes) {
239
+ return {
240
+ success: false,
241
+ error: new ValidationError(
242
+ `${path} must not be shorter than ${def.minGraphemes} graphemes`,
243
+ ),
244
+ }
245
+ }
246
+ }
247
+
248
+ if (typeof def.format === 'string') {
249
+ switch (def.format) {
250
+ case 'datetime':
251
+ return formats.datetime(path, value)
252
+ case 'uri':
253
+ return formats.uri(path, value)
254
+ case 'at-uri':
255
+ return formats.atUri(path, value)
256
+ case 'did':
257
+ return formats.did(path, value)
258
+ case 'handle':
259
+ return formats.handle(path, value)
260
+ case 'at-identifier':
261
+ return formats.atIdentifier(path, value)
262
+ case 'nsid':
263
+ return formats.nsid(path, value)
264
+ case 'cid':
265
+ return formats.cid(path, value)
266
+ case 'language':
267
+ return formats.language(path, value)
268
+ }
269
+ }
270
+
271
+ return { success: true, value }
250
272
  }
251
273
 
252
- export function datetime(
274
+ export function bytes(
253
275
  lexicons: Lexicons,
254
276
  path: string,
255
277
  def: LexUserType,
256
278
  value: unknown,
257
279
  ): ValidationResult {
258
- def = def as LexDatetime
280
+ def = def as LexBytes
259
281
 
260
- // type
261
- const type = typeof value
262
- if (type !== 'string') {
282
+ if (!value || !(value instanceof Uint8Array)) {
263
283
  return {
264
284
  success: false,
265
- error: new ValidationError(`${path} must be a string`),
285
+ error: new ValidationError(`${path} must be a byte array`),
286
+ }
287
+ }
288
+
289
+ // maxLength
290
+ if (typeof def.maxLength === 'number') {
291
+ if (value.byteLength > def.maxLength) {
292
+ return {
293
+ success: false,
294
+ error: new ValidationError(
295
+ `${path} must not be larger than ${def.maxLength} bytes`,
296
+ ),
297
+ }
266
298
  }
267
299
  }
268
300
 
269
- // valid iso-8601
270
- {
271
- try {
272
- if (typeof value !== 'string' || !isValidISODateString(value)) {
273
- throw new ValidationError(
274
- `${path} must be an iso8601 formatted datetime`,
275
- )
301
+ // minLength
302
+ if (typeof def.minLength === 'number') {
303
+ if (value.byteLength < def.minLength) {
304
+ return {
305
+ success: false,
306
+ error: new ValidationError(
307
+ `${path} must not be smaller than ${def.minLength} bytes`,
308
+ ),
276
309
  }
277
- } catch {
278
- throw new ValidationError(`${path} must be an iso8601 formatted datetime`)
279
310
  }
280
311
  }
281
312
 
282
- return { success: true }
313
+ return { success: true, value }
314
+ }
315
+
316
+ export function cidLink(
317
+ lexicons: Lexicons,
318
+ path: string,
319
+ def: LexUserType,
320
+ value: unknown,
321
+ ): ValidationResult {
322
+ if (CID.asCID(value) === null) {
323
+ return {
324
+ success: false,
325
+ error: new ValidationError(`${path} must be a CID`),
326
+ }
327
+ }
328
+
329
+ return { success: true, value }
283
330
  }
284
331
 
285
332
  export function unknown(
@@ -296,5 +343,5 @@ export function unknown(
296
343
  }
297
344
  }
298
345
 
299
- return { success: true }
346
+ return { success: true, value }
300
347
  }