Skip to content

Commit

Permalink
feat: read model for dependent features (#4846)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Sep 27, 2023
1 parent b9910bf commit fd8775f
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { Db } from '../../db/db';
import { DependentFeaturesService } from './dependent-features-service';
import { DependentFeaturesStore } from './dependent-features-store';
import { DependentFeaturesReadModel } from './dependent-features-read-model';
import { FakeDependentFeaturesStore } from './fake-dependent-features-store';
import { FakeDependentFeaturesReadModel } from './fake-dependent-features-read-model';

export const createDependentFeaturesService = (
db: Db,
): DependentFeaturesService => {
const dependentFeaturesStore = new DependentFeaturesStore(db);
return new DependentFeaturesService(dependentFeaturesStore);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
return new DependentFeaturesService(
dependentFeaturesStore,
dependentFeaturesReadModel,
);
};

export const createFakeDependentFeaturesService =
(): DependentFeaturesService => {
const dependentFeaturesStore = new FakeDependentFeaturesStore();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
return new DependentFeaturesService(
dependentFeaturesStore,
dependentFeaturesReadModel,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IDependentFeaturesReadModel {
getChildren(parent: string): Promise<string[]>;
getParents(child: string): Promise<string[]>;
getParentOptions(child: string): Promise<string[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Db } from '../../db/db';
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';

export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
private db: Db;

constructor(db: Db) {
this.db = db;
}

async getChildren(parent: string): Promise<string[]> {
const rows = await this.db('dependent_features').where(
'parent',
parent,
);

return rows.map((row) => row.child);
}

async getParents(child: string): Promise<string[]> {
const rows = await this.db('dependent_features').where('child', child);

return rows.map((row) => row.parent);
}

async getParentOptions(child: string): Promise<string[]> {
const result = await this.db('features as f')
.where('f.name', child)
.select('f.project');
if (result.length === 0) {
return [];
}
const rows = await this.db('features as f')
.leftJoin('dependent_features as df', 'f.name', 'df.child')
.where('f.project', result[0].project)
.andWhere('f.name', '!=', child)
.andWhere('df.child', null)
.select('f.name');

return rows.map((item) => item.name);
}
}
15 changes: 12 additions & 3 deletions src/lib/features/dependent-features/dependent-features-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { InvalidOperationError } from '../../error';
import { CreateDependentFeatureSchema } from '../../openapi';
import { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';

export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore;

constructor(dependentFeaturesStore: IDependentFeaturesStore) {
private dependentFeaturesReadModel: IDependentFeaturesReadModel;

constructor(
dependentFeaturesStore: IDependentFeaturesStore,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
) {
this.dependentFeaturesStore = dependentFeaturesStore;
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
}

async upsertFeatureDependency(
Expand All @@ -16,7 +23,9 @@ export class DependentFeaturesService {
): Promise<void> {
const { enabled, feature: parent, variants } = dependentFeature;

const children = await this.dependentFeaturesStore.getChildren(child);
const children = await this.dependentFeaturesReadModel.getChildren(
child,
);
if (children.length > 0) {
throw new InvalidOperationError(
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
Expand Down Expand Up @@ -50,6 +59,6 @@ export class DependentFeaturesService {
}

async getParentOptions(feature: string): Promise<string[]> {
return this.dependentFeaturesStore.getParentOptions(feature);
return this.dependentFeaturesReadModel.getParentOptions(feature);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';

export interface IDependentFeaturesStore {
upsert(featureDependency: FeatureDependency): Promise<void>;
getChildren(parent: string): Promise<string[]>;
delete(dependency: FeatureDependencyId): Promise<void>;
deleteAll(child: string): Promise<void>;
getParentOptions(child: string): Promise<string[]>;
}
26 changes: 0 additions & 26 deletions src/lib/features/dependent-features/dependent-features-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,6 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
.merge();
}

async getChildren(parent: string): Promise<string[]> {
const rows = await this.db('dependent_features').where(
'parent',
parent,
);

return rows.map((row) => row.child);
}

async getParentOptions(child: string): Promise<string[]> {
const result = await this.db('features as f')
.where('f.name', child)
.select('f.project');
if (result.length === 0) {
return [];
}
const rows = await this.db('features as f')
.leftJoin('dependent_features as df', 'f.name', 'df.child')
.where('f.project', result[0].project)
.andWhere('f.name', '!=', child)
.andWhere('df.child', null)
.select('f.name');

return rows.map((item) => item.name);
}

async delete(dependency: FeatureDependencyId): Promise<void> {
await this.db('dependent_features')
.where('parent', dependency.parent)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';

export class FakeDependentFeaturesReadModel
implements IDependentFeaturesReadModel
{
getChildren(): Promise<string[]> {
return Promise.resolve([]);
}

getParents(): Promise<string[]> {
return Promise.resolve([]);
}

getParentOptions(): Promise<string[]> {
return Promise.resolve([]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ export class FakeDependentFeaturesStore implements IDependentFeaturesStore {
return Promise.resolve();
}

getChildren(): Promise<string[]> {
return Promise.resolve([]);
}

getParentOptions(): Promise<string[]> {
return Promise.resolve([]);
}

delete(): Promise<void> {
return Promise.resolve();
}
Expand Down
11 changes: 7 additions & 4 deletions src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ import {
createGetActiveUsers,
} from '../features/instance-stats/getActiveUsers';
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
import { createDependentFeaturesService } from '../features/dependent-features/createDependentFeaturesService';
import {
createDependentFeaturesService,
createFakeDependentFeaturesService,
} from '../features/dependent-features/createDependentFeaturesService';

// TODO: will be moved to scheduler feature directory
export const scheduleServices = async (
Expand Down Expand Up @@ -303,9 +306,9 @@ export const createServices = (

const eventAnnouncerService = new EventAnnouncerService(stores, config);

const dependentFeaturesService = new DependentFeaturesService(
stores.dependentFeaturesStore,
);
const dependentFeaturesService = db
? createDependentFeaturesService(db)
: createFakeDependentFeaturesService();
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
createDependentFeaturesService(txDb);

Expand Down
16 changes: 10 additions & 6 deletions src/test/e2e/api/client/feature.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ beforeAll(async () => {
db = await dbInit('feature_api_client', getLogger, {
experimental: { flags: { dependentFeatures: true } },
});
app = await setupAppWithCustomConfig(db.stores, {
experimental: {
flags: {
strictSchemaValidation: true,
featureNamingPattern: true,
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
featureNamingPattern: true,
},
},
},
});
db.rawDatabase,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
Expand Down

0 comments on commit fd8775f

Please sign in to comment.