Skip to content

Commit

Permalink
fix: Collections work with polymorphic schemas and class name mangling (
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker authored Jul 15, 2024
1 parent 7427519 commit 428d618
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 128 deletions.
7 changes: 7 additions & 0 deletions .changeset/empty-pillows-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/endpoint': patch
---

Collection.key is shorter and more readable

`[Todo]` for Arrays or `{Todo}` for Values
5 changes: 5 additions & 0 deletions .changeset/fluffy-months-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/normalizr': patch
---

Make normalize and memo arguments accept more valid types
5 changes: 5 additions & 0 deletions .changeset/little-plants-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/endpoint': patch
---

fix: Collection.key robust against class name mangling
7 changes: 7 additions & 0 deletions .changeset/strong-garlics-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/endpoint': patch
---

Collections now work with polymorhpic schemas like Union

Collections.key on polymorphic types lists their possible Entity keys: `[PushEvent;PullRequestEvent]`
1 change: 1 addition & 0 deletions packages/endpoint/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export interface EntityInterface<T = any> extends SchemaSimple {
export interface PolymorphicInterface<T = any, Args extends any[] = any[]>
extends SchemaSimple<T, Args> {
readonly schema: any;
schemaKey(): string;
// this is not an actual member, but is needed for the recursive NormalizeNullable<> type algo
_normalizeNullable(): any;
// this is not an actual member, but is needed for the recursive DenormalizeNullable<> type algo
Expand Down
5 changes: 5 additions & 0 deletions packages/endpoint/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class Array<S extends Schema = Schema> implements SchemaClass {

define(definition: Schema): void;
readonly isSingleSchema: S extends EntityMap ? false : true;
schemaKey(): string;
readonly schema: S;
normalize(
input: any,
Expand Down Expand Up @@ -122,7 +123,9 @@ export class All<

define(definition: Schema): void;
readonly isSingleSchema: S extends EntityMap ? false : true;
schemaKey(): string;
readonly schema: S;
schemaKey(): string;
normalize(
input: any,
parent: any,
Expand Down Expand Up @@ -260,6 +263,7 @@ export interface UnionInstance<
define(definition: Schema): void;
inferSchema: SchemaAttributeFunction<Choices[keyof Choices]>;
getSchemaAttribute: SchemaFunction<keyof Choices>;
schemaKey(): string;
readonly schema: Choices;
normalize(
input: any,
Expand Down Expand Up @@ -327,6 +331,7 @@ export class Values<Choices extends Schema = any> implements SchemaClass {

define(definition: Schema): void;
readonly isSingleSchema: Choices extends EntityMap ? false : true;
schemaKey(): string;
inferSchema: SchemaAttributeFunction<
Choices extends EntityMap ? Choices[keyof Choices] : Choices
>;
Expand Down
16 changes: 11 additions & 5 deletions packages/endpoint/src/schemas/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,7 @@ export default class CollectionSchema<
this.argsKey = params => ({ ...params });
}
}
// this assumes the definition of Array/Values is Entity
this.key = `COLLECT:${this.schema.constructor.name}(${
(this.schema.schema as any).key
})`;
this.key = keyFromSchema(this.schema);
if ((options as any)?.nonFilterArgumentKeys) {
const { nonFilterArgumentKeys } = options as {
nonFilterArgumentKeys: ((key: string) => boolean) | string[] | RegExp;
Expand Down Expand Up @@ -357,9 +354,18 @@ function denormalize(
(this.schema.denormalize(input, args, unvisit) as any)
: (this.schema.denormalize([input], args, unvisit)[0] as any);
}

/**
* We call schema.denormalize and schema.normalize directly
* instead of visit/unvisit as we are not operating on new data
* so the additional checks in those methods are redundant
*/

function keyFromSchema(schema: PolymorphicInterface) {
if (schema instanceof ArraySchema) {
// this assumes the definition of Array/Values is Entity
return `[${schema.schemaKey()}]`;
} else if (schema instanceof Values) {
return `{${schema.schemaKey()}}`;
}
return `Collection:${schema.schemaKey()}`;
}
7 changes: 7 additions & 0 deletions packages/endpoint/src/schemas/Polymorphic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export default class PolymorphicSchema {
return this.schema[attr];
}

schemaKey(): string {
if (this.isSingleSchema) {
return this.schema.key;
}
return Object.values(this.schema).join(';');
}

normalizeValue(value: any, parent: any, key: any, args: any[], visit: Visit) {
if (!value) return value;
const schema = this.inferSchema(value, parent, key);
Expand Down
117 changes: 111 additions & 6 deletions packages/endpoint/src/schemas/__tests__/Collection.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// eslint-env jest
import { initialState } from '@data-client/core';
import { initialState, State } from '@data-client/core';
import { normalize, denormalize, MemoCache } from '@data-client/normalizr';
import { IDEntity } from '__tests__/new';
import { Record } from 'immutable';

import SimpleMemoCache from './denormalize';
import { PolymorphicInterface } from '../..';
import { schema } from '../..';
import PolymorphicSchema from '../Polymorphic';

let dateSpy: jest.SpyInstance;
beforeAll(() => {
Expand Down Expand Up @@ -49,6 +50,54 @@ const userTodos = new schema.Collection(new schema.Array(Todo), {
}),
});

test('key works with custom schema', () => {
class CustomArray extends PolymorphicSchema {
declare schema: any;
normalize(
input: any,
parent: any,
key: any,
args: any[],
visit: any,
addEntity: any,
getEntity: any,
checkLoop: any,
): any {
return input.map((value, index) =>
this.normalizeValue(value, parent, key, args, visit),
);
}

denormalize(
input: any,
args: any[],
unvisit: (schema: any, input: any) => any,
) {
return input.map ?
input.map((entityOrId: any) =>
this.denormalizeValue(entityOrId, unvisit),
)
: input;
}

queryKey(
args: unknown,
queryKey: unknown,
getEntity: unknown,
getIndex: unknown,
): any {
return undefined;
}

_normalizeNullable(): any {}

_denormalizeNullable(): any {}
}

const collection = new schema.Collection(new CustomArray(Todo));
expect(collection.key).toBe('Collection:Todo');
});

describe(`${schema.Collection.name} normalization`, () => {
let warnSpy: jest.SpyInstance;
afterEach(() => {
Expand Down Expand Up @@ -133,10 +182,67 @@ describe(`${schema.Collection.name} normalization`, () => {
expect(state).toMatchSnapshot();
});

describe('polymorphism', () => {
class User extends IDEntity {
type = 'users';
}
class Group extends IDEntity {
type = 'groups';
}
const collection = new schema.Collection(
new schema.Array(
{
users: User,
groups: Group,
},
'type',
),
);
const collectionUnion = new schema.Collection([
new schema.Union(
{
users: User,
groups: Group,
},
'type',
),
]);

test('generates polymorphic key', () => {
expect(collection.key).toBe('[User;Group]');
expect(collectionUnion.key).toBe('[User;Group]');
});

test('works with polymorphic members', () => {
const { entities, result } = normalize(
collection,
[
{ id: '1', type: 'users' },
{ id: '2', type: 'groups' },
],
[{ fakeFilter: false }],
);
expect(result).toMatchSnapshot();
expect(entities).toMatchSnapshot();
});
test('works with Union members', () => {
const { entities, result } = normalize(
collectionUnion,
[
{ id: '1', type: 'users' },
{ id: '2', type: 'groups' },
],
[{ fakeFilter: false }],
);
expect(result).toMatchSnapshot();
expect(entities).toMatchSnapshot();
});
});

test('normalizes push onto the end', () => {
const init = {
entities: {
'COLLECT:ArraySchema(Todo)': {
[User.schema.todos.key]: {
'{"userId":"1"}': ['5'],
},
Todo: {
Expand All @@ -154,7 +260,7 @@ describe(`${schema.Collection.name} normalization`, () => {
},
},
entityMeta: {
'COLLECT:ArraySchema(Todo)': {
[User.schema.todos.key]: {
'{"userId":"1"}': {
date: 1557831718135,
expiresAt: Infinity,
Expand All @@ -177,7 +283,6 @@ describe(`${schema.Collection.name} normalization`, () => {
},
},
indexes: {},
result: '1',
};
const state = normalize(
User.schema.todos.push,
Expand Down Expand Up @@ -344,7 +449,7 @@ describe(`${schema.Collection.name} normalization`, () => {
describe(`${schema.Collection.name} denormalization`, () => {
const normalizeNested = {
entities: {
'COLLECT:ArraySchema(Todo)': {
[userTodos.key]: {
'{"userId":"1"}': ['5'],
},
Todo: {
Expand All @@ -362,7 +467,7 @@ describe(`${schema.Collection.name} denormalization`, () => {
},
},
entityMeta: {
'COLLECT:ArraySchema(Todo)': {
[userTodos.key]: {
'{"userId":"1"}': {
date: 1557831718135,
expiresAt: Infinity,
Expand Down
Loading

0 comments on commit 428d618

Please sign in to comment.