import isPlainObject from "is-plain-object" ; import { ActualFactoryOpts , Entity , EntityConstructor , EntityManager , EntityMetadata , getMetadata , IdOf , isEntity , OptsOf , PrimitiveField , } from "./EntityManager" ; import { isManyToOneField , isOneToOneField , New } from "./index" ; import { tagIfNeeded } from "./keys" ; import { fail } from "./utils" ; /**
 * DeepPartial-esque type specific to our `newTestInstance` factory.
 *
 * Specifically (vs using DeepPartial directly) this:
 *
 * 1. Adds a `use` tag of `Entity | Entity[]` that will be checked for existing
 * entities before creating new ones (i.e. "if `newTestInstance` needs a `Book`,
 * use the one in `use` instead of making a new one).
 *
 * 2. Works specifically against the constructor/entity opts fields.
 */ export type FactoryOpts < T extends Entity > = DeepPartialOpts < T > & { use ? : Entity | Entity [ ] ; } ; export const jan1 = new Date ( 2018 , 0 , 1 ) ; export let testDate = jan1 ; /** Creates a test instance of `T`. */ export function newTestInstance < T extends Entity > ( em , cstr , opts = { } ) { const meta = getMetadata ( cstr ) ; const use = useMap ( opts . use ) ; const fullOpts = Object . fromEntries ( Object . values ( meta . fields ) . map ( ( field ) => { const { fieldName } = field ; if ( fieldName in opts && ( opts as any ) [ fieldName ] !== defaultValueMarker ) { const optValue = ( opts as any ) [ fieldName ] ; if ( optValue === null || ( optValue === undefined && ! field . required ) ) { return [ ] ; } if ( field . kind === "m2o" || field . kind === "o2o" ) { if ( isEntity ( optValue ) ) { return [ fieldName , optValue ] ; } else if ( optValue && typeof optValue === "string" ) { return [ fieldName , em . entities . find ( ( e ) => e . id === optValue || getTestId ( em , e ) === optValue ) || fail ( `Did not find tagged id ${ optValue }` ) , ] ; } else if ( optValue && ! isPlainObject ( optValue ) ) { return [ fieldName , field . otherMetadata ( ) . factory ( em , optValue ) ] ; } const otherField = field . otherMetadata ( ) . fields [ field . otherFieldName ] ! ; return [ fieldName , field . otherMetadata ( ) . factory ( em , { ... applyUse ( optValue || { } , use , field . otherMetadata ( ) ) , [ field . otherFieldName ] : otherField . kind === "o2o" ? null : [ ] , } ) , ] ; } if ( field . kind === "o2m" || field . kind == "m2m" ) { const values = ( optValue as Array < any > ) . map ( ( optValue ) => { if ( isEntity ( optValue ) ) { return optValue ; } else if ( optValue && typeof optValue === "string" ) { return ( em . entities . find ( ( e ) => e . id === optValue || getTestId ( em , e ) === optValue ) || fail ( `Did not find tagged id ${ optValue }` ) ) ; } else if ( optValue && ! isPlainObject ( optValue ) ) { return field . otherMetadata ( ) . factory ( em , optValue ) ; } return field . otherMetadata ( ) . factory ( em , { ... applyUse ( optValue || { } , use , field . otherMetadata ( ) ) , [ field . otherFieldName ] : null , } ) ; } ) ; return [ fieldName , values ] ; } if ( typeof optValue === "string" && optValue . includes ( testIndex ) ) { const actualIndex = getTestIndex ( em , meta . cstr ) ; return [ fieldName , optValue . replace ( testIndex , String ( actualIndex ) ) ] ; } return [ fieldName , optValue ] ; } if ( field . kind === "primitive" && ( field . required || ( opts as any ) [ fieldName ] === defaultValueMarker ) && ! field . derived && ! field . protected ) { return [ fieldName , defaultValueForField ( field ) ] ; } else if ( field . kind === "m2o" ) { const otherMeta = field . otherMetadata ( ) ; const existing = em . entities . filter ( ( e ) => e instanceof otherMeta . cstr ) ; if ( existing . length === 1 ) { return [ fieldName , existing [ 0 ] ] ; } if ( use . has ( otherMeta . cstr ) && ( use . get ( otherMeta . cstr ) ! [ 1 ] || field . required ) ) { return [ fieldName , use . get ( otherMeta . cstr ) ! [ 0 ] ] ; } if ( field . required ) { return [ fieldName , otherMeta . factory ( em , applyUse ( { } , use , otherMeta ) ) ] ; } } else if ( field . kind === "enum" && field . required ) { return [ fieldName , field . enumDetailType . getValues ( ) [ 0 ] ] ; } return [ ] ; } ) . filter ( ( t ) => t . length > 0 ) ) ; const entity = em . create ( meta . cstr , fullOpts as any ) as New < T > ; if ( ! use . has ( entity . constructor ) ) { use . set ( entity . constructor , [ entity , false ] ) ; } return entity ; } /** Given we're going to call a factory, make sure any `use`s are put into `opts`. */ function applyUse ( opts , use , metadata ) { Object . values ( metadata . fields ) . filter ( ( f ) => ! ( f . fieldName in opts ) ) . forEach ( ( f ) => { if ( ( isManyToOneField ( f ) || isOneToOneField ( f ) ) && use . has ( f . otherMetadata ( ) . cstr ) ) { const def = use . get ( f . otherMetadata ( ) . cstr ) ! ; if ( def [ 1 ] ) { ( opts as any ) [ f . fieldName ] = def [ 0 ] ; } } } ) ; return { ... opts , use } ; } /**
 * A marker value for later replacement with the test instance's "unique-ish" index.
 *
 * This is meant to just be a helpful identifier in fields like entity names/descriptions for
 * debugging purposes.
 */ export const testIndex = "TEST_INDEX" ; const defaultValueMarker = { } ; /**
 * A marker value for the default `newTestInstance` behavior.
 *
 * Useful for passing arguments to `newTestInstance` where you sometimes want to
 * provide a specific value, and other times ask for the "pick a default" behavior
 * (i.e. you don't want to pass `undefined` b/c that means explicitly "leave this
 * key unset").
 *
 * Note that this is a function so that we can infer the return type as basically
 * `any` without really using `any` (which would disable type-checking in the rest
 * of the expression).
 */ export function defaultValue < T > ( ) { return defaultValueMarker ; } /**
 * Returns a unique-ish test index for putting in `name` fields.
 *
 * Note that `testIndex` is easier to just include in a string, because it doesn't require passing
 * the `EntityManger` and `type`. But if a factory really wants the test index as a number, they can
 * call this method.
 *
 * Despite the name, these are 1-based, i.e. the first `Author` is `a1`.
 */ export function getTestIndex < T extends Entity > ( em , type ) { const existing = em . entities . filter ( ( e ) => e instanceof type ) ; return existing . length + 1 ; } /** Fakes a probably-right id for un-persisted entities. Solely used for quick lookups in tests/factories. */ function getTestId < T extends Entity > ( em , entity ) { const meta = getMetadata ( entity ) ; const sameType = em . entities . filter ( ( e ) => e instanceof meta . cstr ) ; return tagIfNeeded ( meta , String ( sameType . indexOf ( entity ) + 1 ) ) ; } function defaultValueForField ( field ) { switch ( field . type ) { case "string" : return field . fieldName ; case "number" : return 0 ; case "Date" : return testDate ; case "boolean" : return false ; } return null ; } type DefinedOr < T > = T | undefined ; type DeepPartialOpts < T extends Entity > = AllowRelationsOrPartials < OptsOf < T > > ; type AllowRelationsOrPartials < T > = { [ P in keyof T ] ? : T [ P ] extends DefinedOr < infer U > ? U extends Array < infer V > ? V extends Entity ? Array < V | IdOf < V > | ActualFactoryOpts < V > > : T [ P ] : U extends Entity ? U | IdOf < U > | ActualFactoryOpts < U > : T [ P ] : T [ P ] ; } ; type UseMap = Map < Function , [ Entity , boolean ] > ; function useMap ( use ) { if ( ! use ) { return new Map ( ) ; } else if ( use instanceof Map ) { return use ; } else if ( use instanceof Array ) { const map = new Map ( ) ; use . forEach ( ( e ) => map . set ( e . constructor , [ e , true ] ) ) ; return map ; } else { const map = new Map ( ) ; map . set ( use . constructor , [ use , true ] ) ; return map ; } }