0.0.2 • Published 6 months ago

pontegg.io v0.0.2

Weekly downloads
-
License
MIT
Repository
-
Last release
6 months ago

pontegg.io

"Give us the tools and we finish the job" - Winston Churchill

pontegg.io is opinionated nest.js scaffolding aiming to reduce boilerplate, increase security. It aims to shorten all typical repetitive development tasks allowing to lunch new APIs fast and focus on particular software business logic. It does not makes your coffee yet.

Attention!. It uses mongodb in 'schemaless' manner. Everything depends of robustness of your json schemes.

see the presentation pontegg.io

Installation & Configuration

$ pnpm install

see further installation steps in docs/installation.md

Features

  • automatic CRUD REST API controller creation
  • automatic API documentation (OpenAPI, swagger)
  • multilayer fine grained access control (ACL)
  • role/group based access control to act upon the resource and its individual section (RBAC).
  • api declaratively defined in one json file.
  • simple but powerful state management
  • out of the box file/s upload api
  • automatic data validation json schema
  • automatic data sanitization & coercion
  • automatic data pagination
  • automatic data sorting
  • event sourcing (Events)
  • streamlined tests

Why?

Very often we cross with situation when resource (entity) has complex structure which is build up during process of interaction of different actors which on each step are allowed to commit particular section of the resource.

pontegg.io is not intended for the scenarios where domain has a lot of related entities. It works well when complexity is confined in few arbitrarily complex resources. Currently it does not provide ORM nor GraphQl since 'complexity' is not scattered across many entities/tables.

If domain logic requires many related entities, or there is no interaction of different actors it is better look for other solutions.

Security

App can accessed only by authenticated users. Authentication happens after checking validity of JWT token. Which is renewed periodically.

Solution apply security by default methodology,RBAC and ACL principles. Roles and permissions should be explicitly defined for each operation on each resource.

Every API call is evaluated following the rules:

  1. Is user authenticated?
  2. Is user allowed to access resource api endpoint (ACL)?
  3. Does user meets conditions to access particular resource (RBAC)?
  4. Does user user meets conditions to perform action on resource section?

State management

pontegg.io provides simple but powerful state management. Transition from one state to another is govern by 'rules'. Rules expose conditions which if satisfied allow transition to next sate to happen. Every state change emits 'event' (event sourcing) containing 'payload' and 'actor' data. Event can be intercepted by any other component and act in consequence.

Components

  1. ResourceServices - provide factory method to instantiate 'Resource Model' together with it's controller and services

    • ResourceQueryService - provides CRUD operations wrapped around native mongodb queries
  2. Auth - Responsible for authentication. Provides JWT token validation.

  3. Emails - Allows sending emails: simple, html, with attachments etc.

  4. i18n - Localization

  5. PdfRender - renders pdf from markdown formatted text

  6. Validator - Ajv based json scheme validator

Usage

  1. Setup json schemes for your resource at shared/schemes folder. Each section of the resource should have its own schema. Json Schemes defacto define resource structure and serve to validate incoming requests. For example:

    • shared/schemes/user/user.scheme.ts - schema for user resource
    • shared/schemes/user/profile.section.scheme.ts - schema for user profile section
    • shared/schemes/user/contacts.section.scheme.ts - schema for user contacts section

    Main scheme should be exported as default and sections as named exports. Scheme file name should follow the pattern: [resource-name].scheme.ts. Section file name should follow the pattern: [resource-name].[section-name].scheme.ts. Scheme should be defined following Object notation. For example:

    const scheme = {
      type: 'object',
      properties: {
        name: { type: 'string' },
        email: { type: 'string' },
        password: { type: 'string' },
      },
    } as const;
    export default scheme;

    @ATTENTION. scheme must be defined with as const.

    All Schemes should be valid json schema All resource schemes should be registered and exported in index.ts file. For example:

    import user from './user.scheme';
    import profile from './profile.scheme';
    import contacts from './contacts.scheme';
    
    export const schemes = [
      ['user', user],
      ['profile.section', profile],
      ['contacts.section', contacts],
    ];
  2. Register Types for your resource at shared/types folder. For example:

    import { FromSchema } from 'json-schema-to-ts';
    import { DeserializeDate, WithId } from './common';
    
    import user from '../schemes/user/user.scheme';
    import profile from '../schemes/user/profile.section.scheme';
    import contacts from '../schemes/user/contacts.section.scheme';
    
    export type User = FromSchema<typeof user, DeserializeDate>;
    export type UserProfile = FromSchema<typeof profile, DeserializeDate>;
    export type UserContacts = FromSchema<typeof contacts, DeserializeDate>;
    
    type StoredUser = WithId<User>;
    export default StoredUser;

    Main type should be exported as default and sections as named exports.

    @TIP. You may register some 'partial' types for the convenience.

  3. Set Resource Model using factory method in user/user.module.ts file. For example:

    import { Module, DynamicModule } from '@nestjs/common';
    
    import ResourceBaseService from '../resource/resource.service';
    import { resourceModuleFactory } from '../resource/module-factory';
    
    export class UserService extends ResourceBaseService {}
    
    @Module({})
    export class UserModule {
      static register(): Promise<DynamicModule> {
        return resourceModuleFactory(this, 'user', UserService);
      }
    }

    @TIP. You may override methods of ResourceBaseService class or add new 'specialized'.

  4. Register User Model in app.module.ts file. For example:

    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { TypeOrmModule } from '@nestjs/typeorm';
    
    import { UserModule } from './user/user.module';
    
    @Module({
      imports: [ConfigModule.forRoot(), TypeOrmModule.forRoot(), UserModule.register()],
    })
    export class AppModule {}
  5. Define API behavior at user/user.api.ts:

    import scheme from '@Schemes/user.scheme';
    import API from '@Types/api';
    
    export type Actor = 'user' | 'admin';
    
    const api: API<Actor> = {
      resourceSchemeName: 'user',
      scheme,
      states: scheme.properties.state.enum,
      indexes: [{ key: { userId: 1 } }, { key: { createdAt: 1 } }, { key: { updatedAt: 1 } }],
      sections: {
        profile: {
          create: {
            let: ['admin', { for: 'user', if: { state: ['init'] } }],
            set: { state: 'profileCreated' },
          },
        },
        contacts: {
          create: {
            let: ['admin', { for: 'user', if: { state: ['init'] } }],
            set: { state: 'contactsCreated' },
          },
        },
      },
      get: {
        let: ['admin', { for: 'user', if: { user: '_id' } }],
      },
      create: {
        let: [
          { for: 'user', validate: 'create.user', set: 'authId' },
          { for: 'admin', validate: 'admin.create.user' },
        ],
        set: { state: 'init' },
      },
      delete: {
        let: ['admin'],
      },
      update: {
        roles: ['admin'],
      },
      list: {
        let: ['admin'],
        query: ['userId', 'state'],
        projection: ['userId', 'invoice', 'document'],
      },
    };
    export default api;

We can define CRUD actions for the resource and sections:

  • get - GET resource request
  • create - POST request - creates new resource
  • update - PUT request - updates existing resource
  • delete - DELETE request - deletes existing resource
  • list - GET request listing resources.
  • sections - defines behavior for sections.

For each CRUD endpoint and each 'section'. We can define:

  • let - array of roles (string - name of role or object with some conditions) that are allowed to perform the action.
  • if - defines conditions for the action. For example, where if is set to { state: ['init', 'profileCreated'] } then only resources with state equal to init or profileCreated will be accessible. ex: if: { user: { state: ['init'] } } - it means for 'user' that state should be 'init'.
  • validate - defines validation schema for the action if it differs from the resource scheme or particular section scheme.
  • set - it can be used to change state value (or any other property). For example: { state: 'invoiceFileUploaded' }.

get - defines behavior of GET request.

list may receive additional parameters:

  • query - defines query parameters that are allowed to be used in the request.
  • projection - defines projection for returned query.

On create action 'if' conditions refer to the user properties. For example, if if: { state: ['kyc'] } then only users with state equal to kyc will be able to create resource.

On update action 'if' conditions refer to the present resource properties. For example, if if: { state: ['approved'] } then resource will be updated only when state equal to approved.

There can be multiple 'if' conditions which correspond to 'OR' boolean operation. Which means that if at least one condition is met then the action is allowed.

Extending pontegg.io

Pontegg.io tries to address most common use cases, when it comes short on functionality it is possible to extend it further with custom logic.

  • Each POST, PUT, DELETE operation emits Event (ResourceCreatedEvent, ResourceDeletedEvent, ResourceUpdatedEvent) which can be handled by custom listeners and further work on it. It can be used to send notifications, log events to audit log, etc.

  • If endpoint needs some custom logic which result must be returned to the client, then it can be done by adding custom endpoint to the controller. (usual nest way)

TODO

  • add support SSE
  • add support for (GraphQL)https://graphql.org/
  • add support MongoDB Schemas
  • add support for complex state transition rules (OR, AND, NOT, etc.)
  • add support for rejecting uploads with not whitelisted file extensions
  • resource section versioning

License