Skip to content

Commit

Permalink
Typed schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
allanhortle committed Oct 16, 2023
1 parent 28a10bb commit 9d84892
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 211 deletions.
3 changes: 2 additions & 1 deletion packages/enty/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
rules: {
'prettier/prettier': 0
'prettier/prettier': 0,
eqeqeq: 0
}
};
3 changes: 2 additions & 1 deletion packages/enty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@
"tsdx": "^0.14.1",
"tslib": "^2.2.0",
"typescript": "^4.3.2"
}
},
"dependencies": {}
}
37 changes: 17 additions & 20 deletions packages/enty/src/ArraySchema.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,45 @@
import {NormalizeState} from './util/definitions';
import {DenormalizeState} from './util/definitions';
import {Create} from './util/definitions';
import {StructuralSchemaInterface} from './util/definitions';
import {Merge} from './util/definitions';
import {StructuralSchemaOptions} from './util/definitions';
import {Structure} from './util/definitions';
import {StructureOptions} from './util/definitions';
import {Schema} from './util/definitions';
import {Entities} from './util/definitions';

import REMOVED_ENTITY from './util/RemovedEntity';

export default class ArraySchema<A extends Schema> implements StructuralSchemaInterface<A> {
shape: A;
create: Create;
merge: Merge;
export default class ArraySchema<T extends Array<any>> implements Structure<T> {
shape: Schema<T[number]>;
create: (next: T) => T;
merge: (previous: T, next: T) => T;

constructor(shape: A, options: StructuralSchemaOptions = {}) {
constructor(shape: Schema<T[number]>, options: StructureOptions<T> = {}) {
this.shape = shape;
this.merge = options.merge || ((_, bb) => bb);
this.create = options.create || ((aa) => aa);
}

normalize(data: any, entities: Entities = {}): NormalizeState {
normalize(data: T, entities: Entities = {}): NormalizeState {
let schemas = {};

const result = data.map((item: any): any => {
const {result, schemas: childSchemas} = this.shape.normalize(item, entities);
Object.assign(schemas, childSchemas);
return result;
});

return {entities, schemas, result: this.create(result)};
return {entities, schemas, result: this.create(result as T)};
}

denormalize(denormalizeState: DenormalizeState, path: Array<any> = []): any {
denormalize(denormalizeState: DenormalizeState, path: Array<any> = []): T | null {
const {result, entities} = denormalizeState;

// Filter out any deleted keys
if (result == null) {
return result;
}
// Map denormalize to our result List.
if (result == null) return result;

return result
.map((item: any): any => {
.map((item: unknown) => {
return this.shape.denormalize({result: item, entities}, path);
})
.filter((ii: any) => ii !== REMOVED_ENTITY);
.filter((ii: unknown) => {
return ii !== REMOVED_ENTITY;
});
}
}
66 changes: 30 additions & 36 deletions packages/enty/src/EntitySchema.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,57 @@
import {NormalizeState} from './util/definitions';
import {DenormalizeState} from './util/definitions';
import {EntitySchemaOptions} from './util/definitions';
import {EntitySchemaInterface} from './util/definitions';
import {StructuralSchemaInterface} from './util/definitions';
import {IdAttribute} from './util/definitions';
import {Merge} from './util/definitions';
import {Structure} from './util/definitions';
import {Entities} from './util/definitions';

import {UndefinedIdError} from './util/Error';
import REMOVED_ENTITY from './util/RemovedEntity';
import ObjectSchema from './ObjectSchema';

export default class EntitySchema<A extends StructuralSchemaInterface<any>>
implements EntitySchemaInterface<A> {
type Options<T> = {
shape?: Structure<T>;
id?: (entity: T) => string;
merge?: (previous: T, next: T) => T;
};

export default class EntitySchema<T> {
name: string;
shape: A | null;
id: IdAttribute;
merge: Merge | null | undefined;
shape: Structure<T> | null;
id: (value: T) => string;
merge?: (previous: T, next: T) => T;

constructor(name: string, options: EntitySchemaOptions<any> = {}) {
constructor(name: string, options: Options<T> = {}) {
this.name = name;
this.merge = options.merge;

if (options.shape === null) {
this.shape = null;
this.id = options.id || (data => '' + data);
} else {
this.shape = options.shape || new ObjectSchema({});
this.id = options.id || ((data: any) => data?.id);
}
this.shape = options.shape ?? null;
this.id = options.id || ((data: any) => data?.id);
}

normalize(data: unknown, entities: Entities = {}): NormalizeState {
normalize(data: T, entities: Entities = {}): NormalizeState {
const {shape, name} = this;

let id = this.id(data);
let previousEntity;
let previousEntity: T | null = null;
let schemas: Record<string, any> = {};
let result;
let result: T;

if (id == null) {
throw UndefinedIdError(name, id);
}
if (id == null) throw UndefinedIdError(name, id);
id = id.toString();

entities[name] = entities[name] || {};

// only normalize if we have a defined shape
if (shape == null) {
result = data;
} else {
// only recurse if we have a defined shape
if (shape) {
let _ = shape.normalize(data, entities);
result = _.result;
schemas = _.schemas;
previousEntity = entities[name][id];
previousEntity = entities[name][id] as T;
} else {
result = data;
}

// list this schema as one that has been used
schemas[name] = this;

// Store the entity
entities[name][id] =
previousEntity && shape ? (this.merge || shape.merge)(previousEntity, result) : result;

Expand All @@ -69,14 +62,15 @@ export default class EntitySchema<A extends StructuralSchemaInterface<any>>
};
}

denormalize(denormalizeState: DenormalizeState, path: Array<any> = []): any {
denormalize(
denormalizeState: DenormalizeState,
path: Array<any> = []
): T | typeof REMOVED_ENTITY | null {
const {result, entities} = denormalizeState;
const {shape, name} = this;
const entity = entities?.[name]?.[result];
const entity = entities[name]?.[result] as T | typeof REMOVED_ENTITY;

if (entity == null || entity === REMOVED_ENTITY || shape == null) {
return entity;
}
if (entity == null || entity === REMOVED_ENTITY || shape === null) return entity ?? null;

return shape.denormalize({result: entity, entities}, path);
}
Expand Down
53 changes: 25 additions & 28 deletions packages/enty/src/ObjectSchema.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import {NormalizeState} from './util/definitions';
import {NormalizeState, Structure} from './util/definitions';
import {DenormalizeState} from './util/definitions';
import {StructuralSchemaInterface} from './util/definitions';
import {Create} from './util/definitions';
import {Merge} from './util/definitions';
import {Entities} from './util/definitions';
import {Schema} from './util/definitions';
import {StructuralSchemaOptions} from './util/definitions';

import {StructureOptions} from './util/definitions';
import REMOVED_ENTITY from './util/RemovedEntity';

export default class ObjectSchema<A extends Record<string, Schema>>
implements StructuralSchemaInterface<A> {
create: Create;
merge: Merge;
shape: A;
export default class ObjectSchema<T extends Record<string, unknown>> implements Structure<T> {
create: (value: T) => T;
merge: (previous: T, next: T) => T;
relations: Partial<Record<keyof T, Schema<any>>>;

constructor(shape: A, options: StructuralSchemaOptions = {}) {
this.shape = shape;
this.create = options.create || (item => ({...item}));
constructor(
relations: Partial<Record<keyof T, Schema<any>>>,
options: StructureOptions<T> = {}
) {
this.relations = relations;
this.create = options.create || ((item) => ({...item}));
this.merge = options.merge || ((previous, next) => ({...previous, ...next}));
}

/**
* ObjectSchema.normalize
*/
normalize(data: any, entities: Entities = {}): NormalizeState {
const {shape} = this;
normalize(data: T, entities: Entities = {}): NormalizeState {
const {relations} = this;
const dataMap = data;
let schemas = {};

const result = Object.keys(shape).reduce((result: Object, key: any): any => {
const result = Object.keys(relations).reduce((result: Object, key: any): any => {
const value = dataMap[key];
const schema = shape[key];
const schema = relations[key];
if (!schema) throw new Error(`${String(key)} was not defined in shape`);
if (value) {
const {result: childResult, schemas: childSchemas} = schema.normalize(
value,
Expand All @@ -42,17 +41,17 @@ export default class ObjectSchema<A extends Record<string, Schema>>
}

return result;
}, dataMap);
}, dataMap) as T;

return {entities, schemas, result: this.create(result)};
}

/**
* ObjectSchema.denormalize
*/
denormalize(denormalizeState: DenormalizeState, path: Array<any> = []): any {
denormalize(denormalizeState: DenormalizeState, path: Array<any> = []): T | null {
const {result, entities} = denormalizeState;
const {shape} = this;
const {relations} = this;

if (result == null || result === REMOVED_ENTITY) {
return result;
Expand All @@ -62,19 +61,17 @@ export default class ObjectSchema<A extends Record<string, Schema>>
// if they have a corresponding schema. Otherwise return the plain value.
// Then filter out deleted keys, keeping track of ones deleted
// Then Pump the filtered object through `denormalizeFilter`
let item = {...result};
let item: T = {...result};
if (path.indexOf(this) !== -1) {
return item;
}

const keys = Object.keys(shape);

for (let key of keys) {
const schema = shape[key];
for (let key in relations) {
const schema = relations[key];
const result = item[key];
const value = schema.denormalize({result, entities}, [...path, this]);
const value = schema?.denormalize({result, entities}, [...path, this]);

if (value !== REMOVED_ENTITY && value !== undefined) {
if (value !== REMOVED_ENTITY && value != undefined) {
item[key] = value;
} else {
delete item[key];
Expand Down
19 changes: 0 additions & 19 deletions packages/enty/src/__tests__/ArraySchema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,6 @@ test('ArraySchema will not mutate input objects', () => {
expect(arrayTest).toEqual([{id: '1'}]);
});

test('ArraySchemas can construct custom objects', () => {
class Foo {
data: string[];
constructor(data: string[]) {
this.data = data;
}
map(fn: (x: string) => string) {
this.data = this.data.map(fn);
return this;
}
}
const schema = new ArraySchema(new ObjectSchema({}), {
create: (data) => new Foo(data)
});
const state = schema.normalize([{foo: 1}, {bar: 2}], {});
expect(state.result).toBeInstanceOf(Foo);
expect(state.result.data[0]).toEqual({foo: 1});
});

it('will default replace array on merge', () => {
const foo = new EntitySchema('foo');
const schema = new ArraySchema(foo);
Expand Down

0 comments on commit 9d84892

Please sign in to comment.