mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-21 22:59:07 +00:00
add create by
This commit is contained in:
parent
c06c0f8b38
commit
e728501ddb
@ -2,6 +2,7 @@ import TableColumnType from '../Types/BaseDatabase/TableColumnType';
|
||||
import AnalyticsTableColumn from '../Types/AnalyticsDatabase/TableColumn';
|
||||
import BadDataException from '../Types/Exception/BadDataException';
|
||||
import AnalyticsTableEngine from '../Types/AnalyticsDatabase/AnalyticsTableEngine';
|
||||
import { JSONValue } from '../Types/JSON';
|
||||
|
||||
export default class AnalyticsDataModel {
|
||||
private _tableColumns: Array<AnalyticsTableColumn> = [];
|
||||
@ -101,4 +102,65 @@ export default class AnalyticsDataModel {
|
||||
this.primaryKeys = data.primaryKeys;
|
||||
this.tableColumns = columns;
|
||||
}
|
||||
|
||||
public setColumnValue(
|
||||
columnName: string,
|
||||
value: JSONValue
|
||||
): void {
|
||||
if (this.getTableColumn(columnName)) {
|
||||
return ((this as any)[columnName] = value as any);
|
||||
}
|
||||
}
|
||||
|
||||
public getTableColumn(name: string): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
return column.key === name;
|
||||
}
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
public getTableColumns(): Array<AnalyticsTableColumn> {
|
||||
return this.tableColumns;
|
||||
}
|
||||
|
||||
public getTenantColumn(): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
return column.isTenantId;
|
||||
}
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
public getRequiredColumns(): Array<AnalyticsTableColumn> {
|
||||
return this.tableColumns.filter((column: AnalyticsTableColumn) => {
|
||||
return column.required;
|
||||
});
|
||||
}
|
||||
|
||||
public isDefaultValueColumn(columnName: string): boolean {
|
||||
const column: AnalyticsTableColumn | null = this.getTableColumn(
|
||||
columnName
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return column.isDefaultValueColumn;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -33,6 +33,16 @@ export default class AnalyticsTableColumn {
|
||||
this._required = v;
|
||||
}
|
||||
|
||||
|
||||
private _isTenantId : boolean = false;
|
||||
public get isTenantId() : boolean {
|
||||
return this._isTenantId;
|
||||
}
|
||||
public set isTenantId(v : boolean) {
|
||||
this._isTenantId = v;
|
||||
}
|
||||
|
||||
|
||||
private _type: TableColumnType = TableColumnType.ShortText;
|
||||
public get type(): TableColumnType {
|
||||
return this._type;
|
||||
@ -40,6 +50,23 @@ export default class AnalyticsTableColumn {
|
||||
public set type(v: TableColumnType) {
|
||||
this._type = v;
|
||||
}
|
||||
|
||||
private _forceGetDefaultValueOnCreate?: (() => Date | string | number | boolean) | undefined;
|
||||
public get forceGetDefaultValueOnCreate(): (() => Date | string | number | boolean) | undefined {
|
||||
return this._forceGetDefaultValueOnCreate;
|
||||
}
|
||||
public set forceGetDefaultValueOnCreate(v: (() => Date | string | number | boolean) | undefined) {
|
||||
this._forceGetDefaultValueOnCreate = v;
|
||||
}
|
||||
|
||||
private _isDefaultValueColumn : boolean = false;
|
||||
public get isDefaultValueColumn() : boolean {
|
||||
return this._isDefaultValueColumn;
|
||||
}
|
||||
public set isDefaultValueColumn(v : boolean) {
|
||||
this._isDefaultValueColumn = v;
|
||||
}
|
||||
|
||||
|
||||
public constructor(data: {
|
||||
key: string;
|
||||
@ -47,11 +74,17 @@ export default class AnalyticsTableColumn {
|
||||
description: string;
|
||||
required: boolean;
|
||||
type: TableColumnType;
|
||||
isDefaultValueColumn? : boolean | undefined;
|
||||
isTenantId?: boolean | undefined;
|
||||
forceGetDefaultValueOnCreate?: (() => Date | string | number | boolean) | undefined;
|
||||
}) {
|
||||
this.key = data.key;
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.required = data.required;
|
||||
this.type = data.type;
|
||||
this.isTenantId = data.isTenantId || false;
|
||||
this.forceGetDefaultValueOnCreate = data.forceGetDefaultValueOnCreate;
|
||||
this.isDefaultValueColumn = data.isDefaultValueColumn || false;
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,14 @@ import ClickhouseDatabase, {
|
||||
ClickhouseClient,
|
||||
} from '../Infrastructure/ClickhouseDatabase';
|
||||
import BaseService from './BaseService';
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import logger from '../Utils/Logger';
|
||||
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn';
|
||||
// import CreateBy from "../Types/AnalyticsDatabase/CreateBy";
|
||||
import CreateBy from '../Types/AnalyticsDatabase/CreateBy';
|
||||
import { OnCreate } from '../Types/AnalyticsDatabase/Hooks';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import ModelPermission from '../Utils/ModelPermission';
|
||||
|
||||
export default class AnalyticsDatabaseService<
|
||||
TBaseModel extends AnalyticsBaseModel
|
||||
@ -59,6 +62,19 @@ export default class AnalyticsDatabaseService<
|
||||
return statement;
|
||||
}
|
||||
|
||||
protected generateDefaultValues(data: TBaseModel): TBaseModel {
|
||||
const tableColumns: Array<AnalyticsTableColumn> = data.getTableColumns();
|
||||
|
||||
for (const column of tableColumns) {
|
||||
|
||||
if (column.forceGetDefaultValueOnCreate) {
|
||||
data.setColumnValue(column.key, column.forceGetDefaultValueOnCreate());
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public useDefaultDatabase(): void {
|
||||
this.database = ClickhouseAppInstance;
|
||||
this.databaseClient = this.database.getDataSource() as ClickhouseClient;
|
||||
@ -74,99 +90,120 @@ export default class AnalyticsDatabaseService<
|
||||
});
|
||||
}
|
||||
|
||||
// public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
||||
protected async onBeforeCreate(
|
||||
createBy: CreateBy<TBaseModel>
|
||||
): Promise<OnCreate<TBaseModel>> {
|
||||
// A place holder method used for overriding.
|
||||
return Promise.resolve({
|
||||
createBy: createBy as CreateBy<TBaseModel>,
|
||||
carryForward: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
||||
// ? { createBy, carryForward: [] }
|
||||
// : await this._onBeforeCreate(createBy);
|
||||
private async _onBeforeCreate(
|
||||
createBy: CreateBy<TBaseModel>
|
||||
): Promise<OnCreate<TBaseModel>> {
|
||||
// Private method that runs before create.
|
||||
const projectIdColumn: string | null = this.model.getTenantColumn()?.key || null;
|
||||
|
||||
// let _createdBy: CreateBy<TBaseModel> = onCreate.createBy;
|
||||
if (projectIdColumn && createBy.props.tenantId) {
|
||||
(createBy.data as any)[projectIdColumn] = createBy.props.tenantId;
|
||||
}
|
||||
|
||||
// const carryForward: any = onCreate.carryForward;
|
||||
return await this.onBeforeCreate(createBy);
|
||||
}
|
||||
|
||||
// _createdBy = this.generateSlug(_createdBy);
|
||||
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
||||
|
||||
// let data: TBaseModel = _createdBy.data;
|
||||
const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
||||
? { createBy, carryForward: [] }
|
||||
: await this._onBeforeCreate(createBy);
|
||||
|
||||
// // add tenantId if present.
|
||||
// const tenantColumnName: string | null = data.getTenantColumn();
|
||||
let _createdBy: CreateBy<TBaseModel> = onCreate.createBy;
|
||||
|
||||
// if (tenantColumnName && _createdBy.props.tenantId) {
|
||||
// data.setColumnValue(tenantColumnName, _createdBy.props.tenantId);
|
||||
// }
|
||||
const carryForward: any = onCreate.carryForward;
|
||||
|
||||
// data = this.generateDefaultValues(data);
|
||||
// data = this.checkRequiredFields(data);
|
||||
let data: TBaseModel = _createdBy.data;
|
||||
|
||||
// if (!this.isValid(data)) {
|
||||
// throw new BadDataException('Data is not valid');
|
||||
// }
|
||||
// add tenantId if present.
|
||||
const tenantColumnName: string | null = data.getTenantColumn()?.key || null;
|
||||
|
||||
// // check total items by.
|
||||
if (tenantColumnName && _createdBy.props.tenantId) {
|
||||
data.setColumnValue(tenantColumnName, _createdBy.props.tenantId);
|
||||
}
|
||||
|
||||
// await this.checkTotalItemsBy(_createdBy);
|
||||
data = this.generateDefaultValues(data);
|
||||
data = this.checkRequiredFields(data);
|
||||
|
||||
// // Encrypt data
|
||||
// data = this.encrypt(data);
|
||||
if (!this.isValid(data)) {
|
||||
throw new BadDataException('Data is not valid');
|
||||
}
|
||||
|
||||
// // hash data
|
||||
// data = await this.hash(data);
|
||||
// check total items by
|
||||
|
||||
// ModelPermission.checkCreatePermissions(
|
||||
// this.entityType,
|
||||
// data,
|
||||
// _createdBy.props
|
||||
// );
|
||||
ModelPermission.checkCreatePermissions(
|
||||
this.entityType,
|
||||
data,
|
||||
_createdBy.props
|
||||
);
|
||||
|
||||
// createBy.data = data;
|
||||
createBy.data = data;
|
||||
|
||||
// // check uniqueColumns by:
|
||||
// createBy = await this.checkUniqueColumnBy(createBy);
|
||||
// check uniqueColumns by:
|
||||
createBy = await this.checkUniqueColumnBy(createBy);
|
||||
|
||||
// // serialize.
|
||||
// createBy.data = (await this.sanitizeCreateOrUpdate(
|
||||
// createBy.data,
|
||||
// createBy.props
|
||||
// )) as TBaseModel;
|
||||
// serialize.
|
||||
createBy.data = (await this.sanitizeCreateOrUpdate(
|
||||
createBy.data,
|
||||
createBy.props
|
||||
)) as TBaseModel;
|
||||
|
||||
// try {
|
||||
// createBy.data = await this.getRepository().save(createBy.data);
|
||||
try {
|
||||
createBy.data = await this.getRepository().save(createBy.data);
|
||||
|
||||
// if (!createBy.props.ignoreHooks) {
|
||||
// createBy.data = await this.onCreateSuccess(
|
||||
// {
|
||||
// createBy,
|
||||
// carryForward,
|
||||
// },
|
||||
// createBy.data
|
||||
// );
|
||||
// }
|
||||
if (!createBy.props.ignoreHooks) {
|
||||
createBy.data = await this.onCreateSuccess(
|
||||
{
|
||||
createBy,
|
||||
carryForward,
|
||||
},
|
||||
createBy.data
|
||||
);
|
||||
}
|
||||
|
||||
// // hit workflow.;
|
||||
// if (this.getModel().enableWorkflowOn?.create) {
|
||||
// let tenantId: ObjectID | undefined = createBy.props.tenantId;
|
||||
// hit workflow.;
|
||||
if (this.getModel().enableWorkflowOn?.create) {
|
||||
let tenantId: ObjectID | undefined = createBy.props.tenantId;
|
||||
|
||||
// if (!tenantId && this.getModel().getTenantColumn()) {
|
||||
// tenantId = createBy.data.getValue<ObjectID>(
|
||||
// this.getModel().getTenantColumn()!
|
||||
// );
|
||||
// }
|
||||
if (!tenantId && this.getModel().getTenantColumn()) {
|
||||
tenantId = createBy.data.getValue<ObjectID>(
|
||||
this.getModel().getTenantColumn()!
|
||||
);
|
||||
}
|
||||
|
||||
// if (tenantId) {
|
||||
// await this.onTrigger(
|
||||
// createBy.data.id!,
|
||||
// tenantId,
|
||||
// 'on-create'
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
if (tenantId) {
|
||||
await this.onTrigger(
|
||||
createBy.data.id!,
|
||||
tenantId,
|
||||
'on-create'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// return createBy.data;
|
||||
// } catch (error) {
|
||||
// await this.onCreateError(error as Exception);
|
||||
// throw this.getException(error as Exception);
|
||||
// }
|
||||
// }
|
||||
return createBy.data;
|
||||
} catch (error) {
|
||||
await this.onCreateError(error as Exception);
|
||||
throw this.getException(error as Exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected isValid(data: TBaseModel): boolean {
|
||||
if (!data) {
|
||||
throw new BadDataException('Data cannot be null');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public toColumnsCreateStatement(): string {
|
||||
let columns: string = '';
|
||||
@ -180,6 +217,32 @@ export default class AnalyticsDatabaseService<
|
||||
return columns;
|
||||
}
|
||||
|
||||
protected checkRequiredFields(data: TBaseModel): TBaseModel {
|
||||
// Check required fields.
|
||||
|
||||
for (const columns of data.getRequiredColumns()) {
|
||||
const requiredField: string = columns.key;
|
||||
if (typeof (data as any)[requiredField] === Typeof.Boolean) {
|
||||
if (
|
||||
!(data as any)[requiredField] &&
|
||||
(data as any)[requiredField] !== false &&
|
||||
!data.isDefaultValueColumn(requiredField)
|
||||
) {
|
||||
throw new BadDataException(`${requiredField} is required`);
|
||||
}
|
||||
} else if (
|
||||
!(data as any)[requiredField] &&
|
||||
!data.isDefaultValueColumn(requiredField)
|
||||
) {
|
||||
throw new BadDataException(`${requiredField} is required`);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public toColumnType(type: TableColumnType): string {
|
||||
if (type === TableColumnType.ShortText) {
|
||||
return 'String';
|
||||
|
@ -53,28 +53,9 @@ import Text from 'Common/Types/Text';
|
||||
import logger from '../Utils/Logger';
|
||||
import BaseService from './BaseService';
|
||||
import { getMaxLengthFromTableColumnType } from 'Common/Types/Database/ColumnLength';
|
||||
import { OnCreate, OnDelete, OnFind, OnUpdate } from '../Types/Database/Hooks';
|
||||
|
||||
export type DatabaseTriggerType = 'on-create' | 'on-update' | 'on-delete';
|
||||
|
||||
export interface OnCreate<TBaseModel extends BaseModel> {
|
||||
createBy: CreateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnFind<TBaseModel extends BaseModel> {
|
||||
findBy: FindBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnDelete<TBaseModel extends BaseModel> {
|
||||
deleteBy: DeleteBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnUpdate<TBaseModel extends BaseModel> {
|
||||
updateBy: UpdateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
||||
private postgresDatabase!: PostgresDatabase;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
|
||||
export default interface CreateBy<TBaseModel extends AnalyticsBaseModel> {
|
||||
|
7
CommonServer/Types/AnalyticsDatabase/DeleteBy.ts
Normal file
7
CommonServer/Types/AnalyticsDatabase/DeleteBy.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
|
||||
export default interface DeleteBy<TBaseModel extends AnalyticsBaseModel> {
|
||||
data: TBaseModel;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}
|
7
CommonServer/Types/AnalyticsDatabase/FindBy.ts
Normal file
7
CommonServer/Types/AnalyticsDatabase/FindBy.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
|
||||
export default interface FindBy<TBaseModel extends AnalyticsBaseModel> {
|
||||
data: TBaseModel;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}
|
27
CommonServer/Types/AnalyticsDatabase/Hooks.ts
Normal file
27
CommonServer/Types/AnalyticsDatabase/Hooks.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import BaseModel from "Common/AnalyticsModels/BaseModel";
|
||||
import CreateBy from "./CreateBy";
|
||||
import DeleteBy from "./DeleteBy";
|
||||
import FindBy from "./FindBy";
|
||||
import UpdateBy from "./UpdateBy";
|
||||
|
||||
export type DatabaseTriggerType = 'on-create' | 'on-update' | 'on-delete';
|
||||
|
||||
export interface OnCreate<TBaseModel extends BaseModel> {
|
||||
createBy: CreateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnFind<TBaseModel extends BaseModel> {
|
||||
findBy: FindBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnDelete<TBaseModel extends BaseModel> {
|
||||
deleteBy: DeleteBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnUpdate<TBaseModel extends BaseModel> {
|
||||
updateBy: UpdateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
7
CommonServer/Types/AnalyticsDatabase/UpdateBy.ts
Normal file
7
CommonServer/Types/AnalyticsDatabase/UpdateBy.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
|
||||
export default interface UpdateBy<TBaseModel extends AnalyticsBaseModel> {
|
||||
data: TBaseModel;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}
|
27
CommonServer/Types/Database/Hooks.ts
Normal file
27
CommonServer/Types/Database/Hooks.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import BaseModel from "Common/Models/BaseModel";
|
||||
import CreateBy from "./CreateBy";
|
||||
import DeleteBy from "./DeleteBy";
|
||||
import FindBy from "./FindBy";
|
||||
import UpdateBy from "./UpdateBy";
|
||||
|
||||
export type DatabaseTriggerType = 'on-create' | 'on-update' | 'on-delete';
|
||||
|
||||
export interface OnCreate<TBaseModel extends BaseModel> {
|
||||
createBy: CreateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnFind<TBaseModel extends BaseModel> {
|
||||
findBy: FindBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnDelete<TBaseModel extends BaseModel> {
|
||||
deleteBy: DeleteBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
||||
|
||||
export interface OnUpdate<TBaseModel extends BaseModel> {
|
||||
updateBy: UpdateBy<TBaseModel>;
|
||||
carryForward: any;
|
||||
}
|
23
Llama/Dockerfile.tpl
Normal file
23
Llama/Dockerfile.tpl
Normal file
@ -0,0 +1,23 @@
|
||||
ARG TAG=latest
|
||||
FROM continuumio/miniconda3:$TAG
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND="noninteractive" apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
locales \
|
||||
sudo \
|
||||
build-essential \
|
||||
dpkg-dev \
|
||||
wget \
|
||||
openssh-server \
|
||||
nano \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setting up locales
|
||||
|
||||
RUN locale-gen en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
# Updating conda to the latest version
|
||||
RUN conda update conda -y
|
||||
|
@ -1,4 +1,4 @@
|
||||
import AnalyticsBaseModel from 'Common/Models/AnalyticsBaseModel';
|
||||
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
|
||||
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn';
|
||||
import TableColumnType from 'Common/Types/BaseDatabase/TableColumnType';
|
||||
import AnalyticsTableEngine from 'Common/Types/AnalyticsDatabase/AnalyticsTableEngine';
|
||||
|
@ -32,8 +32,8 @@ If you need advanced features, such as API Access, Advances Workflows, or Advanc
|
||||
## Installation
|
||||
|
||||
- [Install on Kubernetes with Helm](https://artifacthub.io/packages/helm/oneuptime/oneuptime) (recommended for production)
|
||||
- [Install with Docker Compose](https://github.com/OneUptime/oneuptime/blob/master/Docs/Installation/DockerCompose.md) (hobby install, not recommended for production use)
|
||||
- [Install for local development](https://github.com/OneUptime/oneuptime/blob/master/Docs/Installation/Development.md)
|
||||
- [Install with Docker Compose](https://github.com/OneUptime/oneuptime/blob/master/Docs/Installation/DockerCompose.md) (hobby install, not recommended for production)
|
||||
- [Install for Local Development](https://github.com/OneUptime/oneuptime/blob/master/Docs/Installation/Development.md)
|
||||
|
||||
## Philosophy
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user