mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: adapted CRUD store from enterprise into OSS (#6474)
## About the changes This ports the CRUD store into OSS which is an abstraction to reduce the amount of boilerplate code we have to write in stores. By extending CRUDStore, the store becomes simply the type definition: ```typescript type ActionModel = { actionSetId: number; action: string; executionParams: Record<string, unknown>; createdByUserId: number; sortOrder: number; }; export class ActionStore extends CRUDStore< ActionModel & { id: number; createdAt: Date }, ActionModel > { } ``` And eventually specific mappings between those types can be provided (if the mapping is more complex than camelCase -> snake_case): ```typescript toRow: ({ project, name, actor, match, createdByUserId }) => ({ created_by_user_id: createdByUserId, project, name, actor_id: actor, source: match.source, source_id: match.sourceId, payload: match.payload, }), fromRow: ({ id, created_at, created_by_user_id, project, name, actor_id, source, source_id, payload, }) => ({ id, createdAt: created_at, createdByUserId: created_by_user_id, project, name, actor: actor_id, match: { source, sourceId: source_id, payload, }, }), ``` Stores can also be extended to include additional functionality in case you need to join with another table or do an aggregation, but it significantly reduces the amount of boilerplate code needed to create a basic store
This commit is contained in:
parent
9148820a8f
commit
82f4093c04
132
src/lib/db/crud/crud-store.ts
Normal file
132
src/lib/db/crud/crud-store.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { NotFoundError } from '../../error';
|
||||
import { DB_TIME } from '../../metric-events';
|
||||
import { Db, IUnleashConfig } from '../../server-impl';
|
||||
import { Store } from '../../types/stores/store';
|
||||
import metricsHelper from '../../util/metrics-helper';
|
||||
import { defaultFromRow, defaultToRow } from './default-mappings';
|
||||
import { Row } from './row-type';
|
||||
|
||||
export type CrudStoreConfig = Pick<IUnleashConfig, 'eventBus'>;
|
||||
|
||||
/**
|
||||
* This abstract class defines the basic operations for a CRUD store
|
||||
*
|
||||
* Provides default types for:
|
||||
* - RowReadModel turning the properties of ReadModel from camelCase to snake_case
|
||||
* - RowWriteModel turning the properties of WriteModel from camelCase to snake_case
|
||||
* - IdType assumming it's a number
|
||||
*
|
||||
* These types can be overridden to suit different needs.
|
||||
*
|
||||
* Default implementations of toRow and fromRow are provided, but can be overridden.
|
||||
*/
|
||||
export abstract class CRUDStore<
|
||||
ReadModel extends { id: IdType },
|
||||
WriteModel,
|
||||
RowReadModel = Row<ReadModel>,
|
||||
RowWriteModel = Row<WriteModel>,
|
||||
IdType = number,
|
||||
> implements Store<ReadModel, IdType>
|
||||
{
|
||||
protected db: Db;
|
||||
|
||||
protected tableName: string;
|
||||
|
||||
protected readonly timer: (action: string) => Function;
|
||||
|
||||
protected toRow: (item: Partial<WriteModel>) => RowWriteModel;
|
||||
protected fromRow: (item: RowReadModel) => ReadModel;
|
||||
|
||||
constructor(
|
||||
tableName: string,
|
||||
db: Db,
|
||||
{ eventBus }: CrudStoreConfig,
|
||||
options?: Partial<{
|
||||
toRow: (item: WriteModel) => RowWriteModel;
|
||||
fromRow: (item: RowReadModel) => ReadModel;
|
||||
}>,
|
||||
) {
|
||||
this.tableName = tableName;
|
||||
this.db = db;
|
||||
this.timer = (action: string) =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
store: tableName,
|
||||
action,
|
||||
});
|
||||
this.toRow = options?.toRow ?? defaultToRow<WriteModel, RowWriteModel>;
|
||||
this.fromRow =
|
||||
options?.fromRow ?? defaultFromRow<ReadModel, RowReadModel>;
|
||||
}
|
||||
|
||||
async getAll(query?: Partial<WriteModel>): Promise<ReadModel[]> {
|
||||
let allQuery = this.db(this.tableName);
|
||||
if (query) {
|
||||
allQuery = allQuery.where(this.toRow(query) as Record<string, any>);
|
||||
}
|
||||
const items = await allQuery;
|
||||
return items.map(this.fromRow);
|
||||
}
|
||||
|
||||
async insert(item: WriteModel): Promise<ReadModel> {
|
||||
const rows = await this.db(this.tableName)
|
||||
.insert(this.toRow(item))
|
||||
.returning('*');
|
||||
return this.fromRow(rows[0]);
|
||||
}
|
||||
|
||||
async bulkInsert(items: WriteModel[]): Promise<ReadModel[]> {
|
||||
if (!items || items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const rows = await this.db(this.tableName)
|
||||
.insert(items.map(this.toRow))
|
||||
.returning('*');
|
||||
return rows.map(this.fromRow);
|
||||
}
|
||||
|
||||
async update(id: IdType, item: Partial<WriteModel>): Promise<ReadModel> {
|
||||
const rows = await this.db(this.tableName)
|
||||
.where({ id })
|
||||
.update(this.toRow(item))
|
||||
.returning('*');
|
||||
return this.fromRow(rows[0]);
|
||||
}
|
||||
|
||||
async delete(id: IdType): Promise<void> {
|
||||
return this.db(this.tableName).where({ id }).delete();
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
return this.db(this.tableName).delete();
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async exists(id: IdType): Promise<boolean> {
|
||||
const result = await this.db.raw(
|
||||
`SELECT EXISTS(SELECT 1 FROM ${this.tableName} WHERE id = ?) AS present`,
|
||||
[id],
|
||||
);
|
||||
const { present } = result.rows[0];
|
||||
return present;
|
||||
}
|
||||
|
||||
async count(query?: Partial<WriteModel>): Promise<number> {
|
||||
let countQuery = this.db(this.tableName).count('*');
|
||||
if (query) {
|
||||
countQuery = countQuery.where(
|
||||
this.toRow(query) as Record<string, any>,
|
||||
);
|
||||
}
|
||||
const { count } = (await countQuery.first()) ?? { count: 0 };
|
||||
return Number(count);
|
||||
}
|
||||
|
||||
async get(id: IdType): Promise<ReadModel> {
|
||||
const row = await this.db(this.tableName).where({ id }).first();
|
||||
if (!row) {
|
||||
throw new NotFoundError(`No item with id ${id}`);
|
||||
}
|
||||
return this.fromRow(row);
|
||||
}
|
||||
}
|
34
src/lib/db/crud/default-mappings.ts
Normal file
34
src/lib/db/crud/default-mappings.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const camelToSnakeCase = (str: string) =>
|
||||
str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
|
||||
const snakeToCamelCase = (str: string) =>
|
||||
str.replace(/(_\w)/g, (letter) => letter[1].toUpperCase());
|
||||
|
||||
/**
|
||||
* This helper function turns all fields in the item object from camelCase to snake_case
|
||||
*
|
||||
* @param item is the input object
|
||||
* @returns a modified version of item with all fields in snake_case
|
||||
*/
|
||||
export const defaultToRow = <WriteModel, WriteRow>(
|
||||
item: WriteModel,
|
||||
): WriteRow => {
|
||||
const row: Partial<WriteRow> = {};
|
||||
Object.entries(item as Record<string, any>).forEach(([key, value]) => {
|
||||
row[camelToSnakeCase(key)] = value;
|
||||
});
|
||||
return row as WriteRow;
|
||||
};
|
||||
|
||||
/**
|
||||
* This helper function turns all fields in the row object from snake_case to camelCase
|
||||
* @param row is the input object
|
||||
* @returns a modified version of row with all fields in camelCase
|
||||
*/
|
||||
export const defaultFromRow = <ReadModel, ReadRow>(row: ReadRow): ReadModel => {
|
||||
const model: Partial<ReadModel> = {};
|
||||
Object.entries(row as Record<string, any>).forEach(([key, value]) => {
|
||||
model[snakeToCamelCase(key)] = value;
|
||||
});
|
||||
return model as ReadModel;
|
||||
};
|
13
src/lib/db/crud/row-type.ts
Normal file
13
src/lib/db/crud/row-type.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// This defines dynamic name for the generated types
|
||||
type CamelCaseToSnakeCase<S extends string> = S extends `${infer P1}${infer P2}`
|
||||
? P2 extends Uncapitalize<P2>
|
||||
? `${P1}${CamelCaseToSnakeCase<P2>}`
|
||||
: `${P1}_${CamelCaseToSnakeCase<Uncapitalize<P2>>}`
|
||||
: S;
|
||||
|
||||
/**
|
||||
* This helper type turns all fields in the type from camelCase to snake_case
|
||||
*/
|
||||
export type Row<T> = {
|
||||
[K in keyof T as CamelCaseToSnakeCase<K & string>]: T[K];
|
||||
};
|
Loading…
Reference in New Issue
Block a user