UNPKG

@data-client/endpoint

Version:

Declarative Network Interface Definitions

1,193 lines (1,076 loc) 32.7 kB
// eslint-env jest import { normalize, denormalize } from '@data-client/normalizr'; import { INVALID } from '@data-client/normalizr'; import { Temporal } from '@js-temporal/polyfill'; import { IDEntity } from '__tests__/new'; import { fromJS, Record } from 'immutable'; import { SimpleMemoCache } from './denormalize'; import { AbstractInstanceType } from '../../'; import { schema } from '../../'; import Entity from '../Entity'; let dateSpy: jest.SpyInstance; beforeAll(() => { dateSpy = jest .spyOn(global.Date, 'now') .mockImplementation(() => new Date('2019-05-14T11:01:58.135Z').valueOf()); }); afterAll(() => { dateSpy.mockRestore(); }); const values = <T extends { [k: string]: any }>(obj: T) => Object.keys(obj).map(key => obj[key]); class Tacos extends IDEntity { readonly name: string = ''; readonly alias: string | undefined = undefined; } class ArticleEntity extends Entity { readonly id: string = ''; readonly title: string = ''; readonly author: string = ''; readonly content: string = ''; } class WithOptional extends Entity { readonly id: string = ''; readonly article: ArticleEntity | null = null; readonly requiredArticle = ArticleEntity.fromJS(); readonly nextPage: string = ''; static schema = { article: ArticleEntity, requiredArticle: ArticleEntity, }; } describe(`${Entity.name} normalization`, () => { let warnSpy: jest.SpyInstance; afterEach(() => { warnSpy.mockRestore(); }); beforeEach(() => (warnSpy = jest.spyOn(console, 'warn')).mockImplementation(() => {}), ); test('normalizes an entity', () => { class MyEntity extends Entity {} expect(normalize(MyEntity, { id: '1' })).toMatchSnapshot(); }); test('normalize throws error when id missing with no pk', () => { class MyEntity extends Entity {} expect(() => normalize(MyEntity, { slug: '1' }), ).toThrowErrorMatchingSnapshot(); }); test('normalizes already processed entities', () => { class MyEntity extends Entity {} class Nested extends Entity { title = ''; nest = MyEntity.fromJS(); static schema = { nest: MyEntity, }; } expect(normalize(new schema.Array(MyEntity), ['1'])).toMatchSnapshot(); expect( normalize(new schema.Object({ data: MyEntity }), { data: '1' }), ).toMatchSnapshot(); expect( normalize(Nested, { title: 'hi', id: '5', nest: '10' }), ).toMatchSnapshot(); }); test('normalizes does not change value when shouldUpdate() returns false', () => { class MyEntity extends Entity { id = ''; title = ''; static shouldUpdate() { return false; } } const { entities, entityMeta } = normalize(MyEntity, { id: '1', title: 'hi', }); const secondEntities = normalize( MyEntity, { id: '1', title: 'second' }, [], { entities, entityMeta, indexes: {} }, ).entities; expect(entities.MyEntity['1']).toBeDefined(); expect(entities.MyEntity['1']).toBe(secondEntities.MyEntity['1']); }); 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; function normalizeBad() { normalize(schema, { secondthing: 'hi' }); } expect(normalizeBad).toThrowErrorMatchingSnapshot(); }); it('should throw a custom error if data does not include pk (serializes pk)', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; pk() { return `${this.name}`; } } const schema = MyEntity; function normalizeBad() { normalize(schema, { secondthing: 'hi' }); } expect(normalizeBad).toThrowErrorMatchingSnapshot(); }); it('should not throw if schema key is missing from Entity', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; pk() { return this.name; } static schema = { blarb: Temporal.Instant.from, }; } const schema = MyEntity; expect( normalize(schema, { name: 'bob', secondthing: 'hi' }), ).toMatchSnapshot(); }); it('should handle optional schema entries Entity', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly blarb: Date | undefined = undefined; pk() { return this.name; } static schema = { blarb: Temporal.Instant.from, }; } const schema = MyEntity; expect(normalize(schema, { name: 'bob', secondthing: 'hi' })) .toMatchInlineSnapshot(` { "entities": { "MyEntity": { "bob": { "name": "bob", "secondthing": "hi", }, }, }, "entityMeta": { "MyEntity": { "bob": { "date": 1557831718135, "expiresAt": Infinity, "fetchedAt": 0, }, }, }, "indexes": {}, "result": "bob", } `); }); it('should throw a custom error if data loads with no matching props', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; pk() { return this.name; } } const schema = MyEntity; function normalizeBad() { normalize(schema, {}); } expect(normalizeBad).toThrowErrorMatchingSnapshot(); }); it('should throw a custom error loads with array', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; pk() { return this.name; } } const schema = MyEntity; function normalizeBad() { normalize(schema, [ { name: 'hi', secondthing: 'ho' }, { name: 'hi', secondthing: 'ho' }, { name: 'hi', secondthing: 'ho' }, ]); } expect(normalizeBad).toThrowErrorMatchingSnapshot(); }); it('should warn when automaticValidation === "warn"', () => { class MyEntity extends Entity { readonly '0': string = ''; readonly secondthing: string = ''; static automaticValidation = 'warn' as const; pk() { return this[0]; } } const schema = MyEntity; function normalizeBad() { normalize(schema, [ { name: 'hi', secondthing: 'ho' }, { name: 'hi', secondthing: 'ho' }, { name: 'hi', secondthing: 'ho' }, ]); } expect(normalizeBad).not.toThrow(); expect(warnSpy.mock.calls.length).toBe(1); expect(warnSpy.mock.calls).toMatchSnapshot(); }); it('should allow many unexpected as long as none are missing', () => { class MyEntity extends Entity { readonly name: string = ''; readonly a: string = ''; pk() { return this.name; } } const schema = MyEntity; expect( normalize(schema, { name: 'hi', a: 'a', b: 'b', c: 'c', d: 'e', e: 0, f: 0, g: 0, h: 0, i: 0, j: 0, k: 0, l: 0, m: 0, n: 0, o: 0, p: 0, q: 0, r: 0, s: 0, t: 0, u: 0, }), ).toMatchSnapshot(); expect(warnSpy.mock.calls.length).toBe(0); }); it('should throw with custom validator', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly thirdthing: number = 0; get nonexistantthing() { return this.name + 5; } pk() { return this.name; } static validate(value: any) { if (value.nonexistantthing) return 'should not contain getter'; } } function normalizeBad() { normalize(MyEntity, { name: 'hoho', nonexistantthing: 'hi' }); } expect(normalizeBad).toThrow(); }); it('should not expect getters returned', () => { class MyEntity extends Entity { readonly name: string = ''; get other() { return this.name + 5; } get another() { return 'another'; } get yetAnother() { return 'another2'; } pk() { return this.name; } } function normalizeBad() { normalize(MyEntity, { name: 'bob' }); } expect(normalizeBad).not.toThrow(); expect(warnSpy.mock.calls.length).toBe(0); }); it('should do nothing when automaticValidation === "silent"', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly thirdthing: number = 0; static automaticValidation = 'silent' as const; pk() { return this.name; } } const schema = MyEntity; function normalizeBad() { normalize(schema, { name: 'hoho', nonexistantthing: 'hi' }); } expect(normalizeBad).not.toThrow(); expect(warnSpy.mock.calls.length).toBe(0); }); it('should throw a custom error if data loads with string', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly thirdthing: number = 0; pk() { return this.name; } } const schema = { data: MyEntity }; function normalizeBad() { normalize(schema, 'hibho'); } expect(normalizeBad).toThrowErrorMatchingSnapshot(); }); describe('key', () => { test('must be created with a key name', () => { const makeSchema = () => class extends Entity { readonly id: number = 0; }; expect(() => makeSchema().key).toThrow(); }); test('key name must be a string', () => { // @ts-expect-error class MyEntity extends IDEntity { static get key() { return 42; } } }); test('key getter should return key set via `static get key()`', () => { class User extends IDEntity { static get key() { return 'user'; } } expect(User.key).toEqual('user'); }); }); describe('pk()', () => { test('can use a custom pk() string', () => { class User extends Entity { readonly idStr: string = ''; readonly name: string = ''; pk() { return this.idStr; } } expect( normalize(User, { idStr: '134351', name: 'Kathy' }), ).toMatchSnapshot(); }); test('can normalize entity IDs based on their object key', () => { class User extends Entity { readonly name: string = ''; pk(parent?: any, key?: string) { return key; } } const inputSchema = new schema.Values({ users: User }, () => 'users'); expect( normalize(inputSchema, { '4': { name: 'taco' }, '56': { name: 'burrito' }, }), ).toMatchSnapshot(); }); test("can build the entity's ID from the parent object", () => { class User extends Entity { readonly id: string = ''; readonly name: string = ''; pk(parent?: any, key?: string) { return `${parent.name}-${key}-${this.id}`; } } const inputSchema = new schema.Object({ user: User }); expect( normalize(inputSchema, { name: 'tacos', user: { id: '4', name: 'Jimmy' }, }), ).toMatchSnapshot(); }); }); describe('mergeStrategy', () => { test('defaults to plain merging', () => { expect( normalize( [Tacos], [ { id: '1', name: 'foo' }, { id: '1', name: 'bar', alias: 'bar' }, ], ), ).toMatchSnapshot(); }); test('can use a custom merging strategy', () => { class MergeTaco extends Tacos { static merge<T extends typeof Entity>( this: T, existing: AbstractInstanceType<T>, incoming: AbstractInstanceType<T>, ) { const props = Object.assign({}, existing, incoming, { name: (existing as MergeTaco).name, }); return this.fromJS(props); } } expect( normalize( [MergeTaco], [ { id: '1', name: 'foo' }, { id: '1', name: 'bar', alias: 'bar' }, ], ), ).toMatchSnapshot(); }); }); describe('process', () => { test('can use a custom processing strategy', () => { class ProcessTaco extends Tacos { readonly slug: string = ''; static process(input: any, parent: any, key: string | undefined): any { return { ...input, slug: `thing-${(input as unknown as ProcessTaco).id}`, }; } } const { entities, result } = normalize(ProcessTaco, { id: '1', name: 'foo', }); const final = new SimpleMemoCache().denormalize( ProcessTaco, result, entities, ); expect(final).not.toEqual(expect.any(Symbol)); if (typeof final === 'symbol') return; expect(final?.slug).toEqual('thing-1'); expect(final).toMatchSnapshot(); }); test('can use information from the parent in the process strategy', () => { class ChildEntity extends IDEntity { readonly content: string = ''; readonly parentId: string = ''; readonly parentKey: string = ''; static process(input: any, parent: any, key: string | undefined): any { return { ...input, parentId: parent?.id, parentKey: key, }; } } class ParentEntity extends IDEntity { readonly content: string = ''; readonly child: ChildEntity = ChildEntity.fromJS({}); static schema = { child: ChildEntity }; } const { entities, result } = normalize(ParentEntity, { id: '1', content: 'parent', child: { id: '4', content: 'child' }, }); const final = new SimpleMemoCache().denormalize( ParentEntity, result, entities, ); expect(final).not.toEqual(expect.any(Symbol)); if (typeof final === 'symbol') return; expect(final?.child?.parentId).toEqual('1'); expect(final).toMatchSnapshot(); }); test('is run before and passed to the schema denormalization', () => { class AttachmentsEntity extends IDEntity {} class EntriesEntity extends IDEntity { readonly type: string = ''; static schema = { data: { attachment: AttachmentsEntity }, }; static process(input: any, parent: any, key: string | undefined): any { return { ...values(input)[0], type: Object.keys(input)[0], }; } } const { entities, result } = normalize(EntriesEntity, { message: { id: '123', data: { attachment: { id: '456' } } }, }); const final = new SimpleMemoCache().denormalize( EntriesEntity, result, entities, ); expect(final).not.toEqual(expect.any(Symbol)); if (typeof final === 'symbol') return; expect(final?.type).toEqual('message'); expect(final).toMatchSnapshot(); }); test('when undefined is returned, INVALIDate the entity', () => { class ProcessTaco extends Tacos { readonly slug: string = ''; static process(input: any, parent: any, key: string | undefined): any { if (input.id === 'DELETE') return undefined; return { ...input, slug: `thing-${(input as unknown as ProcessTaco).id}`, }; } } let { entities, result } = normalize(ProcessTaco, { id: 'DELETE', name: 'foo', }); let final = new SimpleMemoCache().denormalize( ProcessTaco, result, entities, ); expect(final).toEqual(expect.any(Symbol)); // still work in normal cases ({ entities, result } = normalize(ProcessTaco, { id: '1', name: 'foo', })); final = new SimpleMemoCache().denormalize(ProcessTaco, result, entities); expect(final).not.toEqual(expect.any(Symbol)); if (typeof final === 'symbol') return; expect(final?.slug).toEqual('thing-1'); }); }); }); describe(`${Entity.name} denormalization`, () => { test('denormalizes an entity', () => { const entities = { Tacos: { '1': { id: '1', name: 'foo' }, }, }; expect(denormalize(Tacos, '1', entities)).toMatchSnapshot(); expect(denormalize(Tacos, '1', fromJS(entities))).toMatchSnapshot(); }); class Food extends Entity {} class Menu extends Entity { food: Food = Food.fromJS(); static schema = { food: Food }; } test('denormalizes deep entities', () => { const entities = { Menu: { '1': { id: '1', food: '1' }, '2': { id: '2' }, }, Food: { '1': { id: '1' }, }, }; const de1 = denormalize(Menu, '1', entities); expect(de1).toMatchSnapshot(); expect(denormalize(Menu, '1', fromJS(entities))).toEqual(de1); const de2 = denormalize(Menu, '2', entities); expect(de2).toMatchSnapshot(); expect(denormalize(Menu, '2', fromJS(entities))).toEqual(de2); }); test('denormalizes deep entities while maintaining referential equality', () => { const entities = { Menu: { '1': { id: '1', food: '1' }, '2': { id: '2' }, }, Food: { '1': { id: '1' }, }, }; const memo = new SimpleMemoCache(); const first = memo.denormalize(Menu, '1', entities); const second = memo.denormalize(Menu, '1', entities); expect(first).not.toEqual(expect.any(Symbol)); if (typeof first === 'symbol') return; expect(second).not.toEqual(expect.any(Symbol)); if (typeof second === 'symbol') return; expect(first).toBe(second); expect(first?.food).toBe(second?.food); }); test('denormalizes to undefined when validate() returns string', () => { class MyTacos extends Tacos { static validate(entity) { if (!Object.hasOwn(entity, 'name')) return 'no name'; } } const entities = { MyTacos: { '1': { id: '1' }, }, }; expect(denormalize(MyTacos, '1', entities)).toEqual(expect.any(Symbol)); expect(denormalize(MyTacos, '1', fromJS(entities))).toEqual( expect.any(Symbol), ); }); test('denormalizes to undefined for missing data', () => { const entities = { Menu: { '1': { id: '1', food: '2' }, }, Food: { '1': { id: '1' }, }, }; expect(denormalize(Menu, '1', entities)).toMatchSnapshot(); expect(denormalize(Menu, '1', fromJS(entities))).toMatchSnapshot(); expect(denormalize(Menu, '2', entities)).toMatchSnapshot(); expect(denormalize(Menu, '2', fromJS(entities))).toMatchSnapshot(); }); it('should handle optional schema entries Entity', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly blarb: Date | undefined = undefined; pk() { return this.name; } static schema = { blarb: Temporal.Instant.from, }; } const schema = MyEntity; expect( denormalize(schema, 'bob', { MyEntity: { bob: { name: 'bob', secondthing: 'hi' } }, }), ).toMatchInlineSnapshot(` MyEntity { "blarb": undefined, "name": "bob", "secondthing": "hi", } `); }); it('should handle null schema entries Entity', () => { class MyEntity extends Entity { readonly name: string = ''; readonly secondthing: string = ''; readonly blarb: Date | null = null; pk() { return this.name; } static schema = { blarb: Temporal.Instant.from, }; } const schema = MyEntity; expect( denormalize(schema, 'bob', { MyEntity: { bob: { name: 'bob', secondthing: 'hi', blarb: null } }, }), ).toMatchInlineSnapshot(` MyEntity { "blarb": null, "name": "bob", "secondthing": "hi", } `); }); test('denormalizes to undefined for deleted data', () => { const entities = { Menu: { '1': { id: '1', food: '2' }, '2': INVALID, }, Food: { '1': { id: '1' }, '2': INVALID, }, }; expect(denormalize(Menu, '1', entities)).toMatchSnapshot(); expect(denormalize(Menu, '1', fromJS(entities))).toMatchSnapshot(); expect(denormalize(Menu, '2', entities)).toMatchSnapshot(); expect(denormalize(Menu, '2', fromJS(entities))).toMatchSnapshot(); }); test('denormalizes deep entities with records', () => { const Food = Record<{ id: null | string }>({ id: null }); const MenuR = Record<{ id: null | string; food: null | string }>({ id: null, food: null, }); const entities = { Menu: { '1': new MenuR({ id: '1', food: '1' }), '2': new MenuR({ id: '2' }), }, Food: { '1': new Food({ id: '1' }), }, }; expect(denormalize(Menu, '1', entities)).toMatchSnapshot(); expect(denormalize(Menu, '1', fromJS(entities))).toMatchSnapshot(); expect(denormalize(Menu, '2', entities)).toMatchSnapshot(); expect(denormalize(Menu, '2', fromJS(entities))).toMatchSnapshot(); }); test('can denormalize already partially denormalized data', () => { const entities = { Menu: { '1': { id: '1', food: { id: '1' } }, }, Food: { // TODO: BREAKING CHANGE: Update this to use main entity and only return nested as 'fallback' in case main entity is not set '1': { id: '1', extra: 'hi' }, }, }; expect(denormalize(Menu, '1', entities)).toMatchSnapshot(); expect(denormalize(Menu, '1', fromJS(entities))).toMatchSnapshot(); }); class User extends IDEntity { readonly role = ''; readonly reports: Report[] = []; } class Report extends IDEntity { readonly title: string = ''; readonly draftedBy: User = User.fromJS(); readonly publishedBy: User = User.fromJS(); static schema = { draftedBy: User, publishedBy: User, }; } User.schema = { reports: new schema.Array(Report), }; class Comment extends IDEntity { readonly body: string = ''; readonly author: User = User.fromJS(); static schema = { author: User, }; } test('denormalizes recursive dependencies', () => { const entities = { Report: { '123': { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, User: { '456': { id: '456', role: 'manager', reports: ['123'], }, }, }; expect(denormalize(Report, '123', entities)).toMatchSnapshot(); expect(denormalize(Report, '123', fromJS(entities))).toMatchSnapshot(); expect(denormalize(User, '456', entities)).toMatchSnapshot(); expect(denormalize(User, '456', fromJS(entities))).toMatchSnapshot(); }); test('denormalizes recursive entities with referential equality', () => { const entities = { Report: { '123': { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, Comment: { '999': { id: '999', body: 'Good morning', author: '456', }, }, User: { '456': { id: '456', role: 'manager', reports: ['123'], }, '457': { id: '457', role: 'servant', reports: ['123'], }, }, }; const memo = new SimpleMemoCache(); const denormalizedReport = memo.denormalize(Report, '123', entities); expect(denormalizedReport).not.toEqual(expect.any(Symbol)); if (typeof denormalizedReport === 'symbol') return; expect(denormalizedReport).toBeDefined(); // This is just for TypeScript, the above line actually determines this if (!denormalizedReport) throw new Error('expected to be defined'); expect(denormalizedReport).toBe(denormalizedReport.draftedBy?.reports[0]); expect(denormalizedReport.publishedBy).toBe(denormalizedReport.draftedBy); expect(denormalizedReport.draftedBy?.reports[0].draftedBy).toBe( denormalizedReport.draftedBy, ); const denormalizedReport2 = memo.denormalize(Report, '123', entities); expect(denormalizedReport2).toStrictEqual(denormalizedReport); expect(denormalizedReport2).toBe(denormalizedReport); // NOTE: Given how immutable data works, referential equality can't be // maintained with nested denormalization. }); test('denormalizes maintain referential equality when appropriate', () => { const entities = { Report: { '123': { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, Comment: { '999': { id: '999', body: 'Good morning', author: '456', }, }, User: { '456': { id: '456', role: 'manager', reports: ['123'], }, '457': { id: '457', role: 'servant', reports: ['123'], }, }, }; const memo = new SimpleMemoCache(); const input = { report: '123', comment: '999' }; const sch = new schema.Object({ report: Report, comment: Comment, }); const denormalizedReport = memo.denormalize(sch, input, entities); expect(denormalizedReport).not.toEqual(expect.any(Symbol)); if (typeof denormalizedReport === 'symbol') return; expect(denormalizedReport.report).toBeDefined(); expect(denormalizedReport.comment).toBeDefined(); // This is just for TypeScript, the above line actually determines this if (!denormalizedReport.report || !denormalizedReport.comment) throw new Error('expected to be defined'); expect(denormalizedReport.report.publishedBy).toBe( denormalizedReport.comment.author, ); const denormalizedReport2 = memo.denormalize(sch, input, entities); expect(denormalizedReport2).not.toEqual(expect.any(Symbol)); if (typeof denormalizedReport2 === 'symbol') return; expect(denormalizedReport2).toStrictEqual(denormalizedReport); expect(denormalizedReport2).toBe(denormalizedReport); // should update all uses of user const nextEntities = { ...entities, User: { ...entities.User, '456': { ...entities.User[456], role: 'supervisor', }, }, }; const denormalizedReport3 = memo.denormalize(sch, input, nextEntities); expect(denormalizedReport3).not.toEqual(expect.any(Symbol)); if (typeof denormalizedReport3 === 'symbol') return; expect(denormalizedReport3.comment?.author?.role).toBe('supervisor'); expect(denormalizedReport3.report?.draftedBy?.role).toBe('supervisor'); // NOTE: Given how immutable data works, referential equality can't be // maintained with nested denormalization. }); describe('optional entities', () => { it('should be marked as found even when optional is not there', () => { const denormalized = denormalize(WithOptional, 'abc', { [WithOptional.key]: { abc: { id: 'abc', // this is typed because we're actually sending wrong data to it requiredArticle: '5' as any, nextPage: 'blob', }, }, [ArticleEntity.key]: { ['5']: { id: '5' }, }, }); const response = denormalized; expect(response).toBeDefined(); expect(response).toBeInstanceOf(WithOptional); expect(response).toEqual({ id: 'abc', article: null, requiredArticle: ArticleEntity.fromJS({ id: '5' }), nextPage: 'blob', }); }); it('should be marked as found when nested entity is missing', () => { const denormalized = denormalize(WithOptional, 'abc', { [WithOptional.key]: { abc: WithOptional.fromJS({ id: 'abc', // this is typed because we're actually sending wrong data to it article: '5' as any, nextPage: 'blob', }), }, [ArticleEntity.key]: { ['5']: ArticleEntity.fromJS({ id: '5' }), }, }); expect(denormalized).not.toEqual(expect.any(Symbol)); const response = denormalized; expect(response).toBeDefined(); expect(response).toBeInstanceOf(WithOptional); expect(response).toEqual({ id: 'abc', article: ArticleEntity.fromJS({ id: '5' }), requiredArticle: ArticleEntity.fromJS(), nextPage: 'blob', }); }); it('should be marked as deleted when required entity is deleted symbol', () => { const denormalized = denormalize(WithOptional, 'abc', { [WithOptional.key]: { abc: { id: 'abc', // this is typed because we're actually sending wrong data to it requiredArticle: '5' as any, nextPage: 'blob', }, }, [ArticleEntity.key]: { ['5']: INVALID, }, }); expect(denormalized).toEqual(expect.any(Symbol)); }); it('should be non-required deleted members should not result in deleted indicator', () => { const denormalized = denormalize(WithOptional, 'abc', { [WithOptional.key]: { abc: WithOptional.fromJS({ id: 'abc', // this is typed because we're actually sending wrong data to it article: '5' as any, requiredArticle: '6' as any, nextPage: 'blob', }), }, [ArticleEntity.key]: { ['5']: INVALID, ['6']: ArticleEntity.fromJS({ id: '6' }), }, }); expect(denormalized).not.toEqual(expect.any(Symbol)); const response = denormalized; expect(response).toBeDefined(); expect(response).toBeInstanceOf(WithOptional); expect(response).toEqual({ id: 'abc', article: undefined, requiredArticle: ArticleEntity.fromJS({ id: '6' }), nextPage: 'blob', }); }); it('should be deleted when both are true in different parts of schema', () => { const denormalized = denormalize( new schema.Object({ data: WithOptional, other: ArticleEntity }), { data: 'abc' }, { [WithOptional.key]: { abc: WithOptional.fromJS({ id: 'abc', // this is typed because we're actually sending wrong data to it article: '6' as any, requiredArticle: '5' as any, nextPage: 'blob', }), }, [ArticleEntity.key]: { ['5']: INVALID, ['6']: ArticleEntity.fromJS({ id: '6' }), }, }, ); expect(denormalized).toEqual(expect.any(Symbol)); /*const response = denormalized; expect(response).toBeDefined(); expect(response).toEqual({ data: { id: 'abc', article: ArticleEntity.fromJS({ id: '6' }), requiredArticle: undefined, nextPage: 'blob', }, }); deleted symbol replaces whole denorm value */ }); }); }); describe('Entity.defaults', () => { it('should work with inheritance', () => { abstract class DefaultsEntity extends Entity { static getMyDefaults() { return this.defaults; } } class ID extends DefaultsEntity { id = ''; pk() { return this.id; } } class UserEntity extends ID { username = ''; createdAt = Temporal.Instant.fromEpochSeconds(0); static schema = { createdAt: Temporal.Instant.from, }; } expect(ID.getMyDefaults()).toMatchInlineSnapshot(` ID { "id": "", } `); expect(UserEntity.getMyDefaults()).toMatchInlineSnapshot(` UserEntity { "createdAt": "1970-01-01T00:00:00Z", "id": "", "username": "", } `); }); });