mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-06 00:07:44 +01:00
52fa872fe6
## About the changes This PR adds a method to safeguard us from opening a new transaction while inside another transaction, resulting in two isolated transactions that will not be atomic (if one fails, the other might still complete successfully).bbbe4d4637/lib/knex-builder/make-knex.js (L143C5-L144C88)
We're currently opening transactions at the controller layer2746bd1517/src/lib/features/export-import-toggles/export-import-controller.ts (L206-L208)
but in some other places, we do it at the store level:2746bd1517/src/lib/db/access-store.ts (L577)
## Alternative We can remove store-level transactions and move them to the controller following this approach:cb034976b9/src/lib/services/index.ts (L282-L284)
cb034976b9/src/lib/features/export-import-toggles/export-import-controller.ts (L206-L208)
This option is more expensive because we have to: 1. Write the factory methods that propagate the transaction to the stores (therefore creating the store factory methods as well) 2. Identify the methods for creating the transactions at the store level and backtrack the calls until the controller layer
220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import metricsHelper from '../util/metrics-helper';
|
|
import { DB_TIME } from '../metric-events';
|
|
import { Logger, LogProvider } from '../logger';
|
|
import NotFoundError from '../error/notfound-error';
|
|
import { IApiTokenStore } from '../types/stores/api-token-store';
|
|
import {
|
|
ApiTokenType,
|
|
IApiToken,
|
|
IApiTokenCreate,
|
|
isAllProjects,
|
|
} from '../types/models/api-token';
|
|
import { ALL_PROJECTS } from '../util/constants';
|
|
import { Db } from './db';
|
|
import { inTransaction } from './transaction';
|
|
|
|
const TABLE = 'api_tokens';
|
|
const API_LINK_TABLE = 'api_token_project';
|
|
|
|
const ALL = '*';
|
|
|
|
interface ITokenInsert {
|
|
id: number;
|
|
secret: string;
|
|
username: string;
|
|
type: ApiTokenType;
|
|
expires_at?: Date;
|
|
created_at: Date;
|
|
seen_at?: Date;
|
|
environment: string;
|
|
tokenName?: string;
|
|
}
|
|
|
|
interface ITokenRow extends ITokenInsert {
|
|
project: string;
|
|
}
|
|
|
|
const tokenRowReducer = (acc, tokenRow) => {
|
|
const { project, ...token } = tokenRow;
|
|
if (!acc[tokenRow.secret]) {
|
|
acc[tokenRow.secret] = {
|
|
secret: token.secret,
|
|
tokenName: token.token_name ? token.token_name : token.username,
|
|
type: token.type.toLowerCase(),
|
|
project: ALL,
|
|
projects: [ALL],
|
|
environment: token.environment ? token.environment : ALL,
|
|
expiresAt: token.expires_at,
|
|
createdAt: token.created_at,
|
|
alias: token.alias,
|
|
seenAt: token.seen_at,
|
|
username: token.token_name ? token.token_name : token.username,
|
|
};
|
|
}
|
|
const currentToken = acc[tokenRow.secret];
|
|
if (tokenRow.project) {
|
|
if (isAllProjects(currentToken.projects)) {
|
|
currentToken.projects = [];
|
|
}
|
|
currentToken.projects.push(tokenRow.project);
|
|
currentToken.project = currentToken.projects.join(',');
|
|
}
|
|
return acc;
|
|
};
|
|
|
|
const toRow = (newToken: IApiTokenCreate) => ({
|
|
username: newToken.tokenName ?? newToken.username,
|
|
token_name: newToken.tokenName ?? newToken.username,
|
|
secret: newToken.secret,
|
|
type: newToken.type,
|
|
environment:
|
|
newToken.environment === ALL ? undefined : newToken.environment,
|
|
expires_at: newToken.expiresAt,
|
|
alias: newToken.alias || null,
|
|
});
|
|
|
|
const toTokens = (rows: any[]): IApiToken[] => {
|
|
const tokens = rows.reduce(tokenRowReducer, {});
|
|
return Object.values(tokens);
|
|
};
|
|
|
|
export class ApiTokenStore implements IApiTokenStore {
|
|
private logger: Logger;
|
|
|
|
private timer: Function;
|
|
|
|
private db: Db;
|
|
|
|
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
|
this.db = db;
|
|
this.logger = getLogger('api-tokens.js');
|
|
this.timer = (action: string) =>
|
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
|
store: 'api-tokens',
|
|
action,
|
|
});
|
|
}
|
|
|
|
count(): Promise<number> {
|
|
return this.db(TABLE)
|
|
.count('*')
|
|
.then((res) => Number(res[0].count));
|
|
}
|
|
|
|
async getAll(): Promise<IApiToken[]> {
|
|
const stopTimer = this.timer('getAll');
|
|
const rows = await this.makeTokenProjectQuery();
|
|
stopTimer();
|
|
return toTokens(rows);
|
|
}
|
|
|
|
async getAllActive(): Promise<IApiToken[]> {
|
|
const stopTimer = this.timer('getAllActive');
|
|
const rows = await this.makeTokenProjectQuery()
|
|
.where('expires_at', 'IS', null)
|
|
.orWhere('expires_at', '>', 'now()');
|
|
stopTimer();
|
|
return toTokens(rows);
|
|
}
|
|
|
|
private makeTokenProjectQuery() {
|
|
return this.db<ITokenRow>(`${TABLE} as tokens`)
|
|
.leftJoin(
|
|
`${API_LINK_TABLE} as token_project_link`,
|
|
'tokens.secret',
|
|
'token_project_link.secret',
|
|
)
|
|
.select(
|
|
'tokens.secret',
|
|
'username',
|
|
'token_name',
|
|
'type',
|
|
'expires_at',
|
|
'created_at',
|
|
'alias',
|
|
'seen_at',
|
|
'environment',
|
|
'token_project_link.project',
|
|
);
|
|
}
|
|
|
|
async insert(newToken: IApiTokenCreate): Promise<IApiToken> {
|
|
const response = await inTransaction(this.db, async (tx) => {
|
|
const [row] = await tx<ITokenInsert>(TABLE).insert(
|
|
toRow(newToken),
|
|
['created_at'],
|
|
);
|
|
|
|
const updateProjectTasks = (newToken.projects || [])
|
|
.filter((project) => {
|
|
return project !== ALL_PROJECTS;
|
|
})
|
|
.map((project) => {
|
|
return tx.raw(
|
|
`INSERT INTO ${API_LINK_TABLE} VALUES (?, ?)`,
|
|
[newToken.secret, project],
|
|
);
|
|
});
|
|
await Promise.all(updateProjectTasks);
|
|
return {
|
|
...newToken,
|
|
username: newToken.tokenName,
|
|
alias: newToken.alias || null,
|
|
project: newToken.projects?.join(',') || '*',
|
|
createdAt: row.created_at,
|
|
};
|
|
});
|
|
return response;
|
|
}
|
|
|
|
destroy(): void {}
|
|
|
|
async exists(secret: string): Promise<boolean> {
|
|
const result = await this.db.raw(
|
|
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`,
|
|
[secret],
|
|
);
|
|
const { present } = result.rows[0];
|
|
return present;
|
|
}
|
|
|
|
async get(key: string): Promise<IApiToken> {
|
|
const row = await this.makeTokenProjectQuery().where(
|
|
'tokens.secret',
|
|
key,
|
|
);
|
|
return toTokens(row)[0];
|
|
}
|
|
|
|
async delete(secret: string): Promise<void> {
|
|
return this.db<ITokenRow>(TABLE).where({ secret }).del();
|
|
}
|
|
|
|
async deleteAll(): Promise<void> {
|
|
return this.db<ITokenRow>(TABLE).del();
|
|
}
|
|
|
|
async setExpiry(secret: string, expiresAt: Date): Promise<IApiToken> {
|
|
const rows = await this.makeTokenProjectQuery()
|
|
.update({ expires_at: expiresAt })
|
|
.where({ secret })
|
|
.returning('*');
|
|
if (rows.length > 0) {
|
|
return toTokens(rows)[0];
|
|
}
|
|
throw new NotFoundError('Could not find api-token.');
|
|
}
|
|
|
|
async markSeenAt(secrets: string[]): Promise<void> {
|
|
const now = new Date();
|
|
try {
|
|
await this.db(TABLE)
|
|
.whereIn('secret', secrets)
|
|
.update({ seen_at: now });
|
|
} catch (err) {
|
|
this.logger.error('Could not update lastSeen, error: ', err);
|
|
}
|
|
}
|
|
}
|