@superdevofficial/carrot
v0.3.7
Published
> Carrot est un module bit qui contient un certain nombre d'outil pour développer une application Angular avec une API FeathersJS > {.is-info}
Readme
Angular - Carrot
Carrot est un module bit qui contient un certain nombre d'outil pour développer une application Angular avec une API FeathersJS {.is-info}
Requirements
- npm >= 6.10
- Node >= 10.10
- Angular Core >= 8.2.14
- Angular Material
- FeathersJS
Dependances
Liste reprise depuis le fichier package.json :
- "mingo": "^3.0.2"
- "@angular/material": "^8.0.1"
- "@angular/router": "~8.2.14"
- "@angular/core": "^8.0.0",
- "@angular/forms": "^8.0.0",
- "@angular/common": "^8.0.0",
- "lodash": "^4.0.0",
- "rxjs": "^6.0.0"
Ajoutez ces dependences dans le projet dans lequel vous inserez le module.
Installation dans une app Angular
Pour simplement l'utiliser dans l'application :
npm i @superdevofficial/superdev.angular.carrotEnsuite il vous suffit d'utiliser les components/modules et autres classes de cette manière :
import { Model, Id, AlertService ... } from '@superdevofficial/superdev.angular.carrot';Configuration Docker
Pour que Carrot puisse fonctionner dans une image Docker :
# ------------------------
# exemple : Ultima-Display
# ------------------------
FROM node:10-alpine AS base
WORKDIR /app
FROM base AS builder
ARG BIT_NODE_TOKEN
RUN apk add --update git && rm -rf /tmp/* /var/cache/apk/*
#prod or staging
ARG ANGULAR_ENV
RUN apk add --update git && rm -rf /tmp/* /var/cache/apk/*
COPY client/package.json client/package-lock.json ./
RUN npm set progress=false && npm config set depth 0 && npm cache clean --force && npm config set unsafe-perm true
# add dependencies to build packages
RUN apk --no-cache add --virtual native-deps \
git g++ gcc libgcc libstdc++ linux-headers make python && \
npm config set python /usr/bin/python && \
npm install node-gyp -g && \
echo -e "@bit:registry=https://node.bit.dev\n//node.bit.dev/:_authToken=$BIT_NODE_TOKEN" >> ~/.npmrc
## Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN npm i && mkdir dev && cp -R ./node_modules ./dev
WORKDIR /app/dev
COPY client .
## Build the angular app in production mode and store the artifacts in dist folder
RUN $(npm bin)/ng build --prod --configuration=$ANGULAR_ENV
FROM base AS release
WORKDIR /app
RUN npm config set unsafe-perm true
# Install app dependencies
RUN npm -g install buffet
COPY --from=builder /app/dev/dist /app
EXPOSE 8080
# serve app folder on port 8080
CMD ["buffet", "--notFoundPath", "index.html","--no-log","--no-watch"]
Améliorer Carrot
Il faut en premier lieu installer Bit sur sa machine local : Comment-installer-bit-pour-les-nuls
Puis importer Carrot dans un dossier ./modules/ via la commande :
bit import superdev.angular/carrotIl faudra aussi configurer votre package.json pour prendre en compte le nouveaux module :
{
"bit": {
"env": {
"compiler": "cas-aanzee.test/compilers/[email protected]"
},
"componentsDefaultDirectory": "src/modules/{name}",
"packageManager": "npm",
"overrides": {
"*": {
"dependencies": {
"@angular/core": "-",
"@angular/forms": "-",
"@angular/common": "-",
"lodash": "-",
"rxjs": "-"
},
"devDependencies": {},
"peerDependencies": {
"@angular/core": "^8.0.0",
"@angular/forms": "^8.0.0",
"@angular/common": "^8.0.0",
"lodash": "^4.0.0",
"rxjs": "^6.0.0"
}
}
}
}
}Une fois votre application configurée, il suffiera de modifier les fichiers directement dans le dossier /modules/carrot/**
Classes, Types, Bases
Id
Il s'agit d'un type qui représente les primaryKeys mongo et mysql :
export type Id = string | numberDeletedAt
Il s'agit d'un type à appliquer à la propriété deletedAt pour pouvoir gérer le softDelete de chez FeathersJS :
export type DeletedAt = null | Date | string | false | 0
export interface IDeletedAt {
deletedAt?: DeletedAt
isDeleted(): boolean
}Model
Il s'agit d'une base pour les modèles de votre application pour fonctionner avec une API FeathersJS :
export interface IModel {
id: Id
className: string
createdAt: string
updatedAt: string
validate(): boolean
validateBeforePost(): boolean
validateBeforeUpdate(): boolean
sanitize(idKey: string): any
sanitizeBeforePost(idKey: string): any
sanitizeBeforeUpdate(idKey: string): any
patchValue(datas: any)
toObject(): object
isDeleted(): boolean
}Cette base permet d'accéder à la clé primaire du modèle via l'index id. Il n'y a plus besoin de se soucier de la BDD.
Pour étendre la classes, certaines conditions sont à prendre en compte :
export class MonModele extends Model implements IMonModele {
/**
_forbbidenFields: permet d'ignorer des champs lors de l'envoie à la BDD
*/
protected _forbbidenFields: string[] = [
'_primaryKey',
'_primaryKeyIndex',
'_className',
'_forbbidenFields',
'_fieldsNeedConstructor',
]
/**
_fieldsNeedConstructor: liste les champs qui font référence à un autre
modèle de votre application
*/
protected _fieldsNeedConstructor: any = { user: User }
/**
!!required!!
_primaryKeyIndex: il s'agit du nom de la valeur clé primaire
de votre modèle cté BDD ( "_id", "id" )
*/
protected _primaryKeyIndex = '_id'
/**
La méthode "this.initialize(datas)" doit être appelé dans le constructor
*/
constructor(datas: any = {}) {
super()
this.initialize(datas)
}
}QueryBuilder
Il s'agit d'une classe permettant de construire les queries d'une requête (post, get, update ...). Chaque queryBuilder doit être lié à une propriété d'un modèle ( exemple un queryBuilder pour les emails ). Voici l'interface du queryBuilder :
export interface IQueryBuilder {
key: string
name: string
value: any
formControl: FormControl
applyFilter(query: IQuery): IQuery
isQueryBuilder(): boolean
reset()
initFormControl()
}Les queryBuilder sont définit au sein d'un QueryManager et peuvent se partager entre service. Il sont listé sous cette forme :
{
name: 'email',
key: 'index_de_la_bdd',
filter: {
$lte: true,
$in: null,
$nin: null,
$lt: null,
$lte: null,
$gt: null,
$gte: null,
$ne: null,
$or: null,
$search: null
}
},La propriété "filter" peut accepter tous les filtres listés plus haut. Mais il est possible de réaliser un filtre custom via un callback :
{
name: 'email',
key: 'index_de_la_bdd',
filter: filter: function (query: IQuery) {
if (Helpers.isRealNumber(parseInt(this.value)) && parseInt(this.value) !== 0) {
query[this.key] = parseInt(this.value);
} else if (this.value != null) {
query.name = { $search: this.value };
}
return query;
}
}Un queryBuilder créé directement un FormControl utilisable n'importe où dans votre applciation. Ce formControl se met directement à jour dés que le queryBuilder voit sa valeur changer :
/**
* Dans un lointain component... Très lointain
* qui étend la TableComponent
*/
maFunction(nouvelleValeur: any) {
if (nouvelleValeur) {
this.queriesBuilders.email.value = nouvelleValeur;
} else {
this.queriesBuilders.email.reset();
}
this.queriesBuilders.email.formControl.onValueChange().subscribe(e => console.log(e));
}
Services
ApiService
C'est une classe abstraite à appliquer à votre service qui communiquera avec votre API FeathersJS. Voici son interface :
export interface IApiService {
suffix: string
getAppConfig(): { appDomain: string; apiDomain: string }
getResultData(res: any)
getApiUrl(suffix?: string): string
getRaw<T>(url: String, options?: any): Observable<any>
getOne<T>(T: new (any?) => T, url: String, options?: any): Observable<T>
get<T>(T: new (any?) => T, url: String, options?: any): Observable<T[]>
postRaw<T>(url: String, data?: any, options?: any): Observable<any>
postOne<T>(
T: new (any?) => T,
url: String,
data?: any,
options?: any,
): Observable<T>
updateOne<T>(
T: new (any?) => T,
url: String,
data?: any,
options?: any,
): Observable<T>
updateRaw<T>(url: String, data?: any, options?: any): Observable<any>
deleteRaw(url: String, options?: any): Observable<any>
}Et voici un exemple d'application :
@Injectable()
export class MonApiService extends ApiService {
suffix = '/mon-suffix-api'
constructor(protected http: HttpClient) {
super(http)
}
/**
* Retourner la configuration Angular d'environnement
*/
public getAppConfig(): { appDomain: string; apiDomain: string } {
return { appDomain: AppConfig.appDomain, apiDomain: AppConfig.apiDomain }
}
}AuthenticationService
C'est une classe abstraite à appliquer à votre service qui gère les connexions et la gestion de l'utilisateur connecté avec votre API FeathersJS. Voici son interface :
export interface IAuthenticationService {
authentificated: BehaviorSubject<boolean>
getUser(): any
setUser(user: any)
getToken(): string
isAuthentificated(): boolean
login(email: string, password: string)
verify(token: string): Observable<boolean | any>
logout()
}Et voici un exemple d'application :
@Injectable()
export class AuthenticationService extends CarrotAuthService {
/**
* Ne pas oublier le prefix de la route auth
*/
protected prefix: string = 'mon-prefix'
/**
* La route réel de l'authentification
*/
protected authPath: string = '/ud/authentication'
/**
* Pour éviter des pertes de mémoires
*/
protected currentUser: User
constructor(protected http: HttpClient, protected api: UltimaApiService) {
super(http, api)
}
/**
* Get current authentified user
* @return User
*/
public getUser(): any {
if (!this.currentUser) {
let partialUser: any = this.get('currentUserAuth')
if (partialUser) {
this.currentUser = new User(partialUser)
} else {
this.currentUser = null
}
}
return this.currentUser
}
/**
* Get current authentified user
* @return User
*/
public setUser(user: any) {
if (user) {
this.currentUser = _.isString(user.className) ? user : new User(user)
this.store('currentUserAuth', user)
}
}
}FeathersService
Cette classe permet de reproduire directement un service FeathersJS pour accéder à une ressource. Pour fonctionner, il faut préciser un type de modèle à la place de T. Voici le cul :
export interface IFeathersService<T> {
path: string
model: new (any?) => T
primaryKeyIndex: string
find(queries?: any): Observable<T[]>
get(id: Id, queries?: any): Observable<T>
post(data: T, queries?: any): Observable<T | T[]>
post(data: T[], queries?: any): Observable<T | T[]>
update(id: Id, data: T, queries?: object): Observable<T>
update(data: T, queries?: object): Observable<T>
delete(idOrQueries?: Id | T, queries?: object): Observable<any>
}ResolverService
Cette classe étend le FeathersService pour proposer une gestion global des ressources. A chaques requêtes, la liste des entités se met automatiquement à jour et il est possible d'y souscrire via un observable : entityList. Comme le FeathersService, elle a besoin d'un type de modèle pour fonctionner. Voici son interface :
export interface IResolverService<T> {
currentEntity: T
currentEntity$: Observable<T>
entityList: T[]
entityList$: Observable<T[]>
optionsList: object
isNew: boolean
isListEmpty: boolean
resolve(id?: Id): Observable<T>
emitCurrentEntity(entity: T)
getCurrentEntity(id: Id): Observable<T>
postCurrentEntity(options?: object): Observable<T | T[] | boolean>
updateCurrentEntity(options?: object): Observable<T | boolean>
deleteCurrentEntity(options?: object): Observable<T>
refreshCurrentEntity()
refreshEntityList()
}Ce service gère aussi les ressources appelées via une route grâce au Resolver de Angular. Ainsi il est possible d'accéder à l'entité demandée par une route via la variable currentEntity Un exemple d'utilisation du ResolverService dans un Resolver Angular:
@Injectable()
export class UsersResolver implements Resolve<Observable<User>> {
constructor(private service: UsersService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<User> {
return this.service
.resolve(route.params.id || null)
.pipe(map((result: User) => (result instanceof User ? result : null)))
}
}Ce service peut aussi savoir si l'entité actuelle ( celle demandée par la route ) est nouvelle ou existante via la variable isNew.
Un ResolverService peut aussi attacher un QueryManager pour gérer de manière dynamique et cross-route les query de ses requêtes.
QueryManager
Il s'agit d'un service Angular qui permet, une fois attaché à un ResolverService, de gérer les queries de la requêtes (filtres, sorts, pagination ...). Voici son interface :
export interface IQuery {
[x: string]: any
}
export interface IQueryManager {
query$: Observable<IQuery>
query: IQuery
$sort: any
$total: number
$skip: number
$limit: number
$index: number
buildFormGroup(): FormGroup
getFormControl(key?: string): FormControl | FormControl[]
has(key: string): boolean
get(key: string): IQueryBuilder
set(query: IQueryBuilder)
sortBy(key: string, value: any)
getFilter(key: string): any
filterBy(key: string, value: any)
resetFilter(key?: string)
resetSort()
resetPagination()
resetAll()
}Habituellement, il est constitué de plusieurs QueryBuilder qui correspondent chacuns à une propriété de la ressource ( du modèle ). Chaques queryBuilders va altérer les queries de la requêtes en fonction des valeurs qu'y lui sont attribués. Voici un exemple d'un QueryManager :
@Injectable({ providedIn: 'root' })
export class UserQueryManager extends QueryManager {
constructor() {
super([
/**
* Voici un exemple d'un custom QueryBuilder
*/
{
name: 'term',
filter: function (query: IQuery) {
if (Helpers.isSet(this.value)) {
query.$or = [
{ email: { $regex: this.value } },
{ firstName: { $regex: this.value } },
]
}
return query
},
},
/**
* Par défaut un QueryBuilder appliquera un filtre type "like"
* exemple ci-dessous : { query: { firstname: 'Bobby' } }
*/
{ name: 'firstName' },
/**
* exemple ci-dessous pour le filtre "status" :
* { query: { isActive: 'ma-valeur' } }
*/
{ name: 'status', key: 'isActive' },
/**
* QueryBuilders par défaut pour gérer les variables softDelete,
* createdAt et updatedAt
*/
new UpdatedAtQueryBuilder(),
new CreatedAtQueryBuilder(),
new SoftDeleteQueryBuilder(),
])
}
}AlertService
L'alertService de chez Material est directement disponible via Carrot. On peut utiliser les méthode success et error pour directement afficher une alert. Voici son interface :
export interface IAlertService {
success(message: string, keepAfterNavigationChange: boolean)
error(message: string, keepAfterNavigationChange: boolean)
getMessage(): Observable<AlertResponse>
}Il faut penser à appeler le service dans le AppModule de votre applciation :
import { CarrotAlertModule } from '@superdevofficial/superdev.angular.carrot';
@NgModule({
declarations: [
...
],
imports: [
...,
CarrotAlertModule
],
})
export class AppModule { }Components
TableComponent
Ce component est à utilisé sur les listing d'éléments pour pouvoir gérer la pagination, les filtres, les sorts, la recherche. Voici son interface :
export interface ITableComponent<T> {
$total(): number
$skip(): number
$limit(): number
$index(): number
queriesBuilders: { [x: string]: IQueryBuilder }
onQueryChange(): Observable<IQuery>
filterForm: FormGroup
onListChange(entities: T[])
onFilterChange(event: any)
onSortChange(event: ISortChangeOptions)
resetFilter(key?: string)
resetSort()
resetPagination()
resetAll()
resetPaginationAndFilter()
}Il a besoin du service qui gère les éléments en question, d'un queryManager qui gère les filtres du service et du modèle représentant les éléments.
Le TableComponent donne aussi accès à deux autres propriétés protégées : service, queryManager {.is-warning}
Cette classe est a étendre sur un component qui utilise MatTable de Material :
@Component({
selector: 'app-mon-component',
templateUrl: './mon-component.component.html',
styleUrls: ['./mon-component.component.scss'],
})
export class MonComponent extends TableComponent<MonModel, MonModelService> {
get mesElements() {
return this.service.entityList
}
get mesElements$() {
return this.service.entityList$
}
constructor(protected injector: Injector) {
/**
* !!! Le modelService et le modelQueryManager sont required !!!
*/
super(injector, MonModelService, MonModelQueryManager)
}
ngOnInit() {
super.ngOnInit()
this.usersService.find().subscribe()
}
}Ainsi il est possible d'utiliser côté HTML, le formulaire de filtre, la table avec les sorts, la pagination :
<form [formGroup]="filterForm">
<!-- connecter vos filtres ( queryBuilder ) avec un formControlName -->
<input
formControlName="search"
matInput
placeholder="{{'SEARCH'|translate}}"
/>
<!-- resetPaginationAndFilter() permet de tout remettre à zéro -->
<button mat-button (click)="resetPaginationAndFilter()">Reset</button>
<!-- Vous pouvez accéder aux valeurs des filtres : -->
<div *ngIf="queriesBuilders.search.value === 'hello'">
Don't write hello into this search bar please !
</div>
</form>
<!-- connecter le matSortChange avec le onSortChange($event) -->
<table
mat-table
[dataSource]="mesElements$ | async"
matSort
(matSortChange)="onSortChange($event)"
>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{element.name}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</table>
<h3 *ngIf="mesElements.length === 0">Pas de résultats</h3>
<!--Les valeurs de la pagination sont à l'image de FeathersJS-->
<mat-paginator
[length]="$total"
[pageIndex]="$index"
[pageSize]="$limit"
[pageSizeOptions]="[5, 10, 25, 100]"
(page)="onPaginationChange($event)"
></mat-paginator>AlertComponent
Le alertComponent de chez Material est aussi disponible via Carrot :D
Helpers
Des helpers viennent compléter ceux déjà fournis par Lodash ( disponible dans le rayon suppositoire de Carrot ).
Voici une fausse interface :
export interface IHelpers {
static isId(value: any): boolean
static isModel(value: any): boolean
static isSet(value: any): boolean
static isEmpty(value: any): boolean
static isRealNumber(value: any)
}