mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 21:06:04 +00:00
fix persisting of many to many entity
This commit is contained in:
@@ -165,6 +165,13 @@ export class Entity<
|
|||||||
return this.getField(name);
|
return this.getField(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasField(name: string): boolean;
|
||||||
|
hasField(field: Field): boolean;
|
||||||
|
hasField(nameOrField: string | Field): boolean {
|
||||||
|
const name = typeof nameOrField === "string" ? nameOrField : nameOrField.name;
|
||||||
|
return this.fields.findIndex((field) => field.name === name) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
getFields(include_virtual: boolean = false): Field[] {
|
getFields(include_virtual: boolean = false): Field[] {
|
||||||
if (include_virtual) return this.fields;
|
if (include_virtual) return this.fields;
|
||||||
return this.fields.filter((f) => !f.isVirtual());
|
return this.fields.filter((f) => !f.isVirtual());
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
sort: entity.getDefaultSort(),
|
sort: entity.getDefaultSort(),
|
||||||
select: entity.getSelect()
|
select: entity.getSelect()
|
||||||
};
|
};
|
||||||
//console.log("validated", validated);
|
|
||||||
|
|
||||||
if (!options) return validated;
|
if (!options) return validated;
|
||||||
|
|
||||||
@@ -144,7 +143,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
throw new InvalidSearchParamsException(`Invalid where field(s): ${invalid.join(", ")}`);
|
throw new InvalidSearchParamsException(
|
||||||
|
`Invalid where field(s): ${invalid.join(", ")}`
|
||||||
|
).context({ aliases, entity: entity.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
validated.where = options.where;
|
validated.where = options.where;
|
||||||
@@ -334,7 +335,6 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
|
|
||||||
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
|
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
|
||||||
const { qb, options } = this.buildQuery(_options);
|
const { qb, options } = this.buildQuery(_options);
|
||||||
//console.log("findMany:options", options);
|
|
||||||
|
|
||||||
await this.emgr.emit(
|
await this.emgr.emit(
|
||||||
new Repository.Events.RepositoryFindManyBefore({ entity: this.entity, options })
|
new Repository.Events.RepositoryFindManyBefore({ entity: this.entity, options })
|
||||||
@@ -386,7 +386,6 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//console.log("findManyOptions", newEntity.name, findManyOptions);
|
|
||||||
return this.cloneFor(newEntity).findMany(findManyOptions);
|
return this.cloneFor(newEntity).findMany(findManyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class PrimaryField<Required extends true | false = false> extends Field<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async transformPersist(value: any): Promise<number> {
|
override async transformPersist(value: any): Promise<number> {
|
||||||
throw new Error("This function should not be called");
|
throw new Error("PrimaryField: This function should not be called");
|
||||||
}
|
}
|
||||||
|
|
||||||
override toJsonSchema() {
|
override toJsonSchema() {
|
||||||
|
|||||||
@@ -105,13 +105,13 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getReferenceQuery(entity: Entity, id: number): Partial<RepoQuery> {
|
override getReferenceQuery(entity: Entity, id: number): Partial<RepoQuery> {
|
||||||
const conn = this.connectionEntity;
|
const { other, otherRef } = this.getQueryInfo(entity);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
where: {
|
where: {
|
||||||
[`${conn.name}.${entity.name}_${entity.getPrimaryField().name}`]: id
|
[otherRef]: id
|
||||||
},
|
},
|
||||||
join: [this.target.reference]
|
join: [other.reference]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,47 +160,27 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
|||||||
.whereRef(entityRef, "=", otherRef)
|
.whereRef(entityRef, "=", otherRef)
|
||||||
.innerJoin(...join)
|
.innerJoin(...join)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
/*return qb.select((eb) => {
|
|
||||||
const select: any[] = other.entity.getSelect(other.entity.name);
|
|
||||||
// @todo: also add to find by references
|
|
||||||
if (additionalFields.length > 0) {
|
|
||||||
const conn = this.connectionEntity.name;
|
|
||||||
select.push(
|
|
||||||
jsonBuildObject(
|
|
||||||
Object.fromEntries(
|
|
||||||
additionalFields.map((f) => [f.name, eb.ref(`${conn}.${f.name}`)])
|
|
||||||
)
|
|
||||||
).as(this.connectionTableMappedName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom(other.entity.name)
|
|
||||||
.select(select)
|
|
||||||
.whereRef(entityRef, "=", otherRef)
|
|
||||||
.innerJoin(...join)
|
|
||||||
.limit(limit)
|
|
||||||
).as(other.reference);
|
|
||||||
});*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(em: EntityManager<any>) {
|
initialize(em: EntityManager<any>) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
|
|
||||||
//this.connectionEntity.addField(new RelationField(this.source.entity));
|
const sourceField = RelationField.create(this, this.source);
|
||||||
//this.connectionEntity.addField(new RelationField(this.target.entity));
|
const targetField = RelationField.create(this, this.target);
|
||||||
this.connectionEntity.addField(RelationField.create(this, this.source));
|
|
||||||
this.connectionEntity.addField(RelationField.create(this, this.target));
|
|
||||||
|
|
||||||
// @todo: check this
|
if (em.hasEntity(this.connectionEntity)) {
|
||||||
for (const field of this.additionalFields) {
|
// @todo: also check for correct signatures of field
|
||||||
this.source.entity.addField(new VirtualField(this.connectionTableMappedName));
|
if (!this.connectionEntity.hasField(sourceField)) {
|
||||||
this.target.entity.addField(new VirtualField(this.connectionTableMappedName));
|
this.connectionEntity.addField(sourceField);
|
||||||
|
}
|
||||||
|
if (!this.connectionEntity.hasField(targetField)) {
|
||||||
|
this.connectionEntity.addField(targetField);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.connectionEntity.addField(sourceField);
|
||||||
|
this.connectionEntity.addField(targetField);
|
||||||
|
em.addEntity(this.connectionEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
em.addEntity(this.connectionEntity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override getName(): string {
|
override getName(): string {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export class RelationField extends Field<RelationFieldConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async transformPersist(value: any, em: EntityManager<any>): Promise<any> {
|
override async transformPersist(value: any, em: EntityManager<any>): Promise<any> {
|
||||||
throw new Error("This function should not be called");
|
throw new Error("RelationField: This function should not be called");
|
||||||
}
|
}
|
||||||
|
|
||||||
override toJsonSchema() {
|
override toJsonSchema() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { PrimaryFieldType } from "core";
|
|||||||
import type { Entity, EntityManager } from "../entities";
|
import type { Entity, EntityManager } from "../entities";
|
||||||
import {
|
import {
|
||||||
type EntityRelation,
|
type EntityRelation,
|
||||||
|
ManyToManyRelation,
|
||||||
type MutationOperation,
|
type MutationOperation,
|
||||||
MutationOperations,
|
MutationOperations,
|
||||||
RelationField
|
RelationField
|
||||||
@@ -26,11 +27,26 @@ export class RelationMutator {
|
|||||||
*/
|
*/
|
||||||
getRelationalKeys(): string[] {
|
getRelationalKeys(): string[] {
|
||||||
const references: string[] = [];
|
const references: string[] = [];
|
||||||
|
|
||||||
|
// if persisting a manytomany connection table
|
||||||
|
// @todo: improve later
|
||||||
|
if (this.entity.type === "generated") {
|
||||||
|
const relation = this.em.relations.all.find(
|
||||||
|
(r) => r instanceof ManyToManyRelation && r.connectionEntity.name === this.entity.name
|
||||||
|
);
|
||||||
|
if (relation instanceof ManyToManyRelation) {
|
||||||
|
references.push(
|
||||||
|
...this.entity.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.em.relationsOf(this.entity.name).map((r) => {
|
this.em.relationsOf(this.entity.name).map((r) => {
|
||||||
const info = r.helper(this.entity.name).getMutationInfo();
|
const info = r.helper(this.entity.name).getMutationInfo();
|
||||||
references.push(info.reference);
|
references.push(info.reference);
|
||||||
info.local_field && references.push(info.local_field);
|
info.local_field && references.push(info.local_field);
|
||||||
});
|
});
|
||||||
|
|
||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ export const FieldLabel: React.FC<React.ComponentProps<"label"> & { field: Field
|
|||||||
}) => {
|
}) => {
|
||||||
const desc = field.getDescription();
|
const desc = field.getDescription();
|
||||||
return (
|
return (
|
||||||
<Label {...props} title={desc} className="flex flex-row gap-2 items-center">
|
<Label {...props} title={desc} className="flex flex-row gap-1 items-center">
|
||||||
{field.getLabel()}
|
{field.getLabel()}
|
||||||
|
{field.isRequired() && <span className="font-medium opacity-30">*</span>}
|
||||||
{desc && <TbInfoCircle className="opacity-50" />}
|
{desc && <TbInfoCircle className="opacity-50" />}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const Check = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type TableProps = {
|
export type EntityTableProps = {
|
||||||
data: EntityData[];
|
data: EntityData[];
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
select?: string[];
|
select?: string[];
|
||||||
@@ -44,7 +44,7 @@ type TableProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EntityTable: React.FC<TableProps> = ({
|
export const EntityTable: React.FC<EntityTableProps> = ({
|
||||||
data = [],
|
data = [],
|
||||||
entity,
|
entity,
|
||||||
select,
|
select,
|
||||||
@@ -184,7 +184,7 @@ const SortIndicator = ({
|
|||||||
sort,
|
sort,
|
||||||
field
|
field
|
||||||
}: {
|
}: {
|
||||||
sort: Pick<TableProps, "sort">["sort"];
|
sort: Pick<EntityTableProps, "sort">["sort"];
|
||||||
field: string;
|
field: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!sort || sort.by !== field) return <TbSelector size={18} className="mt-[1px]" />;
|
if (!sort || sort.by !== field) return <TbSelector size={18} className="mt-[1px]" />;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { Popover } from "ui/components/overlay/Popover";
|
|||||||
import { Link } from "ui/components/wouter/Link";
|
import { Link } from "ui/components/wouter/Link";
|
||||||
import { routes } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { EntityTable } from "../EntityTable";
|
import { EntityTable, type EntityTableProps } from "../EntityTable";
|
||||||
|
import type { ResponseObject } from "modules/ModuleApi";
|
||||||
|
|
||||||
// @todo: allow clear if not required
|
// @todo: allow clear if not required
|
||||||
export function EntityRelationalFormField({
|
export function EntityRelationalFormField({
|
||||||
@@ -20,7 +21,7 @@ export function EntityRelationalFormField({
|
|||||||
field,
|
field,
|
||||||
data,
|
data,
|
||||||
disabled,
|
disabled,
|
||||||
tabIndex
|
tabIndex,
|
||||||
}: {
|
}: {
|
||||||
fieldApi: FieldApi<any, any>;
|
fieldApi: FieldApi<any, any>;
|
||||||
field: RelationField;
|
field: RelationField;
|
||||||
@@ -30,12 +31,18 @@ export function EntityRelationalFormField({
|
|||||||
}) {
|
}) {
|
||||||
const { app } = useBknd();
|
const { app } = useBknd();
|
||||||
const entity = app.entity(field.target())!;
|
const entity = app.entity(field.target())!;
|
||||||
const [query, setQuery] = useState<any>({ limit: 10, page: 1, perPage: 10 });
|
const [query, setQuery] = useState<any>({
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
select: entity.getSelect(undefined, "table"),
|
||||||
|
});
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const ref = useRef<any>(null);
|
const ref = useRef<any>(null);
|
||||||
const $q = useEntityQuery(field.target(), undefined, {
|
const $q = useEntityQuery(field.target(), undefined, {
|
||||||
|
select: query.select,
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
offset: (query.page - 1) * query.limit
|
offset: (query.page - 1) * query.limit,
|
||||||
});
|
});
|
||||||
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
|
const [_value, _setValue] = useState<{ id: number | undefined; [key: string]: any }>();
|
||||||
|
|
||||||
@@ -120,8 +127,8 @@ export function EntityRelationalFormField({
|
|||||||
"Enter",
|
"Enter",
|
||||||
() => {
|
() => {
|
||||||
ref.current?.click();
|
ref.current?.click();
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{_value ? (
|
{_value ? (
|
||||||
@@ -179,31 +186,37 @@ export function EntityRelationalFormField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopoverTable = ({ container, entity, query, toggle, onClickRow, onClickPage }) => {
|
type PropoverTableProps = Omit<EntityTableProps, "data"> & {
|
||||||
|
container: ResponseObject;
|
||||||
|
query: any;
|
||||||
|
toggle: () => void;
|
||||||
|
}
|
||||||
|
const PopoverTable = ({ container, entity, query, toggle, onClickRow, onClickPage }: PropoverTableProps) => {
|
||||||
function handleNext() {
|
function handleNext() {
|
||||||
if (query.limit * query.page < container.meta?.count) {
|
if (query.limit * query.page < container.meta?.count) {
|
||||||
onClickPage(query.page + 1);
|
onClickPage?.(query.page + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePrev() {
|
function handlePrev() {
|
||||||
if (query.page > 1) {
|
if (query.page > 1) {
|
||||||
onClickPage(query.page - 1);
|
onClickPage?.(query.page - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
["ArrowRight", handleNext],
|
["ArrowRight", handleNext],
|
||||||
["ArrowLeft", handlePrev],
|
["ArrowLeft", handlePrev],
|
||||||
["Escape", toggle]
|
["Escape", toggle],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<EntityTable
|
<EntityTable
|
||||||
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
|
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
|
||||||
data={container.data ?? []}
|
data={container ?? []}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
select={query.select}
|
||||||
total={container.meta?.count}
|
total={container.meta?.count}
|
||||||
page={query.page}
|
page={query.page}
|
||||||
onClickRow={onClickRow}
|
onClickRow={onClickRow}
|
||||||
|
|||||||
@@ -262,18 +262,18 @@ function EntityDetailInner({
|
|||||||
navigate(routes.data.entity.edit(other.entity.name, row.id));
|
navigate(routes.data.entity.edit(other.entity.name, row.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickNew() {
|
let handleClickNew: any;
|
||||||
try {
|
try {
|
||||||
|
if (other.entity.type !== "system") {
|
||||||
const ref = relation.getReferenceQuery(other.entity, id, other.reference);
|
const ref = relation.getReferenceQuery(other.entity, id, other.reference);
|
||||||
|
handleClickNew = () => {
|
||||||
navigate(routes.data.entity.create(other.entity.name), {
|
navigate(routes.data.entity.create(other.entity.name), {
|
||||||
query: ref.where
|
query: ref.where
|
||||||
});
|
});
|
||||||
//navigate(routes.data.entity.create(other.entity.name) + `?${query}`);
|
//navigate(routes.data.entity.create(other.entity.name) + `?${query}`);
|
||||||
} catch (e) {
|
};
|
||||||
console.error("handleClickNew", e);
|
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {}
|
||||||
|
|
||||||
if (!$q.data) {
|
if (!$q.data) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
const entity = $data.entity(params.entity as string);
|
const entity = $data.entity(params.entity as string);
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
} else if (entity.type !== "regular") {
|
} else if (entity.type === "system") {
|
||||||
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
|
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function EntityCreateButton({ entity }: { entity: Entity }) {
|
|||||||
|
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
if (!entity) return null;
|
if (!entity) return null;
|
||||||
if (entity.type !== "regular") {
|
if (entity.type === "system") {
|
||||||
const system = {
|
const system = {
|
||||||
users: b.app.config.auth.entity_name,
|
users: b.app.config.auth.entity_name,
|
||||||
media: b.app.config.media.entity_name
|
media: b.app.config.media.entity_name
|
||||||
|
|||||||
Reference in New Issue
Block a user