Skip to content
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ If so this is the framework for you.

Functional Models empowers the creation of pure TypeScript/JavaScript function based models that can be used on a client, a web frontend, and/or a backend all the same time. Use this library to create models that can be reused <b>EVERYWHERE</b>.

Write validation code, metadata, property descriptions, and more! Functional Models is fully supportive of both TypeScript and JavaScript. In fact, the typescript empowers some really sweet dynamic type checking, and autocomplete!
Write validation code, metadata, property descriptions, and more! Functional Models is fully supportive of both TypeScript and JavaScript. In fact, the typescript empowers some really sweet dynamic type checking, and autocomplete! Now features Zod implementation for the model and properties.

This framework was born out of the enjoyment and power of working with Django models, but, restricting their "god-like abilities" which can cause developers to make a spaghetti system that is nearly impossible to optimize or improve without starting from scratch.

Expand Down Expand Up @@ -76,6 +76,7 @@ const {
const Trucks = Model({
pluralName: 'Trucks',
namespace: '@my-package/cars',
description: 'This is an optional description of my Trucks model.',
properties: {
id: PrimaryKeyUuidProperty(),
make: TextProperty({ maxLength: 20, minLength: 3, required: true }),
Expand All @@ -90,6 +91,10 @@ const Trucks = Model({
},
})

// Get a Zod Schema for the Truck, automatically built!
const zodSchema = Trucks.getModelDefinition().schema
// Conver to OpenAPI with zod.

// Create an instance of the model. In this case, you don't need 'id', because it gets created automatically with UniquePropertyId()
const myTruck = Trucks.create({
make: 'Ford',
Expand Down Expand Up @@ -157,9 +162,11 @@ import {
Model,
DatetimeProperty,
NumberProperty,
ObjectProperty,
TextProperty,
PrimaryKeyUuidProperty,
} from 'functional-models'
import { z } from 'zod'

// Create an object type. NOTE: Singular Uppercase
type VehicleMake = {
Expand Down Expand Up @@ -188,6 +195,10 @@ const VehicleMakes = Model<VehicleMake>({
},
})

type ToolChest = Readonly<{
toolCount: number
}>

// Create a model for the Vehicle type
const Vehicles = Model<Vehicle>({
pluralName: 'Vehicles',
Expand All @@ -210,6 +221,14 @@ const Vehicles = Model<Vehicle>({
}),
make: ModelReferenceProperty<VehicleMake>(VehicleMakes, { required: true }),
history: BigTextProperty({ required: false }),
// This overrides the automatic zod creation. Useful for complex properties, like objects.
toolChest: ObjectProperty<ToolChest>({
zod: z
.Object<ToolChest>({
toolCount: z.number().int(),
})
.describe('An optional tool chest for the vehicle'),
}),
lastModified: DatetimeProperty({ autoNow: true }),
},
})
Expand Down Expand Up @@ -403,6 +422,8 @@ For additional information on the ORM system see:
There are numerous properties that are supported out of the box that cover most data modeling needs. It is also very easy to create custom properties that encapsulate unique choices
validation requirements, etc.

NOTE: While a simple zod is automatically built for each property, this can be overrided and any zod can be provided. Just a "zod" field to a property that you want to override.

## List of Properties Out-Of-The-Box

### Dates
Expand Down
16 changes: 13 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "functional-models",
"version": "3.3.0",
"version": "3.4.0",
"description": "Functional models is ooey gooey framework for building and using awesome models EVERYWHERE.",
"main": "index.js",
"types": "index.d.ts",
Expand Down Expand Up @@ -88,6 +88,7 @@
"get-random-values": "^1.2.2",
"lodash": "^4.17.21",
"modern-async": "^2.0.4",
"openapi-types": "^12.1.3"
"openapi-types": "^12.1.3",
"zod": "^4.1.11"
}
}
71 changes: 71 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { OpenAPIV3 } from 'openapi-types'
import kebabCase from 'lodash/kebabCase'
import flow from 'lodash/flow'
import merge from 'lodash/merge'
import get from 'lodash/get'
import { z, ZodType } from 'zod'
import {
ApiInfo,
ApiInfoPartialRest,
Expand Down Expand Up @@ -255,6 +257,74 @@ const populateApiInformation = (
return _fillOutRestInfo(pluralName, namespace, partial, nullRest)
}

/**
* Create a zod schema generator for a property given its type and config.
* Returns a function that when called produces the zod schema.
*/
const createZodForProperty =
(propertyType: any, config?: PropertyConfig<any>) => () => {
const myConfig: PropertyConfig<any> = config || {}
const provided = myConfig.zod
if (provided) {
return provided as ZodType<any>
}

const _getZodForPropertyType = (pt: any) => {
switch (pt) {
case 'UniqueId':
return z.string()
case 'Date':
case 'Datetime':
return z.union([z.string(), z.date()])
case 'Integer':
return z.number().int()
case 'Number':
return z.number()
case 'Boolean':
return z.boolean()
case 'Array':
return z.array(z.any())
case 'Object':
return z.object()
case 'Email':
return z.email()
case 'Text':
case 'BigText':
return z.string()
case 'ModelReference':
return z.union([z.string(), z.number()])
default:
return z.any()
}
}

const baseSchema = _getZodForPropertyType(propertyType)
const choices = (config as any)?.choices
const schemaFromChoices =
choices && Array.isArray(choices) && choices.length > 0
? z.union(choices.map((c: any) => z.literal(c)) as any)
: baseSchema

const finalSchema = flow([
s =>
typeof myConfig.minValue === 'number' ? s.min(myConfig.minValue) : s,
s =>
typeof myConfig.maxValue === 'number' ? s.max(myConfig.maxValue) : s,
s =>
typeof myConfig.minLength === 'number' ? s.min(myConfig.minLength) : s,
s =>
typeof myConfig.maxLength === 'number' ? s.max(myConfig.maxLength) : s,
s =>
myConfig.defaultValue !== undefined
? s.default(myConfig.defaultValue)
: s,
s => (myConfig.required ? s : s.optional()),
s => (myConfig.description ? s.describe(myConfig.description) : s),
])(schemaFromChoices)

return finalSchema as ZodType<any>
}

export {
isReferencedProperty,
getValueForModelInstance,
Expand All @@ -269,4 +339,5 @@ export {
populateApiInformation,
NULL_ENDPOINT,
NULL_METHOD,
createZodForProperty,
}
23 changes: 23 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import merge from 'lodash/merge'
import z, { ZodObject, ZodType } from 'zod'
import { toJsonAble } from './serialization'
import { createModelValidator } from './validation'
import {
Expand Down Expand Up @@ -43,6 +44,27 @@ const _convertOptions = <T extends DataDescription>(
return r
}

const _createZod = <T extends DataDescription>(
modelDefinition: MinimalModelDefinition<T>
): ZodObject<DataDescription> => {
if (modelDefinition.schema) {
if (modelDefinition.description) {
return modelDefinition.schema.describe(modelDefinition.description)
}
return modelDefinition.schema
}
const properties = Object.entries(modelDefinition.properties).reduce(
(acc, [key, property]) => {
const asProp = property as PropertyInstance<any>
return merge(acc, {
[key]: asProp.getZod(),
})
},
{} as Record<string, ZodType>
)
return z.object(properties) as ZodObject<DataDescription>
}

const _toModelDefinition = <T extends DataDescription>(
minimal: MinimalModelDefinition<T>
): ModelDefinition<T> => {
Expand All @@ -52,6 +74,7 @@ const _toModelDefinition = <T extends DataDescription>(
description: '',
primaryKeyName: 'id',
modelValidators: [],
schema: _createZod(minimal),
...minimal,
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/properties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import merge from 'lodash/merge'
import get from 'lodash/get'
import { ZodType } from 'zod'
import {
arrayType,
createPropertyValidator,
Expand Down Expand Up @@ -47,6 +48,7 @@ import {
getCommonNumberValidators,
mergeValidators,
isModelInstance,
createZodForProperty,
} from './lib'

const MAX_YEAR = 3000
Expand Down Expand Up @@ -162,6 +164,17 @@ const Property = <
return _propertyValidatorWrapper
}

// Build a zod schema for this property. If a zod schema is provided in the
// config it will be used as an override.
const getZod = (): ZodType<TValue> => {
const provided = (config as any)?.zod
if (provided) {
return provided as ZodType<TValue>
}

return createZodForProperty(propertyType, config)()
}

const propertyInstance: PropertyInstance<
TValue,
TData,
Expand All @@ -175,6 +188,7 @@ const Property = <
getConstantValue,
getPropertyType: () => propertyType,
createGetter,
getZod,
getValidator,
}
return propertyInstance
Expand Down
18 changes: 18 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as openapi from 'openapi-types'
import { ZodObject, ZodType } from 'zod'

/**
* A function that returns the value, or just the value
Expand Down Expand Up @@ -342,6 +343,10 @@ type PropertyInstance<
TModelInstanceExtensions
>
) => ValueGetter<TValue, TData, TModelExtensions, TModelInstanceExtensions>
/**
* Function that exposes a zod schema for this property.
*/
getZod: () => ZodType<TValue>
/**
* Gets a validator for the property. This is not normally used.
* Instead for validation look at {@link ModelInstance.validate}
Expand Down Expand Up @@ -499,6 +504,15 @@ type PropertyConfigOptions<TValue extends Arrayable<DataValue>> = Readonly<
* Additional validators for the property.
*/
validators: readonly PropertyValidatorComponent<any>[]
/**
* An optional zod schema for this property. If provided, it will be used as an
* override for the generated schema.
*/
zod?: ZodType<any>
/**
* A short human readable description of the property for documentation.
*/
description?: string
/**
* The maximum length of the value. (Drives validation)
*/
Expand Down Expand Up @@ -733,6 +747,10 @@ type ModelDefinition<TData extends DataDescription> = Readonly<{
* look at {@link ModelType.getApiInfo}
*/
api?: Partial<ApiInfoPartialRest>
/**
* A zod schema for the model.
*/
schema: ZodObject<DataDescription>
}>

/**
Expand Down
Loading