pontegg.io v0.0.2
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:
- Is user authenticated?
- Is user allowed to access resource api endpoint (ACL)?
- Does user meets conditions to access particular resource (RBAC)?
- 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
ResourceServices - provide factory method to instantiate 'Resource Model' together with it's controller and services
- ResourceQueryService - provides CRUD operations wrapped around native mongodb queries
Auth - Responsible for authentication. Provides JWT token validation.
Emails - Allows sending emails: simple, html, with attachments etc.
i18n - Localization
PdfRender - renders pdf from markdown formatted text
Validator - Ajv based json scheme validator
Usage
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 resourceshared/schemes/user/profile.section.scheme.ts
- schema for user profile sectionshared/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 withas 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], ];
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.
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'.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 {}
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 requestcreate
- POST request - creates new resourceupdate
- PUT request - updates existing resourcedelete
- DELETE request - deletes existing resourcelist
- 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, whereif
is set to{ state: ['init', 'profileCreated'] }
then only resources withstate
equal toinit
orprofileCreated
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
6 months ago