@atproto/lex-schema 0.0.16 → 0.0.17
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/CHANGELOG.md +19 -0
- package/dist/core/schema.d.ts +5 -11
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +10 -16
- package/dist/core/schema.js.map +1 -1
- package/dist/core/validation-error.d.ts +2 -2
- package/dist/core/validation-error.d.ts.map +1 -1
- package/dist/core/validation-error.js +1 -1
- package/dist/core/validation-error.js.map +1 -1
- package/dist/core/validation-issue.d.ts.map +1 -1
- package/dist/core/validation-issue.js +19 -38
- package/dist/core/validation-issue.js.map +1 -1
- package/dist/helpers.d.ts +4 -3
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +6 -1
- package/dist/helpers.js.map +1 -1
- package/dist/schema/payload.d.ts.map +1 -1
- package/dist/schema/payload.js +2 -3
- package/dist/schema/payload.js.map +1 -1
- package/dist/schema/record.d.ts +6 -8
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +1 -1
- package/dist/schema/record.js.map +1 -1
- package/dist/schema/regexp.d.ts +3 -2
- package/dist/schema/regexp.d.ts.map +1 -1
- package/dist/schema/regexp.js +6 -4
- package/dist/schema/regexp.js.map +1 -1
- package/dist/schema/string.d.ts.map +1 -1
- package/dist/schema/string.js +9 -2
- package/dist/schema/string.js.map +1 -1
- package/dist/schema/typed-object.d.ts +5 -7
- package/dist/schema/typed-object.d.ts.map +1 -1
- package/dist/schema/typed-object.js +1 -1
- package/dist/schema/typed-object.js.map +1 -1
- package/package.json +2 -2
- package/src/core/$type.test.ts +9 -5
- package/src/core/schema.ts +19 -16
- package/src/core/validation-error.ts +2 -2
- package/src/core/validation-issue.ts +20 -36
- package/src/helpers.ts +7 -1
- package/src/schema/array.test.ts +1 -1
- package/src/schema/params.test.ts +2 -2
- package/src/schema/payload.ts +2 -3
- package/src/schema/record.test.ts +135 -17
- package/src/schema/record.ts +14 -9
- package/src/schema/regexp.ts +14 -4
- package/src/schema/string.ts +8 -2
- package/src/schema/typed-object.test.ts +77 -0
- package/src/schema/typed-object.ts +11 -10
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ifCid, isLegacyBlobRef, isPlainObject } from '@atproto/lex-data'
|
|
2
2
|
|
|
3
|
+
const STRING_PREVIEW_MAX_LENGTH = 48
|
|
4
|
+
const STRING_PREVIEW_TRUNCATED_SUFFIX = '…'
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Abstract base class for all validation issues.
|
|
5
8
|
*
|
|
@@ -120,7 +123,7 @@ export class IssueInvalidType extends Issue {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
override get message(): string {
|
|
123
|
-
return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${
|
|
126
|
+
return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${stringifyValue(this.input)})`
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
toJSON() {
|
|
@@ -289,55 +292,36 @@ function oneOf(arr: readonly string[]): string {
|
|
|
289
292
|
return `one of ${arr.slice(0, -1).join(', ')} or ${arr.at(-1)}`
|
|
290
293
|
}
|
|
291
294
|
|
|
292
|
-
function stringifyType(value: unknown): string {
|
|
293
|
-
switch (typeof value) {
|
|
294
|
-
case 'object':
|
|
295
|
-
if (value === null) return 'null'
|
|
296
|
-
if (Array.isArray(value)) return 'array'
|
|
297
|
-
if (ifCid(value)) return 'cid'
|
|
298
|
-
if (isLegacyBlobRef(value)) return 'legacy-blob'
|
|
299
|
-
if (value instanceof Date) return 'date'
|
|
300
|
-
if (value instanceof RegExp) return 'regexp'
|
|
301
|
-
if (value instanceof Map) return 'map'
|
|
302
|
-
if (value instanceof Set) return 'set'
|
|
303
|
-
return 'object'
|
|
304
|
-
case 'number':
|
|
305
|
-
if (Number.isInteger(value) && Number.isSafeInteger(value)) {
|
|
306
|
-
return 'integer'
|
|
307
|
-
}
|
|
308
|
-
if (Number.isNaN(value)) {
|
|
309
|
-
return 'NaN'
|
|
310
|
-
}
|
|
311
|
-
if (value === Infinity) {
|
|
312
|
-
return 'Infinity'
|
|
313
|
-
}
|
|
314
|
-
if (value === -Infinity) {
|
|
315
|
-
return '-Infinity'
|
|
316
|
-
}
|
|
317
|
-
return 'float'
|
|
318
|
-
default:
|
|
319
|
-
return typeof value
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
295
|
function stringifyValue(value: unknown): string {
|
|
324
296
|
switch (typeof value) {
|
|
325
297
|
case 'bigint':
|
|
326
298
|
return `${value}n`
|
|
327
299
|
case 'number':
|
|
328
|
-
case 'string':
|
|
329
300
|
case 'boolean':
|
|
330
|
-
return
|
|
301
|
+
return String(value)
|
|
302
|
+
case 'string':
|
|
303
|
+
return JSON.stringify(
|
|
304
|
+
value.length < STRING_PREVIEW_MAX_LENGTH
|
|
305
|
+
? value
|
|
306
|
+
: `${value.slice(0, STRING_PREVIEW_MAX_LENGTH - STRING_PREVIEW_TRUNCATED_SUFFIX.length)}${STRING_PREVIEW_TRUNCATED_SUFFIX}`,
|
|
307
|
+
)
|
|
331
308
|
case 'object':
|
|
309
|
+
if (value === null) return 'null'
|
|
332
310
|
if (Array.isArray(value)) {
|
|
333
311
|
return `[${stringifyArray(value, stringifyValue)}]`
|
|
334
312
|
}
|
|
335
313
|
if (isPlainObject(value)) {
|
|
336
314
|
return `{${stringifyArray(Object.entries(value), stringifyObjectEntry)}}`
|
|
337
315
|
}
|
|
338
|
-
|
|
316
|
+
if (ifCid(value)) return 'cid'
|
|
317
|
+
if (isLegacyBlobRef(value)) return 'legacy-blob'
|
|
318
|
+
if (value instanceof Date) return 'date'
|
|
319
|
+
if (value instanceof RegExp) return 'regexp'
|
|
320
|
+
if (value instanceof Map) return 'map'
|
|
321
|
+
if (value instanceof Set) return 'set'
|
|
322
|
+
return 'object'
|
|
339
323
|
default:
|
|
340
|
-
return
|
|
324
|
+
return typeof value
|
|
341
325
|
}
|
|
342
326
|
}
|
|
343
327
|
|
package/src/helpers.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Subscription,
|
|
10
10
|
object,
|
|
11
11
|
optional,
|
|
12
|
+
regexp,
|
|
12
13
|
string,
|
|
13
14
|
} from './schema.js'
|
|
14
15
|
|
|
@@ -103,7 +104,12 @@ export type InferMethodError<
|
|
|
103
104
|
M extends Procedure | Query | Subscription = Procedure | Query | Subscription,
|
|
104
105
|
> = M extends { errors: readonly (infer E extends string)[] } ? E : never
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @see {@link https://atproto.com/specs/xrpc#error-responses}
|
|
109
|
+
*/
|
|
106
110
|
export const lexErrorDataSchema = object({
|
|
107
|
-
error
|
|
111
|
+
// type name of the error (generic ASCII constant, no whitespace)
|
|
112
|
+
error: regexp(/^[\w_-]+$/, 'Expected ASCII constant with no whitespace'),
|
|
113
|
+
// description of the error, appropriate for display to humans
|
|
108
114
|
message: optional(string()),
|
|
109
115
|
}) satisfies Schema<LexErrorData>
|
package/src/schema/array.test.ts
CHANGED
|
@@ -419,7 +419,7 @@ describe('ParamsSchema', () => {
|
|
|
419
419
|
['name', 'Alice'],
|
|
420
420
|
['bools', 'notabool'],
|
|
421
421
|
]),
|
|
422
|
-
).toThrow('Expected boolean value type (got
|
|
422
|
+
).toThrow('Expected boolean value type (got "notabool") 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 (got
|
|
434
|
+
).toThrow('Expected boolean value type (got "2") at $.foo.bar.bools')
|
|
435
435
|
})
|
|
436
436
|
|
|
437
437
|
it('ignores empty string values', () => {
|
package/src/schema/payload.ts
CHANGED
|
@@ -109,10 +109,9 @@ export class Payload<
|
|
|
109
109
|
matchesEncoding(contentType: string | undefined): boolean {
|
|
110
110
|
const { encoding } = this
|
|
111
111
|
|
|
112
|
-
// Handle undefined cases
|
|
113
112
|
if (encoding === undefined) {
|
|
114
|
-
//
|
|
115
|
-
return
|
|
113
|
+
// When the output is not defined, we don't enforce any rule on the payload.
|
|
114
|
+
return true
|
|
116
115
|
} else if (contentType == null) {
|
|
117
116
|
// Expecting a body, but got no content-type
|
|
118
117
|
return false
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest'
|
|
2
|
+
import { DidString, Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'
|
|
3
|
+
import { integer } from './integer.js'
|
|
3
4
|
import { object } from './object.js'
|
|
5
|
+
import { optional } from './optional.js'
|
|
4
6
|
import { record } from './record.js'
|
|
5
7
|
import { string } from './string.js'
|
|
6
8
|
|
|
@@ -123,41 +125,157 @@ describe('RecordSchema', () => {
|
|
|
123
125
|
})
|
|
124
126
|
})
|
|
125
127
|
|
|
126
|
-
describe('build method', () => {
|
|
128
|
+
describe('build() method', () => {
|
|
127
129
|
const schema = record(
|
|
128
130
|
'any',
|
|
129
|
-
'
|
|
131
|
+
'io.example.record',
|
|
130
132
|
object({
|
|
131
|
-
|
|
132
|
-
text: string(),
|
|
133
|
+
actor: string({ format: 'did' }),
|
|
134
|
+
text: optional(string()),
|
|
133
135
|
}),
|
|
134
136
|
)
|
|
135
137
|
|
|
136
|
-
it('adds
|
|
137
|
-
const result = schema.build({
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
it('adds $type to input', () => {
|
|
139
|
+
const result = schema.build({
|
|
140
|
+
actor: 'did:foo:bar',
|
|
141
|
+
text: 'Hello',
|
|
142
|
+
})
|
|
143
|
+
expect(result).toStrictEqual({
|
|
144
|
+
$type: 'io.example.record',
|
|
145
|
+
actor: 'did:foo:bar',
|
|
146
|
+
text: 'Hello',
|
|
147
|
+
})
|
|
148
|
+
expectTypeOf(result).toEqualTypeOf<{
|
|
149
|
+
$type: 'io.example.record'
|
|
150
|
+
actor: DidString
|
|
151
|
+
text?: string
|
|
152
|
+
}>()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('causes a type error for invalid values', () => {
|
|
156
|
+
const result = schema.build({
|
|
157
|
+
// @ts-expect-error
|
|
158
|
+
actor: 3,
|
|
159
|
+
text: 'Hello',
|
|
160
|
+
})
|
|
161
|
+
expectTypeOf(result).toEqualTypeOf<{
|
|
162
|
+
$type: 'io.example.record'
|
|
163
|
+
actor: DidString
|
|
164
|
+
text?: string
|
|
165
|
+
}>()
|
|
140
166
|
})
|
|
141
167
|
|
|
142
168
|
it('preserves existing properties', () => {
|
|
143
169
|
const result = schema.build({
|
|
144
|
-
|
|
170
|
+
actor: 'did:foo:bar',
|
|
145
171
|
// @ts-expect-error
|
|
146
172
|
extra: 'value',
|
|
147
173
|
})
|
|
148
|
-
expect(result
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
expect(result).toStrictEqual({
|
|
175
|
+
$type: 'io.example.record',
|
|
176
|
+
actor: 'did:foo:bar',
|
|
177
|
+
extra: 'value',
|
|
178
|
+
})
|
|
179
|
+
expectTypeOf(result).toEqualTypeOf<{
|
|
180
|
+
actor: `did:${string}:${string}`
|
|
181
|
+
text?: string | undefined
|
|
182
|
+
$type: 'io.example.record'
|
|
183
|
+
}>()
|
|
152
184
|
})
|
|
153
185
|
|
|
154
186
|
it('overwrites existing $type', () => {
|
|
155
187
|
const result = schema.build({
|
|
156
188
|
// @ts-expect-error
|
|
157
189
|
$type: 'wrong.type',
|
|
158
|
-
|
|
190
|
+
actor: 'did:foo:bar',
|
|
191
|
+
})
|
|
192
|
+
expect(result).toStrictEqual({
|
|
193
|
+
$type: 'io.example.record',
|
|
194
|
+
actor: 'did:foo:bar',
|
|
195
|
+
})
|
|
196
|
+
expectTypeOf(result).toEqualTypeOf<{
|
|
197
|
+
$type: 'io.example.record'
|
|
198
|
+
actor: DidString
|
|
199
|
+
text?: string
|
|
200
|
+
}>()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('build() does not validate', () => {
|
|
204
|
+
const schema = record(
|
|
205
|
+
'any',
|
|
206
|
+
'io.example.record',
|
|
207
|
+
object({
|
|
208
|
+
actor: string({ format: 'did' }),
|
|
209
|
+
count: integer(),
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
it('does not throw for invalid data', () => {
|
|
214
|
+
const result = schema.build({
|
|
215
|
+
// @ts-expect-error
|
|
216
|
+
actor: 'not-a-did',
|
|
217
|
+
count: 123,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(result.$type).toBe('io.example.record')
|
|
221
|
+
expect(result.actor).toBe('not-a-did')
|
|
222
|
+
expect(result.count).toBe(123)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('does not throw for invalid types', () => {
|
|
226
|
+
const result = schema.build({
|
|
227
|
+
actor: 'did:plc:abc123',
|
|
228
|
+
// @ts-expect-error
|
|
229
|
+
count: 'not-a-number',
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
expect(result.$type).toBe('io.example.record')
|
|
233
|
+
expect(result.count).toBe('not-a-number')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('does not throw for missing required fields', () => {
|
|
237
|
+
// @ts-expect-error
|
|
238
|
+
const result = schema.build({})
|
|
239
|
+
|
|
240
|
+
expect(result.$type).toBe('io.example.record')
|
|
241
|
+
expect(result.actor).toBeUndefined()
|
|
242
|
+
expect(result.count).toBeUndefined()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('does not throw for extra fields', () => {
|
|
246
|
+
const result = schema.build({
|
|
247
|
+
actor: 'did:plc:abc123',
|
|
248
|
+
count: 42,
|
|
249
|
+
// @ts-expect-error
|
|
250
|
+
extra: 'unexpected',
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
expect(result.$type).toBe('io.example.record')
|
|
254
|
+
|
|
255
|
+
// @ts-expect-error
|
|
256
|
+
expect(result.extra).toBe('unexpected')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('parse() still validates after build()', () => {
|
|
260
|
+
const built = schema.build({
|
|
261
|
+
// @ts-expect-error
|
|
262
|
+
actor: 'not-a-did',
|
|
263
|
+
count: 123,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
expect(() => schema.parse(built)).toThrow('Invalid DID')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('safeParse() can detect validation errors after build()', () => {
|
|
270
|
+
const built = schema.build({
|
|
271
|
+
// @ts-expect-error
|
|
272
|
+
actor: 'not-a-did',
|
|
273
|
+
count: 123,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const result = schema.safeParse(built)
|
|
277
|
+
expect(result.success).toBe(false)
|
|
159
278
|
})
|
|
160
|
-
expect(result.$type).toBe('app.bsky.feed.post')
|
|
161
279
|
})
|
|
162
280
|
})
|
|
163
281
|
|
package/src/schema/record.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LexMap } from '@atproto/lex-data'
|
|
1
2
|
import {
|
|
2
3
|
$Typed,
|
|
3
4
|
$typed,
|
|
@@ -51,9 +52,9 @@ export type TypedRecord<
|
|
|
51
52
|
* ```
|
|
52
53
|
*/
|
|
53
54
|
export class RecordSchema<
|
|
54
|
-
const TKey extends LexiconRecordKey =
|
|
55
|
-
const TType extends NsidString =
|
|
56
|
-
const TShape extends Validator<
|
|
55
|
+
const TKey extends LexiconRecordKey = LexiconRecordKey,
|
|
56
|
+
const TType extends NsidString = NsidString,
|
|
57
|
+
const TShape extends Validator<LexMap> = Validator<LexMap>,
|
|
57
58
|
> extends Schema<
|
|
58
59
|
$Typed<InferInput<TShape>, TType>,
|
|
59
60
|
$Typed<InferOutput<TShape>, TType>
|
|
@@ -86,9 +87,13 @@ export class RecordSchema<
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
build(
|
|
89
|
-
input: Omit<
|
|
90
|
-
): $Typed<InferOutput<
|
|
91
|
-
|
|
90
|
+
input: Omit<InferOutput<TShape>, '$type'>,
|
|
91
|
+
): $Typed<InferOutput<TShape>, TType>
|
|
92
|
+
build(
|
|
93
|
+
input: Omit<InferInput<TShape>, '$type'>,
|
|
94
|
+
): $Typed<InferInput<TShape>, TType>
|
|
95
|
+
build(input: Record<string, unknown>) {
|
|
96
|
+
return $typed(input, this.$type)
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
isTypeOf<TValue extends { $type?: unknown }>(
|
|
@@ -199,11 +204,11 @@ type AsNsid<T> = T extends `${string}#${string}` ? never : T
|
|
|
199
204
|
export function record<
|
|
200
205
|
const K extends LexiconRecordKey,
|
|
201
206
|
const T extends NsidString,
|
|
202
|
-
const S extends Validator<
|
|
207
|
+
const S extends Validator<LexMap>,
|
|
203
208
|
>(key: K, type: AsNsid<T>, validator: S): RecordSchema<K, T, S>
|
|
204
209
|
export function record<
|
|
205
210
|
const K extends LexiconRecordKey,
|
|
206
|
-
const V extends { $type: NsidString },
|
|
211
|
+
const V extends LexMap & { $type: NsidString },
|
|
207
212
|
>(
|
|
208
213
|
key: K,
|
|
209
214
|
type: AsNsid<V['$type']>,
|
|
@@ -213,7 +218,7 @@ export function record<
|
|
|
213
218
|
export function record<
|
|
214
219
|
const K extends LexiconRecordKey,
|
|
215
220
|
const T extends NsidString,
|
|
216
|
-
const S extends Validator<
|
|
221
|
+
const S extends Validator<LexMap>,
|
|
217
222
|
>(key: K, type: T, validator: S) {
|
|
218
223
|
return new RecordSchema<K, T, S>(key, type, validator)
|
|
219
224
|
}
|
package/src/schema/regexp.ts
CHANGED
|
@@ -20,7 +20,10 @@ export class RegexpSchema<
|
|
|
20
20
|
> extends Schema<TValue> {
|
|
21
21
|
readonly type = 'regexp' as const
|
|
22
22
|
|
|
23
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
public readonly pattern: RegExp,
|
|
25
|
+
public readonly message?: string,
|
|
26
|
+
) {
|
|
24
27
|
super()
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -30,7 +33,11 @@ export class RegexpSchema<
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
if (!this.pattern.test(input)) {
|
|
33
|
-
return ctx.issueInvalidFormat(
|
|
36
|
+
return ctx.issueInvalidFormat(
|
|
37
|
+
input,
|
|
38
|
+
this.pattern.toString(),
|
|
39
|
+
this.message,
|
|
40
|
+
)
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
return ctx.success(input as TValue)
|
|
@@ -67,6 +74,9 @@ export class RegexpSchema<
|
|
|
67
74
|
* ```
|
|
68
75
|
*/
|
|
69
76
|
/*@__NO_SIDE_EFFECTS__*/
|
|
70
|
-
export function regexp<TInput extends string = string>(
|
|
71
|
-
|
|
77
|
+
export function regexp<TInput extends string = string>(
|
|
78
|
+
pattern: RegExp,
|
|
79
|
+
message?: string,
|
|
80
|
+
): RegexpSchema<TInput> {
|
|
81
|
+
return new RegexpSchema<TInput>(pattern, message)
|
|
72
82
|
}
|
package/src/schema/string.ts
CHANGED
|
@@ -99,12 +99,15 @@ export class StringSchema<
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Optimization: count graphemes once
|
|
102
103
|
let lazyGraphLen: number
|
|
103
104
|
|
|
104
105
|
const minGraphemes = this.options.minGraphemes
|
|
105
106
|
if (minGraphemes != null) {
|
|
106
|
-
// Optimization: avoid counting graphemes if the length check already fails
|
|
107
107
|
if (str.length < minGraphemes) {
|
|
108
|
+
// If the JavaScript string length (UTF-16) is below the minimal limit,
|
|
109
|
+
// its grapheme length (which <= .length) will also be below.
|
|
110
|
+
// Fail early.
|
|
108
111
|
return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)
|
|
109
112
|
} else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {
|
|
110
113
|
return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)
|
|
@@ -113,7 +116,10 @@ export class StringSchema<
|
|
|
113
116
|
|
|
114
117
|
const maxGraphemes = this.options.maxGraphemes
|
|
115
118
|
if (maxGraphemes != null) {
|
|
116
|
-
if (
|
|
119
|
+
if (str.length <= maxGraphemes) {
|
|
120
|
+
// If the JavaScript string length (UTF-16) is within the maximum limit,
|
|
121
|
+
// its grapheme length (which <= .length) will also be within.
|
|
122
|
+
} else if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
|
|
117
123
|
return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
|
|
118
124
|
}
|
|
119
125
|
}
|
|
@@ -303,6 +303,83 @@ describe('TypedObjectSchema', () => {
|
|
|
303
303
|
const result = emptySchema.build(input)
|
|
304
304
|
expect(result).toEqual({ $type: 'app.bsky.test' })
|
|
305
305
|
})
|
|
306
|
+
|
|
307
|
+
describe('build() does not validate', () => {
|
|
308
|
+
const validationSchema = typedObject(
|
|
309
|
+
'app.bsky.test',
|
|
310
|
+
'validation',
|
|
311
|
+
object({
|
|
312
|
+
actor: string({ format: 'did' }),
|
|
313
|
+
count: integer(),
|
|
314
|
+
}),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
it('does not throw for invalid data', () => {
|
|
318
|
+
const result = validationSchema.build({
|
|
319
|
+
// @ts-expect-error
|
|
320
|
+
actor: 'not-a-did',
|
|
321
|
+
count: 123,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
expect(result.$type).toBe('app.bsky.test#validation')
|
|
325
|
+
expect(result.actor).toBe('not-a-did')
|
|
326
|
+
expect(result.count).toBe(123)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('does not throw for invalid types', () => {
|
|
330
|
+
const result = validationSchema.build({
|
|
331
|
+
actor: 'did:plc:abc123',
|
|
332
|
+
// @ts-expect-error
|
|
333
|
+
count: 'not-a-number',
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
expect(result.$type).toBe('app.bsky.test#validation')
|
|
337
|
+
expect(result.count).toBe('not-a-number')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('does not throw for missing required fields', () => {
|
|
341
|
+
// @ts-expect-error
|
|
342
|
+
const result = validationSchema.build({})
|
|
343
|
+
|
|
344
|
+
expect(result.$type).toBe('app.bsky.test#validation')
|
|
345
|
+
expect(result.actor).toBeUndefined()
|
|
346
|
+
expect(result.count).toBeUndefined()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('does not throw for extra fields', () => {
|
|
350
|
+
const result = validationSchema.build({
|
|
351
|
+
actor: 'did:plc:abc123',
|
|
352
|
+
count: 42,
|
|
353
|
+
// @ts-expect-error
|
|
354
|
+
extra: 'unexpected',
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
expect(result.$type).toBe('app.bsky.test#validation')
|
|
358
|
+
// @ts-expect-error
|
|
359
|
+
expect(result.extra).toBe('unexpected')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('parse() still validates after build()', () => {
|
|
363
|
+
const built = validationSchema.build({
|
|
364
|
+
// @ts-expect-error
|
|
365
|
+
actor: 'not-a-did',
|
|
366
|
+
count: 123,
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
expect(() => validationSchema.parse(built)).toThrow('Invalid DID')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('safeParse() can detect validation errors after build()', () => {
|
|
373
|
+
const built = validationSchema.build({
|
|
374
|
+
// @ts-expect-error
|
|
375
|
+
actor: 'not-a-did',
|
|
376
|
+
count: 123,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const result = validationSchema.safeParse(built)
|
|
380
|
+
expect(result.success).toBe(false)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
306
383
|
})
|
|
307
384
|
|
|
308
385
|
describe('$build method', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPlainObject } from '@atproto/lex-data'
|
|
1
|
+
import { LexMap, isPlainObject } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
$Type,
|
|
4
4
|
$TypeOf,
|
|
@@ -43,7 +43,7 @@ export type MaybeTypedObject<
|
|
|
43
43
|
*/
|
|
44
44
|
export class TypedObjectSchema<
|
|
45
45
|
const TType extends $Type = $Type,
|
|
46
|
-
const TShape extends Validator<
|
|
46
|
+
const TShape extends Validator<LexMap> = Validator<LexMap>,
|
|
47
47
|
> extends Schema<
|
|
48
48
|
$TypedMaybe<InferInput<TShape>, TType>,
|
|
49
49
|
$TypedMaybe<InferOutput<TShape>, TType>
|
|
@@ -74,12 +74,13 @@ export class TypedObjectSchema<
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
build(
|
|
77
|
-
input: Omit<
|
|
78
|
-
): $Typed<InferOutput<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
input: Omit<InferOutput<TShape>, '$type'>,
|
|
78
|
+
): $Typed<InferOutput<TShape>, TType>
|
|
79
|
+
build(
|
|
80
|
+
input: Omit<InferInput<TShape>, '$type'>,
|
|
81
|
+
): $Typed<InferInput<TShape>, TType>
|
|
82
|
+
build(input: Record<string, unknown>) {
|
|
83
|
+
return $typed(input, this.$type)
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
isTypeOf<TValue extends Record<string, unknown>>(
|
|
@@ -155,7 +156,7 @@ export class TypedObjectSchema<
|
|
|
155
156
|
export function typedObject<
|
|
156
157
|
const N extends NsidString,
|
|
157
158
|
const H extends string,
|
|
158
|
-
const S extends Validator<
|
|
159
|
+
const S extends Validator<LexMap>,
|
|
159
160
|
>(nsid: N, hash: H, validator: S): TypedObjectSchema<$Type<N, H>, S>
|
|
160
161
|
export function typedObject<V extends { $type?: $Type }>(
|
|
161
162
|
nsid: V extends { $type?: infer T extends string }
|
|
@@ -174,7 +175,7 @@ export function typedObject<V extends { $type?: $Type }>(
|
|
|
174
175
|
export function typedObject<
|
|
175
176
|
const N extends NsidString,
|
|
176
177
|
const H extends string,
|
|
177
|
-
const S extends Validator<
|
|
178
|
+
const S extends Validator<LexMap>,
|
|
178
179
|
>(nsid: N, hash: H, validator: S) {
|
|
179
180
|
return new TypedObjectSchema<$Type<N, H>, S>($type(nsid, hash), validator)
|
|
180
181
|
}
|