Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@asteasolutions/angular2-jsonapi
Advanced tools
A lightweight Angular 2 adapter for JSON API
Why this library? Because JSON API is an awesome standard, but the responses that you get and the way to interact with endpoints are not really easy and directly consumable from Angular.
Moreover, using Angular2 and Typescript, we like to interact with classes and models, not with bare JSONs. Thanks to this library, you will be able to map all your data into models and relationships like these:
[
Post{
id: 1,
title: 'My post',
content: 'My content',
comments: [
Comment{
id: 1,
// ...
},
Comment{
id: 2,
// ...
}
]
},
// ...
]
To install this library, run:
$ npm install angular2-jsonapi --save
Add the JsonApiModule
to your app module imports:
import { JsonApiModule } from 'angular2-jsonapi';
@NgModule({
imports: [
BrowserModule,
JsonApiModule
],
declarations: [
AppComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
Firstly, create your Datastore
service:
JsonApiDatastore
class@JsonApiDatastoreConfig
, set the baseUrl
for your APIs and map your models (Optional: you can set apiVersion
, baseUrl
will be suffixed with it)HttpClient
depencency to the parent constructor.import { JsonApiDatastoreConfig, JsonApiDatastore, DatastoreConfig } from 'angular2-jsonapi';
const config: DatastoreConfig = {
baseUrl: 'http://localhost:8000/v1/',
models: {
posts: Post,
comments: Comment,
users: User
}
}
@Injectable()
@JsonApiDatastoreConfig(config)
export class Datastore extends JsonApiDatastore {
constructor(http: HttpClient) {
super(http);
}
}
Then set up your models:
JsonApiModel
class@JsonApiModelConfig
, passing the type
@Attribute
@HasMany
and @BelongsTo
import { JsonApiModelConfig, JsonApiModel, Attribute, HasMany, BelongsTo } from 'angular2-jsonapi';
@JsonApiModelConfig({
type: 'posts'
})
export class Post extends JsonApiModel {
@Attribute()
title: string;
@Attribute()
content: string;
@Attribute({ serializedName: 'created-at' })
createdAt: Date;
@HasMany()
comments: Comment[];
}
@JsonApiModelConfig({
type: 'comments'
})
export class Comment extends JsonApiModel {
@Attribute()
title: string;
@Attribute()
createdAt: Date;
@BelongsTo()
post: Post;
@BelongsTo()
user: User;
}
@JsonApiModelConfig({
type: 'users'
})
export class User extends JsonApiModel {
@Attribute()
name: string;
// ...
}
Now, you can use your Datastore
in order to query your API with the findAll()
method:
// ...
constructor(private datastore: Datastore) { }
getPosts(){
this.datastore.findAll(Post, {
page: { size: 10, number: 1 },
filter: {
title: 'My Post',
},
}).subscribe(
(posts: JsonApiQueryData<Post>) => console.log(posts.getModels())
);
}
Use peekAll()
to retrieve all of the records for a given type that are already loaded into the store, without making a network request:
let posts = this.datastore.peekAll(Post);
Use findRecord()
to retrieve a record by its type and ID:
this.datastore.findRecord(Post, '1').subscribe(
(post: Post) => console.log(post)
);
Use peekRecord()
to retrieve a record by its type and ID, without making a network request. This will return the record only if it is already present in the store:
let post = this.datastore.peekRecord(Post, '1');
You can create records by calling the createRecord()
method on the datastore:
this.datastore.createRecord(Post, {
title: 'My post',
content: 'My content'
});
Making changes to records is as simple as setting the attribute you want to change:
this.datastore.findRecord(Post, '1').subscribe(
(post: Post) => {
post.title = 'New title';
}
);
Records are persisted on a per-instance basis. Call save()
on any instance of JsonApiModel
and it will make a network request.
The library takes care of tracking the state of each record for you, so that newly created records are treated differently from existing records when saving.
Newly created records will be POST
ed:
let post = this.datastore.createRecord(Post, {
title: 'My post',
content: 'My content'
});
post.save().subscribe(); // => POST to '/posts'
Records that already exist on the backend are updated using the HTTP PATCH
verb:
this.datastore.findRecord(Post, '1').subscribe(
(post: Post) => {
post.title = 'New title';
post.save().subscribe(); // => PATCH to '/posts/1'
}
);
The save()
method will return an Observer
that you need to subscribe:
post.save().subscribe(
(post: Post) => console.log(post)
);
Note: always remember to call the subscribe()
method, even if you are not interested in doing something with the response. Since the http
method return a cold Observable, the request won't go out until something subscribes to the observable.
You can tell if a record has outstanding changes that have not yet been saved by checking its hasDirtyAttributes
property.
At this point, you can either persist your changes via save()
or you can roll back your changes. Calling rollbackAttributes()
for a saved record reverts all the dirty attributes to their original value.
this.datastore.findRecord(Post, '1').subscribe(
(post: Post) => {
console.log(post.title); // => 'Old title'
console.log(post.hasDirtyAttributes); // => false
post.title = 'New title';
console.log(post.hasDirtyAttributes); // => true
post.rollbackAttributes();
console.log(post.hasDirtyAttributes); // => false
console.log(post.title); // => 'Old title'
}
);
For deleting a record, just call the datastore's method deleteRecord()
, passing the type and the id of the record:
this.datastore.deleteRecord(Post, '1').subscribe(() => {
// deleted!
});
In order to query an object including its relationships, you can pass in its options the attribute name you want to load with the relationships:
this.datastore.findAll(Post, {
page: { size: 10, number: 1},
include: 'comments'
}).subscribe(
(document) => {
console.log(document.getMeta()); // metadata
console.log(document.getModels()); // models
}
);
The same, if you want to include relationships when finding a record:
this.datastore.findRecord(Post, '1', {
include: 'comments,comments.user'
}).subscribe(
(post: Post) => console.log(post)
);
The library will try to resolve relationships on infinite levels connecting nested objects by reference. So that you can have a Post
, with a list of Comment
s, that have a User
that has Post
s, that have Comment
s... etc.
Note: If you include
multiple relationships, do not use whitespaces in the include
string (e.g. comments, comments.user
) as those will be encoded to %20
and this results in a broken URL.
If the object you want to create has a one-to-many relationship, you can do this:
let post = this.datastore.peekRecord(Post, '1');
let comment = this.datastore.createRecord(Comment, {
title: 'My comment',
post: post
});
comment.save().subscribe();
The library will do its best to discover which relationships map to one another. In the code above, for example, setting the comment
relationship with the post
will update the post.comments
array, automatically adding the comment
object!
If you want to include a relationship when creating a record to have it parsed in the response, you can pass the params
object to the save()
method:
comment.save({
include: 'user'
}).subscribe(
(comment: Comment) => console.log(comment)
);
You can also update an object that comes from a relationship:
this.datastore.findRecord(Post, '1', {
include: 'comments'
}).subscribe(
(post: Post) => {
let comment: Comment = post.comments[0];
comment.title = 'Cool';
comment.save().subscribe((comment: Comment) => {
console.log(comment);
});
}
);
Metadata such as links or data for pagination purposes can also be included in the result.
For each model a specific MetadataModel can be defined. To do this, the class name needs to be added in the ModelConfig.
If no MetadataModel is explicitly defined, the default one will be used, which contains an array of links and meta
property.
@JsonApiModelConfig({
type: 'deals',
meta: JsonApiMetaModel
})
export class Deal extends JsonApiModel
An instance of a class provided in meta
property will get the whole response in a constructor.
Datastore config can be specified through the JsonApiDatastoreConfig
decorator and/or by setting a config
variable of the Datastore
class. If an option is specified in both objects, a value from config
variable will be taken into account.
@JsonApiDatastoreConfig(config: DatastoreConfig)
export class Datastore extends JsonApiDatastore {
private customConfig: DatastoreConfig = {
baseUrl: 'http://something.com'
}
constructor() {
this.config = this.customConfig;
}
}
config
:
models
- all the models which will be stored in the datastorebaseUrl
- base API URLapiVersion
- optional, a string which will be appended to the baseUrloverrides
- used for overriding internal methods to achive custom functionalitiesgetDirtyAttributes
- determines which model attributes are dirtytoQueryString
- transforms query parameters to a query string@JsonApiModelConfig(options: ModelOptions)
export class Post extends JsonApiModel { }
options
:
type
baseUrl
- if not specified, the global baseUrl
will be usedapiVersion
- if not specified, the global apiVersion
will be usedmodelEndpointUrl
- if not specified, type
will be used insteadmeta
- optional, metadata modelAttribute(options: AttributeDecoratorOptions)
AttributeDecoratorOptions
:
converter
, optional, must implement PropertyConverter
interfaceserializedName
, optionalBy default, the library adds these headers, according to the JSON API MIME Types:
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
You can also add your custom headers to be appended to each http call:
this.datastore.headers = new Headers({'Authorization': 'Bearer ' + accessToken});
Or you can pass the headers as last argument of any datastore call method:
this.datastore.findAll(Post, {
include: 'comments'
}, new Headers({'Authorization': 'Bearer ' + accessToken}));
and in the save()
method:
post.save({}, new Headers({'Authorization': 'Bearer ' + accessToken})).subscribe();
You can add your custom request options to be appended to each http call:
this.datastore.requestOptions = {
withCredentials: false,
myOption: 123
}
Error handling is done in the subscribe
method of the returned Observables.
If your server returns valid JSON API Error Objects you can access them in your onError method:
import {ErrorResponse} from "angular2-jsonapi";
...
this.datastore.findAll(Post).subscribe(
(posts: Post[]) => console.log(posts),
(errorResponse) => {
if (errorResponse instanceof ErrorResponse) {
// do something with errorResponse
console.log(errorResponse.errors);
}
}
);
It's also possible to handle errors for all requests by overriding handleError(error: any): Observable
in the datastore.
The library will automatically transform date values into Date
objects and it will serialize them when sending to the server. In order to do that, remember to set the type of the corresponding attribute as Date
:
@JsonApiModelConfig({
type: 'posts'
})
export class Post extends JsonApiModel {
// ...
@Attribute()
createdAt: Date;
}
Moreover, it should be noted that the following assumptions have been made:
2001-02-03T14:05:06+07:00
).To generate all *.js
, *.js.map
and *.d.ts
files:
$ ng build build
To lint all *.ts
files:
$ ng lint library
To test all:
$ ng test library
To publish library:
ng build library
cd dist/library
npm publish
This library is inspired by the draft of this never implemented library.
MIT © Daniele Ghidoli
FAQs
A lightweight Angular adapter for JSON API
We found that @asteasolutions/angular2-jsonapi demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.