Skip to content

Commit

Permalink
feat!: replace constructor for instance creation (#129)
Browse files Browse the repository at this point in the history
* feat: introduce `newInstance(MyEntityType, properties)` for safe property initialisation
* feat: introduce `MyEntityType.create(properties)` for safe property initialisation
* BREAKING CHANGE: use of the JsonapiEntity subtype constructor for property initialisation is deprecated. It does not work everywhere.
  • Loading branch information
junglebarry authored Jun 21, 2022
1 parent 406fbbc commit 3fce25b
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 34 deletions.
54 changes: 54 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,60 @@ Second, if Yayson cannot resolve a relationship to an entity, the information of

# Migrations and breaking changes

## From 2.x to 3.x

The 2.x release line includes a breaking change to allow properties to be partially-specified via the constructor parameter. Unfortunately, this did not work in all cases because subclass properties are assigned after supertype constructors are executed, meaning the change did not work properly in all cases.

We have therefore deprecated the use of constructor parameters with v3, meaning this v2 code:

```typescript
const david = new Author({
id: "david",
name: "David Brooks",
lastLoginDateTime: "2021-07-24T11:00:00.000Z",
});
```

must be rewritten as:

```typescript
const david = Author.create({
id: "david",
name: "David Brooks",
lastLoginDateTime: "2021-07-24T11:00:00.000Z",
});
```

or:

```typescript
const david = newInstance(Author, {
id: "david",
name: "David Brooks",
lastLoginDateTime: "2021-07-24T11:00:00.000Z",
});
```

or the original format:

```typescript
const david = new Author();
david.id = "david";
david.name = "David Brooks";
david.lastLoginDateTime = "2021-07-24T11:00:00.000Z";
```

The v2 format may still work for you, but you are recommended to avoid it, and consider it deprecated:

```typescript
// DON'T DO THIS, IT'S DEPRECATED AND THERE IS NO GUARANTEE IT WILL WORK
const david = new Author({
id: "david",
name: "David Brooks",
lastLoginDateTime: "2021-07-24T11:00:00.000Z",
});
```

## From 1.x to 2.x

The 2.x release line includes a breaking change to allow properties to be partially-specified via the constructor parameter.
Expand Down
66 changes: 63 additions & 3 deletions spec/jsonapi/jsonapi-entity.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, it } from "@jest/globals";
import { attribute, JsonapiEntity, meta, relationship } from "../../src";
import {
attribute,
JsonapiEntity,
meta,
newEntity,
relationship,
} from "../../src";

describe("JsonapiEntity", () => {
class FakeSimpleJsonapiEntity extends JsonapiEntity<FakeSimpleJsonapiEntity> {
Expand All @@ -20,8 +26,8 @@ describe("JsonapiEntity", () => {
someOtherSimple: FakeSimpleJsonapiEntity;
}

describe("construction should permit property specification for all properties", () => {
it("should return true for a valid UnresolvedResourceIdentifier", () => {
describe("construction", () => {
it("should permit property specification for all properties BUT BEWARE THIS DOES NOT WORK EVERYWHERE", () => {
const fake = new FakeComplexJsonapiEntity({
id: "complex1",
someNonJsonapiProperty: "someNonJsonapiProperty!",
Expand All @@ -46,4 +52,58 @@ describe("JsonapiEntity", () => {
);
});
});

describe("newEntity", () => {
it("should permit property specification for all properties", () => {
const fake = newEntity(FakeComplexJsonapiEntity, {
id: "complex1",
someNonJsonapiProperty: "someNonJsonapiProperty!",
someAttr: "someAttr!",
someOtherAttr: "someOtherAttr!",
someMeta: "someMeta!",
someOtherMeta: "someOtherMeta!",
someSimple: new FakeSimpleJsonapiEntity({ id: "simple1" }),
someOtherSimple: new FakeSimpleJsonapiEntity({ id: "simple2" }),
});
expect(fake.id).toEqual("complex1");
expect(fake.someNonJsonapiProperty).toEqual("someNonJsonapiProperty!");
expect(fake.someAttr).toEqual("someAttr!");
expect(fake.someOtherAttr).toEqual("someOtherAttr!");
expect(fake.someMeta).toEqual("someMeta!");
expect(fake.someOtherMeta).toEqual("someOtherMeta!");
expect(fake.someSimple).toEqual(
newEntity(FakeSimpleJsonapiEntity, { id: "simple1" })
);
expect(fake.someOtherSimple).toEqual(
newEntity(FakeSimpleJsonapiEntity, { id: "simple2" })
);
});
});

describe("create", () => {
it("should permit property specification for all properties", () => {
const fake = FakeComplexJsonapiEntity.create({
id: "complex1",
someNonJsonapiProperty: "someNonJsonapiProperty!",
someAttr: "someAttr!",
someOtherAttr: "someOtherAttr!",
someMeta: "someMeta!",
someOtherMeta: "someOtherMeta!",
someSimple: FakeSimpleJsonapiEntity.create({ id: "simple1" }),
someOtherSimple: FakeSimpleJsonapiEntity.create({ id: "simple2" }),
});
expect(fake.id).toEqual("complex1");
expect(fake.someNonJsonapiProperty).toEqual("someNonJsonapiProperty!");
expect(fake.someAttr).toEqual("someAttr!");
expect(fake.someOtherAttr).toEqual("someOtherAttr!");
expect(fake.someMeta).toEqual("someMeta!");
expect(fake.someOtherMeta).toEqual("someOtherMeta!");
expect(fake.someSimple).toEqual(
newEntity(FakeSimpleJsonapiEntity, { id: "simple1" })
);
expect(fake.someOtherSimple).toEqual(
newEntity(FakeSimpleJsonapiEntity, { id: "simple2" })
);
});
});
});
31 changes: 3 additions & 28 deletions src/decorators/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,34 +81,9 @@ export function entity(
const { type } = options;

return (original: ResourceIdentifierConstructor) => {
// a utility function to generate instances of a class
const construct = (
constructorFunc: ResourceIdentifierConstructor,
args
) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomJsonapiEntity: any = function () {
return new constructorFunc(...args);
};
CustomJsonapiEntity.prototype = constructorFunc.prototype;

// construct an instance and bind "type" correctly
const instance = new CustomJsonapiEntity();
instance.type = type;
return instance;
};

// the new constructor behaviour
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const wrappedConstructor: any = (...args) => construct(original, args);

// copy prototype so intanceof operator still works
wrappedConstructor.prototype = original.prototype;

original.prototype.type = type;
// add the type to the reverse lookup for deserialisation
registerEntityConstructorForType(wrappedConstructor, type);

// return new constructor (will override original)
return wrappedConstructor;
registerEntityConstructorForType(original, type);
return original;
};
}
22 changes: 19 additions & 3 deletions src/jsonapi/jsonapi-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,24 @@ export class JsonapiEntity<E extends JsonapiEntity<E>>
readonly type: string;

constructor(properties: Partial<E> = {}) {
Object.keys(properties).forEach((property) => {
this[property] = properties[property];
});
Object.assign(this, properties);
}

static create<E extends JsonapiEntity<E>>(
this: JsonapiEntityConstructor<E>,
properties: Partial<E> = {}
): E {
return newEntity(this, properties);
}
}

export interface JsonapiEntityConstructor<E extends JsonapiEntity<E>> {
new (properties: Partial<E>): E;
}

export function newEntity<E extends JsonapiEntity<E>>(
entity: JsonapiEntityConstructor<E>,
properties: Partial<E> = {}
): E {
return Object.assign(new entity(properties), properties);
}

0 comments on commit 3fce25b

Please sign in to comment.