Skip to content

Commit

Permalink
feat: closes #200
Browse files Browse the repository at this point in the history
  • Loading branch information
edobrb committed Jan 2, 2024
1 parent 3080e4d commit 87cc019
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 71 deletions.
27 changes: 15 additions & 12 deletions packages/example/src/core/user/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import { model } from '@mondrian-framework/model'

export type User = model.Infer<typeof User>
export const User = () =>
model.entity({
id: idType,
firstName: model.string(),
lastName: model.string(),
email: model.email(),
posts: model.array(Post),
givenLikes: model.array(Like),
followers: model.array(Follower),
followeds: model.array(Follower),
registeredAt: model.datetime(),
loginAt: model.datetime(),
})
model.entity(
{
id: idType,
firstName: model.string(),
lastName: model.string(),
email: model.email(),
posts: model.array(Post),
givenLikes: model.array(Like),
followers: model.describe(model.array(Follower), 'Users that follows me'),
followeds: model.describe(model.array(Follower), 'Users followed by me'),
registeredAt: model.datetime(),
loginAt: model.datetime(),
},
{ description: 'User of the system' },
)

export const MyUser = () => User
export type MyUser = User
Expand Down
19 changes: 15 additions & 4 deletions packages/graphql/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,10 @@ function objectToInputGraphQLType(
): GraphQLInputObjectType {
const objectName = generateInputName(object, internalData)
const fields = () =>
mapObject(object.fields, typeToGraphQLInputObjectField({ ...internalData, defaultName: undefined }, objectName))
mapObject(
object.fields,
typeToGraphQLInputObjectField({ ...internalData, defaultName: undefined }, objectName, object.options),
)
return new GraphQLInputObjectType({
name: checkNameOccurencies(objectName, internalData),
fields,
Expand All @@ -292,7 +295,10 @@ function entityToGraphQLType(
): GraphQLObjectType {
const entityName = generateName(entity, internalData)
const fields = () =>
flatMapObject(entity.fields, typeToGraphQLObjectField({ ...internalData, defaultName: undefined }, entityName))
flatMapObject(
entity.fields,
typeToGraphQLObjectField({ ...internalData, defaultName: undefined }, entityName, entity.options),
)
return new GraphQLObjectType({
name: checkNameOccurencies(entityName, internalData),
fields,
Expand All @@ -317,6 +323,7 @@ function entityToInputGraphQLType(
function typeToGraphQLObjectField(
internalData: InternalData,
objectName: string,
objectOptions?: model.ObjectTypeOptions,
): (fieldName: string, fieldType: model.Type) => [string, GraphQLFieldConfig<any, any>][] {
return (fieldName, fieldType) => {
const tags = model.concretise(fieldType).options?.tags ?? {}
Expand Down Expand Up @@ -353,7 +360,7 @@ function typeToGraphQLObjectField(
{
type: canBeMissing ? graphQLType : new GraphQLNonNull(graphQLType),
args: graphqlRetrieveArgs,
description: model.getFirstDescription(fieldType),
description: (objectOptions?.fields ?? {})[fieldName]?.description,
},
],
]
Expand All @@ -380,6 +387,7 @@ function retrieveTypeToGraphqlArgs(
function typeToGraphQLInputObjectField(
internalData: InternalData,
objectName: string,
objectOptions?: model.ObjectTypeOptions,
): (fieldName: string, fieldType: model.Type) => GraphQLInputFieldConfig {
return (fieldName, fieldType) => {
const fieldDefaultName = generateInputName(fieldType, {
Expand All @@ -392,7 +400,10 @@ function typeToGraphQLInputObjectField(
defaultName: fieldDefaultName,
})
const canBeMissing = model.isOptional(fieldType) || model.isNullable(fieldType)
return { type: canBeMissing ? graphQLType : new GraphQLNonNull(graphQLType) }
return {
type: canBeMissing ? graphQLType : new GraphQLNonNull(graphQLType),
description: (objectOptions?.fields ?? {})[fieldName]?.description,
}
}
}

Expand Down
20 changes: 13 additions & 7 deletions packages/model/src/arbitrary/arbitrary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { model } from '../index'
import { model, utils } from '../index'
import { forbiddenObjectFields } from '../utils'
import gen from 'fast-check'

Expand Down Expand Up @@ -304,7 +304,10 @@ export function objectTypeOptions(): gen.Arbitrary<model.ObjectTypeOptions> {
*/
export function object<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.ObjectType<model.Mutability, Ts> | (() => model.ObjectType<model.Mutability, Ts>)> {
): gen.Arbitrary<
| model.ObjectType<model.Mutability, utils.RichFieldsToTypes<Ts>>
| (() => model.ObjectType<model.Mutability, utils.RichFieldsToTypes<Ts>>)
> {
const objectGenerator = gen.oneof(immutableObject(fieldsGenerators), mutableObject(fieldsGenerators))
const makeLazy = <A>(value: A) => {
return () => value
Expand All @@ -318,7 +321,10 @@ export function object<Ts extends model.Types>(
*/
export function entity<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.EntityType<model.Mutability, Ts> | (() => model.EntityType<model.Mutability, Ts>)> {
): gen.Arbitrary<
| model.EntityType<model.Mutability, utils.RichFieldsToTypes<Ts>>
| (() => model.EntityType<model.Mutability, utils.RichFieldsToTypes<Ts>>)
> {
const objectGenerator = gen.oneof(immutableEntity(fieldsGenerators), mutableEntity(fieldsGenerators))
const makeLazy = <A>(value: A) => {
return () => value
Expand All @@ -332,7 +338,7 @@ export function entity<Ts extends model.Types>(
*/
export function immutableObject<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.ObjectType<model.Mutability.Immutable, Ts>> {
): gen.Arbitrary<model.ObjectType<model.Mutability.Immutable, utils.RichFieldsToTypes<Ts>>> {
return orUndefined(objectTypeOptions()).chain((options) => {
return gen.record(fieldsGenerators).map((fields) => {
return model.object(fields, options)
Expand All @@ -346,7 +352,7 @@ export function immutableObject<Ts extends model.Types>(
*/
export function mutableObject<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.ObjectType<model.Mutability.Mutable, Ts>> {
): gen.Arbitrary<model.ObjectType<model.Mutability.Mutable, utils.RichFieldsToTypes<Ts>>> {
return orUndefined(objectTypeOptions()).chain((options) => {
return gen.record(fieldsGenerators).map((fields) => {
return model.mutableObject(fields, options)
Expand All @@ -368,7 +374,7 @@ export function entityTypeOptions(): gen.Arbitrary<model.EntityTypeOptions> {
*/
export function immutableEntity<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.EntityType<model.Mutability.Immutable, Ts>> {
): gen.Arbitrary<model.EntityType<model.Mutability.Immutable, utils.RichFieldsToTypes<Ts>>> {
return orUndefined(entityTypeOptions()).chain((options) => {
return gen.record(fieldsGenerators).map((fields) => {
return model.entity(fields, options)
Expand All @@ -382,7 +388,7 @@ export function immutableEntity<Ts extends model.Types>(
*/
export function mutableEntity<Ts extends model.Types>(
fieldsGenerators: GeneratorsRecord<Ts>,
): gen.Arbitrary<model.EntityType<model.Mutability.Mutable, Ts>> {
): gen.Arbitrary<model.EntityType<model.Mutability.Mutable, utils.RichFieldsToTypes<Ts>>> {
return orUndefined(entityTypeOptions()).chain((options) => {
return gen.record(fieldsGenerators).map((fields) => {
return model.mutableEntity(fields, options)
Expand Down
46 changes: 29 additions & 17 deletions packages/model/src/type-system.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decoding, validation, model, result, encoding } from './index'
import { decoding, validation, model, result, encoding, utils } from './index'
import { NeverType } from './types-exports'
import { memoizeTypeTransformation, memoizeTransformation } from './utils'
import { JSONType, areJsonsEquals, mapObject, failWithInternalError, flatMapObject } from '@mondrian-framework/utils'
Expand Down Expand Up @@ -1310,7 +1310,29 @@ export type ObjectType<M extends Mutability, Ts extends Types> = {
/**
* The options that can be used to define an {@link ObjectType `ObjectType`}.
*/
export type ObjectTypeOptions = BaseOptions
export type ObjectTypeOptions = BaseOptions & {
fields?: { [K in string]?: { description?: string } }
}

/**
* Wraps a type inside a {@link utils.RichField RichField}
* It can be used to add a description to an object ot entity field
* ```
* const User = () => model.entity(
* {
* id: model.number(),
* name: model.string(),
* friends: model.describe(model.array(User), "List of my friends")
* },
* {
* description: "A user of the system"
* }
* )
* ```
*/
export function describe<const T extends model.Type>(type: T, description: string): utils.RichField<T> {
return { field: type, description }
}

/**
* The model of an object in the Mondrian framework.
Expand Down Expand Up @@ -1452,7 +1474,9 @@ export type EntityType<M extends Mutability, Ts extends Types> = {
/**
* The options that can be used to define an {@link EntityType `EntityType`}.
*/
export type EntityTypeOptions = BaseOptions
export type EntityTypeOptions = BaseOptions & {
fields?: { [K in string]?: { description?: string } }
}

/**
* The model of a sequence of elements in the Mondrian framework.
Expand Down Expand Up @@ -2192,18 +2216,6 @@ export function isOptional(type: Type): boolean {
return hasWrapper(type, Kind.Optional)
}

/**
* @param type the type to check
* @returns the first description navigating this type.
*/
export function getFirstDescription(type: Type): string | undefined {
return match(type, {
array: (t) => t.options?.description,
wrapper: (t) => t.options?.description ?? getFirstDescription(t.wrappedType),
otherwise: (t) => t.options?.description,
})
}

/**
* @param type the type to check
* @returns true if the type is a nullable type
Expand Down Expand Up @@ -2234,8 +2246,8 @@ export function isNever(type: Type): type is NeverType {
}

/**
* Unwraps all wrappers around a {@link Type}.
* The wrappers are: {@link OptionalType}, {@link NullableType}, {@link ReferenceType}, {@link ArrayType}
* Unwraps all wrappers around a {@link Type} and concretise the result.
* The wrappers are: {@link OptionalType}, {@link NullableType}, {@link ArrayType}
* @param type the type to unwrap.
* @returns the unwrapped type.
*/
Expand Down
32 changes: 21 additions & 11 deletions packages/model/src/type-system/native/entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decoding, path, model, validation, encoding } from '../..'
import { decoding, path, model, validation, encoding, utils } from '../..'
import { assertSafeObjectFields } from '../../utils'
import { BaseType } from './base'
import { JSONType, filterMapObject } from '@mondrian-framework/utils'
Expand Down Expand Up @@ -33,18 +33,28 @@ import gen from 'fast-check'
* }
* ```
*/
export function entity<Ts extends model.Types>(
export function entity<Ts extends utils.RichFields>(
fields: Ts,
options?: model.EntityTypeOptions,
): model.EntityType<model.Mutability.Immutable, Ts> {
return new EntityTypeImpl(model.Mutability.Immutable, fields, options)
options?: Omit<model.ObjectTypeOptions, 'fields'>,
): model.EntityType<model.Mutability.Immutable, utils.RichFieldsToTypes<Ts>> {
const { fields: fieldsOptions, types } = utils.richFieldsToTypes(fields)
return new EntityTypeImpl(
model.Mutability.Immutable,
types,
fieldsOptions ? { ...options, fields: fieldsOptions } : options,
)
}

export function mutableEntity<Ts extends model.Types>(
export function mutableEntity<Ts extends utils.RichFields>(
fields: Ts,
options?: model.EntityTypeOptions,
): model.EntityType<model.Mutability.Mutable, Ts> {
return new EntityTypeImpl(model.Mutability.Mutable, fields, options)
options?: Omit<model.ObjectTypeOptions, 'fields'>,
): model.EntityType<model.Mutability.Mutable, utils.RichFieldsToTypes<Ts>> {
const { fields: fieldsOptions, types } = utils.richFieldsToTypes(fields)
return new EntityTypeImpl(
model.Mutability.Mutable,
types,
fieldsOptions ? { ...options, fields: fieldsOptions } : options,
)
}

class EntityTypeImpl<M extends model.Mutability, Ts extends model.Types>
Expand All @@ -59,8 +69,8 @@ class EntityTypeImpl<M extends model.Mutability, Ts extends model.Types>
protected fromOptions = (options: model.EntityTypeOptions) =>
new EntityTypeImpl(this.mutability, this.fields, options)

immutable = () => entity(this.fields, this.options)
mutable = () => mutableEntity(this.fields, this.options)
immutable = () => entity(this.fields, this.options) as any
mutable = () => mutableEntity(this.fields, this.options) as any

constructor(mutability: M, fields: Ts, options?: model.EntityTypeOptions) {
super(options)
Expand Down
32 changes: 21 additions & 11 deletions packages/model/src/type-system/native/object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decoding, path, model, validation, encoding } from '../..'
import { decoding, path, model, validation, encoding, utils } from '../..'
import { assertSafeObjectFields } from '../../utils'
import { BaseType } from './base'
import { JSONType, filterMapObject } from '@mondrian-framework/utils'
Expand Down Expand Up @@ -33,18 +33,28 @@ import gen from 'fast-check'
* }
* ```
*/
export function object<Ts extends model.Types>(
export function object<Ts extends utils.RichFields>(
fields: Ts,
options?: model.ObjectTypeOptions,
): model.ObjectType<model.Mutability.Immutable, Ts> {
return new ObjectTypeImpl(model.Mutability.Immutable, fields, options)
options?: Omit<model.ObjectTypeOptions, 'fields'>,
): model.ObjectType<model.Mutability.Immutable, utils.RichFieldsToTypes<Ts>> {
const { fields: fieldsOptions, types } = utils.richFieldsToTypes(fields)
return new ObjectTypeImpl(
model.Mutability.Immutable,
types,
fieldsOptions ? { ...options, fields: fieldsOptions } : options,
)
}

export function mutableObject<Ts extends model.Types>(
export function mutableObject<Ts extends utils.RichFields>(
fields: Ts,
options?: model.ObjectTypeOptions,
): model.ObjectType<model.Mutability.Mutable, Ts> {
return new ObjectTypeImpl(model.Mutability.Mutable, fields, options)
options?: Omit<model.ObjectTypeOptions, 'fields'>,
): model.ObjectType<model.Mutability.Mutable, utils.RichFieldsToTypes<Ts>> {
const { fields: fieldsOptions, types } = utils.richFieldsToTypes(fields)
return new ObjectTypeImpl(
model.Mutability.Mutable,
types,
fieldsOptions ? { ...options, fields: fieldsOptions } : options,
)
}

class ObjectTypeImpl<M extends model.Mutability, Ts extends model.Types>
Expand All @@ -59,8 +69,8 @@ class ObjectTypeImpl<M extends model.Mutability, Ts extends model.Types>
protected fromOptions = (options: model.ObjectTypeOptions) =>
new ObjectTypeImpl(this.mutability, this.fields, options)

immutable = () => object(this.fields, this.options)
mutable = () => mutableObject(this.fields, this.options)
immutable = () => object(this.fields, this.options) as any
mutable = () => mutableObject(this.fields, this.options) as any

constructor(mutability: M, fields: Ts, options?: model.ObjectTypeOptions) {
super(options)
Expand Down
30 changes: 30 additions & 0 deletions packages/model/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { model } from '.'
import { mapObject } from '@mondrian-framework/utils'

type TypeTransformer<T extends model.Type> = (type: T) => model.Type
export function memoizeTypeTransformation<T extends model.Type>(mapper: TypeTransformer<T>): (type: T) => model.Type {
Expand Down Expand Up @@ -52,3 +53,32 @@ export function assertSafeObjectFields(record: Record<string, unknown>) {
}
}
}

export type RichField<T extends model.Type = model.Type> = { field: T; description?: string }
export type RichFields = { readonly [K in string]: model.Type | RichField }

//prettier-ignore
export type RichFieldsToTypes<Ts extends RichFields> = {
readonly [K in keyof Ts]
: Ts[K] extends { field: infer T extends model.Type } ? T
: Ts[K] extends model.Type ? Ts[K]
: any
}

export function richFieldsToTypes<Ts extends RichFields>(
richTypes: Ts,
): {
types: RichFieldsToTypes<Ts>
fields?: model.ObjectTypeOptions['fields']
} {
const fields: model.ObjectTypeOptions['fields'] = {}
const types = mapObject(richTypes, (k, t) => {
if ('field' in t) {
fields[k] = { description: t.description }
return t.field
} else {
return t
}
})
return { types: types as RichFieldsToTypes<Ts>, fields: Object.keys(fields).length === 0 ? undefined : fields }
}

0 comments on commit 87cc019

Please sign in to comment.