mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Datatypes Seeded and Not Found page added
This commit is contained in:
+2
-11
@@ -1,28 +1,19 @@
|
||||
import { fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { relationships } from "@/lib/schemas/relationship-schema";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { eq, ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
|
||||
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
|
||||
|
||||
export const getNextSequence = (fields: FieldType[]): number => {
|
||||
|
||||
if (fields.length == 0)
|
||||
return 0;
|
||||
|
||||
const maxSequenceItem = fields.reduce((max, field: FieldType) => {
|
||||
return field.sequence > max.sequence ? field : max
|
||||
});
|
||||
return maxSequenceItem.sequence + 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const cloneField = (field: FieldType): FieldType => {
|
||||
return {
|
||||
...field,
|
||||
|
||||
+130
-7
@@ -1,7 +1,10 @@
|
||||
import { ForeignKeyActions } from "@/lib/field"
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema"
|
||||
import { FieldType } from "@/lib/schemas/field-schema"
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema"
|
||||
import { TableType } from "@/lib/schemas/table-schema"
|
||||
|
||||
import { tablesRelations, TableType } from "@/lib/schemas/table-schema"
|
||||
import { v4 } from "uuid"
|
||||
import pluralize from 'pluralize';
|
||||
export const getRelationshipSourceAndTarget = (sourceTableId: string, sourceField: FieldType, targetTableId: string, targetField: FieldType) => {
|
||||
if (sourceField.isPrimary || targetField.isPrimary) {
|
||||
if (sourceField.isPrimary) {
|
||||
@@ -56,11 +59,11 @@ export const getForeignRelationships = (table: TableType): RelationshipType[] =>
|
||||
relationship.cardinality == Cardinality.many_to_one
|
||||
) : []).map((relationship: RelationshipType) => {
|
||||
return {
|
||||
...relationship ,
|
||||
sourceTable : relationship.targetTable ,
|
||||
targetTable : relationship.sourceTable ,
|
||||
sourceField : relationship.targetField ,
|
||||
targetField : relationship.sourceField ,
|
||||
...relationship,
|
||||
sourceTable: relationship.targetTable,
|
||||
targetTable: relationship.sourceTable,
|
||||
sourceField: relationship.targetField,
|
||||
targetField: relationship.sourceField,
|
||||
|
||||
}
|
||||
});
|
||||
@@ -78,4 +81,124 @@ export const getDefaultRelationshipName = (relationship: RelationshipType) => {
|
||||
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
|
||||
return "";
|
||||
return `fk_${relationship.sourceTable?.name}_${relationship.targetTable?.name}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const decomposeManyToMany = (database: DatabaseType): DatabaseType => {
|
||||
// Clone the tables and relationships to avoid mutating the original database object
|
||||
let relationships: RelationshipType[] = [...database.relationships];
|
||||
let tables: TableType[] = [...database.tables];
|
||||
|
||||
// Filter out all many-to-many relationships to process
|
||||
let manyToManyRelationships = relationships.filter(
|
||||
(relationship: RelationshipType) => relationship.cardinality == Cardinality.many_to_many
|
||||
);
|
||||
|
||||
// Loop through each many-to-many relationship
|
||||
for (const relationship of manyToManyRelationships) {
|
||||
// Find the source and target tables
|
||||
const sourceTable: TableType = tables.find(
|
||||
(table: TableType) => table.id == relationship.sourceTableId
|
||||
) as TableType;
|
||||
|
||||
const targetTable: TableType = tables.find(
|
||||
(table: TableType) => table.id == relationship.targetTableId
|
||||
) as TableType;
|
||||
|
||||
// Find the source and target fields involved in the relationship
|
||||
const sourceField: FieldType = sourceTable.fields.find(
|
||||
(field: FieldType) => field.id == relationship.sourceFieldId
|
||||
) as FieldType;
|
||||
|
||||
const targetField: FieldType = targetTable.fields.find(
|
||||
(field: FieldType) => field.id == relationship.targetFieldId
|
||||
) as FieldType;
|
||||
|
||||
// Convert table names to singular if they are plural
|
||||
const singularSourceTableName: string = pluralize.isPlural(sourceTable.name)
|
||||
? pluralize.singular(sourceTable.name)
|
||||
: sourceTable.name;
|
||||
|
||||
const singularTargetTableName: string = pluralize.isPlural(targetTable.name)
|
||||
? pluralize.singular(targetTable.name)
|
||||
: targetTable.name;
|
||||
|
||||
// Create a foreign key field pointing to the source table
|
||||
const junctionSourceField: FieldType = {
|
||||
id: v4(),
|
||||
name: `${singularSourceTableName}_${sourceField.name}`,
|
||||
nullable: false,
|
||||
typeId: sourceField.typeId,
|
||||
type: sourceField.type,
|
||||
isPrimary: true
|
||||
} as FieldType;
|
||||
|
||||
// Create a foreign key field pointing to the target table
|
||||
const junctionTargetField: FieldType = {
|
||||
id: v4(),
|
||||
name: `${singularTargetTableName}_${targetField.name}`,
|
||||
nullable: false,
|
||||
typeId: targetField.typeId,
|
||||
type: targetField.type,
|
||||
isPrimary: true
|
||||
} as FieldType;
|
||||
|
||||
// Define the new junction table combining both foreign keys
|
||||
const junctionTable: TableType = {
|
||||
id: v4(),
|
||||
name: `${singularSourceTableName}_${targetTable.name}`,
|
||||
fields: [
|
||||
junctionSourceField,
|
||||
junctionTargetField
|
||||
]
|
||||
} as TableType;
|
||||
|
||||
// Create a one-to-many relationship from the source table to the junction table
|
||||
const junctionSourceRelationship: RelationshipType = {
|
||||
id: v4(),
|
||||
sourceTable: sourceTable,
|
||||
targetTable: junctionTable,
|
||||
sourceTableId: sourceTable.id,
|
||||
targetTableId: junctionTable.id,
|
||||
|
||||
sourceField: sourceField,
|
||||
sourceFieldId: sourceField.id,
|
||||
targetField: junctionSourceField,
|
||||
targetFieldId: junctionSourceField.id,
|
||||
|
||||
cardinality: Cardinality.one_to_many,
|
||||
onDelete: ForeignKeyActions.CASCADE
|
||||
} as RelationshipType;
|
||||
|
||||
// Create a one-to-many relationship from the target table to the junction table
|
||||
const junctionTargetRelationship: RelationshipType = {
|
||||
id: v4(),
|
||||
sourceTable: targetTable,
|
||||
targetTable: junctionTable,
|
||||
sourceTableId: targetTable.id,
|
||||
targetTableId: junctionTable.id,
|
||||
|
||||
sourceField: targetField,
|
||||
sourceFieldId: targetField.id,
|
||||
targetField: junctionTargetField,
|
||||
targetFieldId: junctionTargetField.id,
|
||||
|
||||
cardinality: Cardinality.one_to_many,
|
||||
onDelete: ForeignKeyActions.CASCADE
|
||||
} as RelationshipType;
|
||||
|
||||
// Add the new junction table and the two relationships to the database
|
||||
tables.push(junctionTable);
|
||||
relationships.push(junctionSourceRelationship);
|
||||
relationships.push(junctionTargetRelationship);
|
||||
}
|
||||
|
||||
// Return the updated database object with the new junction tables and relationships added
|
||||
return {
|
||||
...database,
|
||||
tables,
|
||||
relationships
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
|
||||
|
||||
let renderableTables: RenderableTable[] = database.tables.map((table: TableType) => toRenderableTable(table, database));
|
||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||
|
||||
try {
|
||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||
|
||||
@@ -32,8 +33,10 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
|
||||
if (database.dialect == DatabaseDialect.POSTGRES) {
|
||||
postgresEnums = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||
if (postgresEnums.length > 0) {
|
||||
for (const postgresEnum of postgresEnums)
|
||||
for (const postgresEnum of postgresEnums) {
|
||||
(postgresEnum as any).table = table;
|
||||
dbAst.push(PotgresEnumToAst(postgresEnum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +44,16 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
|
||||
|
||||
if (table.indices && table.indices.length > 0)
|
||||
for (const index of table.indices)
|
||||
dbAst.push(IndexToAst({
|
||||
...index,
|
||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
|
||||
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
|
||||
) as FieldType[],
|
||||
}, table));
|
||||
if (index.fieldIndices?.length > 0)
|
||||
dbAst.push(IndexToAst({
|
||||
...index,
|
||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
|
||||
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
|
||||
) as FieldType[],
|
||||
}, table));
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
throw error;
|
||||
|
||||
}
|
||||
return dbAst;
|
||||
}
|
||||
@@ -78,20 +80,17 @@ export const TableToAst = (table: TableType, data_types: DataType[], isPostgresD
|
||||
...foreignKeysConstraints
|
||||
];
|
||||
|
||||
const filed_definitions = table.fields.map((field: FieldType) => {
|
||||
const filed_definitions = table.fields.filter((field: FieldType) => field.type).map((field: FieldType) => {
|
||||
const isPostgresEnum: boolean = isPostgresDialect && field.type.name == "enum";
|
||||
|
||||
return FieldToAst({
|
||||
...field,
|
||||
type: !(isPostgresEnum) ?
|
||||
data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType :
|
||||
{ name: `${field.name.toLowerCase()}_enum` } as DataType,
|
||||
{ name: `${table.name}_${field.name.toLowerCase()}_enum` } as DataType,
|
||||
}, multiPrimaryKeys, !isPostgresEnum)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return {
|
||||
keyword: "table",
|
||||
type: "create",
|
||||
@@ -286,7 +285,7 @@ export const PotgresEnumToAst = (field: FieldType) => {
|
||||
resource: "enum",
|
||||
name: {
|
||||
schema: null,
|
||||
name: `${field.name.toLowerCase()}_enum`
|
||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||
},
|
||||
keyword: "type",
|
||||
create_definitions: {
|
||||
@@ -340,7 +339,7 @@ export const relationshipToAst = (relationship: RelationshipType) => {
|
||||
|
||||
if (relationship.onDelete) {
|
||||
const value: string | null = foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
|
||||
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on delete",
|
||||
@@ -363,7 +362,7 @@ export const relationshipToAst = (relationship: RelationshipType) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
constraint: null,
|
||||
definition: [
|
||||
@@ -388,7 +387,7 @@ export const relationshipToAst = (relationship: RelationshipType) => {
|
||||
}
|
||||
],
|
||||
keyword: "references",
|
||||
on_action
|
||||
on_action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
import { relationshipToAst } from "./database_to_ast";
|
||||
import { DatabaseInsertType } from "@/lib/schemas/database-schema";
|
||||
|
||||
|
||||
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
|
||||
@@ -89,9 +87,9 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
{ name: column.name.name }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst([referenceColumn], table);
|
||||
|
||||
|
||||
try {
|
||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
||||
relationships = relationships.concat(newRelationships);
|
||||
@@ -104,7 +102,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
}
|
||||
|
||||
if ((instructionAst[0] as any).constraints && (instructionAst[0] as any).constraints.length > 0) {
|
||||
|
||||
|
||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst((instructionAst[0] as any).constraints, table)
|
||||
try {
|
||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
||||
@@ -206,15 +204,16 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
|
||||
for (const alterTable of alterTableStatements) {
|
||||
try {
|
||||
const instructionAst = parser.astify(alterTable, {
|
||||
let instructionAst: any = parser.astify(alterTable, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (instructionAst) {
|
||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
|
||||
...instructionAst,
|
||||
table: (instructionAst as any).table?.[0].table
|
||||
...instructionAst[0],
|
||||
table: instructionAst[0].table?.[0].table
|
||||
}) as RelationshipInsertType[];
|
||||
|
||||
|
||||
relationships = relationships.concat(extractedRelationships)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -330,7 +329,7 @@ export const astToRelationship = (tables: TableInsertType[], constraintAst?: any
|
||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.reference_definition?.definition?.[0].column);
|
||||
|
||||
const on_action: any | undefined = expression.create_definitions?.reference_definition?.on_action;
|
||||
|
||||
|
||||
let onDelete: ForeignKeyActions | undefined;
|
||||
let onUpdate: ForeignKeyActions | undefined;
|
||||
if (on_action) {
|
||||
@@ -515,6 +514,7 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n
|
||||
});
|
||||
autoIncrement = true;
|
||||
}
|
||||
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
let length: number | undefined;
|
||||
@@ -546,12 +546,17 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n
|
||||
const constraints: any[] | undefined = ast.constraints;
|
||||
if (constraints && constraints.length > 0) {
|
||||
|
||||
|
||||
const nullableConstraints: any | undefined = constraints.find((c: any) => c.type == "not null");
|
||||
const defaultValueConstraints: any | undefined = constraints.find((c: any) => c.type == "default");
|
||||
|
||||
if (defaultValueConstraints) {
|
||||
console.log(defaultValueConstraints.default)
|
||||
if (defaultValueConstraints.default.type == "keyword" && defaultValueConstraints.default.keyword == "current_timestamp")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "call" && defaultValueConstraints.default.function?.name == "now")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "cast" && defaultValueConstraints.default.operand)
|
||||
defaultValue = String(defaultValueConstraints.default.operand.value)
|
||||
else
|
||||
@@ -589,11 +594,8 @@ export const postgresAstToField = (ast: any, data_types: DataType[], sequence: n
|
||||
} as FieldInsertType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]): RelationshipInsertType[] => {
|
||||
|
||||
|
||||
const relationships: RelationshipInsertType[] = [];
|
||||
const changes = ast.changes;
|
||||
|
||||
@@ -606,18 +608,13 @@ export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]):
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
|
||||
|
||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == foreignKeyConstraint.foreignTable.name);
|
||||
|
||||
const targetField: FieldInsertType | undefined = targetTable.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.localColumns[0]?.name)
|
||||
|
||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.foreignColumns[0]?.name)
|
||||
|
||||
const onDelete = foreignKeyConstraint.onDelete ? astToForiegnKeyAction( foreignKeyConstraint.onDelete) : undefined ;
|
||||
const onUpdate = foreignKeyConstraint.onUpdate ? astToForiegnKeyAction( foreignKeyConstraint.onUpdate) : undefined ;
|
||||
|
||||
|
||||
|
||||
const onDelete = foreignKeyConstraint.onDelete ? astToForiegnKeyAction(foreignKeyConstraint.onDelete) : undefined;
|
||||
const onUpdate = foreignKeyConstraint.onUpdate ? astToForiegnKeyAction(foreignKeyConstraint.onUpdate) : undefined;
|
||||
|
||||
if (!sourceField || !targetField || !sourceTable)
|
||||
continue;
|
||||
|
||||
@@ -627,12 +624,11 @@ export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]):
|
||||
targetTableId: targetTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
targetFieldId: targetField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many ,
|
||||
onDelete , onUpdate
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete, onUpdate
|
||||
} as RelationshipInsertType)
|
||||
}
|
||||
|
||||
|
||||
if (relationships.length == foreignKeyConstraints.length)
|
||||
return relationships;
|
||||
else
|
||||
@@ -641,8 +637,6 @@ export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]):
|
||||
message: "Failed to Extract all relationships",
|
||||
relationships
|
||||
} as any)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -664,9 +658,9 @@ const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableIns
|
||||
{
|
||||
name: constraint.foreignColumns?.[0].name
|
||||
}
|
||||
],
|
||||
onDelete : constraint.onDelete ,
|
||||
onUpdate : constraint.onUpdate ,
|
||||
],
|
||||
onDelete: constraint.onDelete,
|
||||
onUpdate: constraint.onUpdate,
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -677,7 +671,6 @@ const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableIns
|
||||
name: table.name
|
||||
},
|
||||
changes
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,12 +681,9 @@ export const astToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType
|
||||
throw Error("table not found");
|
||||
|
||||
const fieldNames: string[] = ast.index_columns.map((column: any) => column.column);
|
||||
|
||||
|
||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
||||
.map((field: FieldInsertType) => field.id);
|
||||
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.index,
|
||||
|
||||
@@ -86,10 +86,10 @@ export function fixSQLiteColumnOrder(sql: string): string {
|
||||
const lines = sql.split('\n');
|
||||
let currentColumn = '';
|
||||
const result: string[] = [];
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
|
||||
if (isTableStructureLine(trimmed)) {
|
||||
if (currentColumn) {
|
||||
result.push(processSQLiteIntegerColumn(currentColumn));
|
||||
@@ -98,7 +98,7 @@ export function fixSQLiteColumnOrder(sql: string): string {
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (trimmed.endsWith(',')) {
|
||||
currentColumn += ' ' + trimmed.slice(0, -1);
|
||||
result.push(processSQLiteIntegerColumn(currentColumn) + ',');
|
||||
@@ -107,7 +107,7 @@ export function fixSQLiteColumnOrder(sql: string): string {
|
||||
currentColumn += ' ' + trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentColumn) result.push(processSQLiteIntegerColumn(currentColumn));
|
||||
return result.join('\n');
|
||||
}
|
||||
@@ -123,7 +123,7 @@ function processSQLiteIntegerColumn(columnDef: string): string {
|
||||
const isPrimaryKey = rest.match(/\bPRIMARY\s+KEY\b/i);
|
||||
const isAutoIncrement = rest.match(/\bAUTOINCREMENT\b/i);
|
||||
const isNotNull = rest.match(/\bNOT\s+NULL\b/i);
|
||||
|
||||
|
||||
if (!isPrimaryKey && !isAutoIncrement) {
|
||||
return columnDef; // Leave non-PK INTEGER columns unchanged
|
||||
}
|
||||
@@ -138,7 +138,7 @@ function processSQLiteIntegerColumn(columnDef: string): string {
|
||||
|
||||
// Reconstruct with SQLite's required order
|
||||
let reconstructed = `${colName} INTEGER`;
|
||||
|
||||
|
||||
if (isPrimaryKey) reconstructed += ' PRIMARY KEY';
|
||||
if (isAutoIncrement) reconstructed += ' AUTOINCREMENT';
|
||||
if (isNotNull) reconstructed += ' NOT NULL';
|
||||
@@ -148,7 +148,7 @@ function processSQLiteIntegerColumn(columnDef: string): string {
|
||||
}
|
||||
|
||||
export interface CircularDependencyError {
|
||||
cycle : string[] ;
|
||||
success : boolean ;
|
||||
message : string ;
|
||||
cycle: string[];
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
Reference in New Issue
Block a user