🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
Book a DemoInstallSign in
Socket

json-api-models

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

json-api-models - npm Package Compare versions

Comparing version

to
0.2.0-beta.1

1

dist/index.d.ts
export * from './types';
export * from './store';
export * from './model';
export * from './query';

2

dist/index.es.js

@@ -1,1 +0,1 @@

class t{constructor(t,e){this.store=e,this.attributes={},this.relationships={},this.meta={},this.links={},this.casts={},this.merge(t)}getAttribute(t){const e=this.attributes[t],i=this.casts[t];return i&&null!=e?i!==String&&i!==Number&&i!==Boolean&&((s=i).prototype&&s.prototype.constructor.name)?new i(e):i(e):e;var s}getRelationship(t){const e=this.relationships[t].data;return Array.isArray(e),this.store.find(e)}identifier(){return{id:this.id,type:this.type}}merge(t){"type"in t&&(this.type=t.type),"id"in t&&(this.id=t.id),"attributes"in t&&(Object.assign(this.attributes,t.attributes),Object.keys(t.attributes).forEach((t=>{Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this),t)||Object.getOwnPropertyDescriptor(this,t)||(this[t]=null,Object.defineProperty(this,t,{get:()=>this.getAttribute(t),configurable:!0,enumerable:!0}))}))),"relationships"in t&&Object.entries(t.relationships).forEach((([t,e])=>{this.relationships[t]=this.relationships[t]||{},Object.assign(this.relationships[t],e),Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this),t)||Object.getOwnPropertyDescriptor(this,t)||(this[t]=null,Object.defineProperty(this,t,{get:()=>this.getRelationship(t),configurable:!0,enumerable:!0}))})),"links"in t&&(this.links=t.links),"meta"in t&&(this.meta=t.meta)}}class e{constructor(t={}){this.models=t,this.graph={}}find(t,e){return null===t?null:Array.isArray(t)?t.map((t=>this.find(t))):"object"==typeof t?this.find(t.type,t.id):this.graph[t]&&this.graph[t][e]||null}findAll(t){return this.graph[t]?Object.keys(this.graph[t]).map((e=>this.graph[t][e])):[]}sync(t){const e=this.syncResource.bind(this);return"included"in t&&t.included.map(e),Array.isArray(t.data)?t.data.map(e):e(t.data)}syncResource(t){const{type:e,id:i}=t;return this.graph[e]=this.graph[e]||{},this.graph[e][i]?this.graph[e][i].merge(t):this.graph[e][i]=this.createModel(t),this.graph[e][i]}createModel(e){return new(this.models[e.type]||this.models["*"]||t)(e,this)}forget(t){delete this.graph[t.type][t.id]}reset(){this.graph={}}}function i(t){return encodeURIComponent(t).replace(/[!'()*]/g,(function(t){return"%"+t.charCodeAt(0).toString(16).toUpperCase()}))}class s{constructor(t={}){this.query=Object.assign({},t)}append(t,e){return"object"==typeof t?Object.entries(t).map((t=>this.append.apply(this,t))):this.query[t]=(this.query[t]?this.query[t]+",":"")+e,this}set(t,e){return"object"==typeof t?Object.entries(t).map((t=>this.set.apply(this,t))):this.query[t]=e,this}delete(t){return Array.isArray(t)?t.forEach((t=>this.delete(t))):delete this.query[t],this}toString(){return Object.entries(this.query).sort(((t,e)=>t[0].localeCompare(e[0]))).map((([t,e])=>i(t)+"="+i(e))).join("&")}}export{t as Model,s as Query,e as Store};
const e=class{constructor(e){Object.defineProperty(this,"type",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"id",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"attributes",{enumerable:!0,configurable:!0,writable:!0,value:{}}),Object.defineProperty(this,"relationships",{enumerable:!0,configurable:!0,writable:!0,value:{}}),Object.defineProperty(this,"meta",{enumerable:!0,configurable:!0,writable:!0,value:{}}),Object.defineProperty(this,"links",{enumerable:!0,configurable:!0,writable:!0,value:{}}),this.type=e.type,this.id=e.id,this.merge(e)}identifier(){return{id:this.id,type:this.type}}merge(e){this.links=e.links??this.links,this.meta=e.meta??this.meta,e.attributes&&Object.assign(this.attributes,e.attributes),e.relationships&&Object.entries(e.relationships).forEach((([e,t])=>{this.relationships[e]=this.relationships[e]||{},Object.assign(this.relationships[e],t)}))}};class t{constructor(e={}){Object.defineProperty(this,"models",{enumerable:!0,configurable:!0,writable:!0,value:e}),Object.defineProperty(this,"graph",{enumerable:!0,configurable:!0,writable:!0,value:{}})}find(e,t){return null===e?null:Array.isArray(e)?e.map((e=>this.find(e))):"object"==typeof e?this.find(e.type,e.id):t&&this.graph[e]?.[t]||null}findAll(e){return this.graph[e]?Object.keys(this.graph[e]).map((t=>this.graph[e][t])):[]}sync(e){return e.included?.map((e=>this.syncResource(e))),Array.isArray(e.data)?e.data.map((e=>this.syncResource(e))):this.syncResource(e.data)}syncResource(e){const{type:t,id:i}=e;return this.graph[t]=this.graph[t]||{},this.graph[t][i]?this.graph[t][i].merge(e):this.graph[t][i]=this.createModel(e),this.graph[t][i]}createModel(t){const i=this.models[t.type]||e;return new Proxy(new i(t),{get:(e,t,i)=>{if("string"==typeof t){if(e.attributes?.[t])return e.attributes[t];const i=e.relationships?.[t]?.data;if(i)return Array.isArray(i),this.find(i)}return Reflect.get(e,t,i)}})}forget(e){delete this.graph[e.type][e.id]}reset(){this.graph={}}}export{e as Model,t as Store};

@@ -1,27 +0,37 @@

import { Store } from './store';
import { JsonApiIdentifier, JsonApiRelationships, JsonApiResource, KeyValueObject } from './types';
export declare type CastAttributes = {
[key: string]: StringConstructor | NumberConstructor | BooleanConstructor | ((value: any) => any) | (new (value: any) => any);
import { JsonApiResource, ModelForType, SchemaCollection } from './types';
type PartialJsonApiResource<T extends JsonApiResource> = {
[P in keyof T]?: Partial<T[P]>;
};
export declare class Model<Type extends string = any> implements JsonApiResource<Type> {
protected store: Store;
type: Type;
id: string;
attributes: KeyValueObject;
relationships: JsonApiRelationships;
meta: KeyValueObject;
links: KeyValueObject;
protected casts: CastAttributes;
declare class ModelBase<Schema extends JsonApiResource = JsonApiResource> {
type: Schema['type'];
id: Schema['id'];
attributes: NonNullable<Schema['attributes']>;
relationships: NonNullable<Schema['relationships']>;
meta: Schema['meta'];
links: Schema['links'];
[field: string]: any;
constructor(data: JsonApiResource<Type>, store: Store);
getAttribute(name: string): any;
getRelationship(name: string): any;
constructor(data: Schema);
/**
* Make a resource identifier object for this model.
*/
identifier(): JsonApiIdentifier<Type>;
identifier(): {
id: Schema["id"];
type: Schema["type"];
};
/**
* Merge new JSON:API resource data into the model.
*/
merge(data: Partial<JsonApiResource<Type>>): void;
merge(data: PartialJsonApiResource<Schema>): void;
}
type ProxiedModel<Schema extends JsonApiResource, Schemas extends Record<string, JsonApiResource>> = Schema & Schema['attributes'] & {
[Property in keyof NonNullable<Schema['relationships']>]: NonNullable<NonNullable<Schema['relationships']>[Property]> extends {
data?: infer Data;
} ? Data extends {
type: infer RelatedType extends string;
} ? ModelForType<RelatedType, Schemas> | undefined : Data extends {
type: infer RelatedType extends string;
}[] ? ModelForType<RelatedType, Schemas>[] : null : never;
};
export type Model<Schema extends JsonApiResource = JsonApiResource, Schemas extends SchemaCollection = SchemaCollection> = JsonApiResource<Schema['type']> & ModelBase<Schema> & ProxiedModel<Schema, Schemas>;
export declare const Model: new <Schema extends JsonApiResource = JsonApiResource, Schemas extends SchemaCollection = SchemaCollection>(data: JsonApiResource<Schema['type']>) => Model<Schema, Schemas>;
export {};

@@ -1,31 +0,19 @@

import { Model } from './model';
import { JsonApiDocument, JsonApiIdentifier, JsonApiResource } from './types';
declare type Graph = {
[type: string]: {
[id: string]: any;
import { JsonApiDocument, JsonApiIdentifier, JsonApiResource, ModelForType, ModelMap, SchemaCollection } from './types';
export declare class Store<Schemas extends SchemaCollection = {}> {
models: ModelMap<Schemas>;
protected graph: {
[type: string]: {
[id: string]: any;
};
};
};
export declare type ModelConstructor<Type extends string> = {
new (data: JsonApiResource<Type>, store: Store): Model<Type>;
};
export declare type ModelCollection<Models> = {
[Type in keyof Models & string]: ModelConstructor<Type>;
} & {
'*'?: ModelConstructor<any>;
};
export declare type ModelForType<Type extends string, Models extends ModelCollection<Models>> = Type extends keyof Models ? InstanceType<Models[Type]> : Model;
export declare class Store<Models extends ModelCollection<Models> = any> {
models: Models;
protected graph: Graph;
constructor(models?: Models);
find<Type extends string>(identifier: JsonApiIdentifier<Type>): ModelForType<Type, Models> | null;
find<Type extends string>(identifiers: JsonApiIdentifier<Type>[]): ModelForType<Type, Models>[];
find<Type extends string>(type: Type, id: string): ModelForType<Type, Models> | null;
findAll<Type extends string>(type: Type): ModelForType<Type, Models>[];
sync<Type extends string>(document: JsonApiDocument<Type>): ModelForType<Type, Models> | ModelForType<Type, Models>[] | null;
syncResource<Type extends string>(data: JsonApiResource<Type>): ModelForType<Type, Models>;
protected createModel<Type extends string>(data: JsonApiResource<Type>): ModelForType<Type, Models>;
constructor(models?: ModelMap<Schemas>);
find<Type extends (keyof Schemas & string) | (string & {})>(identifier: JsonApiIdentifier<Type> | null): ModelForType<Type, Schemas> | null;
find<Type extends (keyof Schemas & string) | (string & {})>(identifiers: JsonApiIdentifier<Type>[]): ModelForType<Type, Schemas>[];
find<Type extends (keyof Schemas & string) | (string & {})>(type: Type, id: string): ModelForType<Type, Schemas> | null;
findAll<Type extends (keyof Schemas & string) | (string & {})>(type: Type): ModelForType<Type, Schemas>[];
sync<Type extends (keyof Schemas & string) | (string & {})>(document: JsonApiDocument<Type>): ModelForType<Type, Schemas> | ModelForType<Type, Schemas>[] | null;
syncResource<Type extends (keyof Schemas & string) | (string & {})>(data: JsonApiResource<Type>): ModelForType<Type, Schemas>;
createModel<Type extends (keyof Schemas & string) | (string & {})>(data: JsonApiResource<Type>): ModelForType<Type, Schemas>;
forget(data: JsonApiIdentifier): void;
reset(): void;
}
export {};

@@ -1,9 +0,7 @@

export declare type KeyValueObject = {
[key: string]: any;
};
import { Model } from './model.ts';
export interface JsonApiDocument<Type extends string = string> {
data: JsonApiResource<Type> | JsonApiResource<Type>[];
included?: JsonApiResource[];
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}

@@ -15,14 +13,12 @@ export interface JsonApiIdentifier<Type extends string = string> {

export interface JsonApiResource<Type extends string = string> extends JsonApiIdentifier<Type> {
attributes?: KeyValueObject;
attributes?: Record<string, any>;
relationships?: JsonApiRelationships;
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}
export interface JsonApiRelationships {
[relationName: string]: JsonApiRelationship;
}
export type JsonApiRelationships = Record<string, JsonApiRelationship>;
export interface JsonApiRelationship<Type extends string = string> {
data?: JsonApiIdentifier<Type> | JsonApiIdentifier<Type>[] | null;
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}

@@ -35,1 +31,8 @@ export interface JsonApiRelationshipToOne<Type extends string = string> extends JsonApiRelationship<Type> {

}
export type SchemaCollection = {
[Type in string]: JsonApiResource<Type>;
};
export type ModelMap<Schemas extends SchemaCollection = SchemaCollection> = {
[Type in keyof Schemas & string]?: new (data: JsonApiResource<Type>) => Schemas[Type];
};
export type ModelForType<Type extends string, Schemas> = Type extends keyof Schemas ? Schemas[Type] : Model<JsonApiResource<Type>>;
{
"name": "json-api-models",
"description": "A lightweight layer for working with JSON:API data.",
"version": "0.1.0-beta.8",
"author": "Toby Zerner",
"license": "MIT",
"main": "./dist/index.cjs.js",
"module": "./dist/index.es.js",
"unpkg": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"src",
"dist",
"README.md",
"!**/.DS_Store"
],
"scripts": {
"test": "jest",
"build": "rollup -c",
"build:watch": "rollup -cw",
"release": "release-it --npm.tag=latest"
},
"devDependencies": {
"@release-it/keep-a-changelog": "^2.3.0",
"@rollup/plugin-typescript": "^8.2.5",
"@types/jest": "^26.0.20",
"jest": "^26.6.3",
"release-it": "^14.11.5",
"rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^26.4.4",
"tslib": "^2.3.1",
"typescript": "^4.6.3"
},
"release-it": {
"github": {
"release": true
"name": "json-api-models",
"description": "A lightweight layer for working with JSON:API data.",
"version": "0.2.0-beta.1",
"author": "Toby Zerner",
"license": "MIT",
"module": "./dist/index.es.js",
"types": "./dist/index.d.ts",
"files": [
"src",
"dist",
"README.md",
"!**/.DS_Store"
],
"scripts": {
"test": "jest",
"build": "rollup -c && tsc --emitDeclarationOnly --declaration",
"release": "release-it --npm.tag=latest"
},
"plugins": {
"@release-it/keep-a-changelog": {
"filename": "CHANGELOG.md",
"addUnreleased": true,
"addVersionUrl": true
}
"devDependencies": {
"@release-it/keep-a-changelog": "^5.0.0",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^26.0.20",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
"rollup": "^4.17.2",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"typescript": "^5.4"
},
"hooks": {
"after:bump": "npm run build"
"release-it": {
"github": {
"release": true
},
"plugins": {
"@release-it/keep-a-changelog": {
"filename": "CHANGELOG.md",
"addUnreleased": true,
"addVersionUrl": true
}
},
"hooks": {
"after:bump": "npm run build"
}
},
"prettier": {
"singleQuote": true
}
}
}

@@ -21,20 +21,22 @@ # json-api-models

data: {
type: 'humans',
type: 'users',
id: '1',
attributes: { name: 'Toby' },
relationships: {
dog: { data: { type: 'dogs', id: '1' }}
}
pet: { data: { type: 'dogs', id: '1' } },
},
},
included: [{
type: 'dogs',
id: '1',
attributes: { name: 'Rosie' }
}]
included: [
{
type: 'dogs',
id: '1',
attributes: { name: 'Rosie' },
},
],
});
// Resource data is transformed into easy-to-consume models
const human = models.find('humans', '1');
human.name // Toby
human.dog // { type: 'dogs', id: '1', name: 'Rosie' }
const user = models.find('users', '1');
user.name; // Toby
user.pet; // { type: 'dogs', id: '1', name: 'Rosie' }
```

@@ -44,3 +46,4 @@

Use the `sync` method to load your JSON:API response document into the store. Both the primary `data` and any `included` resources will be synced. The return value will be a model, or an array of models, corresponding to the primary data.
Use the `sync` method to load your JSON:API response document into the store. Both the primary `data` and any `included`
resources will be synced. The return value will be a model, or an array of models, corresponding to the primary data.

@@ -51,3 +54,4 @@ ```ts

If any of the synced resources already exist within the store, the new data will be **merged** into the old model. The model instance will not change so references to it throughout your application will remain intact.
If any of the synced resources already exist within the store, the new data will be **merged** into the old model. The
model instance will not change so references to it throughout your application will remain intact.

@@ -60,3 +64,3 @@ You can also sync an individual resource using the `syncResource` method:

id: '1',
attributes: { name: 'Toby' }
attributes: { name: 'Toby' },
});

@@ -67,10 +71,11 @@ ```

Specific models can be retrieved from the store using the `find` method. Pass it a type and an ID, a resource identifier object, or an array of resource identifier objects:
Specific models can be retrieved from the store using the `find` method. Pass it a type and an ID, a resource identifier
object, or an array of resource identifier objects:
```ts
const model = models.find('users', '1');
const model = models.find({ type: 'users', id: '1' });
const models = models.find([
const user = models.find('users', '1');
const user = models.find({ type: 'users', id: '1' });
const users = models.find([
{ type: 'users', id: '1' },
{ type: 'users', id: '2' }
{ type: 'users', id: '2' },
]);

@@ -82,3 +87,3 @@ ```

```ts
const models = models.findAll('users');
const users = models.findAll('users');
```

@@ -88,15 +93,18 @@

Models are a *superset* of JSON:API resource objects, meaning they contain all of the members you would expect (`type`, `id`, `attributes`, `relationships`, `meta`, `links`) plus some additional functionality.
Models are a _superset_ of JSON:API resource objects, meaning they contain all of the members you would
expect (`type`, `id`, `attributes`, `relationships`, `meta`, `links`) plus some additional functionality.
Getters are automatically defined for all fields, allowing you to easily access their contents. Relationship fields are automatically resolved to their related models (if present within the store):
Getters are automatically defined for all fields, allowing you to easily access their contents. Relationship fields are
automatically resolved to their related models (if present within the store):
```ts
model.name // => model.attributes.name
model.dog // => models.find(model.relationships.dog.data)
model.name; // => model.attributes.name
model.pet; // => models.find(model.relationships.pet.data)
```
To easily retrieve a resource identifier object for the model, the `identifier` method is available. This is useful when constructing relationships in JSON:API request documents.
To easily retrieve a resource identifier object for the model, the `identifier` method is available. This is useful when
constructing relationships in JSON:API request documents.
```ts
model.identifier() // { type: 'users', id: '1' }
model.identifier(); // { type: 'users', id: '1' }
```

@@ -106,3 +114,4 @@

Remove a model from the store using the `forget` method, which accepts a resource identifier object. This means you can pass a model directly into it:
Remove a model from the store using the `forget` method, which accepts a resource identifier object. This means you can
pass a model directly into it:

@@ -115,3 +124,4 @@ ```ts

You can define custom model classes to add your own functionality. Custom models must extend the `Model` base class. This is useful if you wish to add any custom getters or methods to models for a specific resource type, and also to define types for each resource field:
You can define custom model classes to add your own functionality. Custom models must extend the `Model` base class.
This is useful if you wish to add any custom getters or methods to models for a specific resource type:

@@ -121,6 +131,3 @@ ```ts

class User extends Model<'users'> {
public declare name: string;
public declare age: number;
class User extends Model {
get firstName() {

@@ -140,20 +147,41 @@ return this.name.split(' ')[0];

#### Attribute Casts
### TypeScript
You can define typecasts for attributes on your custom models:
For TypeScript autocompletion of model attributes and relationships, provide the raw JSON:API resource schema when defining your models.
```ts
class User extends Model<'users'> {
declare public name: string;
declare public createdAt: Date;
protected casts = {
createdAt: Date,
type UsersSchema = {
type: 'users';
id: string;
attributes: {
name: string;
};
}
relationships: {
dog: { data?: { type: 'dogs'; id: string } | null };
};
};
class User extends Model<UsersSchema> {}
```
To type related resources, you can provide a collection of all models as the second generic.
```ts
type DogsSchema = {
// ...
};
type Schemas = {
users: User;
dogs: Dog;
};
class User extends Model<UsersSchema, Schemas> {}
class Dog extends Model<DogsSchema, Schemas> {}
```
### API Consumption Tips
This library is completely unopinionated about how you interact with your JSON:API server. It merely gives you an easy way to work with the resulting JSON:API data. An example integration with `fetch` is demonstrated below:
This library is completely unopinionated about how you interact with your JSON:API server. It merely gives you an easy
way to work with the resulting JSON:API data. An example integration with `fetch` is demonstrated below:

@@ -172,5 +200,5 @@ ```ts

return fetch('http://example.org/api/' + url, options)
.then(async response => {
if (response.status === 204) {
return fetch('http://example.org/api/' + url, options).then(
async (response) => {
if (response.status === 204) {
return { response };

@@ -182,3 +210,4 @@ } else {

}
});
},
);
}

@@ -191,3 +220,5 @@

When constructing API requests, remember that JSON:API resource objects contain `links` that can be used instead of rebuilding the URL. Also, models contain an `identifier` method that can be used to spread the `type` and `id` members into the document `data` (required by the specification). Here is an example of a request to update a resource:
When constructing API requests, remember that JSON:API resource objects contain `links` that can be used instead of
rebuilding the URL. Also, models contain an `identifier` method that can be used to spread the `type` and `id` members
into the document `data` (required by the specification). Here is an example of a request to update a resource:

@@ -202,31 +233,8 @@ ```ts

...user.identifier(),
attributes: { name: 'Changed' }
}
}
attributes: { name: 'Changed' },
},
},
});
```
### Building Queries
Building query strings for your JSON:API requests can be tedious, and sometimes they may need to be constructed dynamically with merge logic for certain parameters. The `Query` class takes care of this:
```ts
import { Query } from 'json-api-models';
const query = new Query({
'include': 'foo',
'fields[users]': 'name',
});
query.append('include', 'bar');
query.append('fields[users]', 'age');
query.toString(); // include=foo,bar&fields[users]=name,age
query.delete('fields[users]');
query.set('include', 'replaced');
query.toString(); // include=replaced
```
## Contributing

@@ -233,0 +241,0 @@

export * from './types';
export * from './store';
export * from './model';
export * from './query';

@@ -1,56 +0,31 @@

import { Store } from './store';
import { JsonApiIdentifier, JsonApiRelationships, JsonApiResource, KeyValueObject } from './types';
import { JsonApiResource, ModelForType, SchemaCollection } from './types';
export type CastAttributes = {
[key: string]: StringConstructor | NumberConstructor | BooleanConstructor | ((value: any) => any) | (new(value: any) => any);
}
type PartialJsonApiResource<T extends JsonApiResource> = {
[P in keyof T]?: Partial<T[P]>;
};
function isConstructor(obj: any): obj is (new(value: any) => any) {
return !! obj.prototype && !! obj.prototype.constructor.name;
}
class ModelBase<Schema extends JsonApiResource = JsonApiResource> {
public type: Schema['type'];
public id: Schema['id'];
public attributes: NonNullable<Schema['attributes']> = {};
public relationships: NonNullable<Schema['relationships']> = {};
public meta: Schema['meta'] = {};
public links: Schema['links'] = {};
export class Model<Type extends string = any> implements JsonApiResource<Type> {
public type: Type;
public id: string;
public attributes: KeyValueObject = {};
public relationships: JsonApiRelationships = {};
public meta: KeyValueObject = {};
public links: KeyValueObject = {};
[field: string]: any;
protected casts: CastAttributes = {};
constructor(data: Schema) {
this.type = data.type;
this.id = data.id;
[field: string]: any;
constructor(data: JsonApiResource<Type>, protected store: Store) {
this.merge(data);
}
public getAttribute(name: string): any {
const value = this.attributes[name];
const cast = this.casts[name];
if (cast && value !== null && value !== undefined) {
if (cast === String || cast === Number || cast === Boolean || ! isConstructor(cast)) {
return (cast as any)(value);
}
return new cast(value);
}
return value;
}
public getRelationship(name: string): any {
const data = this.relationships[name].data;
// https://github.com/microsoft/TypeScript/issues/14107
return Array.isArray(data) ? this.store.find(data) : this.store.find(data);
}
/**
* Make a resource identifier object for this model.
*/
identifier(): JsonApiIdentifier<Type> {
public identifier() {
return {
id: this.id,
type: this.type
type: this.type,
};

@@ -62,59 +37,49 @@ }

*/
merge(data: Partial<JsonApiResource<Type>>): void {
if ('type' in data) {
this.type = data.type as Type;
}
public merge(data: PartialJsonApiResource<Schema>): void {
this.links = data.links ?? this.links;
this.meta = data.meta ?? this.meta;
if ('id' in data) {
this.id = data.id;
if (data.attributes) {
Object.assign(this.attributes, data.attributes);
}
if ('attributes' in data) {
Object.assign(this.attributes, data.attributes);
Object.keys(data.attributes).forEach(name => {
if (
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), name)
|| Object.getOwnPropertyDescriptor(this, name)
) return;
this[name] = null;
Object.defineProperty(this, name, {
get: () => this.getAttribute(name),
configurable: true,
enumerable: true,
});
});
if (data.relationships) {
Object.entries(data.relationships).forEach(
([name, relationship]) => {
this.relationships[name] = this.relationships[name] || {};
Object.assign(this.relationships[name], relationship);
},
);
}
}
}
if ('relationships' in data) {
Object.entries(data.relationships).forEach(([name, relationship]) => {
this.relationships[name] = this.relationships[name] || {};
type ProxiedModel<
Schema extends JsonApiResource,
Schemas extends Record<string, JsonApiResource>,
> = Schema &
Schema['attributes'] & {
[Property in keyof NonNullable<Schema['relationships']>]: NonNullable<
NonNullable<Schema['relationships']>[Property]
> extends { data?: infer Data }
? Data extends { type: infer RelatedType extends string }
? ModelForType<RelatedType, Schemas> | undefined
: Data extends { type: infer RelatedType extends string }[]
? ModelForType<RelatedType, Schemas>[]
: null
: never;
};
Object.assign(this.relationships[name], relationship);
export type Model<
Schema extends JsonApiResource = JsonApiResource,
Schemas extends SchemaCollection = SchemaCollection,
> = JsonApiResource<Schema['type']> &
ModelBase<Schema> &
ProxiedModel<Schema, Schemas>;
if (
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), name)
|| Object.getOwnPropertyDescriptor(this, name)
) return;
this[name] = null;
Object.defineProperty(this, name, {
get: () => this.getRelationship(name),
configurable: true,
enumerable: true,
});
});
}
if ('links' in data) {
this.links = data.links;
}
if ('meta' in data) {
this.meta = data.meta;
}
}
}
export const Model: new <
Schema extends JsonApiResource = JsonApiResource,
Schemas extends SchemaCollection = SchemaCollection,
>(
data: JsonApiResource<Schema['type']>,
) => Model<Schema, Schemas> = ModelBase as any;
import { Model } from './model';
import { JsonApiDocument, JsonApiIdentifier, JsonApiResource } from './types';
import {
JsonApiDocument,
JsonApiIdentifier,
JsonApiResource,
ModelForType,
ModelMap,
SchemaCollection,
} from './types';
type Graph = {
[type: string]: {
[id: string]: any;
};
};
export class Store<Schemas extends SchemaCollection = {}> {
protected graph: { [type: string]: { [id: string]: any } } = {};
export type ModelConstructor<Type extends string> = {
new(data: JsonApiResource<Type>, store: Store): Model<Type>;
};
public constructor(public models: ModelMap<Schemas> = {}) {}
export type ModelCollection<Models> = { [Type in keyof Models & string]: ModelConstructor<Type> } & { '*'?: ModelConstructor<any> };
export type ModelForType<Type extends string, Models extends ModelCollection<Models>> = Type extends keyof Models ? InstanceType<Models[Type]> : Model;
export class Store<Models extends ModelCollection<Models> = any> {
protected graph: Graph = {};
public constructor(public models: Models = {} as Models) {}
public find<Type extends string>(identifier: JsonApiIdentifier<Type>): ModelForType<Type, Models> | null;
public find<Type extends string>(identifiers: JsonApiIdentifier<Type>[]): ModelForType<Type, Models>[];
public find<Type extends string>(type: Type, id: string): ModelForType<Type, Models> | null;
public find<Type extends string>(a: JsonApiIdentifier<Type> | JsonApiIdentifier<Type>[] | string, b?: string) {
public find<Type extends (keyof Schemas & string) | (string & {})>(
identifier: JsonApiIdentifier<Type> | null,
): ModelForType<Type, Schemas> | null;
public find<Type extends (keyof Schemas & string) | (string & {})>(
identifiers: JsonApiIdentifier<Type>[],
): ModelForType<Type, Schemas>[];
public find<Type extends (keyof Schemas & string) | (string & {})>(
type: Type,
id: string,
): ModelForType<Type, Schemas> | null;
public find<Type extends (keyof Schemas & string) | (string & {})>(
a: JsonApiIdentifier<Type> | JsonApiIdentifier<Type>[] | Type | null,
b?: string,
) {
if (a === null) {

@@ -32,3 +35,3 @@ return null;

if (Array.isArray(a)) {
return a.map((identifier: JsonApiIdentifier<Type>) => this.find(identifier));
return a.map((identifier) => this.find(identifier));
}

@@ -40,27 +43,32 @@

return (this.graph[a] && this.graph[a][b]) || null;
if (b) {
return this.graph[a]?.[b] || null;
}
return null;
}
public findAll<Type extends string>(type: Type): ModelForType<Type, Models>[] {
if (! this.graph[type]) {
public findAll<Type extends (keyof Schemas & string) | (string & {})>(
type: Type,
): ModelForType<Type, Schemas>[] {
if (!this.graph[type]) {
return [];
}
return Object.keys(this.graph[type])
.map(id => this.graph[type][id]);
return Object.keys(this.graph[type]).map((id) => this.graph[type][id]);
}
public sync<Type extends string>(document: JsonApiDocument<Type>): ModelForType<Type, Models> | ModelForType<Type, Models>[] | null {
const syncResource = this.syncResource.bind(this);
public sync<Type extends (keyof Schemas & string) | (string & {})>(
document: JsonApiDocument<Type>,
): ModelForType<Type, Schemas> | ModelForType<Type, Schemas>[] | null {
document.included?.map((resource) => this.syncResource(resource));
if ('included' in document) {
document.included.map(syncResource);
}
return Array.isArray(document.data)
? document.data.map(syncResource)
: syncResource(document.data);
? document.data.map((resource) => this.syncResource(resource))
: this.syncResource(document.data);
}
public syncResource<Type extends string>(data: JsonApiResource<Type>): ModelForType<Type, Models> {
public syncResource<Type extends (keyof Schemas & string) | (string & {})>(
data: JsonApiResource<Type>,
): ModelForType<Type, Schemas> {
const { type, id } = data;

@@ -79,6 +87,23 @@

protected createModel<Type extends string>(data: JsonApiResource<Type>): ModelForType<Type, Models> {
const ModelClass = this.models[data.type as keyof Models] || this.models['*'] || Model;
public createModel<Type extends (keyof Schemas & string) | (string & {})>(
data: JsonApiResource<Type>,
): ModelForType<Type, Schemas> {
const ModelClass = this.models[data.type] || Model;
return new ModelClass(data, this) as ModelForType<Type, Models>;
return new Proxy(new ModelClass(data), {
get: (target, prop, receiver) => {
if (typeof prop === 'string') {
if (target.attributes?.[prop]) {
return target.attributes[prop];
}
const data = target.relationships?.[prop]?.data;
if (data) {
return Array.isArray(data)
? this.find(data)
: this.find(data);
}
}
return Reflect.get(target, prop, receiver);
},
}) as ModelForType<Type, Schemas>;
}

@@ -85,0 +110,0 @@

@@ -1,4 +0,2 @@

export type KeyValueObject = {
[key: string]: any;
}
import { Model } from './model.ts';

@@ -8,4 +6,4 @@ export interface JsonApiDocument<Type extends string = string> {

included?: JsonApiResource[];
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}

@@ -18,25 +16,39 @@

export interface JsonApiResource<Type extends string = string> extends JsonApiIdentifier<Type> {
attributes?: KeyValueObject;
export interface JsonApiResource<Type extends string = string>
extends JsonApiIdentifier<Type> {
attributes?: Record<string, any>;
relationships?: JsonApiRelationships;
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}
export interface JsonApiRelationships {
[relationName: string]: JsonApiRelationship;
}
export type JsonApiRelationships = Record<string, JsonApiRelationship>;
export interface JsonApiRelationship<Type extends string = string> {
data?: JsonApiIdentifier<Type> | JsonApiIdentifier<Type>[] | null;
meta?: KeyValueObject;
links?: KeyValueObject;
meta?: Record<string, any>;
links?: Record<string, any>;
}
export interface JsonApiRelationshipToOne<Type extends string = string> extends JsonApiRelationship<Type> {
export interface JsonApiRelationshipToOne<Type extends string = string>
extends JsonApiRelationship<Type> {
data?: JsonApiIdentifier<Type> | null;
}
export interface JsonApiRelationshipToMany<Type extends string = string> extends JsonApiRelationship<Type> {
export interface JsonApiRelationshipToMany<Type extends string = string>
extends JsonApiRelationship<Type> {
data?: JsonApiIdentifier<Type>[];
}
export type SchemaCollection = { [Type in string]: JsonApiResource<Type> };
export type ModelMap<Schemas extends SchemaCollection = SchemaCollection> = {
[Type in keyof Schemas & string]?: new (
data: JsonApiResource<Type>,
) => Schemas[Type];
};
export type ModelForType<
Type extends string,
Schemas,
> = Type extends keyof Schemas ? Schemas[Type] : Model<JsonApiResource<Type>>;