Skip to content

Commit

Permalink
enhance: Detect more cases of network mismatching schema (#331)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed May 6, 2020
1 parent edc5f73 commit 2af464f
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 22 deletions.
43 changes: 38 additions & 5 deletions packages/normalizr/src/entities/Entity.ts
Expand Up @@ -62,20 +62,53 @@ export default abstract class Entity extends SimpleRecord {
) {
// TODO: what's store needs to be a differing type from fromJS
const processedEntity = this.fromJS(input, parent, key);
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const instanceSample = new (this as any)();
const keysOfRecord = new Set(Object.keys(instanceSample));
let [sameCount, diffCount] = [0, 0];
let extraKey = false;
for (const keyOfProps of Entity.keysDefined(processedEntity)) {
if (keysOfRecord.has(keyOfProps)) {
sameCount++;
} else {
diffCount++;
extraKey = true;
}
}
diffCount += keysOfRecord.size - sameCount;
if (diffCount > sameCount && keysOfRecord.size && extraKey) {
const error = new Error(
`Attempted to initialize ${
this.name
} with substantially different than expected keys
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Expected keys: ${Object.keys(instanceSample)}
Value: ${JSON.stringify(Entity.toObjectDefined(processedEntity), null, 2)}`,
);
(error as any).status = 400;
throw error;
}
}
const id = processedEntity.pk(parent, key);
/* istanbul ignore next */
if (id === undefined) {
if (process.env.NODE_ENV !== 'production' && id === undefined) {
throw new Error(
if (id === undefined || id === '') {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const error = new Error(
`Missing usable resource key when normalizing response.
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Entity: ${this}
Entity: ${this.name}
Value: ${input && JSON.stringify(input, null, 2)}
`,
);
(error as any).status = 400;
throw error;
} else {
throw new Error('undefined pk');
}
Expand Down
32 changes: 32 additions & 0 deletions packages/normalizr/src/entities/__tests__/Entity.test.ts
Expand Up @@ -20,6 +20,36 @@ describe(`${Entity.name} normalization`, () => {
expect(normalize({ id: '1' }, MyEntity)).toMatchSnapshot();
});

it('should throw a custom error if data does not include pk', () => {
class MyEntity extends Entity {
readonly name: string = '';
readonly secondthing: string = '';
pk() {
return this.name;
}
}
const schema = MyEntity.asSchema();
function normalizeBad() {
normalize({ secondthing: 'hi' }, schema);
}
expect(normalizeBad).toThrowErrorMatchingSnapshot();
});

it('should throw a custom error if data loads with unexpected props', () => {
class MyEntity extends Entity {
readonly name: string = '';
readonly secondthing: string = '';
pk() {
return this.name;
}
}
const schema = MyEntity.asSchema();
function normalizeBad() {
normalize({ nonexistantthing: 'hi' }, schema);
}
expect(normalizeBad).toThrowErrorMatchingSnapshot();
});

describe('key', () => {
test('must be created with a key name', () => {
const makeSchema = () =>
Expand Down Expand Up @@ -206,6 +236,8 @@ describe(`${Entity.name} normalization`, () => {
test('is run before and passed to the schema normalization', () => {
class AttachmentsEntity extends IDEntity {}
class EntriesEntity extends IDEntity {
readonly type: string = '';

static schema = {
data: { attachment: AttachmentsEntity.asSchema() },
};
Expand Down
Expand Up @@ -454,3 +454,28 @@ Object {
"result": "123",
}
`;

exports[`Entity normalization should throw a custom error if data does not include pk 1`] = `
"Missing usable resource key when normalizing response.
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Entity: MyEntity
Value: {
\\"secondthing\\": \\"hi\\"
}
"
`;

exports[`Entity normalization should throw a custom error if data loads with unexpected props 1`] = `
"Attempted to initialize MyEntity with substantially different than expected keys
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Expected keys: name,secondthing
Value: {
\\"nonexistantthing\\": \\"hi\\"
}"
`;
1 change: 1 addition & 0 deletions packages/normalizr/src/schemas/Array.js
@@ -1,4 +1,5 @@
import PolymorphicSchema from './Polymorphic';
import { isImmutable } from './ImmutableUtils';

const validateSchema = definition => {
const isArray = Array.isArray(definition);
Expand Down
15 changes: 14 additions & 1 deletion packages/normalizr/src/schemas/__tests__/Array.test.js
Expand Up @@ -7,6 +7,15 @@ import IDEntity from '../../entities/IDEntity';

describe(`${schema.Array.name} normalization`, () => {
describe('Object', () => {
test('should throw a custom error if data loads with unexpected value', () => {
class User extends IDEntity {}
const schema = [User.asSchema()];
function normalizeBad() {
normalize('5', schema);
}
expect(normalizeBad).toThrowErrorMatchingSnapshot();
});

test(`normalizes plain arrays as shorthand for ${schema.Array.name}`, () => {
class User extends IDEntity {}
expect(
Expand All @@ -24,6 +33,8 @@ describe(`${schema.Array.name} normalization`, () => {

test('passes its parent to its children when normalizing', () => {
class Child extends IDEntity {
content = '';

static fromJS(entity, parent, key) {
return super.fromJS({
...entity,
Expand All @@ -33,6 +44,9 @@ describe(`${schema.Array.name} normalization`, () => {
}
}
class Parent extends IDEntity {
content = '';
children = [];

static schema = {
children: [Child.asSchema()],
};
Expand Down Expand Up @@ -492,7 +506,6 @@ describe(`${schema.Array.name} denormalization`, () => {
catSchema,
fromJS(entities),
);
console.log(fromJS(entities).getIn(['Cat', '1']));
expect(value).toMatchSnapshot();
expect(found).toBe(true);
});
Expand Down
Expand Up @@ -1041,3 +1041,5 @@ Object {
"result": "1",
}
`;

exports[`ArraySchema normalization Object should throw a custom error if data loads with unexpected value 1`] = `"Unexpected input given to normalize. Expected type to be \\"object\\", found \\"string\\"."`;
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Resource getEntitySchema() merging should match snapshot 1`] = `
exports[`Resource asSchema() merging should match snapshot 1`] = `
Object {
"entities": Object {
"http://test.com/article-cooler/": Object {
Expand Down Expand Up @@ -39,3 +39,16 @@ Object {
],
}
`;

exports[`Resource asSchema() should throw a custom error if data does not include pk 1`] = `
"Missing usable resource key when normalizing response.
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Entity: CoolerArticleResource
Value: {
\\"content\\": \\"hi\\"
}
"
`;
19 changes: 4 additions & 15 deletions packages/rest-hooks/src/resource/__tests__/resource.ts
Expand Up @@ -446,7 +446,7 @@ describe('Resource', () => {
});
});

describe('getEntitySchema()', () => {
describe('asSchema()', () => {
describe('merging', () => {
const nested = [
{
Expand Down Expand Up @@ -496,22 +496,11 @@ describe('Resource', () => {
});

it('should throw a custom error if data does not include pk', () => {
const schema = CoolerArticleResource.getEntitySchema();
const schema = CoolerArticleResource.asSchema();
function normalizeBad() {
normalize({ weirdthing: 'hi' }, schema);
normalize({ content: 'hi' }, schema);
}
expect(normalizeBad).toThrowErrorMatchingInlineSnapshot(`
"Missing usable resource key when normalizing response.
This is likely due to a malformed response.
Try inspecting the network response or fetch() return value.
Entity: CoolerArticleResource::http://test.com/article-cooler/
Value: {
\\"weirdthing\\": \\"hi\\"
}
"
`);
expect(normalizeBad).toThrowErrorMatchingSnapshot();
});
});
});

0 comments on commit 2af464f

Please sign in to comment.