@atproto/lex-schema 0.0.14 → 0.0.16

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 (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/core/schema.d.ts +5 -4
  3. package/dist/core/schema.d.ts.map +1 -1
  4. package/dist/core/schema.js +7 -2
  5. package/dist/core/schema.js.map +1 -1
  6. package/dist/core/standard-schema.d.ts +14 -0
  7. package/dist/core/standard-schema.d.ts.map +1 -0
  8. package/dist/core/standard-schema.js +27 -0
  9. package/dist/core/standard-schema.js.map +1 -0
  10. package/dist/core/string-format.d.ts +24 -17
  11. package/dist/core/string-format.d.ts.map +1 -1
  12. package/dist/core/string-format.js +57 -30
  13. package/dist/core/string-format.js.map +1 -1
  14. package/dist/core/validation-error.d.ts +10 -2
  15. package/dist/core/validation-error.d.ts.map +1 -1
  16. package/dist/core/validation-error.js +10 -0
  17. package/dist/core/validation-error.js.map +1 -1
  18. package/dist/core/validation-issue.d.ts +15 -15
  19. package/dist/core/validation-issue.d.ts.map +1 -1
  20. package/dist/core/validation-issue.js +33 -29
  21. package/dist/core/validation-issue.js.map +1 -1
  22. package/dist/core/validator.d.ts +29 -14
  23. package/dist/core/validator.d.ts.map +1 -1
  24. package/dist/core/validator.js +4 -2
  25. package/dist/core/validator.js.map +1 -1
  26. package/dist/core.d.ts +0 -1
  27. package/dist/core.d.ts.map +1 -1
  28. package/dist/core.js +0 -1
  29. package/dist/core.js.map +1 -1
  30. package/dist/schema/blob.d.ts +10 -8
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +39 -14
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/custom.d.ts +1 -1
  35. package/dist/schema/custom.d.ts.map +1 -1
  36. package/dist/schema/custom.js.map +1 -1
  37. package/dist/schema/never.d.ts +1 -1
  38. package/dist/schema/nullable.d.ts +1 -1
  39. package/dist/schema/payload.d.ts +2 -2
  40. package/dist/schema/payload.d.ts.map +1 -1
  41. package/dist/schema/payload.js.map +1 -1
  42. package/dist/schema/record.d.ts +1 -1
  43. package/dist/schema/ref.d.ts +1 -1
  44. package/dist/schema/ref.d.ts.map +1 -1
  45. package/dist/schema/ref.js.map +1 -1
  46. package/dist/schema/refine.d.ts +1 -1
  47. package/dist/schema/refine.d.ts.map +1 -1
  48. package/dist/schema/refine.js.map +1 -1
  49. package/dist/schema/string.js +1 -1
  50. package/dist/schema/string.js.map +1 -1
  51. package/dist/schema/typed-ref.d.ts +1 -1
  52. package/dist/schema/typed-union.d.ts +1 -1
  53. package/dist/schema/union.d.ts +2 -2
  54. package/dist/schema/union.d.ts.map +1 -1
  55. package/package.json +5 -3
  56. package/src/core/schema.ts +12 -11
  57. package/src/core/standard-schema.test.ts +124 -0
  58. package/src/core/standard-schema.ts +31 -0
  59. package/src/core/string-format.ts +73 -31
  60. package/src/core/validation-error.ts +16 -1
  61. package/src/core/validation-issue.ts +32 -32
  62. package/src/core/validator.ts +26 -6
  63. package/src/core.ts +0 -1
  64. package/src/schema/array.test.ts +2 -2
  65. package/src/schema/blob.test.ts +317 -49
  66. package/src/schema/blob.ts +56 -23
  67. package/src/schema/custom.ts +1 -7
  68. package/src/schema/params.test.ts +2 -2
  69. package/src/schema/payload.ts +2 -2
  70. package/src/schema/ref.ts +1 -5
  71. package/src/schema/refine.ts +0 -1
  72. package/src/schema/string.test.ts +63 -0
  73. package/src/schema/string.ts +1 -1
  74. package/dist/core/property-key.d.ts +0 -2
  75. package/dist/core/property-key.d.ts.map +0 -1
  76. package/dist/core/property-key.js +0 -3
  77. package/dist/core/property-key.js.map +0 -1
  78. package/src/core/property-key.ts +0 -1
@@ -1,5 +1,4 @@
1
1
  import { ifCid, isLegacyBlobRef, isPlainObject } from '@atproto/lex-data'
2
- import { PropertyKey } from './property-key.js'
3
2
 
4
3
  /**
5
4
  * Abstract base class for all validation issues.
@@ -9,10 +8,13 @@ import { PropertyKey } from './property-key.js'
9
8
  * - The path to the invalid value in the data structure
10
9
  * - The actual input value that failed validation
11
10
  *
12
- * Subclasses add specific properties relevant to each issue type and
13
- * implement the {@link toString} method for human-readable error messages.
11
+ * Subclasses add specific properties relevant to each issue type and implement
12
+ * the {@link message} property for human-readable error messages (that don't
13
+ * contain the error path)
14
14
  */
15
15
  export abstract class Issue {
16
+ abstract readonly message: string
17
+
16
18
  constructor(
17
19
  readonly code: string,
18
20
  readonly path: readonly PropertyKey[],
@@ -22,7 +24,9 @@ export abstract class Issue {
22
24
  /**
23
25
  * Returns a human-readable description of the validation issue.
24
26
  */
25
- abstract toString(): string
27
+ toString() {
28
+ return `${this.message}${stringifyPath(this.path)}`
29
+ }
26
30
 
27
31
  /**
28
32
  * Converts the issue to a JSON-serializable object.
@@ -33,7 +37,7 @@ export abstract class Issue {
33
37
  return {
34
38
  code: this.code,
35
39
  path: this.path,
36
- message: this.toString(),
40
+ message: this.message,
37
41
  }
38
42
  }
39
43
  }
@@ -51,10 +55,6 @@ export class IssueCustom extends Issue {
51
55
  ) {
52
56
  super('custom', path, input)
53
57
  }
54
-
55
- toString() {
56
- return `${this.message}${stringifyPath(this.path)}`
57
- }
58
58
  }
59
59
 
60
60
  /**
@@ -67,20 +67,13 @@ export class IssueInvalidFormat extends Issue {
67
67
  path: readonly PropertyKey[],
68
68
  input: unknown,
69
69
  readonly format: string,
70
- readonly message?: string,
70
+ readonly detail?: string,
71
71
  ) {
72
72
  super('invalid_format', path, input)
73
73
  }
74
74
 
75
- toString() {
76
- return `Invalid ${this.formatDescription}${this.message ? ` (${this.message})` : ''}${stringifyPath(this.path)} (got ${stringifyValue(this.input)})`
77
- }
78
-
79
- toJSON() {
80
- return {
81
- ...super.toJSON(),
82
- format: this.format,
83
- }
75
+ override get message(): string {
76
+ return `Invalid ${this.formatDescription}${this.detail ? ` (${this.detail}, ` : ' ('}got ${stringifyValue(this.input)})`
84
77
  }
85
78
 
86
79
  /** Returns a human-readable description of the expected format. */
@@ -102,6 +95,13 @@ export class IssueInvalidFormat extends Issue {
102
95
  return this.format
103
96
  }
104
97
  }
98
+
99
+ toJSON() {
100
+ return {
101
+ ...super.toJSON(),
102
+ format: this.format,
103
+ }
104
+ }
105
105
  }
106
106
 
107
107
  /**
@@ -119,8 +119,8 @@ export class IssueInvalidType extends Issue {
119
119
  super('invalid_type', path, input)
120
120
  }
121
121
 
122
- toString() {
123
- return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type${stringifyPath(this.path)} (got ${stringifyType(this.input)})`
122
+ override get message(): string {
123
+ return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${stringifyType(this.input)})`
124
124
  }
125
125
 
126
126
  toJSON() {
@@ -146,8 +146,8 @@ export class IssueInvalidValue extends Issue {
146
146
  super('invalid_value', path, input)
147
147
  }
148
148
 
149
- toString() {
150
- return `Expected ${oneOf(this.values.map(stringifyValue))}${stringifyPath(this.path)} (got ${stringifyValue(this.input)})`
149
+ override get message(): string {
150
+ return `Expected ${oneOf(this.values.map(stringifyValue))} (got ${stringifyValue(this.input)})`
151
151
  }
152
152
 
153
153
  toJSON() {
@@ -170,8 +170,8 @@ export class IssueRequiredKey extends Issue {
170
170
  super('required_key', path, input)
171
171
  }
172
172
 
173
- toString() {
174
- return `Missing required key "${String(this.key)}"${stringifyPath(this.path)}`
173
+ override get message(): string {
174
+ return `Missing required key "${String(this.key)}"`
175
175
  }
176
176
 
177
177
  toJSON() {
@@ -214,8 +214,8 @@ export class IssueTooBig extends Issue {
214
214
  super('too_big', path, input)
215
215
  }
216
216
 
217
- toString() {
218
- return `${this.type} too big (maximum ${this.maximum})${stringifyPath(this.path)} (got ${this.actual})`
217
+ override get message(): string {
218
+ return `${this.type} too big (maximum ${this.maximum}, got ${this.actual})`
219
219
  }
220
220
 
221
221
  toJSON() {
@@ -241,8 +241,8 @@ export class IssueTooSmall extends Issue {
241
241
  super('too_small', path, input)
242
242
  }
243
243
 
244
- toString() {
245
- return `${this.type} too small (minimum ${this.minimum})${stringifyPath(this.path)} (got ${this.actual})`
244
+ override get message(): string {
245
+ return `${this.type} too small (minimum ${this.minimum}, got ${this.actual})`
246
246
  }
247
247
 
248
248
  toJSON() {
@@ -274,9 +274,9 @@ function buildJsonPath(path: readonly PropertyKey[]): string {
274
274
  }
275
275
 
276
276
  function toJsonPathSegment(segment: PropertyKey): string {
277
- if (typeof segment === 'number') {
278
- return `[${segment}]`
279
- } else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment as string)) {
277
+ if (typeof segment === 'number' || typeof segment === 'symbol') {
278
+ return `[${String(segment)}]`
279
+ } else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment)) {
280
280
  return `.${segment}`
281
281
  } else {
282
282
  return `[${JSON.stringify(segment)}]`
@@ -1,5 +1,5 @@
1
- import { PropertyKey } from './property-key.js'
2
- import { ResultFailure, ResultSuccess, failure, success } from './result.js'
1
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2
+ import { ResultFailure, ResultSuccess, success } from './result.js'
3
3
  import { LexValidationError } from './validation-error.js'
4
4
  import {
5
5
  Issue,
@@ -21,8 +21,12 @@ export type ValidationSuccess<Value = unknown> = ResultSuccess<Value>
21
21
 
22
22
  /**
23
23
  * Represents a failed validation result containing a {@link LexValidationError}.
24
+ *
25
+ * @extends ResultFailure<LexValidationError>
26
+ * @see {@link ResultFailure}
27
+ * @see {@link LexValidationError}
24
28
  */
25
- export type ValidationFailure = ResultFailure<LexValidationError>
29
+ export type ValidationFailure = LexValidationError
26
30
 
27
31
  /**
28
32
  * Discriminated union representing the outcome of a validation operation.
@@ -148,11 +152,13 @@ export type ValidationOptions = {
148
152
  /**
149
153
  * The validation mode determining how transformations are handled.
150
154
  *
151
- * - `"validate"` (default): Strict validation where the result must be
155
+ * - `"validate"`: Strict validation where the result must be
152
156
  * strictly equal to the input value. No transformations such as applying
153
157
  * default values are allowed.
154
158
  * - `"parse"`: Allows the schema to transform the input value, such as
155
159
  * applying default values or performing type coercion.
160
+ *
161
+ * @default "validate"
156
162
  */
157
163
  mode?: 'validate' | 'parse'
158
164
 
@@ -169,6 +175,17 @@ export type ValidationOptions = {
169
175
  * ```
170
176
  */
171
177
  path?: readonly PropertyKey[]
178
+
179
+ /**
180
+ * Whether to enforce strict validation rules (e.g., MIME type matching, size
181
+ * limits, datetime format).
182
+ *
183
+ * This is typically useful to allow more lax validation when parsing server
184
+ * responses, while enforcing strict validation for user input.
185
+ *
186
+ * @default true
187
+ */
188
+ strict?: boolean
172
189
  }
173
190
 
174
191
  /**
@@ -217,6 +234,7 @@ export class ValidationContext {
217
234
  mode: 'parse'
218
235
  },
219
236
  ): ValidationResult<InferOutput<V>>
237
+
220
238
  /**
221
239
  * Validates input against a validator in validate mode (default).
222
240
  *
@@ -237,6 +255,7 @@ export class ValidationContext {
237
255
  mode?: 'validate'
238
256
  },
239
257
  ): ValidationResult<I & InferInput<V>>
258
+
240
259
  /**
241
260
  * Validates input against a validator with configurable options.
242
261
  *
@@ -258,6 +277,7 @@ export class ValidationContext {
258
277
  const context = new ValidationContext({
259
278
  path: options?.path ?? [],
260
279
  mode: options?.mode ?? 'validate',
280
+ strict: options?.strict ?? true,
261
281
  })
262
282
  return context.validate(input, validator)
263
283
  }
@@ -326,7 +346,7 @@ export class ValidationContext {
326
346
  if (this.issues.length > 0) {
327
347
  // Validator returned a success but issues were added via the context.
328
348
  // This means the overall validation failed.
329
- return failure(new LexValidationError(Array.from(this.issues)))
349
+ return new LexValidationError(Array.from(this.issues))
330
350
  }
331
351
 
332
352
  if (this.options.mode !== 'parse' && !Object.is(result.value, input)) {
@@ -426,7 +446,7 @@ export class ValidationContext {
426
446
  * @returns A failed validation result
427
447
  */
428
448
  failure(reason: LexValidationError): ValidationFailure {
429
- return failure(reason)
449
+ return reason
430
450
  }
431
451
 
432
452
  /**
package/src/core.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './core/$type.js'
2
- export * from './core/property-key.js'
3
2
  export * from './core/record-key.js'
4
3
  export * from './core/result.js'
5
4
  export * from './core/schema.js'
@@ -77,11 +77,11 @@ describe('ArraySchema', () => {
77
77
  it('rejects single values', () => {
78
78
  const schema = array(string())
79
79
  const result = schema.safeValidate(3)
80
- expect(result).toEqual({
80
+ expect(result).toMatchObject({
81
81
  success: false,
82
82
  reason: expect.objectContaining({
83
83
  message: expect.stringContaining(
84
- 'Expected array value type at $ (got integer)',
84
+ 'Expected array value type (got integer) at $',
85
85
  ),
86
86
  }),
87
87
  })
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { assert, describe, expect, it } from 'vitest'
2
2
  import { parseCid } from '@atproto/lex-data'
3
3
  import { blob } from './blob.js'
4
4
 
@@ -214,37 +214,62 @@ describe('BlobSchema', () => {
214
214
  })
215
215
 
216
216
  describe('strict validation', () => {
217
- const strictSchema = blob({ strict: true })
217
+ const schema = blob()
218
218
 
219
219
  it('accepts valid raw CID in strict mode', () => {
220
- const result = strictSchema.safeParse({
221
- $type: 'blob',
222
- ref: blobCid,
223
- mimeType: 'image/jpeg',
224
- size: 10000,
225
- })
220
+ const result = schema.safeParse(
221
+ {
222
+ $type: 'blob',
223
+ ref: blobCid,
224
+ mimeType: 'image/jpeg',
225
+ size: 10000,
226
+ },
227
+ { strict: true },
228
+ )
226
229
  expect(result.success).toBe(true)
227
230
  })
228
231
 
229
232
  it('rejects non-raw CID in strict mode', () => {
230
- const result = strictSchema.safeParse({
231
- $type: 'blob',
232
- ref: lexCid,
233
- mimeType: 'image/jpeg',
234
- size: 10000,
235
- })
233
+ const result = schema.safeParse(
234
+ {
235
+ $type: 'blob',
236
+ ref: lexCid,
237
+ mimeType: 'image/jpeg',
238
+ size: 10000,
239
+ },
240
+ { strict: true },
241
+ )
236
242
  expect(result.success).toBe(false)
237
243
  })
238
244
 
239
245
  it('accepts non-raw CID in non-strict mode', () => {
240
- const nonStrictSchema = blob({ strict: false })
241
- const result = nonStrictSchema.safeParse({
246
+ const result = schema.safeParse(
247
+ {
248
+ $type: 'blob',
249
+ ref: lexCid,
250
+ mimeType: 'image/jpeg',
251
+ size: 10000,
252
+ },
253
+ { strict: false },
254
+ )
255
+ expect(result.success).toBe(true)
256
+ })
257
+
258
+ it('coerces legacy blob format in non-strict parse mode', () => {
259
+ const result = schema.safeParse(
260
+ {
261
+ cid: lexCid.toString(),
262
+ mimeType: 'image/jpeg',
263
+ },
264
+ { strict: false },
265
+ )
266
+ assert(result.success)
267
+ expect(result.value).toEqual({
242
268
  $type: 'blob',
243
269
  ref: lexCid,
244
270
  mimeType: 'image/jpeg',
245
- size: 10000,
271
+ size: -1,
246
272
  })
247
- expect(result.success).toBe(true)
248
273
  })
249
274
  })
250
275
 
@@ -455,51 +480,294 @@ describe('BlobSchema', () => {
455
480
  })
456
481
  })
457
482
 
458
- describe('combined options', () => {
459
- it('validates with strict and allowLegacy both true', () => {
460
- const schema = blob({ strict: true, allowLegacy: true })
483
+ describe('legacy blob format with strict mode combinations', () => {
484
+ describe('allowLegacy: false (default)', () => {
485
+ const schema = blob()
486
+
487
+ describe('strict: true (default)', () => {
488
+ it('rejects legacy blob format', () => {
489
+ const result = schema.safeParse({
490
+ cid: blobCid.toString(),
491
+ mimeType: 'image/jpeg',
492
+ })
493
+ expect(result.success).toBe(false)
494
+ })
495
+
496
+ it('accepts standard BlobRef', () => {
497
+ const result = schema.safeParse({
498
+ $type: 'blob',
499
+ ref: blobCid,
500
+ mimeType: 'image/jpeg',
501
+ size: 10000,
502
+ })
503
+ expect(result.success).toBe(true)
504
+ })
505
+ })
506
+
507
+ describe('strict: false', () => {
508
+ it('coerces legacy blob format into BlobRef', () => {
509
+ const result = schema.safeParse(
510
+ {
511
+ cid: blobCid.toString(),
512
+ mimeType: 'image/jpeg',
513
+ },
514
+ { strict: false },
515
+ )
516
+ assert(result.success)
517
+ expect(result.value).toEqual({
518
+ $type: 'blob',
519
+ ref: blobCid,
520
+ mimeType: 'image/jpeg',
521
+ size: -1,
522
+ })
523
+ })
524
+
525
+ it('coerces legacy blob format with lexCid', () => {
526
+ const result = schema.safeParse(
527
+ {
528
+ cid: lexCid.toString(),
529
+ mimeType: 'image/png',
530
+ },
531
+ { strict: false },
532
+ )
533
+ assert(result.success)
534
+ expect(result.value).toEqual({
535
+ $type: 'blob',
536
+ ref: lexCid,
537
+ mimeType: 'image/png',
538
+ size: -1,
539
+ })
540
+ })
541
+
542
+ it('rejects legacy blob format with invalid cid', () => {
543
+ const result = schema.safeParse(
544
+ {
545
+ cid: 'invalid-cid',
546
+ mimeType: 'image/jpeg',
547
+ },
548
+ { strict: false },
549
+ )
550
+ expect(result.success).toBe(false)
551
+ })
552
+
553
+ it('accepts standard BlobRef with non-raw CID', () => {
554
+ const result = schema.safeParse(
555
+ {
556
+ $type: 'blob',
557
+ ref: lexCid,
558
+ mimeType: 'image/jpeg',
559
+ size: 10000,
560
+ },
561
+ { strict: false },
562
+ )
563
+ expect(result.success).toBe(true)
564
+ })
565
+ })
566
+ })
567
+
568
+ describe('allowLegacy: true', () => {
569
+ const schema = blob({ allowLegacy: true })
461
570
 
462
- // Should accept strict BlobRef
463
- const blobRefResult = schema.safeParse({
464
- $type: 'blob',
465
- ref: blobCid,
466
- mimeType: 'image/jpeg',
467
- size: 10000,
571
+ describe('strict: true (default)', () => {
572
+ it('accepts legacy blob format as LegacyBlobRef', () => {
573
+ const result = schema.safeParse({
574
+ cid: blobCid.toString(),
575
+ mimeType: 'image/jpeg',
576
+ })
577
+ assert(result.success)
578
+ expect('cid' in result.value && result.value.cid).toBe(
579
+ blobCid.toString(),
580
+ )
581
+ })
582
+
583
+ it('accepts standard BlobRef', () => {
584
+ const result = schema.safeParse({
585
+ $type: 'blob',
586
+ ref: blobCid,
587
+ mimeType: 'image/jpeg',
588
+ size: 10000,
589
+ })
590
+ expect(result.success).toBe(true)
591
+ })
592
+
593
+ it('rejects non-raw CID in BlobRef format (strict)', () => {
594
+ const result = schema.safeParse({
595
+ $type: 'blob',
596
+ ref: lexCid,
597
+ mimeType: 'image/jpeg',
598
+ size: 10000,
599
+ })
600
+ expect(result.success).toBe(false)
601
+ })
602
+ })
603
+
604
+ describe('strict: false', () => {
605
+ it('accepts legacy blob format as LegacyBlobRef', () => {
606
+ const result = schema.safeParse(
607
+ {
608
+ cid: blobCid.toString(),
609
+ mimeType: 'image/jpeg',
610
+ },
611
+ { strict: false },
612
+ )
613
+ assert(result.success)
614
+ expect('cid' in result.value && result.value.cid).toBe(
615
+ blobCid.toString(),
616
+ )
617
+ })
618
+
619
+ it('accepts standard BlobRef with non-raw CID (non-strict)', () => {
620
+ const result = schema.safeParse(
621
+ {
622
+ $type: 'blob',
623
+ ref: lexCid,
624
+ mimeType: 'image/jpeg',
625
+ size: 10000,
626
+ },
627
+ { strict: false },
628
+ )
629
+ expect(result.success).toBe(true)
630
+ })
631
+
632
+ it('accepts standard BlobRef with raw CID', () => {
633
+ const result = schema.safeParse(
634
+ {
635
+ $type: 'blob',
636
+ ref: blobCid,
637
+ mimeType: 'image/jpeg',
638
+ size: 10000,
639
+ },
640
+ { strict: false },
641
+ )
642
+ expect(result.success).toBe(true)
643
+ })
468
644
  })
469
- expect(blobRefResult.success).toBe(true)
645
+ })
646
+ })
470
647
 
471
- // Should accept LegacyBlobRef
472
- const legacyResult = schema.safeParse({
473
- cid: blobCid.toString(),
474
- mimeType: 'image/jpeg',
648
+ describe('mime and size checks depend on strict mode', () => {
649
+ describe('accept constraint', () => {
650
+ const schema = blob({ accept: ['image/jpeg', 'image/png'] })
651
+
652
+ it('rejects non-matching mime type in strict mode (default)', () => {
653
+ const result = schema.safeParse({
654
+ $type: 'blob',
655
+ ref: blobCid,
656
+ mimeType: 'image/gif',
657
+ size: 10000,
658
+ })
659
+ expect(result.success).toBe(false)
660
+ })
661
+
662
+ it('accepts non-matching mime type in non-strict mode', () => {
663
+ const result = schema.safeParse(
664
+ {
665
+ $type: 'blob',
666
+ ref: blobCid,
667
+ mimeType: 'image/gif',
668
+ size: 10000,
669
+ },
670
+ { strict: false },
671
+ )
672
+ expect(result.success).toBe(true)
475
673
  })
476
- expect(legacyResult.success).toBe(true)
477
674
 
478
- // Should reject non-raw CID in BlobRef format
479
- const nonRawResult = schema.safeParse({
480
- $type: 'blob',
481
- ref: lexCid,
482
- mimeType: 'image/jpeg',
483
- size: 10000,
675
+ it('accepts matching mime type in strict mode', () => {
676
+ const result = schema.safeParse({
677
+ $type: 'blob',
678
+ ref: blobCid,
679
+ mimeType: 'image/jpeg',
680
+ size: 10000,
681
+ })
682
+ expect(result.success).toBe(true)
484
683
  })
485
- expect(nonRawResult.success).toBe(false)
486
684
  })
487
685
 
488
- it('validates with all options combined', () => {
686
+ describe('maxSize constraint', () => {
687
+ const schema = blob({ maxSize: 1000 })
688
+
689
+ it('rejects oversized blob in strict mode (default)', () => {
690
+ const result = schema.safeParse({
691
+ $type: 'blob',
692
+ ref: blobCid,
693
+ mimeType: 'image/jpeg',
694
+ size: 5000,
695
+ })
696
+ expect(result.success).toBe(false)
697
+ })
698
+
699
+ it('accepts oversized blob in non-strict mode', () => {
700
+ const result = schema.safeParse(
701
+ {
702
+ $type: 'blob',
703
+ ref: blobCid,
704
+ mimeType: 'image/jpeg',
705
+ size: 5000,
706
+ },
707
+ { strict: false },
708
+ )
709
+ expect(result.success).toBe(true)
710
+ })
711
+
712
+ it('accepts correctly sized blob in strict mode', () => {
713
+ const result = schema.safeParse({
714
+ $type: 'blob',
715
+ ref: blobCid,
716
+ mimeType: 'image/jpeg',
717
+ size: 500,
718
+ })
719
+ expect(result.success).toBe(true)
720
+ })
721
+ })
722
+
723
+ describe('combined accept and maxSize constraints', () => {
489
724
  const schema = blob({
490
- strict: true,
491
- allowLegacy: true,
492
725
  accept: ['image/jpeg'],
493
726
  maxSize: 20000,
494
727
  })
495
728
 
496
- const result = schema.safeParse({
497
- $type: 'blob',
498
- ref: blobCid,
499
- mimeType: 'image/jpeg',
500
- size: 10000,
729
+ it('accepts valid blob in strict mode', () => {
730
+ const result = schema.safeParse({
731
+ $type: 'blob',
732
+ ref: blobCid,
733
+ mimeType: 'image/jpeg',
734
+ size: 10000,
735
+ })
736
+ expect(result.success).toBe(true)
737
+ })
738
+
739
+ it('rejects wrong mime in strict mode', () => {
740
+ const result = schema.safeParse({
741
+ $type: 'blob',
742
+ ref: blobCid,
743
+ mimeType: 'image/png',
744
+ size: 10000,
745
+ })
746
+ expect(result.success).toBe(false)
747
+ })
748
+
749
+ it('rejects oversized in strict mode', () => {
750
+ const result = schema.safeParse({
751
+ $type: 'blob',
752
+ ref: blobCid,
753
+ mimeType: 'image/jpeg',
754
+ size: 30000,
755
+ })
756
+ expect(result.success).toBe(false)
757
+ })
758
+
759
+ it('accepts wrong mime and oversized in non-strict mode', () => {
760
+ const result = schema.safeParse(
761
+ {
762
+ $type: 'blob',
763
+ ref: blobCid,
764
+ mimeType: 'video/mp4',
765
+ size: 99999,
766
+ },
767
+ { strict: false },
768
+ )
769
+ expect(result.success).toBe(true)
501
770
  })
502
- expect(result.success).toBe(true)
503
771
  })
504
772
  })
505
773
  })