Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nested arrays #48

Merged
merged 1 commit into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions src/genSchema/generateZodSchemaCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { getDetailsFromDefinition } from './getDetailsFromDefinition.js'
import {generateZodSchemaCode} from "./generateZodSchemaCode.js";

describe('generateZodSchemaCode', () => {
describe('basic schema', () => {
it('returns schema for simple object', () => {
const definition = `
DEFINE FIELD reviews ON TABLE product TYPE array<string>;
DEFINE FIELD user ON TABLE product TYPE record<user>;
DEFINE FIELD rating ON TABLE product TYPE number;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
const generatedSchema = generateZodSchemaCode(fields, 'schema')

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
reviews: z.array(z.string()),
user: recordId('user'),
rating: z.number()
})
`)
})
})

describe('object schema', () => {
it('returns schema for simple object', () => {
const definition = `
DEFINE FIELD review ON TABLE product TYPE object;
DEFINE FIELD review.rating ON TABLE product TYPE number;
DEFINE FIELD review.comment ON TABLE product TYPE string;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
console.log("fields", fields)
const generatedSchema = generateZodSchemaCode(fields, 'schema')

console.log("generatedSchema", generatedSchema)

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
review: z.object({
rating: z.number(),
comment: z.string()
})
})
`)
})

it('returns schema for optional object', () => {
const definition = `
DEFINE FIELD review ON TABLE product TYPE option<object>;
DEFINE FIELD review.rating ON TABLE product TYPE number;
DEFINE FIELD review.comment ON TABLE product TYPE string;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
console.log("fields", fields)
const generatedSchema = generateZodSchemaCode(fields, 'schema')

console.log("generatedSchema", generatedSchema)

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
review: z.object({
rating: z.number(),
comment: z.string()
}).optional()
})
`)
})

it('returns schema for optional object derived from all values being optional', () => {
const definition = `
DEFINE FIELD review ON TABLE product TYPE object;
DEFINE FIELD review.rating ON TABLE product TYPE option<number>;
DEFINE FIELD review.comment ON TABLE product TYPE option<string>;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
console.log("fields", fields)
const generatedSchema = generateZodSchemaCode(fields, 'schema')

console.log("generatedSchema", generatedSchema)

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
review: z.object({
rating: z.number().optional(),
comment: z.string().optional()
}).optional()
})
`)
})

it('returns schema for object with nested array', () => {
const definition = `
DEFINE FIELD review ON TABLE product TYPE object;
DEFINE FIELD review.related ON TABLE product TYPE array<object>;
DEFINE FIELD review.related[*].name ON TABLE product TYPE string;
DEFINE FIELD review.related[*].rating ON TABLE product TYPE number;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
const generatedSchema = generateZodSchemaCode(fields, 'schema')


expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
review: z.object({
related: z.object({
name: z.string(),
rating: z.number()
}).array()
})
})
`)
})

it('returns schema for complex object', () => {
const definition = `
DEFINE FIELD name ON TABLE product TYPE string;
DEFINE FIELD price ON TABLE product TYPE number;
DEFINE FIELD published_at ON TABLE product TYPE datetime;
DEFINE FIELD is_published ON TABLE product TYPE bool;
DEFINE FIELD related_authors ON TABLE product TYPE option<array<record<author>>>;
DEFINE FIELD review ON TABLE product TYPE object;
DEFINE FIELD review.rating ON TABLE product TYPE number;
DEFINE FIELD review.comment ON TABLE product TYPE string;
DEFINE FIELD review.author ON TABLE product TYPE object;
DEFINE FIELD review.author.name ON TABLE product TYPE string;
DEFINE FIELD review.author.email ON TABLE product TYPE string;
DEFINE FIELD review.author.tags ON TABLE product TYPE array<string>;
DEFINE FIELD review.author.user ON TABLE product TYPE record<user>;
DEFINE FIELD review.related ON TABLE product TYPE array<object>;
DEFINE FIELD review.related[*].name ON TABLE product TYPE string;
DEFINE FIELD review.related[*].rating ON TABLE product TYPE number;
DEFINE FIELD review.related[*].book ON TABLE product TYPE record<book>;
DEFINE FIELD review.related[*].meta ON TABLE product type object;
DEFINE FIELD review.related[*].meta.rating ON TABLE product TYPE number;
DEFINE FIELD review.related[*].meta.comment ON TABLE product TYPE string;
DEFINE FIELD review.related[*].meta.tags ON TABLE product TYPE array<string>;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
console.log("fields", fields)
const generatedSchema = generateZodSchemaCode(fields, 'schema')

console.log("generatedSchema", generatedSchema)

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
name: z.string(),
price: z.number(),
published_at: z.string().datetime(),
is_published: z.boolean(),
related_authors: recordId('author').array().optional(),
review: z.object({
rating: z.number(),
comment: z.string(),
author: z.object({
name: z.string(),
email: z.string(),
tags: z.array(z.string()),
user: recordId('user')
}),
related: z.object({
name: z.string(),
rating: z.number(),
book: recordId('book'),
meta: z.object({
rating: z.number(),
comment: z.string(),
tags: z.array(z.string())
})
}).array()
})
})
`)
})

it('returns schema for complex object with duplicate field with asterisk syntax', () => {
const definition = `
DEFINE FIELD review ON TABLE product TYPE object;
DEFINE FIELD review.rating ON TABLE product TYPE number;
DEFINE FIELD review.comment ON TABLE product TYPE string;
DEFINE FIELD review.author ON TABLE product TYPE object;
DEFINE FIELD review.author.name ON TABLE product TYPE string;
DEFINE FIELD review.author.email ON TABLE product TYPE string;
DEFINE FIELD review.author.tags ON TABLE product TYPE array<string>;
DEFINE FIELD review.author.tags[*] ON TABLE product TYPE string;
DEFINE FIELD review.author.user ON TABLE product TYPE record<user>;
`
const fields = definition.split(';').filter(x => x.trim().length).map(def => getDetailsFromDefinition(def, false))
const generatedSchema = generateZodSchemaCode(fields, 'schema')

expect(generatedSchema).toEqualIgnoringWhitespace(`
const schema = z.object({
review: z.object({
rating: z.number(),
comment: z.string(),
author: z.object({
name: z.string(),
email: z.string(),
tags: z.string().array(),
user: recordId('user')
})
})
})
`)
})

})
})
24 changes: 18 additions & 6 deletions src/genSchema/generateZodSchemaCode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import type { FieldDetail } from './getDetailsFromDefinition.js'

const escapeRegExp = (string:string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};

const createRegex = (key:string) => {
const escapedKey = escapeRegExp(key);
return new RegExp(`(?<!${escapedKey})\\[\\*\\]`, 'g');
};

export const generateZodSchemaCode = (fields: FieldDetail[], schemaName: string): string => {
console.log("fields", fields)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const buildSchema = (fieldMap: { [key: string]: any }, fields: FieldDetail[]) => {
for (const field of fields) {
Expand All @@ -15,9 +25,6 @@ export const generateZodSchemaCode = (fields: FieldDetail[], schemaName: string)
if (i === parts.length - 1) {
// Leaf node
let zodString = field.zodString
if (field.type?.startsWith('array')) {
zodString = `z.array(${zodString})`
}
if (fieldDefault !== undefined) {
zodString += `.default(${fieldDefault})`
}
Expand All @@ -39,8 +46,13 @@ export const generateZodSchemaCode = (fields: FieldDetail[], schemaName: string)
const buildObject = (obj: { [key: string]: any }, parentKey = ''): string => {
const entries = Object.entries(obj).map(([key, value]) => {
const fullKey = parentKey ? `${parentKey}.${key}` : key
const regex = createRegex(key)
const isArray = fields.some(f => {
return f.name.replace(regex, '').includes(`${fullKey}[*]`)
})

if (typeof value === 'string') {
return `${key}: ${value}`
return `${key}: ${value}${isArray ? '.array()' : ''}`
}
const innerObject = buildObject(value, fullKey)
let objectSchema = `z.object({\n${innerObject}\n })`
Expand All @@ -52,8 +64,7 @@ export const generateZodSchemaCode = (fields: FieldDetail[], schemaName: string)
const fieldSchema = fields.find(f => f.name === fullKey)
const isOptionalFromSchema = fieldSchema?.zodString.includes('.optional()')

// Check if this object should be an array
if (fields.some(f => f.name.includes(`${fullKey}[*]`))) {
if (isArray) {
objectSchema += '.array()'
}

Expand All @@ -66,6 +77,7 @@ export const generateZodSchemaCode = (fields: FieldDetail[], schemaName: string)
return entries.join(',\n ')
}


const schema = `z.object({\n${buildObject(fieldMap)}\n})`
return `const ${schemaName} = ${schema}`
}
Expand Down
2 changes: 1 addition & 1 deletion src/genSchema/mergeNested.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('mergeNested', () => {
price: z.number(),
ratings: z.object({
score: z.number().optional()
}).optional()
}).array().optional()
}).array().optional()
})
`)
Expand Down