-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(metadata): add DomainObject.metadata prop, expose getMetadataKey…
…s and omitMetadataValues methods
- Loading branch information
1 parent
e39da1e
commit 34b38f5
Showing
8 changed files
with
166 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { getMetadataKeys } from './getMetadataKeys'; | ||
import { DomainObject } from '../instantiation/DomainObject'; | ||
import { DomainEntity } from '../instantiation/DomainEntity'; | ||
|
||
describe('getMetadataKeys', () => { | ||
it('should return the defaults, if not explicitly defined', () => { | ||
interface Mineral { | ||
id?: number; | ||
name: string; | ||
} | ||
class Mineral extends DomainObject<Mineral> implements Mineral {} | ||
const mineral = new Mineral({ name: 'magnesium' }); | ||
const metadataKeys = getMetadataKeys(mineral); | ||
expect(metadataKeys).toEqual(['id', 'uuid', 'createdAt', 'updatedAt', 'effectiveAt']); | ||
}); | ||
it('should return the explicitly defined metadata keys, if defined', () => { | ||
interface Mineral { | ||
id?: number; | ||
uuid?: string; | ||
name: string; | ||
} | ||
class Mineral extends DomainObject<Mineral> implements Mineral { | ||
public static metadata = ['id', 'uuid']; | ||
} | ||
const mineral = new Mineral({ name: 'magnesium' }); | ||
const metadataKeys = getMetadataKeys(mineral); | ||
expect(metadataKeys).toEqual(['id', 'uuid']); | ||
}); | ||
it('should not include uuid in the default metadata keys of a DomainEntity which specified uuid as part of its unique key', () => { | ||
interface Mineral { | ||
uuid: string; | ||
name: string; | ||
} | ||
class Mineral extends DomainEntity<Mineral> implements Mineral { | ||
public static unique = ['uuid']; | ||
} | ||
const mineral = new Mineral({ uuid: '__UUID__', name: 'magnesium' }); | ||
const metadataKeys = getMetadataKeys(mineral); | ||
expect(metadataKeys).toEqual(['id', 'createdAt', 'updatedAt', 'effectiveAt']); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { DomainObject } from '../instantiation/DomainObject'; | ||
import { DomainEntity } from '../instantiation/DomainEntity'; | ||
import { DomainEvent } from '../instantiation/DomainEvent'; | ||
import { DomainEntityUniqueKeysMustBeDefinedError } from './DomainEntityUniqueKeysMustBeDefinedError'; | ||
|
||
const DEFAULT_METADATA_KEYS = ['id', 'uuid', 'createdAt', 'updatedAt', 'effectiveAt']; | ||
|
||
/** | ||
* returns the metadata keys defined on the class of the domain object | ||
*/ | ||
export const getMetadataKeys = (obj: DomainObject<any>, options?: { nameOfFunctionNeededFor?: string }): string[] => { | ||
// make sure its an instance of DomainObject | ||
if (!(obj instanceof DomainObject)) | ||
throw new Error('getMetadataKeys called on object that is not an instance of a DomainObject. Are you sure you instantiated the object?'); | ||
|
||
// see if metadata was explicitly defined | ||
const metadataKeysDeclared = (obj.constructor as typeof DomainObject).metadata as string[]; | ||
if (metadataKeysDeclared) return metadataKeysDeclared; | ||
|
||
// if it wasn't explicitly declared and its a DomainEntity or DomainEvent, then check to see if uuid is part of the unique key and augment default keys based on that | ||
if (obj instanceof DomainEntity || obj instanceof DomainEvent) { | ||
const className = (obj.constructor as typeof DomainEntity).name; | ||
const uniqueKeys = (obj.constructor as typeof DomainEntity).unique; | ||
if (!uniqueKeys) | ||
throw new DomainEntityUniqueKeysMustBeDefinedError({ | ||
entityName: className, | ||
nameOfFunctionNeededFor: options?.nameOfFunctionNeededFor ?? 'getMetadataKeys', | ||
}); | ||
if (uniqueKeys.flat().includes('uuid')) return DEFAULT_METADATA_KEYS.filter((key) => key !== 'uuid'); // if the unique key includes uuid, then uuid is not metadata | ||
} | ||
|
||
// otherwise, return the defaults | ||
return DEFAULT_METADATA_KEYS; | ||
}; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import omit from 'lodash.omit'; | ||
|
||
import { assertDomainObjectIsSafeToManipulate } from '../constraints/assertDomainObjectIsSafeToManipulate'; | ||
import { DomainObject } from '../instantiation/DomainObject'; | ||
import { getMetadataKeys } from './getMetadataKeys'; | ||
|
||
/** | ||
* exposes a function which properly handles any value that can could have been defined for an object property | ||
* - if domain object, omits autogenerated values | ||
* - if array, recursively omits on each item in the array | ||
* - if neither of the above, then its the terminal condition - return it, its fully omitted | ||
*/ | ||
const recursivelyOmitMetadataValuesFromObjectValue: any = (thisValue: any) => { | ||
// handle directly nested domain object | ||
if (thisValue instanceof DomainObject) return omitMetadataValues(thisValue); // eslint-disable-line @typescript-eslint/no-use-before-define | ||
|
||
// handle an array of one level deep (doesn't handle Array of Array, for simplicity) | ||
if (Array.isArray(thisValue)) return thisValue.map(recursivelyOmitMetadataValuesFromObjectValue); // run self on each item in the array, (i.e., recursively) | ||
|
||
// handle any other value type | ||
return thisValue; | ||
}; | ||
|
||
/** | ||
* omits all metadata values on a domain object | ||
* | ||
* features: | ||
* - utilizes the `.metadata` property of the domain object definition to identify metadata keys | ||
* - recursive, applies omission deeply | ||
*/ | ||
export const omitMetadataValues = <T extends DomainObject<Record<string, any>>>(obj: T): T => { | ||
// make sure its an instance of DomainObject | ||
if (!(obj instanceof DomainObject)) | ||
throw new Error( | ||
'omitMetadataValues called on object that is not an instance of a DomainObject. Are you sure you instantiated the object? (Related: see `DomainObject.nested`)', | ||
); | ||
|
||
// determine if its an entity or a value object | ||
const Constructor = (obj.constructor as any) as { new (...args: any): T }; // https://stackoverflow.com/a/61444747/3068233 | ||
const metadataKeys = getMetadataKeys(obj, { nameOfFunctionNeededFor: 'omitMetadataValues' }); | ||
|
||
// make sure that its safe to manipulate | ||
assertDomainObjectIsSafeToManipulate(obj); | ||
|
||
// object with omit applied recursively on each property | ||
const objectWithEachDomainObjectKeyRecursivelyOmitted: typeof obj = Object.entries(obj).reduce((summary, [thisKey, thisValue]) => { | ||
return { ...summary, [thisKey]: recursivelyOmitMetadataValuesFromObjectValue(thisValue) }; | ||
}, {} as typeof obj); | ||
|
||
// omit all of the metadata keys | ||
const objWithoutBaseCaseAutogeneratedValues = omit(objectWithEachDomainObjectKeyRecursivelyOmitted, metadataKeys); | ||
|
||
// return the instantiated object | ||
return new Constructor(objWithoutBaseCaseAutogeneratedValues); | ||
}; |