Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schematics): add entity generation as part of feature schematic #3850

Merged
merged 10 commits into from
Aug 5, 2023
6 changes: 6 additions & 0 deletions modules/schematics/src/entity/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
"default": false,
"description": "Group actions, reducers and effects within relative subfolders",
"aliases": ["g"]
},
"feature": {
"type": "boolean",
"default": false,
"description": "Flag to indicate if part of a feature schematic.",
"visible": false
}
},
"required": []
Expand Down
5 changes: 5 additions & 0 deletions modules/schematics/src/entity/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ export interface Schema {
*/

group?: boolean;

/**
* Specifies if this is grouped within a feature
*/
feature?: boolean;
}
172 changes: 172 additions & 0 deletions modules/schematics/src/feature/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Feature Schematic should create all files of a feature with an entity 1`] = `
"import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Update } from '@ngrx/entity';

import { Foo } from './foo.model';

export const FooActions = createActionGroup({
source: 'Foo/API',
events: {
'Load Foos': props<{ foos: Foo[] }>(),
'Add Foo': props<{ foo: Foo }>(),
'Upsert Foo': props<{ foo: Foo }>(),
'Add Foos': props<{ foo: Foo[] }>(),
'Upsert Foos': props<{ foo: Foo[] }>(),
'Update Foo': props<{ foo: Update<Foo> }>(),
'Update Foos': props<{ foos: Update<Foo>[] }>(),
'Delete Foo': props<{ id: string }>(),
'Delete Foos': props<{ ids: string[] }>(),
'Clear Foos': emptyProps(),
}
});
"
`;

exports[`Feature Schematic should create all files of a feature with an entity 2`] = `
"import { createFeature, createReducer, on } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { Foo } from './foo.model';
import { FooActions } from './foo.actions';

export const foosFeatureKey = 'foos';

export interface State extends EntityState<Foo> {
// additional entities state properties
}

export const adapter: EntityAdapter<Foo> = createEntityAdapter<Foo>();

export const initialState: State = adapter.getInitialState({
// additional entity state properties
});

export const reducer = createReducer(
initialState,
on(FooActions.addFoo,
(state, action) => adapter.addOne(action.foo, state)
),
on(FooActions.upsertFoo,
(state, action) => adapter.upsertOne(action.foo, state)
),
on(FooActions.addFoos,
(state, action) => adapter.addMany(action.foos, state)
),
on(FooActions.upsertFoos,
(state, action) => adapter.upsertMany(action.foos, state)
),
on(FooActions.updateFoo,
(state, action) => adapter.updateOne(action.foo, state)
),
on(FooActions.updateFoos,
(state, action) => adapter.updateMany(action.foos, state)
),
on(FooActions.deleteFoo,
(state, action) => adapter.removeOne(action.id, state)
),
on(FooActions.deleteFoos,
(state, action) => adapter.removeMany(action.ids, state)
),
on(FooActions.loadFoos,
(state, action) => adapter.setAll(action.foos, state)
),
on(FooActions.clearFoos,
state => adapter.removeAll(state)
),
);

export const foosFeature = createFeature({
name: foosFeatureKey,
reducer,
extraSelectors: ({ selectFoosState }) => ({
...adapter.getSelectors(selectFoosState)
}),
});

export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = foosFeature;
Comment on lines +87 to +92
Copy link
Member

@timdeschryver timdeschryver Aug 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timdeschryver I've removed the Actions spec file as well as the Selector/Selector spec files from the code in your commit. The Entity schematic doesn't have templates for any of these files at the moment.

You mentioned that we should let the Entity schematic handle these things for now. If you/the team would prefer that selectors be generated as well, I can update the Feature schematic to run the Selector schematic every time.

Snapshots have been updated!

The selectors are already included within this feature file.
If I'm missing something feel free to correct me.

"
`;

exports[`Feature Schematic should create all files of a feature with an entity 3`] = `
"import { reducer, initialState } from './foo.reducer';

describe('Foo Reducer', () => {
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;

const result = reducer(initialState, action);

expect(result).toBe(initialState);
});
});
});
"
`;

exports[`Feature Schematic should create all files of a feature with an entity 4`] = `
"import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';

import { concatMap } from 'rxjs/operators';
import { Observable, EMPTY } from 'rxjs';
import { FooActions } from './foo.actions';

@Injectable()
export class FooEffects {


loadFoos$ = createEffect(() => {
return this.actions$.pipe(

ofType(FooActions.loadFoos),
/** An EMPTY observable only emits completion. Replace with your own observable API request */
concatMap(() => EMPTY as Observable<{ type: string }>)
);
});

constructor(private actions$: Actions) {}
}
"
`;

exports[`Feature Schematic should create all files of a feature with an entity 5`] = `
"import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';

import { FooEffects } from './foo.effects';

describe('FooEffects', () => {
let actions$: Observable<any>;
let effects: FooEffects;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
FooEffects,
provideMockActions(() => actions$)
]
});

effects = TestBed.inject(FooEffects);
});

it('should be created', () => {
expect(effects).toBeTruthy();
});
});
"
`;

exports[`Feature Schematic should create all files of a feature with an entity 6`] = `
"export interface Foo {
id: string;
}
"
`;

exports[`Feature Schematic should have all api actions in reducer if api flag enabled 1`] = `
"import { createFeature, createReducer, on } from '@ngrx/store';
import { FooActions } from './foo.actions';
Expand Down
25 changes: 25 additions & 0 deletions modules/schematics/src/feature/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Feature Schematic', () => {
project: 'bar',
module: '',
group: false,
entity: false,
};

const projectPath = getTestProjectPath();
Expand Down Expand Up @@ -64,6 +65,7 @@ describe('Feature Schematic', () => {
expect(
files.includes(`${projectPath}/src/app/foo.selectors.spec.ts`)
).toBeTruthy();
expect(files.includes(`${projectPath}/src/app/foo.model.ts`)).toBeFalsy();
});

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

expect(fileContent).toMatchSnapshot();
});

it('should create all files of a feature with an entity', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we use snapshot tests for this

const options = { ...defaultOptions, entity: true };

const tree = await schematicRunner.runSchematic(
'feature',
options,
appTree
);
const paths = [
`${projectPath}/src/app/foo.actions.ts`,
`${projectPath}/src/app/foo.reducer.ts`,
`${projectPath}/src/app/foo.reducer.spec.ts`,
`${projectPath}/src/app/foo.effects.ts`,
`${projectPath}/src/app/foo.effects.spec.ts`,
`${projectPath}/src/app/foo.model.ts`,
];

paths.forEach((path) => {
expect(tree.files.includes(path)).toBeTruthy();
expect(tree.readContent(path)).toMatchSnapshot();
});
});
});
115 changes: 67 additions & 48 deletions modules/schematics/src/feature/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,78 @@
import {
chain,
Rule,
schematic,
SchematicContext,
Tree,
chain,
schematic,
} from '@angular-devkit/schematics';

import { Schema as FeatureOptions } from './schema';

export default function (options: FeatureOptions): Rule {
return (host: Tree, context: SchematicContext) => {
return chain([
schematic('action', {
flat: options.flat,
group: options.group,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
api: options.api,
prefix: options.prefix,
}),
schematic('reducer', {
flat: options.flat,
group: options.group,
module: options.module,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
reducers: options.reducers,
feature: true,
api: options.api,
prefix: options.prefix,
}),
schematic('effect', {
flat: options.flat,
group: options.group,
module: options.module,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
feature: true,
api: options.api,
prefix: options.prefix,
}),
schematic('selector', {
flat: options.flat,
group: options.group,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
feature: true,
}),
])(host, context);
return chain(
(options.entity
? [
schematic('entity', {
name: options.name,
path: options.path,
project: options.project,
flat: options.flat,
skipTests: options.skipTests,
module: options.module,
reducers: options.reducers,
group: options.group,
feature: true,
}),
]
: [
schematic('action', {
flat: options.flat,
group: options.group,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
api: options.api,
prefix: options.prefix,
}),
schematic('reducer', {
flat: options.flat,
group: options.group,
module: options.module,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
reducers: options.reducers,
feature: true,
api: options.api,
prefix: options.prefix,
}),
schematic('selector', {
flat: options.flat,
group: options.group,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
feature: true,
}),
]
).concat([
schematic('effect', {
flat: options.flat,
group: options.group,
module: options.module,
name: options.name,
path: options.path,
project: options.project,
skipTests: options.skipTests,
feature: true,
api: options.api,
prefix: options.prefix,
}),
])
)(host, context);
};
}
7 changes: 7 additions & 0 deletions modules/schematics/src/feature/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@
"type": "string",
"default": "load",
"x-prompt": "What should be the prefix of the action, effect and reducer?"
},
"entity": {
"description": "Toggle whether an entity is created as part of the feature",
"type": "boolean",
"aliases": ["e"],
"x-prompt": "Should we use @ngrx/entity to create the reducer?",
"default": "false"
}
},
"required": []
Expand Down
2 changes: 2 additions & 0 deletions modules/schematics/src/feature/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ export interface Schema {
api?: boolean;

prefix?: string;

entity?: boolean;
}