Skip to content

Commit f08f0be

Browse files
committed
✨ feat: add generate store command
1 parent 6ca75d0 commit f08f0be

File tree

7 files changed

+335
-6
lines changed

7 files changed

+335
-6
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
2-
3-
41
<div align="center">
52

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

59-
60-
6156
## :information_source: About
6257

6358
<div align="center">
@@ -159,6 +154,17 @@ ngxd generate service api <service-name>
159154
ngxd g s a <service-name>
160155
```
161156

157+
### Stores
158+
159+
##### :hammer_and_wrench: **ng-simple-state**
160+
161+
```bash
162+
# create a new ng-simple-state store
163+
ngxd generate store ng-simple-state <store-name>
164+
# or
165+
ngxd g st sst <store-name>
166+
```
167+
162168
## :boy: **Author**
163169

164170
<div align="center">

src/commands/generate/generate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const COMMAND: GluegunCommand = {
1212

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

15-
const GENERATE_MODEL_TYPE_OPTIONS = ['component', 'directive', 'guard', 'interceptor', 'module'];
15+
const GENERATE_MODEL_TYPE_OPTIONS = ['component', 'directive', 'guard', 'interceptor', 'module', 'store'];
1616

1717
const modelTypeResponse: GluegunAskResponse = await prompt.ask({
1818
type: 'select',
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { filesystem } from 'gluegun';
2+
3+
import { runNgxdCLI } from '../../../../utils/cli-test-setup';
4+
import { getLineNumber } from '../../../../utils/functions.test.helper';
5+
6+
describe('Commands: [Generate] => [Store] => [NgSimpleState]', () => {
7+
const TESTING_DIR = '__SST_TEST__';
8+
const COMMAND = 'g st sst';
9+
10+
beforeEach(() => {
11+
jest.useFakeTimers();
12+
jest.setTimeout(100000);
13+
});
14+
15+
afterEach(() => {
16+
jest.clearAllTimers();
17+
});
18+
19+
afterAll(() => {
20+
filesystem.remove(TESTING_DIR);
21+
});
22+
23+
test('should generate a ng-simple-state store with 2 files', async () => {
24+
const name = 'base-fruit';
25+
await runNgxdCLI(`${COMMAND} ${name}`);
26+
27+
const ts = filesystem.read(`${name}/${name}.store.ts`);
28+
const spec = filesystem.read(`${name}/${name}.store.spec.ts`);
29+
30+
expect(ts).toBeDefined();
31+
expect(spec).toBeDefined();
32+
33+
filesystem.remove(name);
34+
});
35+
36+
test('should generate a ng-simple-state store at given path', async () => {
37+
const path = `${TESTING_DIR}/store`;
38+
const name = 'fruit1';
39+
40+
await runNgxdCLI(`${COMMAND} ${name} --path ${path}`);
41+
42+
const ts = filesystem.read(`${path}/${name}/${name}.store.ts`);
43+
const spec = filesystem.read(`${path}/${name}/${name}.store.spec.ts`);
44+
45+
expect(ts).toBeDefined();
46+
expect(spec).toBeDefined();
47+
});
48+
49+
test('should generate a ng-simple-state store.ts file with correct content', async () => {
50+
const path = `${TESTING_DIR}/store`;
51+
const name = 'fruit2';
52+
53+
await runNgxdCLI(`${COMMAND} ${name} --path ${path}`);
54+
55+
const ts = filesystem.read(`${path}/${name}/${name}.store.ts`);
56+
57+
const lines = ts.split(/\r?\n/);
58+
59+
expect(getLineNumber(lines, 1)).toContain(`import { Injectable, Injector } from '@angular/core'`);
60+
61+
expect(getLineNumber(lines, 3)).toContain(`import { NgSimpleStateBaseStore } from 'ng-simple-state'`);
62+
expect(getLineNumber(lines, 4)).toContain(`import { Observable, switchMap, tap } from 'rxjs'`);
63+
64+
expect(getLineNumber(lines, 6)).toContain(`export interface Fruit2State {`);
65+
expect(getLineNumber(lines, 7)).toContain(`fruit2s: Fruit2Response[];`);
66+
expect(getLineNumber(lines, 8)).toContain(`}`);
67+
68+
expect(getLineNumber(lines, 10)).toContain(`export const FRUIT2_INITIAL_STATE: Fruit2State = {`);
69+
expect(getLineNumber(lines, 11)).toContain(`fruit2s: [],`);
70+
expect(getLineNumber(lines, 12)).toContain(`}`);
71+
72+
expect(getLineNumber(lines, 14)).toContain(`@Injectable()`);
73+
expect(getLineNumber(lines, 15)).toContain(
74+
`export class Fruit2Store extends NgSimpleStateBaseStore<Fruit2State> {`
75+
);
76+
expect(getLineNumber(lines, 16)).toContain('constructor(');
77+
expect(getLineNumber(lines, 17)).toContain('injector: Injector,');
78+
expect(getLineNumber(lines, 18)).toContain('private readonly fruit2ApiService: Fruit2ApiService');
79+
expect(getLineNumber(lines, 19)).toContain(') {');
80+
expect(getLineNumber(lines, 20)).toContain('super(injector);');
81+
expect(getLineNumber(lines, 21)).toContain('}');
82+
83+
expect(getLineNumber(lines, 23)).toContain('initialState(): Fruit2State {');
84+
expect(getLineNumber(lines, 24)).toContain('return FRUIT2_INITIAL_STATE;');
85+
expect(getLineNumber(lines, 25)).toContain('}');
86+
87+
expect(getLineNumber(lines, 27)).toContain('create(fruit2Request: Fruit2Request): Observable<Fruit2Response> {');
88+
expect(getLineNumber(lines, 28)).toContain('return this.fruit2ApiService.create(fruit2Request).pipe(');
89+
expect(getLineNumber(lines, 29)).toContain('tap((fruit2) => {');
90+
expect(getLineNumber(lines, 30)).toContain(
91+
'this.setState((state) => ({ ...state, fruit2s: [...state.fruit2s, fruit2] }));'
92+
);
93+
expect(getLineNumber(lines, 31)).toContain('})');
94+
expect(getLineNumber(lines, 32)).toContain(');');
95+
expect(getLineNumber(lines, 33)).toContain('}');
96+
97+
expect(getLineNumber(lines, 35)).toContain('update(fruit2ID: number, fruit2Request: Fruit2Request) {');
98+
expect(getLineNumber(lines, 36)).toContain('return this.fruit2ApiService.update(fruit2ID, fruit2Request).pipe(');
99+
expect(getLineNumber(lines, 37)).toContain('tap((fruit2) => {');
100+
expect(getLineNumber(lines, 38)).toContain('this.setState((state) => {');
101+
expect(getLineNumber(lines, 39)).toContain(
102+
'const targetFruit2Index = state.fruit2s.findIndex((item) => item.fruit2ID === fruit2ID);'
103+
);
104+
expect(getLineNumber(lines, 40)).toContain('const fruit2s = [...state.fruit2s];');
105+
expect(getLineNumber(lines, 41)).toContain('fruit2s[targetFruit2Index] = fruit2;');
106+
expect(getLineNumber(lines, 42)).toContain('return { ...state, fruit2s };');
107+
expect(getLineNumber(lines, 43)).toContain('});');
108+
expect(getLineNumber(lines, 44)).toContain('})');
109+
expect(getLineNumber(lines, 45)).toContain(');');
110+
expect(getLineNumber(lines, 46)).toContain('}');
111+
112+
expect(getLineNumber(lines, 48)).toContain('remove(fruit2ID: number) {');
113+
expect(getLineNumber(lines, 49)).toContain('return this.fruit2ApiService.remove(fruit2ID).pipe(');
114+
expect(getLineNumber(lines, 50)).toContain('tap(() => {');
115+
expect(getLineNumber(lines, 51)).toContain('this.setState((state) => ({');
116+
expect(getLineNumber(lines, 52)).toContain(
117+
'fruit2s: state.fruit2s.filter((fruit2) => fruit2.fruit2ID !== fruit2ID),'
118+
);
119+
expect(getLineNumber(lines, 53)).toContain('}));');
120+
expect(getLineNumber(lines, 54)).toContain('})');
121+
expect(getLineNumber(lines, 55)).toContain(');');
122+
expect(getLineNumber(lines, 56)).toContain('}');
123+
124+
expect(getLineNumber(lines, 58)).toContain('findAll(fruit2ID: number): Observable<Fruit2Response[]> {');
125+
expect(getLineNumber(lines, 59)).toContain('return this.fruit2ApiService.findAll(+ministryID).pipe(');
126+
expect(getLineNumber(lines, 60)).toContain('tap((fruit2s) => {');
127+
expect(getLineNumber(lines, 61)).toContain('this.setState((state) => ({ ...state, fruit2s }));');
128+
expect(getLineNumber(lines, 62)).toContain('})');
129+
expect(getLineNumber(lines, 63)).toContain('switchMap(() => this.selectState((state) => state.fruit2s))');
130+
expect(getLineNumber(lines, 64)).toContain(');');
131+
expect(getLineNumber(lines, 65)).toContain('}');
132+
133+
expect(getLineNumber(lines, 67)).toContain('findByID(fruit2ID: number): Observable<Fruit2Response> {');
134+
expect(getLineNumber(lines, 68)).toContain('return this.fruit2ApiService.findByID(fruit2ID);');
135+
expect(getLineNumber(lines, 69)).toContain('}');
136+
expect(getLineNumber(lines, 70)).toContain('}');
137+
138+
expect(lines.length).toBe(71);
139+
140+
filesystem.remove(`${name}`);
141+
});
142+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { GluegunCommand, GluegunToolbox, strings } from 'gluegun';
2+
3+
import { getEntityName, getEntityPath, printCreated } from '../../../../utils/functions.helper';
4+
5+
const COMMAND: GluegunCommand = {
6+
name: 'ng-simple-state',
7+
alias: ['sst'],
8+
description: 'cria uma store do tipo NgSimpleState',
9+
run: async (toolbox: GluegunToolbox) => {
10+
const { parameters, print, prompt, template } = toolbox;
11+
const {
12+
options: { path }
13+
} = parameters;
14+
15+
const storeName = parameters.first ?? (await getEntityName(prompt, 'store'));
16+
const storePath = getEntityPath(path, storeName);
17+
18+
function toConstantCase(str: string) {
19+
return str.replace(/([A-Z])/g, '_$1').toUpperCase();
20+
}
21+
22+
const nameConstantCase = toConstantCase(storeName);
23+
24+
template.generate({
25+
template: 'ng-simple-state.template.ts.ejs',
26+
target: `${storePath}.store.ts`,
27+
props: {
28+
type: 'store',
29+
name: storeName,
30+
nameConstantCase,
31+
...strings,
32+
toConstantCase
33+
}
34+
});
35+
36+
template.generate({
37+
template: 'ng-simple-state.template.spec.ts.ejs',
38+
target: `${storePath}.store.spec.ts`,
39+
props: {
40+
type: 'store',
41+
name: storeName,
42+
...strings
43+
}
44+
});
45+
46+
printCreated(print, `${storePath}.store.ts`);
47+
printCreated(print, `${storePath}.store.spec.ts`);
48+
}
49+
};
50+
51+
module.exports = COMMAND;

src/commands/generate/store/store.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { GluegunCommand, GluegunToolbox } from 'gluegun';
2+
import { GluegunAskResponse } from 'gluegun/build/types/toolbox/prompt-types';
3+
4+
import { findCommand } from '../../../utils/functions.helper';
5+
6+
const COMMAND: GluegunCommand = {
7+
name: 'store',
8+
alias: ['st'],
9+
description: 'Cria uma store para gerenciar o estado da aplicação',
10+
11+
run: async (toolbox: GluegunToolbox) => {
12+
const { parameters, prompt } = toolbox;
13+
14+
const storeName = parameters.first;
15+
16+
const question = 'Qual tipo de store você deseja criar?';
17+
const availableTypes = ['ng-simple-state'];
18+
19+
const storeTypeResponse: GluegunAskResponse = await prompt.ask({
20+
type: 'select',
21+
name: 'type',
22+
message: question,
23+
choices: availableTypes
24+
});
25+
26+
const storeType = storeTypeResponse.type;
27+
const command = findCommand(toolbox, storeType);
28+
29+
toolbox.parameters.first = storeName;
30+
command?.run(toolbox);
31+
}
32+
};
33+
34+
module.exports = COMMAND;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NgSimpleStateModule } from 'ng-simple-state';
2+
3+
import { TestBed } from '@angular/core/testing';
4+
5+
describe('[Store] => <%= pascalCase(props.type) %>', () => {
6+
beforeEach(() => {
7+
TestBed.configureTestingModule({
8+
imports: [
9+
NgSimpleStateModule.forRoot({
10+
enableDevTool: false,
11+
enableLocalStorage: false
12+
})
13+
]
14+
});
15+
});
16+
17+
it('should successfully get initial state', () => {});
18+
19+
it('should retrieve all <%= camelCase(props.type) %>s', () => {});
20+
21+
it('should retrieve a <%= camelCase(props.type) %> by ID', () => {});
22+
23+
it('should create a <%= camelCase(props.type) %>', () => {});
24+
25+
it('should delete a <%= camelCase(props.type) %> from the store', () => {});
26+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Injectable, Injector } from '@angular/core';
2+
3+
import { NgSimpleStateBaseStore } from 'ng-simple-state';
4+
import { Observable, switchMap, tap } from 'rxjs';
5+
6+
export interface <%= pascalCase(props.name) %>State {
7+
<%= camelCase(props.name) %>s: <%= pascalCase(props.name) %>Response[];
8+
}
9+
10+
export const <%= props.nameConstantCase %>_INITIAL_STATE: <%= pascalCase(props.name) %>State = {
11+
<%= camelCase(props.name) %>s: [],
12+
};
13+
14+
@Injectable()
15+
export class <%= pascalCase(props.name) %>Store extends NgSimpleStateBaseStore<<%= pascalCase(props.name) %>State> {
16+
constructor(
17+
injector: Injector,
18+
private readonly <%= camelCase(props.name) %>ApiService: <%= pascalCase(props.name) %>ApiService,
19+
) {
20+
super(injector);
21+
}
22+
23+
initialState(): <%= pascalCase(props.name) %>State {
24+
return <%= props.nameConstantCase %>_INITIAL_STATE;
25+
}
26+
27+
create(<%= camelCase(props.name) %>Request: <%= pascalCase(props.name) %>Request): Observable<<%= pascalCase(props.name) %>Response> {
28+
return this.<%= camelCase(props.name) %>ApiService.create(<%= camelCase(props.name) %>Request).pipe(
29+
tap((<%= camelCase(props.name) %>) => {
30+
this.setState((state) => ({ ...state, <%= camelCase(props.name) %>s: [...state.<%= camelCase(props.name) %>s, <%= camelCase(props.name) %>] }));
31+
})
32+
);
33+
}
34+
35+
update(<%= camelCase(props.name) %>ID: number, <%= camelCase(props.name) %>Request: <%= pascalCase(props.name) %>Request) {
36+
return this.<%= camelCase(props.name) %>ApiService.update(<%= camelCase(props.name) %>ID, <%= camelCase(props.name) %>Request).pipe(
37+
tap((<%= camelCase(props.name) %>) => {
38+
this.setState((state) => {
39+
const target<%= pascalCase(props.name) %>Index = state.<%= camelCase(props.name) %>s.findIndex((item) => item.<%= camelCase(props.name) %>ID === <%= camelCase(props.name) %>ID);
40+
const <%= camelCase(props.name) %>s = [...state.<%= camelCase(props.name) %>s];
41+
<%= camelCase(props.name) %>s[target<%= pascalCase(props.name) %>Index] = <%= camelCase(props.name) %>;
42+
return { ...state, <%= camelCase(props.name) %>s };
43+
});
44+
})
45+
);
46+
}
47+
48+
remove(<%= camelCase(props.name) %>ID: number) {
49+
return this.<%= camelCase(props.name) %>ApiService.remove(<%= camelCase(props.name) %>ID).pipe(
50+
tap(() => {
51+
this.setState((state) => ({
52+
<%= camelCase(props.name) %>s: state.<%= camelCase(props.name) %>s.filter((<%= camelCase(props.name) %>) => <%= camelCase(props.name) %>.<%= camelCase(props.name) %>ID !== <%= camelCase(props.name) %>ID),
53+
}));
54+
})
55+
);
56+
}
57+
58+
findAll(<%= camelCase(props.name) %>ID: number): Observable<<%= pascalCase(props.name) %>Response[]> {
59+
return this.<%= camelCase(props.name) %>ApiService.findAll(+ministryID).pipe(
60+
tap((<%= camelCase(props.name) %>s) => {
61+
this.setState((state) => ({ ...state, <%= camelCase(props.name) %>s }));
62+
}),
63+
switchMap(() => this.selectState((state) => state.<%= camelCase(props.name) %>s))
64+
);
65+
}
66+
67+
findByID(<%= camelCase(props.name) %>ID: number): Observable<<%= pascalCase(props.name) %>Response> {
68+
return this.<%= camelCase(props.name) %>ApiService.findByID(<%= camelCase(props.name) %>ID);
69+
}
70+
}

0 commit comments

Comments
 (0)