Skip to content

Commit

Permalink
feat(manipulation): add method to getUpdatableProperties
Browse files Browse the repository at this point in the history
  • Loading branch information
uladkasach committed Aug 6, 2023
1 parent 87ced46 commit 517ee23
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export { DomainEntity } from './instantiation/DomainEntity';
export { DomainEvent } from './instantiation/DomainEvent';
export { HelpfulJoiValidationError } from './instantiation/validate/HelpfulJoiValidationError';
export { HelpfulYupValidationError } from './instantiation/validate/HelpfulYupValidationError';
export { DomainEntityUniqueKeysMustBeDefinedError } from './manipulation/DomainEntityUniqueKeysMustBeDefinedError';
export { DomainEntityUpdatablePropertiesMustBeDefinedError } from './manipulation/DomainEntityUpdatablePropertiesMustBeDefinedError';

export { getUniqueIdentifier } from './manipulation/getUniqueIdentifier';
export { getUpdatableProperties } from './manipulation/getUpdatableProperties';
export { getMetadataKeys } from './manipulation/getMetadataKeys';
export { omitMetadataValues } from './manipulation/omitMetadataValues';
export { serialize } from './manipulation/serde/serialize';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export class DomainEntityUpdatablePropertiesMustBeDefinedError extends Error {
constructor({
entityName,
nameOfFunctionNeededFor,
}: {
entityName: string;
nameOfFunctionNeededFor: string;
}) {
const message = `
\`${entityName}.updatable\` must be defined, to be able to \`${nameOfFunctionNeededFor}\`.
Example:
\`\`\`
interface RocketShip {
serialNumber: string;
fuelQuantity: number;
passengers: number;
}
class RocketShip extends DomainEntity<RocketShip> implements RocketShip {
public static updatable = ['fuelQuantity', 'passengers];
}
\`\`\`
`;
super(message);
}
}
81 changes: 81 additions & 0 deletions src/manipulation/getUpdatableProperties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { v4 as uuid } from 'uuid';

import { DomainObjectNotSafeToManipulateError } from '../constraints/assertDomainObjectIsSafeToManipulate';
import { DomainEntity } from '../instantiation/DomainEntity';
import { DomainEntityUniqueKeysMustBeDefinedError } from './DomainEntityUniqueKeysMustBeDefinedError';
import { DomainEntityUpdatablePropertiesMustBeDefinedError } from './DomainEntityUpdatablePropertiesMustBeDefinedError';
import { getUniqueIdentifier } from './getUniqueIdentifier';
import { getUpdatableProperties } from './getUpdatableProperties';

describe('getUpdatableProperties', () => {
describe('entity', () => {
it('should be able to get updatable properties accurately', () => {
interface RocketShip {
serialNumber: string;
fuelQuantity: number;
passengers: number;
}
class RocketShip extends DomainEntity<RocketShip> implements RocketShip {
public static updatable = ['fuelQuantity', 'passengers'];
}
const ship = new RocketShip({
serialNumber: 'SN5',
fuelQuantity: 9001,
passengers: 21,
});
const updatableProps = getUpdatableProperties(ship);
expect(updatableProps).toEqual({ fuelQuantity: 9001, passengers: 21 });
});
it('should throw an error if .updatable is not defined on the entity', () => {
interface RocketShip {
serialNumber: string;
fuelQuantity: number;
passengers: number;
}
class RocketShip extends DomainEntity<RocketShip> implements RocketShip {}
const ship = new RocketShip({
serialNumber: 'SN5',
fuelQuantity: 9001,
passengers: 21,
});
try {
getUpdatableProperties(ship);
throw new Error('should not reach here');
} catch (error) {
if (!(error instanceof Error)) throw error;
expect(error.message).toContain(
'`RocketShip.updatable` must be defined, to be able to `getUpdatableProperties`',
);
expect(error).toBeInstanceOf(
DomainEntityUpdatablePropertiesMustBeDefinedError,
);
}
});
});
describe('safety', () => {
it('should throw an error if domain object is not safe to manipulate', () => {
interface PlaneManufacturer {
name: string;
}
interface Plane {
uuid: string;
manufacturer: PlaneManufacturer;
passengerLimit: number;
}
class Plane extends DomainEntity<Plane> implements Plane {
public static unique = ['uuid'];
public static updatable = ['passengerLimit'];
}
const phone = new Plane({
uuid: uuid(),
manufacturer: { name: 'boeing' },
passengerLimit: 821,
});
try {
getUpdatableProperties(phone);
} catch (error) {
expect(error).toBeInstanceOf(DomainObjectNotSafeToManipulateError);
}
});
});
});
42 changes: 42 additions & 0 deletions src/manipulation/getUpdatableProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pick from 'lodash.pick';

import { assertDomainObjectIsSafeToManipulate } from '../constraints/assertDomainObjectIsSafeToManipulate';
import { DomainEntity } from '../instantiation/DomainEntity';
import { DomainObject } from '../instantiation/DomainObject';
import { DomainEntityUpdatablePropertiesMustBeDefinedError } from './DomainEntityUpdatablePropertiesMustBeDefinedError';

/**
* Extracts an object that contains only the updatable properties of the domain object, for DomainEntity
*
* Uses the definition of a DomainEntity in order to extract the properties that are updatable for the domain object generically
*
* note
* - this should not be called on DomainValueObject's or DomainEvent's, since by definition they can not have updatable properties
*/
export const getUpdatableProperties = <T extends Record<string, any>>(
dobj: DomainEntity<T>,
): Partial<T> => {
// make sure its an instance of DomainObject
if (!(dobj instanceof DomainObject))
throw new Error(
'getUpdatableProperties called on object that is not an instance of a DomainObject. Are you sure you instantiated the object? (Related: see `DomainObject.nested`)',
);

// make sure that its safe to manipulate
assertDomainObjectIsSafeToManipulate(dobj);

// handle DomainEntity
if (dobj instanceof DomainEntity) {
const className = (dobj.constructor as typeof DomainEntity).name;
const updatableProps = (dobj.constructor as typeof DomainEntity).updatable;
if (!updatableProps)
throw new DomainEntityUpdatablePropertiesMustBeDefinedError({
entityName: className,
nameOfFunctionNeededFor: 'getUpdatableProperties',
});
return pick(dobj, updatableProps.flat());
}

// throw error we get here, this is unexpected
throw new Error('unexpected domain object type');
};

0 comments on commit 517ee23

Please sign in to comment.