npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

sailer

v1.0.1

Published

Create a REST CRUD service with nested collections support

Downloads

4

Readme

Introduction

sailer is a server-side library for creating a RESTful API service with support for nested resources. The library focus is on helping the programmer write a RESTful service with minimium code.

Important Note

The library is on it's very early days but stable. Code comments are partial, documentation is being written, contribution guide is missing and a number of features.

Features

  • Collection interface
  • Single tone interface
  • Nested collections
  • In memory collection implementation included
  • express based
  • Written in Typescript

Roadmap

  • supoort for after and before middlewares.
  • support and mongodb style filter
  • filter and limit support in ItemsManager<T>
  • mongoose
  • sailer api for error handling
  • support for HTTP-Patch

How does it work

The Magic

The special thing about this library is the fact that you can nest collections within collections. The library knows how to propagate within your collections untill it reaches the desired resource, for example:

GET /users/15/books/4/pages/1
  • First the library will get the user with id 15 from a registered users collection
  • Then book with id 4 from the user's books collection
  • And Finally will get the page with id 1 from the book's pages collection.

The same behavior will take place for all other HTTP requests..

The magic is done by using an OOP aproach. You simply need to implement two interfaces:

  • ICrudCollection in your collection
  • ICrudItem in each item within a collection

Then register your collection using sailer.collection() (new name will be chosen in the near future)

Item Description

Every item that is returned to the client should have a description. An item description is the way you expose the item in your API. Most of the time you'll want to hide some internal members and in that case you simply need to return a description in describe() function and return a new object which holds only the members that you want to expose, for example:

class User implements ICrudItem{
    protected name: string;
    protected height: number;
    protected isAdmin: boolean;
    
    public describe(): any{
        return {
            name: this.name;
            height: this.height;
            // hide `isAdmin` member
        }
    }
}

A good practice is to expect the client to send the same structure in each API route.

API

  • Data formatting is JSON based therefore server returns data in JSON format and received data in JSON.

  • The action mapping is as follows: (users collection)

POST    /users              ->  create a new user
GET     /users/:userId      ->  get user with specific id
GET     /users              ->  get multiple users (support for filter and limit soon)
PUT     /users/:userId      ->  update a specific user
PUT     /users              ->  update multiple users (support for filter and limit soon)
DELETE  /users/:userId      ->  delete a specific user
DELETE  /users/             ->  delete multiple users (support for filter and limit soon)
  • The action for nested items is the same:
POST    /users/:userId/books/           ->  create a new book inside a specific user's books
GET     /users/:userId/books/:bookId    ->  get a specific book from a specific user's books
GET     /users/:userId/books            ->  get multiple books of a specific user (support for filter and limit soon)
PUT     /users/:userId/books/:bookId    ->  update a specific book from a specific users' books
PUT     /users/:userId/books            ->  update multiple books of a specific (support for filter and limit soon)
DELETE  /users/:userId/books/:bookId    ->  delete a specific book from a specific user's book
DELETE  /users/:userId/books            ->  delete multiple books of a specific user (support for filter and limit soon)
  • And so on for any nesting level..

Error handling

Currently any error that is thrown from your classes is catched by sailer and returned to the client with the thrown object in the response body with http 500 status code - INTERNAL_SERVER_ERROR.

An error throwing api will be added in the near future.

Examples

An example for implementing a class which contains a collection and is contained within another collection:

export class User implements ICrudItem{   
    protected _id: string;
    
    // users/:userId/books/:bookId
    protected books: BooksCollection;
    
    // assignment to `id` is only allowed when using the constructor
    public get id(): string{
        return this._id;
    }
    
    // will be called from usersCollection.create()
    constructor(id: string, public name: string, public height: number, private privateMember: string){       
        this._id = id;
        this.name = name;
        this.height = height;        
        this.privateMember = privateMember;
        this.books = new BooksCollection();
    }
    
    public getCollection(collectionName: string): ICrudCollection | undefined{
        let collection: ICrudCollection;
        
        if(collectionName == "books"){
            collection = this.books;
        }
        
        return collection;
    }
    
    // return a description of user and not a full representation
    public describe(): any{
        let description = {
            id: this.id,
            name: this.name,
            height: this.height,
            books: this.books.describe(),   // get a description of books collection 
        }

        return description;
    }
    
    public async update(fields: any): Promise<IDescriptor>{
        // override each property except books collection
        for(let prop in fields){
            let currentProp = (<any>this)[prop];
            if(currentProp && prop != "books"){
                (<any>this)[prop] = fields[param];
            }
        }
        
        return this;
    }
    
    // no need to implement ICrudItem.read(), the function is logical, intended for single tones 
    public async read(): Promise<any>{
        
    }
}

An example for creating a collection:

export class UsersCollection extends ICrudCollection{
    protected _users: Map<string, User>;    

    public async create(item: any): Promise<string> {
        let newUser = new User((this._itemsCounter++).toString(), item.name);        

        this._items.set(newUser.id, newUser);

        return newUser.id;
    }
    
    public async readById(id: string): Promise<IDescriptor> {
        return <T>this._items.get(id);
    }
    
    // return all items
    public async readMany(limit?: number | undefined, filter?: IParam[] | undefined): Promise<IDescriptor[]> {
        let items: any[] = new Array<any>();
        
        this._items.forEach((item: T, id: string) => {
            items.push(item);
        });

        return items;
    }
    ...
    ...
    ...
}

How to register your routes:

let app = express();
let sailer = new Sailer();

let usersManager = new UsersCollection();

let usersREST = sailer.collection('/users/:userId', usersManager);
let booksREST = sailer.collection('/users/:userId/books/:bookId', usersManager);
let pagesREST = sailer.collection('/users/:userId/books/:bookId/pages/:pageId', usersManager);
app.use(usersREST);
app.use(booksREST);
app.use(pagesREST);

app.listen(3000, () => {
    console.log('listening on port 3000');
});

License

The MIT License (MIT)

Copyright (c) 2018 Matan Givoni

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.