Skip to content

Commit 19ebb0a

Browse files
wgd3wgd3timdeschryver
authored
feat(schematics): add entity generation as part of feature schematic (#3850)
Co-authored-by: wgd3 <wallace.daniel3@gmail.com> Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
1 parent 0b98a65 commit 19ebb0a

File tree

7 files changed

+284
-48
lines changed

7 files changed

+284
-48
lines changed

modules/schematics/src/entity/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
"default": false,
5353
"description": "Group actions, reducers and effects within relative subfolders",
5454
"aliases": ["g"]
55+
},
56+
"feature": {
57+
"type": "boolean",
58+
"default": false,
59+
"description": "Flag to indicate if part of a feature schematic.",
60+
"visible": false
5561
}
5662
},
5763
"required": []

modules/schematics/src/entity/schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ export interface Schema {
3636
*/
3737

3838
group?: boolean;
39+
40+
/**
41+
* Specifies if this is grouped within a feature
42+
*/
43+
feature?: boolean;
3944
}

modules/schematics/src/feature/__snapshots__/index.spec.ts.snap

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,177 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`Feature Schematic should create all files of a feature with an entity 1`] = `
4+
"import { createActionGroup, emptyProps, props } from '@ngrx/store';
5+
import { Update } from '@ngrx/entity';
6+
7+
import { Foo } from './foo.model';
8+
9+
export const FooActions = createActionGroup({
10+
source: 'Foo/API',
11+
events: {
12+
'Load Foos': props<{ foos: Foo[] }>(),
13+
'Add Foo': props<{ foo: Foo }>(),
14+
'Upsert Foo': props<{ foo: Foo }>(),
15+
'Add Foos': props<{ foo: Foo[] }>(),
16+
'Upsert Foos': props<{ foo: Foo[] }>(),
17+
'Update Foo': props<{ foo: Update<Foo> }>(),
18+
'Update Foos': props<{ foos: Update<Foo>[] }>(),
19+
'Delete Foo': props<{ id: string }>(),
20+
'Delete Foos': props<{ ids: string[] }>(),
21+
'Clear Foos': emptyProps(),
22+
}
23+
});
24+
"
25+
`;
26+
27+
exports[`Feature Schematic should create all files of a feature with an entity 2`] = `
28+
"import { createFeature, createReducer, on } from '@ngrx/store';
29+
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
30+
import { Foo } from './foo.model';
31+
import { FooActions } from './foo.actions';
32+
33+
export const foosFeatureKey = 'foos';
34+
35+
export interface State extends EntityState<Foo> {
36+
// additional entities state properties
37+
}
38+
39+
export const adapter: EntityAdapter<Foo> = createEntityAdapter<Foo>();
40+
41+
export const initialState: State = adapter.getInitialState({
42+
// additional entity state properties
43+
});
44+
45+
export const reducer = createReducer(
46+
initialState,
47+
on(FooActions.addFoo,
48+
(state, action) => adapter.addOne(action.foo, state)
49+
),
50+
on(FooActions.upsertFoo,
51+
(state, action) => adapter.upsertOne(action.foo, state)
52+
),
53+
on(FooActions.addFoos,
54+
(state, action) => adapter.addMany(action.foos, state)
55+
),
56+
on(FooActions.upsertFoos,
57+
(state, action) => adapter.upsertMany(action.foos, state)
58+
),
59+
on(FooActions.updateFoo,
60+
(state, action) => adapter.updateOne(action.foo, state)
61+
),
62+
on(FooActions.updateFoos,
63+
(state, action) => adapter.updateMany(action.foos, state)
64+
),
65+
on(FooActions.deleteFoo,
66+
(state, action) => adapter.removeOne(action.id, state)
67+
),
68+
on(FooActions.deleteFoos,
69+
(state, action) => adapter.removeMany(action.ids, state)
70+
),
71+
on(FooActions.loadFoos,
72+
(state, action) => adapter.setAll(action.foos, state)
73+
),
74+
on(FooActions.clearFoos,
75+
state => adapter.removeAll(state)
76+
),
77+
);
78+
79+
export const foosFeature = createFeature({
80+
name: foosFeatureKey,
81+
reducer,
82+
extraSelectors: ({ selectFoosState }) => ({
83+
...adapter.getSelectors(selectFoosState)
84+
}),
85+
});
86+
87+
export const {
88+
selectIds,
89+
selectEntities,
90+
selectAll,
91+
selectTotal,
92+
} = foosFeature;
93+
"
94+
`;
95+
96+
exports[`Feature Schematic should create all files of a feature with an entity 3`] = `
97+
"import { reducer, initialState } from './foo.reducer';
98+
99+
describe('Foo Reducer', () => {
100+
describe('unknown action', () => {
101+
it('should return the previous state', () => {
102+
const action = {} as any;
103+
104+
const result = reducer(initialState, action);
105+
106+
expect(result).toBe(initialState);
107+
});
108+
});
109+
});
110+
"
111+
`;
112+
113+
exports[`Feature Schematic should create all files of a feature with an entity 4`] = `
114+
"import { Injectable } from '@angular/core';
115+
import { Actions, createEffect, ofType } from '@ngrx/effects';
116+
117+
import { concatMap } from 'rxjs/operators';
118+
import { Observable, EMPTY } from 'rxjs';
119+
import { FooActions } from './foo.actions';
120+
121+
@Injectable()
122+
export class FooEffects {
123+
124+
125+
loadFoos$ = createEffect(() => {
126+
return this.actions$.pipe(
127+
128+
ofType(FooActions.loadFoos),
129+
/** An EMPTY observable only emits completion. Replace with your own observable API request */
130+
concatMap(() => EMPTY as Observable<{ type: string }>)
131+
);
132+
});
133+
134+
constructor(private actions$: Actions) {}
135+
}
136+
"
137+
`;
138+
139+
exports[`Feature Schematic should create all files of a feature with an entity 5`] = `
140+
"import { TestBed } from '@angular/core/testing';
141+
import { provideMockActions } from '@ngrx/effects/testing';
142+
import { Observable } from 'rxjs';
143+
144+
import { FooEffects } from './foo.effects';
145+
146+
describe('FooEffects', () => {
147+
let actions$: Observable<any>;
148+
let effects: FooEffects;
149+
150+
beforeEach(() => {
151+
TestBed.configureTestingModule({
152+
providers: [
153+
FooEffects,
154+
provideMockActions(() => actions$)
155+
]
156+
});
157+
158+
effects = TestBed.inject(FooEffects);
159+
});
160+
161+
it('should be created', () => {
162+
expect(effects).toBeTruthy();
163+
});
164+
});
165+
"
166+
`;
167+
168+
exports[`Feature Schematic should create all files of a feature with an entity 6`] = `
169+
"export interface Foo {
170+
id: string;
171+
}
172+
"
173+
`;
174+
3175
exports[`Feature Schematic should have all api actions in reducer if api flag enabled 1`] = `
4176
"import { createFeature, createReducer, on } from '@ngrx/store';
5177
import { FooActions } from './foo.actions';

modules/schematics/src/feature/index.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('Feature Schematic', () => {
2121
project: 'bar',
2222
module: '',
2323
group: false,
24+
entity: false,
2425
};
2526

2627
const projectPath = getTestProjectPath();
@@ -64,6 +65,7 @@ describe('Feature Schematic', () => {
6465
expect(
6566
files.includes(`${projectPath}/src/app/foo.selectors.spec.ts`)
6667
).toBeTruthy();
68+
expect(files.includes(`${projectPath}/src/app/foo.model.ts`)).toBeFalsy();
6769
});
6870

6971
it('should not create test files when skipTests is true', async () => {
@@ -269,4 +271,27 @@ describe('Feature Schematic', () => {
269271

270272
expect(fileContent).toMatchSnapshot();
271273
});
274+
275+
it('should create all files of a feature with an entity', async () => {
276+
const options = { ...defaultOptions, entity: true };
277+
278+
const tree = await schematicRunner.runSchematic(
279+
'feature',
280+
options,
281+
appTree
282+
);
283+
const paths = [
284+
`${projectPath}/src/app/foo.actions.ts`,
285+
`${projectPath}/src/app/foo.reducer.ts`,
286+
`${projectPath}/src/app/foo.reducer.spec.ts`,
287+
`${projectPath}/src/app/foo.effects.ts`,
288+
`${projectPath}/src/app/foo.effects.spec.ts`,
289+
`${projectPath}/src/app/foo.model.ts`,
290+
];
291+
292+
paths.forEach((path) => {
293+
expect(tree.files.includes(path)).toBeTruthy();
294+
expect(tree.readContent(path)).toMatchSnapshot();
295+
});
296+
});
272297
});
Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,78 @@
11
import {
2+
chain,
23
Rule,
4+
schematic,
35
SchematicContext,
46
Tree,
5-
chain,
6-
schematic,
77
} from '@angular-devkit/schematics';
8+
89
import { Schema as FeatureOptions } from './schema';
910

1011
export default function (options: FeatureOptions): Rule {
1112
return (host: Tree, context: SchematicContext) => {
12-
return chain([
13-
schematic('action', {
14-
flat: options.flat,
15-
group: options.group,
16-
name: options.name,
17-
path: options.path,
18-
project: options.project,
19-
skipTests: options.skipTests,
20-
api: options.api,
21-
prefix: options.prefix,
22-
}),
23-
schematic('reducer', {
24-
flat: options.flat,
25-
group: options.group,
26-
module: options.module,
27-
name: options.name,
28-
path: options.path,
29-
project: options.project,
30-
skipTests: options.skipTests,
31-
reducers: options.reducers,
32-
feature: true,
33-
api: options.api,
34-
prefix: options.prefix,
35-
}),
36-
schematic('effect', {
37-
flat: options.flat,
38-
group: options.group,
39-
module: options.module,
40-
name: options.name,
41-
path: options.path,
42-
project: options.project,
43-
skipTests: options.skipTests,
44-
feature: true,
45-
api: options.api,
46-
prefix: options.prefix,
47-
}),
48-
schematic('selector', {
49-
flat: options.flat,
50-
group: options.group,
51-
name: options.name,
52-
path: options.path,
53-
project: options.project,
54-
skipTests: options.skipTests,
55-
feature: true,
56-
}),
57-
])(host, context);
13+
return chain(
14+
(options.entity
15+
? [
16+
schematic('entity', {
17+
name: options.name,
18+
path: options.path,
19+
project: options.project,
20+
flat: options.flat,
21+
skipTests: options.skipTests,
22+
module: options.module,
23+
reducers: options.reducers,
24+
group: options.group,
25+
feature: true,
26+
}),
27+
]
28+
: [
29+
schematic('action', {
30+
flat: options.flat,
31+
group: options.group,
32+
name: options.name,
33+
path: options.path,
34+
project: options.project,
35+
skipTests: options.skipTests,
36+
api: options.api,
37+
prefix: options.prefix,
38+
}),
39+
schematic('reducer', {
40+
flat: options.flat,
41+
group: options.group,
42+
module: options.module,
43+
name: options.name,
44+
path: options.path,
45+
project: options.project,
46+
skipTests: options.skipTests,
47+
reducers: options.reducers,
48+
feature: true,
49+
api: options.api,
50+
prefix: options.prefix,
51+
}),
52+
schematic('selector', {
53+
flat: options.flat,
54+
group: options.group,
55+
name: options.name,
56+
path: options.path,
57+
project: options.project,
58+
skipTests: options.skipTests,
59+
feature: true,
60+
}),
61+
]
62+
).concat([
63+
schematic('effect', {
64+
flat: options.flat,
65+
group: options.group,
66+
module: options.module,
67+
name: options.name,
68+
path: options.path,
69+
project: options.project,
70+
skipTests: options.skipTests,
71+
feature: true,
72+
api: options.api,
73+
prefix: options.prefix,
74+
}),
75+
])
76+
)(host, context);
5877
};
5978
}

modules/schematics/src/feature/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@
6565
"type": "string",
6666
"default": "load",
6767
"x-prompt": "What should be the prefix of the action, effect and reducer?"
68+
},
69+
"entity": {
70+
"description": "Toggle whether an entity is created as part of the feature",
71+
"type": "boolean",
72+
"aliases": ["e"],
73+
"x-prompt": "Should we use @ngrx/entity to create the reducer?",
74+
"default": "false"
6875
}
6976
},
7077
"required": []

modules/schematics/src/feature/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ export interface Schema {
4646
api?: boolean;
4747

4848
prefix?: string;
49+
50+
entity?: boolean;
4951
}

0 commit comments

Comments
 (0)