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(modelreferences): add ModelReferences back in for ModelInstances #18

Merged
merged 1 commit into from
Apr 17, 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "functional-models",
"version": "2.1.2",
"version": "2.1.3",
"description": "A library for creating JavaScript function based models.",
"main": "index.js",
"types": "index.d.ts",
Expand Down
18 changes: 12 additions & 6 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,19 @@ interface ModelReferencePropertyInstance<
T extends FunctionalModel,
TProperty extends Arrayable<FunctionalValue>,
TModel extends Model<T> = Model<T>,
TModelInstance extends ModelInstance<T, TModel> = ModelInstance<T, TModel>,
> extends PropertyInstance<TProperty> {
readonly getReferencedId: (
instanceValues: ModelReference<T>
instanceValues: ModelReference<T, TModel, TModelInstance>
) => Maybe<PrimaryKeyType>
readonly getReferencedModel: () => TModel
}

type ModelReference<T extends FunctionalModel> =
| T
| TypedJsonObj<T>
| PrimaryKeyType
type ModelReference<
T extends FunctionalModel,
TModel extends Model<T> = Model<T>,
TModelInstance extends ModelInstance<T, TModel> = ModelInstance<T, TModel>,
> = T | TModelInstance | TypedJsonObj<T> | PrimaryKeyType

type DefaultPropertyValidators = Readonly<{
required?: boolean
Expand Down Expand Up @@ -204,10 +206,14 @@ type PropertyConfigContents<T extends Arrayable<FunctionalValue>> = Readonly<{
type ModelFetcher = <
T extends FunctionalModel,
TModel extends Model<T> = Model<T>,
TModelInstance extends ModelInstance<T, TModel> = ModelInstance<T, TModel>,
>(
model: TModel,
primaryKey: PrimaryKeyType
) => Promise<T | TypedJsonObj<T>> | Promise<null> | Promise<undefined>
) =>
| Promise<T | TModelInstance | TypedJsonObj<T>>
| Promise<null>
| Promise<undefined>

type PropertyConfig<T extends Arrayable<FunctionalValue>> =
| (PropertyConfigContents<T> & DefaultPropertyValidators)
Expand Down
14 changes: 14 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const getValueForReferencedModel = async (
return modelInstance.references[head]()
}
const modelReference = await modelInstance.get[head]()
if (modelReference.toObj) {
const [nestedHead, nestedTail] = createHeadAndTail(tail.split('.'), '.')
const value = await modelReference.get[nestedHead]()
if (nestedTail) {
return get(value, nestedTail)
}
return value
}
return get(modelReference, tail)
}

Expand Down Expand Up @@ -91,6 +99,11 @@ const mergeValidators = <T extends Arrayable<FunctionalValue>>(
return [...validators, ...(config?.validators ? config.validators : [])]
}

const isModelInstance = (obj: any): obj is ModelInstance<any, any> => {
// @ts-ignore
return Boolean(obj && obj.getPrimaryKeyName)
}

export {
isReferencedProperty,
getValueForModelInstance,
Expand All @@ -99,4 +112,5 @@ export {
getValidatorFromConfigElseEmpty,
getCommonNumberValidators,
mergeValidators,
isModelInstance,
}
70 changes: 54 additions & 16 deletions src/properties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import merge from 'lodash/merge'
import { createPropertyValidator, isType, meetsRegex } from './validation'
import {
createPropertyValidator,
isType,
meetsRegex,
referenceTypeMatch,
} from './validation'
import { PROPERTY_TYPES } from './constants'
import { lazyValue } from './lazy'
import { createHeadAndTail, createUuid } from './utils'
Expand All @@ -20,6 +25,7 @@ import {
FunctionalModel,
JsonAble,
PropertyModifier,
TypedJsonObj,
} from './interfaces'
import {
getValueForModelInstance,
Expand All @@ -28,6 +34,7 @@ import {
getCommonTextValidators,
getCommonNumberValidators,
mergeValidators,
isModelInstance,
} from './lib'

const EMAIL_REGEX =
Expand Down Expand Up @@ -279,15 +286,17 @@ const ModelReferenceProperty = <
config: PropertyConfig<TModifier> = {},
additionalMetadata = {}
) =>
AdvancedModelReferenceProperty<T, Model<T>, TModifier>(
model,
config,
additionalMetadata
)
AdvancedModelReferenceProperty<
T,
Model<T>,
ModelInstance<T, Model<T>>,
TModifier
>(model, config, additionalMetadata)

const AdvancedModelReferenceProperty = <
T extends FunctionalModel,
TModel extends Model<T> = Model<T>,
TModelInstance extends ModelInstance<T, TModel> = ModelInstance<T, TModel>,
TModifier extends PropertyModifier<ModelReference<T>> = PropertyModifier<
ModelReference<T>
>,
Expand All @@ -307,10 +316,15 @@ const AdvancedModelReferenceProperty = <
return model
}

const validators = mergeValidators(config, [])
const validator = referenceTypeMatch<T, TModel, TModelInstance>(model)
const validators = mergeValidators(config, [
// @ts-ignore
validator,
])

const _getId =
(instanceValues: ModelReference<T>) => (): Maybe<PrimaryKeyType> => {
(instanceValues: ModelReference<T, TModel> | TModifier) =>
(): Maybe<PrimaryKeyType> => {
if (!instanceValues) {
return null
}
Expand All @@ -320,29 +334,53 @@ const AdvancedModelReferenceProperty = <
if (typeof instanceValues === 'string') {
return instanceValues
}
if ((instanceValues as TModelInstance).getPrimaryKey) {
return (instanceValues as TModelInstance).getPrimaryKey()
}

const theModel = _getModel()
const primaryKey = theModel.getPrimaryKeyName()

return (instanceValues as T)[primaryKey] as PrimaryKeyType
return (instanceValues as TypedJsonObj<T>)[primaryKey] as PrimaryKeyType
}

const lazyLoadMethod = async (instanceValues: T) => {
// Path for returning a TypedJsonObj / T
const lazyLoadMethod = async (instanceValues: TModifier) => {
const valueIsModelInstance = isModelInstance(instanceValues)
const _getInstanceReturn = (objToUse: TModifier) => {
// We need to determine if the object we just got is an actual model instance to determine if we need to make one.
const objIsModelInstance = isModelInstance(objToUse)
// @ts-ignore
const instance = objIsModelInstance
? objToUse
: _getModel().create(objToUse as TypedJsonObj<T>)
// We are replacing the toObj function, because the reference type in the end should be the primary key when serialized.
return merge({}, instance, {
toObj: _getId(instanceValues),
})
}

// @ts-ignore
if (valueIsModelInstance) {
return _getInstanceReturn(instanceValues)
}
if (config?.fetcher) {
const id = await _getId(instanceValues)()
const model = _getModel()
if (id !== null && id !== undefined) {
return config.fetcher<T>(model, id)
const obj = await config.fetcher<T>(model, id)
return _getInstanceReturn(obj as TModifier)
}
return null
}

// This is just an id.
return _getId(instanceValues)()
}

const p: ModelReferencePropertyInstance<T, TModifier> = merge(
const p: ModelReferencePropertyInstance<
T,
TModifier,
TModel,
TModelInstance
> = merge(
Property<TModifier>(
PROPERTY_TYPES.ReferenceProperty,
merge({}, config, {
Expand All @@ -352,7 +390,7 @@ const AdvancedModelReferenceProperty = <
additionalMetadata
),
{
getReferencedId: (instanceValues: ModelReference<T>) =>
getReferencedId: (instanceValues: ModelReference<T, TModel>) =>
_getId(instanceValues)(),
getReferencedModel: _getModel,
}
Expand Down
10 changes: 7 additions & 3 deletions src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import {
toObj,
FunctionalModel,
TypedJsonObj,
ModelInstance,
} from './interfaces'

const isModelInstance = (obj: any): obj is ModelInstance<any> => {
return Boolean(obj.toObj)
}

const _getValue = async (value: any): Promise<JsonAble | null> => {
if (value === undefined) {
return null
Expand All @@ -20,9 +25,8 @@ const _getValue = async (value: any): Promise<JsonAble | null> => {
return _getValue(await asFunction())
}
// Nested Object
const asModel = value.toObj
if (asModel) {
return _getValue(await asModel())
if (isModelInstance(value)) {
return _getValue(await value.toObj())
}
// Dates
const asDate = value as Date
Expand Down
35 changes: 35 additions & 0 deletions src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
ValidatorConfiguration,
ValuePropertyValidatorComponent,
ValidationErrors,
MaybeFunction,
PropertyValidatorComponentTypeAdvanced,
} from './interfaces'

const TYPE_PRIMITIVES = {
Expand Down Expand Up @@ -393,6 +395,38 @@ const isValid = <T extends FunctionalModel>(errors: ModelErrors<T>) => {
return Object.keys(errors).length < 1
}

const referenceTypeMatch = <
T extends FunctionalModel,
TModel extends Model<T> = Model<T>,
TModelInstance extends ModelInstance<T, TModel> = ModelInstance<T, TModel>,
>(
referencedModel: MaybeFunction<TModel>
): PropertyValidatorComponentTypeAdvanced<
ModelInstance<T, TModel>,
T,
TModel,
TModelInstance
> => {
return (value?: ModelInstance<T, TModel>) => {
if (!value) {
return 'Must include a value'
}
// This needs to stay here, as it delays the creation long enough for
// self referencing types.
const model =
typeof referencedModel === 'function'
? referencedModel()
: referencedModel
// Assumption: By the time this is received, value === a model instance.
const eModel = model.getName()
const aModel = value.getModel().getName()
if (eModel !== aModel) {
return `Model should be ${eModel} instead, received ${aModel}`
}
return undefined
}
}

export {
isNumber,
isBoolean,
Expand All @@ -415,4 +449,5 @@ export {
arrayType,
isValid,
TYPE_PRIMITIVES,
referenceTypeMatch,
}
Loading
Loading