@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,32 +1,36 @@
1
1
  import {
2
2
  BlobRef,
3
- BlobRefCheckOptions,
4
3
  LegacyBlobRef,
5
4
  isBlobRef,
6
5
  isLegacyBlobRef,
6
+ parseCidSafe,
7
7
  } from '@atproto/lex-data'
8
8
  import { Schema, ValidationContext } from '../core.js'
9
9
  import { memoizedOptions } from '../util/memoize.js'
10
10
 
11
11
  /**
12
12
  * Configuration options for blob schema validation.
13
- *
14
- * @property allowLegacy - Whether to allow legacy blob references format
15
- * @property accept - List of accepted MIME types (supports wildcards like 'image/*' or '*\/*')
16
- * @property maxSize - Maximum blob size in bytes
17
13
  */
18
- export type BlobSchemaOptions = BlobRefCheckOptions & {
14
+ export type BlobSchemaOptions = {
19
15
  /**
20
16
  * Whether to allow legacy blob references format
17
+ *
18
+ * @default false
21
19
  * @see {@link LegacyBlobRef}
22
20
  */
23
21
  allowLegacy?: boolean
22
+
24
23
  /**
25
- * List of accepted mime types
24
+ * List of accepted MIME types (supports wildcards like 'image/*' or '*\/*')
25
+ *
26
+ * @default undefined // accepts all MIME types
26
27
  */
27
28
  accept?: string[]
29
+
28
30
  /**
29
- * Maximum size in bytes
31
+ * Maximum blob size in bytes
32
+ *
33
+ * @default undefined // no size limit
30
34
  */
31
35
  maxSize?: number
32
36
  }
@@ -61,27 +65,24 @@ export class BlobSchema<
61
65
  }
62
66
 
63
67
  validateInContext(input: unknown, ctx: ValidationContext) {
64
- const blob: null | BlobRef | LegacyBlobRef =
65
- (input as any)?.$type !== undefined
66
- ? isBlobRef(input, this.options)
67
- ? input
68
- : null
69
- : this.options?.allowLegacy === true && isLegacyBlobRef(input)
70
- ? input
71
- : null
68
+ const blob = parseValue.call(ctx, input, this.options)
72
69
 
73
70
  if (!blob) {
74
71
  return ctx.issueUnexpectedType(input, 'blob')
75
72
  }
76
73
 
77
- const accept = this.options?.accept
78
- if (accept && !matchesMime(blob.mimeType, accept)) {
79
- return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept)
80
- }
74
+ // In non-strict mode, we allow blob refs to pass through without MIME
75
+ // type or size checks.
76
+ if (ctx.options.strict) {
77
+ const accept = this.options?.accept
78
+ if (accept && !matchesMime(blob.mimeType, accept)) {
79
+ return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept)
80
+ }
81
81
 
82
- const maxSize = this.options?.maxSize
83
- if (maxSize != null && 'size' in blob && blob.size > maxSize) {
84
- return ctx.issueTooBig(blob, 'blob', maxSize, blob.size)
82
+ const maxSize = this.options?.maxSize
83
+ if (maxSize != null && 'size' in blob && blob.size > maxSize) {
84
+ return ctx.issueTooBig(blob, 'blob', maxSize, blob.size)
85
+ }
85
86
  }
86
87
 
87
88
  return ctx.success(blob)
@@ -94,6 +95,38 @@ export class BlobSchema<
94
95
  }
95
96
  }
96
97
 
98
+ function parseValue(
99
+ this: ValidationContext,
100
+ input: unknown,
101
+ options?: BlobSchemaOptions,
102
+ ): BlobRef | LegacyBlobRef | null {
103
+ // If there is a $type property, we treat if as a potential BlobRef and
104
+ // validate accordingly.
105
+ if ((input as any)?.$type !== undefined) {
106
+ // Use the context's option for the "strict" check
107
+ return isBlobRef(input, this.options) ? input : null
108
+ }
109
+
110
+ // If there is no $type property, we may be dealing with a legacy blob ref. If
111
+ // legacy refs are allowed, validate against the legacy format. If not
112
+ // allowed, but we are in non-strict "parse" mode, coerce legacy refs into
113
+ // standard BlobRef format for backward compatibility. Otherwise, reject the
114
+ // value.
115
+ if (options?.allowLegacy) {
116
+ if (isLegacyBlobRef(input)) {
117
+ return input
118
+ }
119
+ } else if (!this.options.strict && this.options.mode === 'parse') {
120
+ if (isLegacyBlobRef(input)) {
121
+ const { cid, mimeType } = input
122
+ const ref = parseCidSafe(cid)
123
+ if (ref) return { $type: 'blob', ref, mimeType, size: -1 }
124
+ }
125
+ }
126
+
127
+ return null
128
+ }
129
+
97
130
  function matchesMime(mime: string, accepted: string[]): boolean {
98
131
  if (accepted.includes('*/*')) return true
99
132
  if (accepted.includes(mime)) return true
@@ -1,10 +1,4 @@
1
- import {
2
- Issue,
3
- IssueCustom,
4
- PropertyKey,
5
- Schema,
6
- ValidationContext,
7
- } from '../core.js'
1
+ import { Issue, IssueCustom, Schema, ValidationContext } from '../core.js'
8
2
 
9
3
  /**
10
4
  * Context object provided to custom assertion functions.
@@ -419,7 +419,7 @@ describe('ParamsSchema', () => {
419
419
  ['name', 'Alice'],
420
420
  ['bools', 'notabool'],
421
421
  ]),
422
- ).toThrow('Expected boolean value type at $.bools (got string)')
422
+ ).toThrow('Expected boolean value type (got string) at $.bools')
423
423
 
424
424
  expect(() =>
425
425
  schema.fromURLSearchParams(
@@ -431,7 +431,7 @@ describe('ParamsSchema', () => {
431
431
  path: ['foo', 'bar'],
432
432
  },
433
433
  ),
434
- ).toThrow('Expected boolean value type at $.foo.bar.bools (got string)')
434
+ ).toThrow('Expected boolean value type (got string) at $.foo.bar.bools')
435
435
  })
436
436
 
437
437
  it('ignores empty string values', () => {
@@ -1,5 +1,5 @@
1
1
  import { LexValue } from '@atproto/lex-data'
2
- import { Infer, Schema, Validator } from '../core.js'
2
+ import { InferInput, Schema, Validator } from '../core.js'
3
3
  import { ObjectSchema, object } from './object.js'
4
4
 
5
5
  export type { LexValue }
@@ -15,7 +15,7 @@ type ToBodyType<
15
15
  TSchema,
16
16
  TBinary,
17
17
  > = TSchema extends Schema
18
- ? Infer<TSchema>
18
+ ? InferInput<TSchema>
19
19
  : TEncoding extends `application/json`
20
20
  ? LexValue
21
21
  : TBinary
package/src/schema/ref.ts CHANGED
@@ -32,11 +32,7 @@ export type RefSchemaGetter<out TValidator extends Validator> = () => TValidator
32
32
  * ```
33
33
  */
34
34
  export class RefSchema<const TValidator extends Validator>
35
- extends Schema<
36
- InferInput<TValidator>,
37
- InferOutput<TValidator>,
38
- TValidator['__lex']
39
- >
35
+ extends Schema<InferInput<TValidator>, InferOutput<TValidator>>
40
36
  implements WrappedValidator<TValidator>
41
37
  {
42
38
  readonly type = 'ref' as const
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  InferInput,
3
3
  IssueCustom,
4
- PropertyKey,
5
4
  ValidationContext,
6
5
  ValidationResult,
7
6
  Validator,
@@ -265,6 +265,69 @@ describe('StringSchema', () => {
265
265
  const result = schema.safeParse('12/25/2023')
266
266
  expect(result.success).toBe(false)
267
267
  })
268
+
269
+ it('rejects datetime without timezone', () => {
270
+ const result = schema.safeParse('2023-12-25T12:00:00')
271
+ expect(result.success).toBe(false)
272
+ })
273
+
274
+ it('rejects date-only strings', () => {
275
+ // Date-only is not a valid datetime in either strict or loose mode
276
+ const result = schema.safeParse('2023-12-25')
277
+ expect(result.success).toBe(false)
278
+ })
279
+
280
+ describe('loose validation', () => {
281
+ it('accepts datetime without timezone', () => {
282
+ const result = schema.safeParse('2023-12-25T12:00:00', {
283
+ strict: false,
284
+ })
285
+ expect(result.success).toBe(true)
286
+ })
287
+
288
+ it('accepts datetime without separator', () => {
289
+ const result = schema.safeParse('20231225T120000', { strict: false })
290
+ expect(result.success).toBe(true)
291
+ })
292
+
293
+ it('rejects datetime with "-00:00" timezone offset', () => {
294
+ const result = schema.safeParse('2023-12-25T12:00:00-00:00', {
295
+ strict: false,
296
+ })
297
+ expect(result.success).toBe(false)
298
+ })
299
+
300
+ it('rejects date-only strings', () => {
301
+ const result = schema.safeParse('2023-12-25', { strict: false })
302
+ expect(result.success).toBe(false)
303
+ })
304
+
305
+ it('accepts datetime with timezone offset', () => {
306
+ const result = schema.safeParse('2023-12-25T12:00:00+05:30', {
307
+ strict: false,
308
+ })
309
+ expect(result.success).toBe(true)
310
+ })
311
+
312
+ it('still rejects completely invalid strings', () => {
313
+ expect(schema.safeParse('not a date', { strict: false }).success).toBe(
314
+ false,
315
+ )
316
+ expect(schema.safeParse('12/25/2023', { strict: false }).success).toBe(
317
+ false,
318
+ )
319
+ expect(schema.safeParse('', { strict: false }).success).toBe(false)
320
+ })
321
+
322
+ it('uses strict mode by default', () => {
323
+ // Datetime without timezone is not AT Protocol compliant
324
+ expect(schema.safeParse('2023-12-25T12:00:00').success).toBe(false)
325
+ // Date-only is not AT Protocol compliant
326
+ expect(schema.safeParse('2023-12-25').success).toBe(false)
327
+ // With timezone offset (AT Protocol compliant)
328
+ expect(schema.safeParse('2023-12-25T12:00:00+05:30').success).toBe(true)
329
+ })
330
+ })
268
331
  })
269
332
 
270
333
  describe('format: uri', () => {
@@ -119,7 +119,7 @@ export class StringSchema<
119
119
  }
120
120
 
121
121
  const format = this.options.format
122
- if (format != null && !isStringFormat(str, format)) {
122
+ if (format != null && !isStringFormat(str, format, ctx.options)) {
123
123
  return ctx.issueInvalidFormat(str, format)
124
124
  }
125
125
 
@@ -1,2 +0,0 @@
1
- export type PropertyKey = string | number;
2
- //# sourceMappingURL=property-key.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"property-key.d.ts","sourceRoot":"","sources":["../../src/core/property-key.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAA"}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=property-key.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"property-key.js","sourceRoot":"","sources":["../../src/core/property-key.ts"],"names":[],"mappings":"","sourcesContent":["export type PropertyKey = string | number\n"]}
@@ -1 +0,0 @@
1
- export type PropertyKey = string | number