-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(data-types): added support for records and tuples
- Loading branch information
Showing
12 changed files
with
455 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Opts, Ref, ZodToOneField } from "../converter-type"; | ||
import { KeySchema, ZodRecord, ZodTypeAny } from "zod"; | ||
|
||
export type ZodRecordOneField = { | ||
type: "object"; | ||
required: true; | ||
}; | ||
|
||
export const convertRecordSchema = < | ||
Key extends KeySchema, | ||
Value extends ZodTypeAny, | ||
>( | ||
_: ZodRecord<Key, Value>, | ||
ref: Ref, | ||
opts: Opts, | ||
): ZodToOneField<ZodRecord<Key, Value>> => { | ||
opts.logger?.debug( | ||
`A record is specified at \`${ref.currentPath.join(".")}\`. Records cannot only be represented as a generic object in OneTable, so it will be typed as \`Record<any, any>\` instead, clobbering typing on all internal keys and values.`, | ||
); | ||
return { type: "object", required: true } as ZodToOneField< | ||
ZodRecord<Key, Value> | ||
>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { zodOneFieldSchema } from "../"; | ||
import { Opts, Ref, ZodToOneField } from "../converter-type"; | ||
import { ZodTuple, ZodTypeAny } from "zod"; | ||
|
||
type AreAllSame<T extends [ZodTypeAny, ...ZodTypeAny[]]> = T extends [ | ||
infer First, | ||
...infer Rest, | ||
] | ||
? Rest[number] extends First // Check if all rest elements extend First | ||
? First extends Rest[number] // Ensure both directions to handle empty tuples correctly | ||
? true | ||
: false | ||
: false | ||
: true; // Base case for single element | ||
|
||
export type ZodTupleOneField< | ||
T extends [ZodTypeAny, ...ZodTypeAny[]] | [], | ||
Rest extends ZodTypeAny | null = null, | ||
> = Rest extends null | ||
? T extends [ZodTypeAny] | ||
? { type: "array"; required: true; items: ZodToOneField<T[number]> } | ||
: T extends [ZodTypeAny, ...ZodTypeAny[]] | ||
? AreAllSame<T> extends false | ||
? { type: "array"; required: true } | ||
: { type: "array"; required: true; items: ZodToOneField<T[number]> } | ||
: { type: "array"; required: true } // base case | ||
: Rest extends ZodTypeAny | ||
? T extends [ZodTypeAny] | ||
? AreAllSame<[Rest, ...T]> extends false | ||
? { type: "array"; required: true } | ||
: { type: "array"; required: true; items: ZodToOneField<Rest> } | ||
: T extends [ZodTypeAny, ...ZodTypeAny[]] | ||
? AreAllSame<[Rest, ...T]> extends false | ||
? { type: "array"; required: true } | ||
: { type: "array"; required: true; items: ZodToOneField<T[number]> } | ||
: { type: "array"; required: true } // base case | ||
: { type: "array"; required: true }; // base case | ||
|
||
const zodSchemasAreSame = ( | ||
schema1: ZodTypeAny, | ||
schema2: ZodTypeAny, | ||
): boolean => { | ||
// Check if they are the same Zod type | ||
if (schema1._def.typeName !== schema2._def.typeName) { | ||
return false; | ||
} | ||
|
||
// Special case for ZodObject, where we want to compare shapes | ||
if ( | ||
schema1._def.typeName === "ZodObject" && | ||
schema2._def.typeName === "ZodObject" | ||
) { | ||
return ( | ||
JSON.stringify(schema1._def.shape()) === | ||
JSON.stringify(schema2._def.shape()) | ||
); | ||
} | ||
|
||
// For other types, compare the definitions directly | ||
return JSON.stringify(schema1._def) === JSON.stringify(schema2._def); | ||
}; | ||
|
||
export const convertTupleSchema = < | ||
T extends [ZodTypeAny, ...ZodTypeAny[]] | [], | ||
Rest extends ZodTypeAny | null = null, | ||
>( | ||
zodSchema: ZodTuple<T, Rest>, | ||
ref: Ref, | ||
opts: Opts, | ||
): ZodToOneField<ZodTuple<T, Rest>> => { | ||
opts.logger?.debug( | ||
`A tuple is specified at \`${ref.currentPath.join(".")}\`. OneTable does not support tuples natively, will cast to an array instead.`, | ||
); | ||
const { items, rest } = zodSchema._def; | ||
const allItems = rest == null ? items : [rest as ZodTypeAny, ...items]; | ||
if (allItems.length === 0) { | ||
opts.logger?.debug( | ||
`A tuple with no internal schema is specified at \`${ref.currentPath.join(".")}\`. Cannot infer an \`items\` value with a tuple without an internal schema, will type as \`any[]\`.`, | ||
); | ||
return { type: "array", required: true } as ZodToOneField< | ||
ZodTuple<T, Rest> | ||
>; | ||
} | ||
if (allItems.length === 1) { | ||
const innnerType = allItems[0]; | ||
const items = zodOneFieldSchema( | ||
innnerType, | ||
{ currentPath: [...ref.currentPath, "0"] }, | ||
opts, | ||
); | ||
return { type: "array", required: true, items } as ZodToOneField< | ||
ZodTuple<T, Rest> | ||
>; | ||
} | ||
const { allIdentical } = allItems.reduce( | ||
({ lastType, allIdentical }, curr) => ({ | ||
lastType: curr, | ||
allIdentical: allIdentical && zodSchemasAreSame(lastType, curr), | ||
}), | ||
{ lastType: allItems[0], allIdentical: true }, | ||
); | ||
if (allIdentical) { | ||
const innnerType = allItems[0]; | ||
const items = zodOneFieldSchema( | ||
innnerType, | ||
{ currentPath: [...ref.currentPath, "0"] }, | ||
opts, | ||
); | ||
return { type: "array", required: true, items } as ZodToOneField< | ||
ZodTuple<T, Rest> | ||
>; | ||
} else { | ||
opts.logger?.debug( | ||
`A tuple with various internal schemas is specified at \`${ref.currentPath.join(".")}\`. OneTable does not support multiple data-types in arrays - will use \`any[]\` instead.`, | ||
); | ||
return { type: "array", required: true } as ZodToOneField< | ||
ZodTuple<T, Rest> | ||
>; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { z } from "zod"; | ||
import { convertRecordSchema } from "../../src/converters/record"; | ||
import { mock } from "vitest-mock-extended"; | ||
import { Logger } from "winston"; | ||
|
||
const mockLogger = mock<Logger>(); | ||
const mockOpts = { logger: mockLogger }; | ||
const mockRefs = { currentPath: [] }; | ||
|
||
describe("convertRecordSchema", () => { | ||
const keyTypes = [z.string(), z.number(), z.symbol()]; | ||
const testValueTypes = [ | ||
z.string(), | ||
z.number(), | ||
z.object({ hello: z.string() }), | ||
z.record(z.string(), z.unknown()), | ||
z.unknown(), | ||
z.array(z.string()), | ||
z.symbol(), | ||
z.set(z.string()), | ||
]; | ||
const testableZodRecordMatrix = keyTypes.flatMap((keySchema) => | ||
testValueTypes.map((valueSchema) => z.record(keySchema, valueSchema)), | ||
); | ||
|
||
it.each(testableZodRecordMatrix)( | ||
"should return schemaless object supplied for keyType $_def.keyType._def.typeName and valueType $_def.valueType._def.typeName", | ||
(zodRecordSchema) => { | ||
// Act | ||
const onefield = convertRecordSchema(zodRecordSchema, mockRefs, mockOpts); | ||
|
||
expect(onefield).toEqual({ type: "object", required: true }); | ||
}, | ||
); | ||
|
||
it("should notify the user that `z.record()` clobbers OneTable typing via debug", () => { | ||
// Assemble | ||
const zodRecordSchema = z.record(z.string(), z.unknown()); | ||
|
||
// Act | ||
convertRecordSchema( | ||
zodRecordSchema, | ||
{ currentPath: ["hello", "world"] }, | ||
mockOpts, | ||
); | ||
|
||
// Assert | ||
expect(mockLogger.debug.mock.lastCall).toEqual([ | ||
"A record is specified at `hello.world`. Records cannot only be represented as a generic object in OneTable, so it will be typed as `Record<any, any>` instead, clobbering typing on all internal keys and values.", | ||
]); | ||
}); | ||
}); |
Oops, something went wrong.