Skip to content

Commit

Permalink
feature: async schema support (#26)
Browse files Browse the repository at this point in the history
* feature: async schema support

* add $async support for meta object
  • Loading branch information
vitalics authored Jan 20, 2024
1 parent c820d83 commit 09c54ff
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 9 deletions.
13 changes: 13 additions & 0 deletions .changeset/six-pianos-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"ajv-ts": patch
---

Add async keyword support

example:

```ts
const obj = s.object({}).async()

obj.schema // {type: 'object', $async: true}
```
49 changes: 41 additions & 8 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ type AnySchemaBuilder =
| UnknownSchemaBuilder<unknown>
| NotSchemaBuilder

type MetaObject = Pick<BaseSchema, 'title'> &
Pick<BaseSchema, 'description'> &
Pick<BaseSchema, 'deprecated'> &
Pick<BaseSchema, '$id'>
type MetaObject =
& Pick<BaseSchema, 'title'>
& Pick<BaseSchema, 'description'>
& Pick<BaseSchema, 'deprecated'>
& Pick<BaseSchema, '$id'>
& Pick<BaseSchema, '$async'>
& Pick<BaseSchema, '$ref'>

export type SafeParseResult<T> = SafeParseSuccessResult<T> | SafeParseErrorResult

Expand Down Expand Up @@ -187,6 +190,12 @@ abstract class SchemaBuilder<
if (obj.$id) {
this.schema.$id = obj.$id
}
if (obj.$ref) {
this.schema.$ref = obj.$ref
}
if (obj.$async) {
this.schema.$async = obj.$async
}
}
return this
}
Expand All @@ -208,6 +217,28 @@ abstract class SchemaBuilder<
return this
}

/**
* set `$async=true` for your current schema.
*
* @see {@link https://ajv.js.org/guide/async-validation.html ajv async validation}
*/
async() {
(this.schema as Record<string, unknown>).$async = true;
return this;
}

/**
* set `$async=false` for your current schema.
* @param [remove=false] applies `delete` operator for `schema.$async` property.
*/
sync(remove: boolean = false) {
(this.schema as AnySchema).$async = false;
if (remove) {
delete (this.schema as AnySchema).$async
}
return this;
}

/**
* Construct Array schema. Same as `s.array(s.number())`
*
Expand Down Expand Up @@ -839,7 +870,6 @@ class ObjectSchemaBuilder<
super({
type: 'object',
properties: {},
required: [],
})
Object.entries(def).forEach(([key, d]) => {
this.schema.properties![key] = d.schema
Expand Down Expand Up @@ -889,7 +919,7 @@ class ObjectSchemaBuilder<
partialFor<Key extends keyof T = keyof T>(
key: Key,
): ObjectSchemaBuilder<Definition, ObjectTypes.OptionalByKey<T, Key>> {
const required = this.schema.required as string[]
const required = this.schema.required ?? [] as string[]
const findedIndex = required.indexOf(key as string)
// remove element from array. e.g. "email" for ['name', 'email'] => ['name']
// opposite of push
Expand Down Expand Up @@ -961,7 +991,7 @@ class ObjectSchemaBuilder<
...keys: Key[]
): ObjectSchemaBuilder<Definition, ObjectTypes.RequiredByKeys<T, (typeof keys)[number]>> {
this.schema.required = [
...new Set([...this.schema.required!, ...keys as string[]]),
...new Set([...(this.schema.required ?? []), ...keys as string[]]),
]
return this as never
}
Expand All @@ -975,7 +1005,7 @@ class ObjectSchemaBuilder<
const allProperties = Object.keys(this.schema.properties!)
// keep unique only
this.schema.required = [
...new Set([...this.schema.required!, ...allProperties]),
...new Set([...(this.schema.required ?? []), ...allProperties]),
]
return this as never
}
Expand Down Expand Up @@ -1020,6 +1050,9 @@ class ObjectSchemaBuilder<
* type C = s.infer<typeof c> // {num: number; str: string}
*/
extend<ObjDef extends ObjectDefinition = ObjectDefinition>(def: ObjDef): ObjectSchemaBuilder<Merge<Definition, ObjDef>> {
if (!this.schema.properties || typeof this.schema.properties !== 'object') {
this.schema.properties = {};
}
Object.entries(def).forEach(([key, def]) => {
this.schema.properties![key] = def.schema
})
Expand Down
1 change: 1 addition & 0 deletions src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type BaseSchema = SchemaObject & {
not?: AnySchemaOrAnnotation
type?: string
$ref?: string
$async?: boolean;
/**
* ## New in draft 7
* The `$comment` keyword is strictly intended for adding comments to a schema.
Expand Down
43 changes: 42 additions & 1 deletion tests/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,45 @@ test('should throws for "undefined" value for nullable schema', () => {
const str = s.string().nullable()

expect(() => str.parse(undefined)).toThrow(Error)
})
})

test('async schema', () => {
const Schema = s.object({
name: s.string()
}).async()

const a = Schema.parse({ name: 'hello' })
expect(Schema.schema).toMatchObject({
type: 'object',
$async: true,
properties: {
name: { type: 'string' }
}
})

expect(a.name).toBe('hello')
})

test('make sync schema', () => {
const async = s.object({}).async()

expect(async.schema).toMatchObject({
$async: true,
type: 'object',
properties: {},
})
const sync = async.sync()

expect(sync.schema).toMatchObject({
type: 'object',
$async: false,
properties: {},
})


const syncRemoved = sync.sync(true)
expect(syncRemoved.schema).toMatchObject({
type: 'object',
properties: {},
})
})

0 comments on commit 09c54ff

Please sign in to comment.