Skip to content

Commit

Permalink
✨ feat: add generate store command
Browse files Browse the repository at this point in the history
  • Loading branch information
gleisonkz committed Jun 25, 2022
1 parent 6ca75d0 commit f08f0be
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 6 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@



<div align="center">

```bash
Expand Down Expand Up @@ -56,8 +53,6 @@ A CLI built to modernize, standardize and make it easier to create/update Angula
<a href="#balance_scale-license">License</a>
</p>



## :information_source: About

<div align="center">
Expand Down Expand Up @@ -159,6 +154,17 @@ ngxd generate service api <service-name>
ngxd g s a <service-name>
```

### Stores

##### :hammer_and_wrench: **ng-simple-state**

```bash
# create a new ng-simple-state store
ngxd generate store ng-simple-state <store-name>
# or
ngxd g st sst <store-name>
```

## :boy: **Author**

<div align="center">
Expand Down
2 changes: 1 addition & 1 deletion src/commands/generate/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const COMMAND: GluegunCommand = {

const GENERATE_MODEL_TYPE_QUESTION = 'Qual o tipo de entidade que você deseja criar?';

const GENERATE_MODEL_TYPE_OPTIONS = ['component', 'directive', 'guard', 'interceptor', 'module'];
const GENERATE_MODEL_TYPE_OPTIONS = ['component', 'directive', 'guard', 'interceptor', 'module', 'store'];

const modelTypeResponse: GluegunAskResponse = await prompt.ask({
type: 'select',
Expand Down
142 changes: 142 additions & 0 deletions src/commands/generate/store/ng-simple-state/ng-simple-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { filesystem } from 'gluegun';

import { runNgxdCLI } from '../../../../utils/cli-test-setup';
import { getLineNumber } from '../../../../utils/functions.test.helper';

describe('Commands: [Generate] => [Store] => [NgSimpleState]', () => {
const TESTING_DIR = '__SST_TEST__';
const COMMAND = 'g st sst';

beforeEach(() => {
jest.useFakeTimers();
jest.setTimeout(100000);
});

afterEach(() => {
jest.clearAllTimers();
});

afterAll(() => {
filesystem.remove(TESTING_DIR);
});

test('should generate a ng-simple-state store with 2 files', async () => {
const name = 'base-fruit';
await runNgxdCLI(`${COMMAND} ${name}`);

const ts = filesystem.read(`${name}/${name}.store.ts`);
const spec = filesystem.read(`${name}/${name}.store.spec.ts`);

expect(ts).toBeDefined();
expect(spec).toBeDefined();

filesystem.remove(name);
});

test('should generate a ng-simple-state store at given path', async () => {
const path = `${TESTING_DIR}/store`;
const name = 'fruit1';

await runNgxdCLI(`${COMMAND} ${name} --path ${path}`);

const ts = filesystem.read(`${path}/${name}/${name}.store.ts`);
const spec = filesystem.read(`${path}/${name}/${name}.store.spec.ts`);

expect(ts).toBeDefined();
expect(spec).toBeDefined();
});

test('should generate a ng-simple-state store.ts file with correct content', async () => {
const path = `${TESTING_DIR}/store`;
const name = 'fruit2';

await runNgxdCLI(`${COMMAND} ${name} --path ${path}`);

const ts = filesystem.read(`${path}/${name}/${name}.store.ts`);

const lines = ts.split(/\r?\n/);

expect(getLineNumber(lines, 1)).toContain(`import { Injectable, Injector } from '@angular/core'`);

expect(getLineNumber(lines, 3)).toContain(`import { NgSimpleStateBaseStore } from 'ng-simple-state'`);
expect(getLineNumber(lines, 4)).toContain(`import { Observable, switchMap, tap } from 'rxjs'`);

expect(getLineNumber(lines, 6)).toContain(`export interface Fruit2State {`);
expect(getLineNumber(lines, 7)).toContain(`fruit2s: Fruit2Response[];`);
expect(getLineNumber(lines, 8)).toContain(`}`);

expect(getLineNumber(lines, 10)).toContain(`export const FRUIT2_INITIAL_STATE: Fruit2State = {`);
expect(getLineNumber(lines, 11)).toContain(`fruit2s: [],`);
expect(getLineNumber(lines, 12)).toContain(`}`);

expect(getLineNumber(lines, 14)).toContain(`@Injectable()`);
expect(getLineNumber(lines, 15)).toContain(
`export class Fruit2Store extends NgSimpleStateBaseStore<Fruit2State> {`
);
expect(getLineNumber(lines, 16)).toContain('constructor(');
expect(getLineNumber(lines, 17)).toContain('injector: Injector,');
expect(getLineNumber(lines, 18)).toContain('private readonly fruit2ApiService: Fruit2ApiService');
expect(getLineNumber(lines, 19)).toContain(') {');
expect(getLineNumber(lines, 20)).toContain('super(injector);');
expect(getLineNumber(lines, 21)).toContain('}');

expect(getLineNumber(lines, 23)).toContain('initialState(): Fruit2State {');
expect(getLineNumber(lines, 24)).toContain('return FRUIT2_INITIAL_STATE;');
expect(getLineNumber(lines, 25)).toContain('}');

expect(getLineNumber(lines, 27)).toContain('create(fruit2Request: Fruit2Request): Observable<Fruit2Response> {');
expect(getLineNumber(lines, 28)).toContain('return this.fruit2ApiService.create(fruit2Request).pipe(');
expect(getLineNumber(lines, 29)).toContain('tap((fruit2) => {');
expect(getLineNumber(lines, 30)).toContain(
'this.setState((state) => ({ ...state, fruit2s: [...state.fruit2s, fruit2] }));'
);
expect(getLineNumber(lines, 31)).toContain('})');
expect(getLineNumber(lines, 32)).toContain(');');
expect(getLineNumber(lines, 33)).toContain('}');

expect(getLineNumber(lines, 35)).toContain('update(fruit2ID: number, fruit2Request: Fruit2Request) {');
expect(getLineNumber(lines, 36)).toContain('return this.fruit2ApiService.update(fruit2ID, fruit2Request).pipe(');
expect(getLineNumber(lines, 37)).toContain('tap((fruit2) => {');
expect(getLineNumber(lines, 38)).toContain('this.setState((state) => {');
expect(getLineNumber(lines, 39)).toContain(
'const targetFruit2Index = state.fruit2s.findIndex((item) => item.fruit2ID === fruit2ID);'
);
expect(getLineNumber(lines, 40)).toContain('const fruit2s = [...state.fruit2s];');
expect(getLineNumber(lines, 41)).toContain('fruit2s[targetFruit2Index] = fruit2;');
expect(getLineNumber(lines, 42)).toContain('return { ...state, fruit2s };');
expect(getLineNumber(lines, 43)).toContain('});');
expect(getLineNumber(lines, 44)).toContain('})');
expect(getLineNumber(lines, 45)).toContain(');');
expect(getLineNumber(lines, 46)).toContain('}');

expect(getLineNumber(lines, 48)).toContain('remove(fruit2ID: number) {');
expect(getLineNumber(lines, 49)).toContain('return this.fruit2ApiService.remove(fruit2ID).pipe(');
expect(getLineNumber(lines, 50)).toContain('tap(() => {');
expect(getLineNumber(lines, 51)).toContain('this.setState((state) => ({');
expect(getLineNumber(lines, 52)).toContain(
'fruit2s: state.fruit2s.filter((fruit2) => fruit2.fruit2ID !== fruit2ID),'
);
expect(getLineNumber(lines, 53)).toContain('}));');
expect(getLineNumber(lines, 54)).toContain('})');
expect(getLineNumber(lines, 55)).toContain(');');
expect(getLineNumber(lines, 56)).toContain('}');

expect(getLineNumber(lines, 58)).toContain('findAll(fruit2ID: number): Observable<Fruit2Response[]> {');
expect(getLineNumber(lines, 59)).toContain('return this.fruit2ApiService.findAll(+ministryID).pipe(');
expect(getLineNumber(lines, 60)).toContain('tap((fruit2s) => {');
expect(getLineNumber(lines, 61)).toContain('this.setState((state) => ({ ...state, fruit2s }));');
expect(getLineNumber(lines, 62)).toContain('})');
expect(getLineNumber(lines, 63)).toContain('switchMap(() => this.selectState((state) => state.fruit2s))');
expect(getLineNumber(lines, 64)).toContain(');');
expect(getLineNumber(lines, 65)).toContain('}');

expect(getLineNumber(lines, 67)).toContain('findByID(fruit2ID: number): Observable<Fruit2Response> {');
expect(getLineNumber(lines, 68)).toContain('return this.fruit2ApiService.findByID(fruit2ID);');
expect(getLineNumber(lines, 69)).toContain('}');
expect(getLineNumber(lines, 70)).toContain('}');

expect(lines.length).toBe(71);

filesystem.remove(`${name}`);
});
});
51 changes: 51 additions & 0 deletions src/commands/generate/store/ng-simple-state/ng-simple-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GluegunCommand, GluegunToolbox, strings } from 'gluegun';

import { getEntityName, getEntityPath, printCreated } from '../../../../utils/functions.helper';

const COMMAND: GluegunCommand = {
name: 'ng-simple-state',
alias: ['sst'],
description: 'cria uma store do tipo NgSimpleState',
run: async (toolbox: GluegunToolbox) => {
const { parameters, print, prompt, template } = toolbox;
const {
options: { path }
} = parameters;

const storeName = parameters.first ?? (await getEntityName(prompt, 'store'));
const storePath = getEntityPath(path, storeName);

function toConstantCase(str: string) {
return str.replace(/([A-Z])/g, '_$1').toUpperCase();
}

const nameConstantCase = toConstantCase(storeName);

template.generate({
template: 'ng-simple-state.template.ts.ejs',
target: `${storePath}.store.ts`,
props: {
type: 'store',
name: storeName,
nameConstantCase,
...strings,
toConstantCase
}
});

template.generate({
template: 'ng-simple-state.template.spec.ts.ejs',
target: `${storePath}.store.spec.ts`,
props: {
type: 'store',
name: storeName,
...strings
}
});

printCreated(print, `${storePath}.store.ts`);
printCreated(print, `${storePath}.store.spec.ts`);
}
};

module.exports = COMMAND;
34 changes: 34 additions & 0 deletions src/commands/generate/store/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { GluegunCommand, GluegunToolbox } from 'gluegun';
import { GluegunAskResponse } from 'gluegun/build/types/toolbox/prompt-types';

import { findCommand } from '../../../utils/functions.helper';

const COMMAND: GluegunCommand = {
name: 'store',
alias: ['st'],
description: 'Cria uma store para gerenciar o estado da aplicação',

run: async (toolbox: GluegunToolbox) => {
const { parameters, prompt } = toolbox;

const storeName = parameters.first;

const question = 'Qual tipo de store você deseja criar?';
const availableTypes = ['ng-simple-state'];

const storeTypeResponse: GluegunAskResponse = await prompt.ask({
type: 'select',
name: 'type',
message: question,
choices: availableTypes
});

const storeType = storeTypeResponse.type;
const command = findCommand(toolbox, storeType);

toolbox.parameters.first = storeName;
command?.run(toolbox);
}
};

module.exports = COMMAND;
26 changes: 26 additions & 0 deletions src/templates/ng-simple-state.template.spec.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NgSimpleStateModule } from 'ng-simple-state';

import { TestBed } from '@angular/core/testing';

describe('[Store] => <%= pascalCase(props.type) %>', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NgSimpleStateModule.forRoot({
enableDevTool: false,
enableLocalStorage: false
})
]
});
});

it('should successfully get initial state', () => {});

it('should retrieve all <%= camelCase(props.type) %>s', () => {});

it('should retrieve a <%= camelCase(props.type) %> by ID', () => {});

it('should create a <%= camelCase(props.type) %>', () => {});

it('should delete a <%= camelCase(props.type) %> from the store', () => {});
});
70 changes: 70 additions & 0 deletions src/templates/ng-simple-state.template.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Injectable, Injector } from '@angular/core';

import { NgSimpleStateBaseStore } from 'ng-simple-state';
import { Observable, switchMap, tap } from 'rxjs';

export interface <%= pascalCase(props.name) %>State {
<%= camelCase(props.name) %>s: <%= pascalCase(props.name) %>Response[];
}

export const <%= props.nameConstantCase %>_INITIAL_STATE: <%= pascalCase(props.name) %>State = {
<%= camelCase(props.name) %>s: [],
};

@Injectable()
export class <%= pascalCase(props.name) %>Store extends NgSimpleStateBaseStore<<%= pascalCase(props.name) %>State> {
constructor(
injector: Injector,
private readonly <%= camelCase(props.name) %>ApiService: <%= pascalCase(props.name) %>ApiService,
) {
super(injector);
}

initialState(): <%= pascalCase(props.name) %>State {
return <%= props.nameConstantCase %>_INITIAL_STATE;
}

create(<%= camelCase(props.name) %>Request: <%= pascalCase(props.name) %>Request): Observable<<%= pascalCase(props.name) %>Response> {
return this.<%= camelCase(props.name) %>ApiService.create(<%= camelCase(props.name) %>Request).pipe(
tap((<%= camelCase(props.name) %>) => {
this.setState((state) => ({ ...state, <%= camelCase(props.name) %>s: [...state.<%= camelCase(props.name) %>s, <%= camelCase(props.name) %>] }));
})
);
}

update(<%= camelCase(props.name) %>ID: number, <%= camelCase(props.name) %>Request: <%= pascalCase(props.name) %>Request) {
return this.<%= camelCase(props.name) %>ApiService.update(<%= camelCase(props.name) %>ID, <%= camelCase(props.name) %>Request).pipe(
tap((<%= camelCase(props.name) %>) => {
this.setState((state) => {
const target<%= pascalCase(props.name) %>Index = state.<%= camelCase(props.name) %>s.findIndex((item) => item.<%= camelCase(props.name) %>ID === <%= camelCase(props.name) %>ID);
const <%= camelCase(props.name) %>s = [...state.<%= camelCase(props.name) %>s];
<%= camelCase(props.name) %>s[target<%= pascalCase(props.name) %>Index] = <%= camelCase(props.name) %>;
return { ...state, <%= camelCase(props.name) %>s };
});
})
);
}

remove(<%= camelCase(props.name) %>ID: number) {
return this.<%= camelCase(props.name) %>ApiService.remove(<%= camelCase(props.name) %>ID).pipe(
tap(() => {
this.setState((state) => ({
<%= camelCase(props.name) %>s: state.<%= camelCase(props.name) %>s.filter((<%= camelCase(props.name) %>) => <%= camelCase(props.name) %>.<%= camelCase(props.name) %>ID !== <%= camelCase(props.name) %>ID),
}));
})
);
}

findAll(<%= camelCase(props.name) %>ID: number): Observable<<%= pascalCase(props.name) %>Response[]> {
return this.<%= camelCase(props.name) %>ApiService.findAll(+ministryID).pipe(
tap((<%= camelCase(props.name) %>s) => {
this.setState((state) => ({ ...state, <%= camelCase(props.name) %>s }));
}),
switchMap(() => this.selectState((state) => state.<%= camelCase(props.name) %>s))
);
}

findByID(<%= camelCase(props.name) %>ID: number): Observable<<%= pascalCase(props.name) %>Response> {
return this.<%= camelCase(props.name) %>ApiService.findByID(<%= camelCase(props.name) %>ID);
}
}

0 comments on commit f08f0be

Please sign in to comment.