Skip to content

Commit

Permalink
feat!: deserializers (#3)
Browse files Browse the repository at this point in the history
* fix: using resource linkage correctly

* test: removed test for dumped file and started serialize resource object fixes

* fix: link type, relationship type

* chore: removed deserialize testing samplr

* fix: cleaned up top

* fix: serialize resource linkage and serialize resource object tests

* test: serialize resource linkage

* refactor: light rework

* feat: deserializers

* fix: readme links

* refactor: `serializeResourceLinkage`

* test: start of deserializer tests

* test: full coverage on deserializer
  • Loading branch information
ryanhaticus authored Nov 24, 2024
1 parent a220bc0 commit 62dece6
Show file tree
Hide file tree
Showing 25 changed files with 658 additions and 365 deletions.
89 changes: 75 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ npm install @tsmetadata/json-api
- [Resource Object](#resource-object)
- [Relationship Object](#relationship-object)
- [Included Resource Objects](#included-resource-objects)
- [📄 Deserializers](#deserializers)
- [Resource Object](#resource-object-1)
- [✨ Types](#types)
- [Attributes Object](#attributes-object)
- [Error Object](#error-object)
Expand All @@ -39,7 +41,8 @@ npm install @tsmetadata/json-api
- [Relationship Object](#relationship-object)
- [Relationships Object](#relationships-object)
- [Resource Identifier Object](#resource-identifier-object)
- [Resource Object](#resource-object-1)
- [Resource Linkage](#resource-linkage)
- [Resource Object](#resource-object-2)
- [Top Level Object](#top-level-object)

## ⚙️ Usage
Expand Down Expand Up @@ -99,20 +102,20 @@ The foreign key is type-safe to the field type.

ex.
```typescript
import { Relationship } from '@tsmetadata/json-api';
import { Relationship, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

class Account {
@Relationship('accounts')
primaryDebtor: Customer;
primaryDebtor: Customer | JSONAPIResourceLinkage;

@Relatioship('accounts')
coDebtors: Customer[];
@Relationship('accounts')
coDebtors: Customer[] | JSONAPIResourceLinkage;
}

class Customer {
@Relationship('primaryDebtor')
@Relationship('coDebtors')
accounts: Account[];
accounts: Account[] | JSONAPIResourceLinkage;
}
```

Expand Down Expand Up @@ -237,7 +240,7 @@ The `serializeResourceObject(classInstance: object, keys: string[])` function wi

ex.
```typescript
import { Resource, Id, Link, serializeIncludedResourceObjects } from '@tsmetadata/json-api';
import { Resource, Id, Link, serializeIncludedResourceObjects, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

// For the sake of brevity, the `Account` class definition is not included.

Expand All @@ -248,10 +251,10 @@ class User {

@Relationship('primaryDebtor')
@Relationship('coDebtors')
accounts: Account[];
accounts: Account[] | JSONAPIResourceLinkage;

@Relationship('spouse')
spouse: User;
spouse: User | JSONAPIResourceLinkage;
}

const user1 = new User();
Expand All @@ -265,6 +268,48 @@ user2.accounts = [someAccount, someOtherAccount];
serializeIncludedResourceObjects(user1, ['accounts', 'spouse']);
```

### Deserializers

### Resource Object
The `serializeResourceObject(classInstance: object)` function will produce a [resource object](https://jsonapi.org/format/#document-resource-objects) from a decorated class instance.

ex.
```typescript
import { Resource, Id, Attribute, serializeResourceObject, deserializeResourceObject } from '@tsmetadata/json-api';

@Resource('users')
class User {
@Id()
customerId: string;

@Attribute()
active: boolean;
}

const user = new User();
user.customerId = '123';
user.active = false;

const serializedUser = serializeResourceObject(user);

/*
{
"type": "users".
"id": "123",
"attributes": {
"active": false
}
}
*/

const deserializedUser = deserializeResourceObject(user, User);

/*
user.customerId === '123'
user.active === false
*/
```

### Types

#### Attributes Object
Expand Down Expand Up @@ -367,6 +412,16 @@ ex.
import type { JSONAPIResourceIdentifierObject } from '@tsmetadata/json-api';
```

#### Resource Linkage

- [Specification](https://jsonapi.org/format/#document-resource-object-linkage)
- [Definition](https://github.com/tsmetadata/json-api/blob/main/src/types/resourceLinkage.ts)

ex.
```typescript
import type { JSONAPIResourceLinkage } from '@tsmetadata/json-api';
```

#### Resource Object

- [Specification](https://jsonapi.org/format/#document-resource-objects)
Expand All @@ -389,7 +444,8 @@ import type { JSONAPITopLevelObject } from '@tsmetadata/json-api';

## 😍 Full Example
```typescript
import { Attribute, Link, Meta, Relationship, Resource, serializeIncludedResourceObjects, serializeResourceObject } from '@tsmetadata/json-api';
import { Attribute, Link, Meta, Relationship, Resource, serializeIncludedResourceObjects,
serializeResourceObject, deserializeResourceObject, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

@Resource('accounts')
export class Account {
Expand All @@ -400,10 +456,10 @@ export class Account {
pastDue: boolean;

@Relationship('accounts')
primaryDebtor: Customer;
primaryDebtor: Customer | JSONAPIResourceLinkage;

@Relationship('accounts')
coDebtors: Customer[];
coDebtors: Customer[] | JSONAPIResourceLinkage;

@Link()
self: string;
Expand All @@ -422,7 +478,7 @@ export class Customer {

@Relationship('primaryDebtor')
@Relationship('coDebtors')
accounts: Account[];
accounts: Account[] | JSONAPIResourceLinkage;

@Link()
self: string;
Expand All @@ -443,12 +499,17 @@ customer.self = 'some-url';
account.primaryDebtor = customer;
customer.accounts = [account];

const serializedCustomer = serializeResourceObject(customer);

// Try logging out the results on your own!
console.log(
serializeResourceObject(customer),
serializedCustomer,
serializeRelationshipObject(customer),
serializeIncludedResourceObjects(customer, ['accounts'])
);

// You can deserialize too!
const customerWithResourceLinkages = deserializeResourceObject(serializedCustomer, Customer);
```

## ❓ FAQ
Expand Down
File renamed without changes.
123 changes: 123 additions & 0 deletions __tests__/serializers/deserializeResourceObject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Chance } from 'chance';
import { attributesSymbol } from '../../src/decorators/attribute';
import { idSymbol } from '../../src/decorators/id';
import { linksSymbol } from '../../src/decorators/link';
import { metaSymbol } from '../../src/decorators/meta';
import { relationshipsSymbol } from '../../src/decorators/relationship';
import { resourceSymbol } from '../../src/decorators/resource';
import { deserializeResourceObject } from '../../src/serializers/deserializeResourceObject';
import { getMetadataBySymbol } from '../../src/serializers/utils/getMetadataBySymbol';

import type { JSONAPIResourceLinkage } from '../../src/types/resourceLinkage';

jest.mock('../../src/serializers/utils/getMetadataBySymbol');
const getMetadataBySymbolMocked = jest.mocked(getMetadataBySymbol);

describe('`deserializeResourceObject`', () => {
let chance: Chance.Chance;

beforeEach(() => {
chance = new Chance();
});

describe('`type`', () => {
it('should throw an error if the type of the resource object does not match the expected type', () => {
const expectedType = chance.string();

getMetadataBySymbolMocked.mockImplementation((_, symbol) => {
if (symbol === resourceSymbol) {
return expectedType;
}
});

const resourceObject = {
type: chance.string(),
id: chance.string(),
};

class SomeResource {}

expect(() =>
deserializeResourceObject(resourceObject, SomeResource),
).toThrow(
`Failed to deserialize resource object because the type ${resourceObject.type} does not match the expected type ${expectedType}.`,
);
});
});

it('should deserialize a resource object into a class instance', () => {
class SomeResource {
someIdField!: string;
someAttributeField!: string;
someLinkField!: string;
someMetaField!: string;
someRelationshipField!: JSONAPIResourceLinkage;
}

const classInstance = new SomeResource();
classInstance.someIdField = chance.string();
classInstance.someAttributeField = chance.string();
classInstance.someLinkField = chance.string();
classInstance.someMetaField = chance.string();

const type = chance.string();

getMetadataBySymbolMocked.mockImplementation((_, symbol) => {
if (symbol === resourceSymbol) {
return type;
}

if (symbol === idSymbol) {
return 'someIdField';
}

if (symbol === attributesSymbol) {
return ['someAttributeField'];
}

if (symbol === linksSymbol) {
return ['someLinkField'];
}

if (symbol === metaSymbol) {
return ['someMetaField'];
}

if (symbol === relationshipsSymbol) {
return [['someRelationshipField']];
}
});

const resourceObject = {
type,
id: classInstance.someIdField,
attributes: {
someAttributeField: classInstance.someAttributeField,
},
links: {
someLinkField: classInstance.someLinkField,
},
meta: {
someMetaField: classInstance.someMetaField,
},
relationships: {
someRelationshipField: {
data: {
type: chance.string(),
id: chance.string(),
},
},
},
};

const result = deserializeResourceObject(resourceObject, SomeResource);

expect(result.someIdField).toBe(classInstance.someIdField);
expect(result.someAttributeField).toBe(classInstance.someAttributeField);
expect(result.someLinkField).toBe(classInstance.someLinkField);
expect(result.someMetaField).toBe(classInstance.someMetaField);
expect(result.someRelationshipField).toBe(
resourceObject.relationships.someRelationshipField.data,
);
});
});
Loading

0 comments on commit 62dece6

Please sign in to comment.