Skip to content

Commit

Permalink
feat(normalization): require users to explicitly state that entity is…
Browse files Browse the repository at this point in the history
… unique on uuid, disallow entity.unique.length = 0
  • Loading branch information
uladkasach committed Feb 26, 2020
1 parent 9614e24 commit 614a804
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { normalizeDeclarationContents } from './normalizeDeclarationContents';
import { throwErrorIfAnyReservedPropertyNamesAreUsed } from './throwErrorIfAnyReservedPropertyNamesAreUsed';
import { throwErrorIfAnyUniqueIsNotInProperties } from './throwErrorIfAnyUniqueIsNotInProperties';
import { throwErrorIfNamingConventionsNotFollowed } from './throwErrorIfNamingConventionsNotFollowed';
import { throwErrorIfNotUniqueOnAnything } from './throwErrorIfNotUniqueOnAnything';

jest.mock('./throwErrorIfAnyReservedPropertyNamesAreUsed');
const throwErrorIfAnyReservedPropertyNamesAreUsedMock = throwErrorIfAnyReservedPropertyNamesAreUsed as jest.Mock;
Expand All @@ -14,6 +15,9 @@ const throwErrorIfNamingConventionsNotFollowedMock = throwErrorIfNamingConventio
jest.mock('./throwErrorIfAnyUniqueIsNotInProperties');
const throwErrorIfAnyUniqueIsNotInPropertiesMock = throwErrorIfAnyUniqueIsNotInProperties;

jest.mock('./throwErrorIfNotUniqueOnAnything');
const throwErrorIfNotUniqueOnAnythingMock = throwErrorIfNotUniqueOnAnything;

describe('normalizeDeclarationContents', () => {
beforeEach(() => jest.clearAllMocks());
it('should throw an error if an entities object is not exported from the source file', () => {
Expand Down Expand Up @@ -55,12 +59,12 @@ describe('normalizeDeclarationContents', () => {
expect(throwErrorIfAnyUniqueIsNotInPropertiesMock).toHaveBeenCalledTimes(1);
expect(throwErrorIfAnyUniqueIsNotInPropertiesMock).toHaveBeenCalledWith({ entity: exampleEntity });
});
it("should set entities to be unique on uuid if they're unique on nothing", () => {
const contents = {
entities: [new Entity({ name: 'job', properties: {}, unique: [] })],
};
const { entities } = normalizeDeclarationContents({ contents });
expect(entities[0].unique).toEqual(['uuid']);
it('should throw an error if entity is unique on nothing', () => {
const exampleEntity = new Entity({ name: 'burrito', properties: { lbs: prop.INT() }, unique: ['lbs'] });
const contents = { entities: [exampleEntity] };
normalizeDeclarationContents({ contents });
expect(throwErrorIfNotUniqueOnAnythingMock).toHaveBeenCalledTimes(1);
expect(throwErrorIfNotUniqueOnAnythingMock).toHaveBeenCalledWith({ entity: exampleEntity });
});
it('should return the entities and value objects found in the contents', () => {
const plant = new ValueObject({ name: 'plant', properties: { genus: prop.VARCHAR(255) } });
Expand All @@ -73,19 +77,14 @@ describe('normalizeDeclarationContents', () => {
const order = new Entity({
name: 'order',
properties: { customer_id: prop.REFERENCES(customer), vase_id: prop.REFERENCES(vase) },
unique: [], // logically unique on nothing - same order can be placed many times! -> we'll require the user to pass in a uuid for idempotency
unique: ['uuid'], // logically unique on nothing - same order can be placed many times! -> we'll require the user to pass in a uuid for idempotency
});
const contents = {
entities: [plant, vase, customer, order],
};
const { entities } = normalizeDeclarationContents({ contents });

// check that we return everything as expected
expect(entities).toEqual([
plant,
vase,
customer,
new Entity({ ...order, unique: ['uuid'] }), // cast order to be unique on uuid, since we normalize this
]);
expect(entities).toEqual([plant, vase, customer, order]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Entity } from '../../../../types';
import { throwErrorIfAnyReservedPropertyNamesAreUsed } from './throwErrorIfAnyReservedPropertyNamesAreUsed';
import { throwErrorIfAnyUniqueIsNotInProperties } from './throwErrorIfAnyUniqueIsNotInProperties';
import { throwErrorIfNamingConventionsNotFollowed } from './throwErrorIfNamingConventionsNotFollowed';
import { throwErrorIfNotUniqueOnAnything } from './throwErrorIfNotUniqueOnAnything';

export const normalizeDeclarationContents = ({ contents }: { contents: any }) => {
// 1. check that 'entities' is exported
Expand All @@ -28,17 +29,11 @@ export const normalizeDeclarationContents = ({ contents }: { contents: any }) =>
throwErrorIfAnyUniqueIsNotInProperties({ entity });
});

// 5. set entity to be unique on "uuid" if it is unique on nothing
const entitiesWithNormalizedUniqueness = entities.map((entity) => {
// if unique on nothing, then set it to be unique on uuid
if (entity.unique.length === 0) {
return new Entity({ ...entity, unique: ['uuid'] });
}

// otherwise, change nothing
return entity;
// 5. check that the entity is unique on _something_
entities.forEach((entity: Entity) => {
throwErrorIfNotUniqueOnAnything({ entity });
});

// 6. return the entities now that we've validate them
return { entities: entitiesWithNormalizedUniqueness };
return { entities };
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Entity } from '../../../../types';

const reservedPropertyNames = ['id', 'uuid', 'createdAt', 'updatedAt', 'effectiveAt']; // these columns are autogenerated for managing entities and thus cant be used
const reservedPropertyNames = ['id', 'uuid', 'created_at', 'updated_at', 'effective_at']; // we do not allow overwriting these autogenerated values.
export const throwErrorIfAnyReservedPropertyNamesAreUsed = ({ entity }: { entity: Entity }) => {
const usedPropertyNames = Object.keys(entity.properties);
usedPropertyNames.forEach((name) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('throwErrorIfAnyUniqueIsNotInProperties', () => {
first_name: prop.VARCHAR(255),
last_name: prop.VARCHAR(255),
},
unique: ['phoneNumber'], // unique on phone number
unique: ['phoneNumber'], // note the misspelling
});
try {
throwErrorIfAnyUniqueIsNotInProperties({ entity: user });
Expand All @@ -22,4 +22,16 @@ describe('throwErrorIfAnyUniqueIsNotInProperties', () => {
);
}
});
it('should not throw an error if the entity does not have it explicitly defined but that property _is_ a static autogenerated property', () => {
const user = new Entity({
name: 'user',
properties: {
phone_number: prop.VARCHAR(255),
first_name: prop.VARCHAR(255),
last_name: prop.VARCHAR(255),
},
unique: ['uuid'], // unique on uuid
});
throwErrorIfAnyUniqueIsNotInProperties({ entity: user });
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Entity } from '../../../../types';

const STATIC_AUTOGENERATED_PROPERTIES = ['id', 'uuid', 'created_at'];
export const throwErrorIfAnyUniqueIsNotInProperties = ({ entity }: { entity: Entity }) => {
// for each unique, check that there is a property with that same exact name
const propertyNames = Object.keys(entity.properties);
entity.unique.forEach((uniquePropertyName) => {
if (!propertyNames.includes(uniquePropertyName)) {
if (!propertyNames.includes(uniquePropertyName) && !STATIC_AUTOGENERATED_PROPERTIES.includes(uniquePropertyName)) {
throw new Error(
`entity '${entity.name}' was defined to be unique on '${uniquePropertyName}' but does not have that defined in its properties`,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity } from '../../../../types';
import { prop } from '../../../define';
import { throwErrorIfNotUniqueOnAnything } from './throwErrorIfNotUniqueOnAnything';

describe('throwErrorIfNotUniqueOnAnything', () => {
it('should throw an error if entity.unique is empty', () => {
const user = new Entity({
name: 'user',
properties: {
phone_number: prop.VARCHAR(255),
first_name: prop.VARCHAR(255),
last_name: prop.VARCHAR(255),
},
unique: [], // note the misspelling
});
try {
throwErrorIfNotUniqueOnAnything({ entity: user });
throw new Error('should not reach here');
} catch (error) {
expect(error.message).toEqual(
"Entity 'user' must be unique on atleast one property. If it is not unique on any natural keys, you can make it unique on the autogenerated 'uuid' property instead.",
);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Entity } from '../../../../types';

export const throwErrorIfNotUniqueOnAnything = ({ entity }: { entity: Entity }) => {
if (entity.unique.length === 0) {
throw new Error(
`Entity '${entity.name}' must be unique on atleast one property. If it is not unique on any natural keys, you can make it unique on the autogenerated 'uuid' property instead.`,
);
}
};

0 comments on commit 614a804

Please sign in to comment.