Skip to content

Commit

Permalink
Added missing archivedAt to featureSchema (#1779)
Browse files Browse the repository at this point in the history
* Added missing archivedAt to featureSchema

* Added archivedAt to feature toggle.
Added archived_at to db

* Add test

* Add test

* Bug fix

* Bug fix

* update archivedAt to date-time

* Code refactoring done

* Conver to static and remove unused methods

* Add tests

* Fixes

* Fix

* Removed docker file from linting

* Fix segment test

* Fix failing test

* Make fixes

Co-authored-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: andreas-unleash <104830839+andreas-unleash@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 1, 2022
1 parent 617955b commit 04fb065
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 203 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Expand Up @@ -29,6 +29,7 @@ jobs:
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build_coverage.yaml
Expand Up @@ -29,6 +29,7 @@ jobs:
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build_prs_jest_report.yaml
Expand Up @@ -24,6 +24,7 @@ jobs:
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/db/feature-strategy-store.ts
Expand Up @@ -15,6 +15,7 @@ import {
} from '../types/model';
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
import { PartialSome } from '../types/partial';
import FeatureToggleStore from './feature-toggle-store';
import { ensureStringValue } from '../util/ensureStringValue';
import { mapValues } from '../util/map-values';

Expand Down Expand Up @@ -246,7 +247,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'environments.name',
)
.where('features.name', featureName)
.andWhere('features.archived', archived ? 1 : 0);
.modify(FeatureToggleStore.filterByArchived, archived);
stopTimer();
if (rows.length > 0) {
const featureToggle = rows.reduce((acc, r) => {
Expand Down Expand Up @@ -318,7 +319,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
archived: boolean = false,
): Promise<IFeatureOverview[]> {
const rows = await this.db('features')
.where({ project: projectId, archived })
.where({ project: projectId })
.select(
'features.name as feature_name',
'features.type as type',
Expand All @@ -330,6 +331,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'environments.type as environment_type',
'environments.sort_order as environment_sort_order',
)
.modify(FeatureToggleStore.filterByArchived, archived)
.fullOuterJoin(
'feature_environments',
'feature_environments.feature_name',
Expand Down
6 changes: 2 additions & 4 deletions src/lib/db/feature-toggle-client-store.ts
Expand Up @@ -11,6 +11,7 @@ import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client
import { DEFAULT_ENV } from '../util/constants';
import { PartialDeep } from '../types/partial';
import EventEmitter from 'events';
import FeatureToggleStore from './feature-toggle-store';
import { ensureStringValue } from '../util/ensureStringValue';
import { mapValues } from '../util/map-values';

Expand Down Expand Up @@ -82,6 +83,7 @@ export default class FeatureToggleClientStore

let query = this.db('features')
.select(selectColumns)
.modify(FeatureToggleStore.filterByArchived, archived)
.fullOuterJoin(
this.db('feature_strategies')
.select('*')
Expand All @@ -105,10 +107,6 @@ export default class FeatureToggleClientStore
)
.fullOuterJoin('segments', `segments.id`, `fss.segment_id`);

query = query.where({
archived,
});

if (featureQuery) {
if (featureQuery.tag) {
const tagQuery = this.db
Expand Down
49 changes: 26 additions & 23 deletions src/lib/db/feature-toggle-store.ts
Expand Up @@ -17,6 +17,7 @@ const FEATURE_COLUMNS = [
'created_at',
'impression_data',
'last_seen_at',
'archived_at',
];

export interface FeaturesTable {
Expand All @@ -29,6 +30,8 @@ export interface FeaturesTable {
last_seen_at?: Date;
created_at?: Date;
impression_data: boolean;
archived?: boolean;
archived_at?: Date;
}

const TABLE = 'features';
Expand Down Expand Up @@ -57,10 +60,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
stale?: boolean;
} = { archived: false },
): Promise<number> {
const { archived, ...rest } = query;
return this.db
.from(TABLE)
.count('*')
.where(query)
.where(rest)
.modify(FeatureToggleStore.filterByArchived, archived)
.then((res) => Number(res[0].count));
}

Expand All @@ -85,18 +90,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
stale?: boolean;
} = { archived: false },
): Promise<FeatureToggle[]> {
const { archived, ...rest } = query;
const rows = await this.db
.select(FEATURE_COLUMNS)
.from(TABLE)
.where(query);
return rows.map(this.rowToFeature);
}

async getFeatures(archived: boolean): Promise<FeatureToggle[]> {
const rows = await this.db
.select(FEATURE_COLUMNS)
.from(TABLE)
.where({ archived });
.where(rest)
.modify(FeatureToggleStore.filterByArchived, archived);
return rows.map(this.rowToFeature);
}

Expand Down Expand Up @@ -126,15 +125,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
return present;
}

async getArchivedFeatures(): Promise<FeatureToggle[]> {
const rows = await this.db
.select(FEATURE_COLUMNS)
.from(TABLE)
.where({ archived: true })
.orderBy('name', 'asc');
return rows.map(this.rowToFeature);
}

async setLastSeen(toggleNames: string[]): Promise<void> {
const now = new Date();
try {
Expand All @@ -153,6 +143,15 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
}
}

static filterByArchived: Knex.QueryCallbackWithArgs = (
queryBuilder: Knex.QueryBuilder,
archived: boolean,
) => {
return archived
? queryBuilder.whereNotNull('archived_at')
: queryBuilder.whereNull('archived_at');
};

rowToFeature(row: FeaturesTable): FeatureToggle {
if (!row) {
throw new NotFoundError('No feature toggle found');
Expand All @@ -169,6 +168,8 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
createdAt: row.created_at,
lastSeenAt: row.last_seen_at,
impressionData: row.impression_data,
archivedAt: row.archived_at,
archived: row.archived_at != null,
};
}

Expand All @@ -188,7 +189,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
description: data.description,
type: data.type,
project,
archived: data.archived || false,
archived_at: data.archived ? new Date() : null,
stale: data.stale,
created_at: data.createdAt,
impression_data: data.impressionData,
Expand Down Expand Up @@ -228,23 +229,25 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
}

async archive(name: string): Promise<FeatureToggle> {
const now = new Date();
const row = await this.db(TABLE)
.where({ name })
.update({ archived: true })
.update({ archived_at: now })
.returning(FEATURE_COLUMNS);
return this.rowToFeature(row[0]);
}

async delete(name: string): Promise<void> {
await this.db(TABLE)
.where({ name, archived: true }) // Feature toggle must be archived to allow deletion
.where({ name }) // Feature toggle must be archived to allow deletion
.whereNotNull('archived_at')
.del();
}

async revive(name: string): Promise<FeatureToggle> {
const row = await this.db(TABLE)
.where({ name })
.update({ archived: false })
.update({ archived_at: null })
.returning(FEATURE_COLUMNS);
return this.rowToFeature(row[0]);
}
Expand Down
5 changes: 5 additions & 0 deletions src/lib/openapi/spec/feature-schema.ts
Expand Up @@ -41,6 +41,11 @@ export const featureSchema = {
format: 'date-time',
nullable: true,
},
archivedAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
lastSeenAt: {
type: 'string',
format: 'date-time',
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/model.ts
Expand Up @@ -45,6 +45,7 @@ export interface FeatureToggleDTO {
type?: string;
stale?: boolean;
archived?: boolean;
archivedAt?: Date;
createdAt?: Date;
impressionData?: boolean;
variants?: IVariant[];
Expand Down
38 changes: 38 additions & 0 deletions src/migrations/20220603081324-add-archive-at-to-feature-toggle.js
@@ -0,0 +1,38 @@
'use strict';

exports.up = function (db, callback) {
db.runSql(
`
ALTER TABLE features ADD archived_at TIMESTAMP WITH TIME ZONE;
UPDATE features f
SET archived_at = res.archived_at
FROM (SELECT f.name, e.created_at AS archived_at
FROM features f
INNER JOIN events e
ON e.feature_name = f.NAME
AND e.created_at =
(SELECT Max(created_at) date
FROM events
WHERE type = 'feature-archived'
AND e.feature_name = f.NAME)) res
WHERE res.NAME = f.NAME;
UPDATE features
SET archived_at = Now()
WHERE archived = TRUE
AND archived_at IS NULL;
`,
callback,
);
};

exports.down = function (db, callback) {
db.runSql(
`
UPDATE features
SET archived = TRUE
WHERE archived_at IS NOT NULL;
ALTER TABLE features DROP COLUMN archived_at;
`,
callback,
);
};
13 changes: 13 additions & 0 deletions src/test/e2e/api/admin/feature-archive.e2e.test.ts
Expand Up @@ -98,6 +98,19 @@ test('returns three archived toggles', async () => {
});
});

test('returns three archived toggles with archivedAt', async () => {
expect.assertions(3);
return app.request
.get('/api/admin/archive/features')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.features.length).toEqual(3);
expect(res.body.features.every((f) => f.archived)).toEqual(true);
expect(res.body.features.every((f) => f.archivedAt)).toEqual(true);
});
});

test('revives a feature by name', async () => {
return app.request
.post('/api/admin/archive/revive/featureArchivedX')
Expand Down
2 changes: 1 addition & 1 deletion src/test/e2e/api/admin/feature.e2e.test.ts
Expand Up @@ -25,7 +25,7 @@ beforeAll(async () => {
app = await setupApp(db.stores);

const createToggle = async (
toggle: Omit<FeatureSchema, 'createdAt'>,
toggle: Omit<FeatureSchema, 'archivedAt' | 'createdAt'>,
strategy: Omit<FeatureStrategySchema, 'id'> = defaultStrategy,
projectId: string = 'default',
username: string = 'test',
Expand Down
Expand Up @@ -1185,6 +1185,11 @@ Object {
"archived": Object {
"type": "boolean",
},
"archivedAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"createdAt": Object {
"format": "date-time",
"nullable": true,
Expand Down

0 comments on commit 04fb065

Please sign in to comment.