Skip to content

Commit

Permalink
Feat/impression data (#1310)
Browse files Browse the repository at this point in the history
* feat: add impression data column

* fix: add default value to impressionData

* fix: allow client api to return impressionData

* fix: add tests for impressionData

* fix: reset server-dev

* fix: add test for adding a toggle with impression data on a different project

* fix: update tests
  • Loading branch information
FredrikOseberg committed Feb 3, 2022
1 parent fb7014a commit 6520aa1
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/lib/db/feature-strategy-store.ts
Expand Up @@ -206,6 +206,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'features.project as project',
'features.stale as stale',
'features.variants as variants',
'features.impression_data as impression_data',
'features.created_at as created_at',
'features.last_seen_at as last_seen_at',
'feature_environments.enabled as enabled',
Expand Down Expand Up @@ -249,6 +250,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
acc.environments = {};
}
acc.name = r.name;
acc.impressionData = r.impression_data;
acc.description = r.description;
acc.project = r.project;
acc.stale = r.stale;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/db/feature-toggle-client-store.ts
Expand Up @@ -74,6 +74,7 @@ export default class FeatureToggleClientStore
'features.type as type',
'features.project as project',
'features.stale as stale',
'features.impression_data as impression_data',
'features.variants as variants',
'features.created_at as created_at',
'features.last_seen_at as last_seen_at',
Expand Down Expand Up @@ -137,6 +138,7 @@ export default class FeatureToggleClientStore
if (r.strategy_name) {
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
}
feature.impressionData = r.impression_data;
feature.enabled = !!r.enabled;
feature.name = r.name;
feature.description = r.description;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/db/feature-toggle-store.ts
Expand Up @@ -15,6 +15,7 @@ const FEATURE_COLUMNS = [
'stale',
'variants',
'created_at',
'impression_data',
'last_seen_at',
];

Expand All @@ -27,6 +28,7 @@ export interface FeaturesTable {
project: string;
last_seen_at?: Date;
created_at?: Date;
impression_data: boolean;
}

const TABLE = 'features';
Expand Down Expand Up @@ -166,6 +168,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
variants: sortedVariants,
createdAt: row.created_at,
lastSeenAt: row.last_seen_at,
impressionData: row.impression_data,
};
}

Expand All @@ -188,6 +191,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
archived: data.archived || false,
stale: data.stale,
created_at: data.createdAt,
impression_data: data.impressionData,
};
if (!row.created_at) {
delete row.created_at;
Expand Down
10 changes: 10 additions & 0 deletions src/lib/schema/feature-schema.test.ts
Expand Up @@ -4,6 +4,7 @@ test('should require URL firendly name', () => {
const toggle = {
name: 'io`dasd',
enabled: false,
impressionData: false,
strategies: [{ name: 'default' }],
};

Expand All @@ -15,6 +16,7 @@ test('should be valid toggle name', () => {
const toggle = {
name: 'app.name',
enabled: false,
impressionData: false,
strategies: [{ name: 'default' }],
};

Expand All @@ -28,6 +30,7 @@ test('should strip extra variant fields', () => {
type: 'release',
enabled: false,
stale: false,
impressionData: false,
strategies: [{ name: 'default' }],
variants: [
{
Expand All @@ -49,6 +52,7 @@ test('should allow weightType=fix', () => {
type: 'release',
project: 'default',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
Expand All @@ -71,6 +75,7 @@ test('should disallow weightType=unknown', () => {
name: 'app.name',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
Expand All @@ -95,6 +100,7 @@ test('should be possible to define variant overrides', () => {
type: 'release',
project: 'some',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [{ name: 'default' }],
Expand Down Expand Up @@ -125,6 +131,7 @@ test('variant overrides must have corect shape', async () => {
name: 'app.name',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [{ name: 'default' }],
variants: [
Expand Down Expand Up @@ -154,6 +161,7 @@ test('should keep constraints', () => {
type: 'release',
project: 'default',
enabled: false,
impressionData: false,
stale: false,
archived: false,
strategies: [
Expand All @@ -180,6 +188,7 @@ test('should not accept empty constraint values', () => {
name: 'app.constraints.empty.value',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [
{
Expand All @@ -206,6 +215,7 @@ test('should not accept empty list of constraint values', () => {
name: 'app.constraints.empty.value.list',
type: 'release',
enabled: false,
impressionData: false,
stale: false,
strategies: [
{
Expand Down
12 changes: 12 additions & 0 deletions src/lib/schema/feature-schema.ts
Expand Up @@ -56,6 +56,12 @@ export const featureMetadataSchema = joi
archived: joi.boolean().default(false),
type: joi.string().default('release'),
description: joi.string().allow('').allow(null).optional(),
impressionData: joi
.boolean()
.allow(true)
.allow(false)
.default(false)
.optional(),
createdAt: joi.date().optional().allow(null),
})
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
Expand All @@ -70,6 +76,12 @@ export const featureSchema = joi
type: joi.string().default('release'),
project: joi.string().default('default'),
description: joi.string().allow('').allow(null).optional(),
impressionData: joi
.boolean()
.allow(true)
.allow(false)
.default(false)
.optional(),
strategies: joi
.array()
.min(0)
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/model.ts
Expand Up @@ -38,6 +38,7 @@ export interface FeatureToggleDTO {
stale?: boolean;
archived?: boolean;
createdAt?: Date;
impressionData?: boolean;
}

export interface FeatureToggle extends FeatureToggleDTO {
Expand Down
19 changes: 19 additions & 0 deletions src/migrations/20220128081242-add-impressiondata-to-features.js
@@ -0,0 +1,19 @@
'use strict';

exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE features ADD COLUMN "impression_data" BOOLEAN DEFAULT FALSE;
`,
cb,
);
};

exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE features DROP COLUMN "impression_data";
`,
cb,
);
};
85 changes: 84 additions & 1 deletion src/test/e2e/api/admin/project/features.e2e.test.ts
Expand Up @@ -512,13 +512,19 @@ test('Should patch feature toggle', async () => {
const name = 'new.toggle.patch';
await app.request
.post(url)
.send({ name, description: 'some', type: 'release' })
.send({
name,
description: 'some',
type: 'release',
impressionData: true,
})
.expect(201);
await app.request
.patch(`${url}/${name}`)
.send([
{ op: 'replace', path: '/description', value: 'New desc' },
{ op: 'replace', path: '/type', value: 'kill-switch' },
{ op: 'replace', path: '/impressionData', value: false },
])
.expect(200);

Expand All @@ -527,6 +533,7 @@ test('Should patch feature toggle', async () => {
expect(toggle.name).toBe(name);
expect(toggle.description).toBe('New desc');
expect(toggle.type).toBe('kill-switch');
expect(toggle.impressionData).toBe(false);
expect(toggle.archived).toBeFalsy();
const events = await db.stores.eventStore.getAll({
type: FEATURE_METADATA_UPDATED,
Expand Down Expand Up @@ -1983,3 +1990,79 @@ test('should not update project with PATCH', async () => {
})
.expect(200);
});

test('Can create a feature with impression data', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'new.toggle.with.impressionData',
impressionData: true,
})
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
});

test('Can create a feature without impression data', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'new.toggle.without.impressionData',
})
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});

test('Can update impression data with PUT', async () => {
const toggle = {
name: 'update.toggle.with.impressionData',
impressionData: true,
};
await app.request
.post('/api/admin/projects/default/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});

await app.request
.put(`/api/admin/projects/default/features/${toggle.name}`)
.send({ ...toggle, impressionData: false })
.expect(200)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});

test('Can create toggle with impression data on different project', async () => {
db.stores.projectStore.create({
id: 'impression-data',
name: 'ImpressionData',
description: '',
});

const toggle = {
name: 'project.impression.data',
impressionData: true,
};

await app.request
.post('/api/admin/projects/impression-data/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});

await app.request
.put(`/api/admin/projects/impression-data/features/${toggle.name}`)
.send({ ...toggle, impressionData: false })
.expect(200)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});
55 changes: 55 additions & 0 deletions src/test/e2e/api/client/feature.e2e.test.ts
Expand Up @@ -14,6 +14,7 @@ beforeAll(async () => {
{
name: 'featureX',
description: 'the #1 feature',
impressionData: true,
},
'test',
);
Expand Down Expand Up @@ -134,6 +135,24 @@ test('gets a feature by name', async () => {
.expect(200);
});

test('returns a feature toggles impression data', async () => {
return app.request
.get('/api/client/features/featureX')
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});
});

test('returns a false for impression data when not specified', async () => {
return app.request
.get('/api/client/features/featureZ')
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.impressionData).toBe(false);
});
});

test('cant get feature that does not exist', async () => {
return app.request
.get('/api/client/features/myfeature')
Expand Down Expand Up @@ -255,3 +274,39 @@ test('Can use multiple filters', async () => {
expect(res.body.features[0].name).toBe('test.feature');
});
});

test('returns a feature toggles impression data for a different project', async () => {
const project = {
id: 'impression-data-client',
name: 'ImpressionData',
description: '',
};

db.stores.projectStore.create(project);

const toggle = {
name: 'project-client.impression.data',
impressionData: true,
};

await app.request
.post('/api/admin/projects/impression-data-client/features')
.send(toggle)
.expect(201)
.expect((res) => {
expect(res.body.impressionData).toBe(true);
});

return app.request
.get('/api/client/features')
.expect('Content-Type', /json/)
.expect((res) => {
const projectToggle = res.body.features.find(
(resToggle) => resToggle.project === project.id,
);

expect(projectToggle.name).toBe(toggle.name);
expect(projectToggle.project).toBe(project.id);
expect(projectToggle.impressionData).toBe(true);
});
});

0 comments on commit 6520aa1

Please sign in to comment.