mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #54 from bknd-io/fix/nextjs-schema-update
fixed schema updates to fail in nextjs envs due to lodash's merge
This commit is contained in:
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "bun:test";
|
|||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth";
|
||||||
|
import { DebugLogger } from "../../src/core";
|
||||||
import { EventManager } from "../../src/core/events";
|
import { EventManager } from "../../src/core/events";
|
||||||
import { Default, stripMark } from "../../src/core/utils";
|
import { Default, stripMark } from "../../src/core/utils";
|
||||||
import { EntityManager } from "../../src/data";
|
import { EntityManager } from "../../src/data";
|
||||||
@@ -17,6 +18,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
emgr: new EventManager(),
|
emgr: new EventManager(),
|
||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
|
logger: new DebugLogger(false),
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { cloneDeep, get, has, mergeWith, omit, set } from "lodash-es";
|
import { get, has, omit, set } from "lodash-es";
|
||||||
import {
|
import {
|
||||||
Default,
|
Default,
|
||||||
type Static,
|
type Static,
|
||||||
type TObject,
|
type TObject,
|
||||||
getFullPathKeys,
|
getFullPathKeys,
|
||||||
mark,
|
mergeObjectWith,
|
||||||
parse,
|
parse,
|
||||||
stripMark
|
stripMark
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
@@ -33,7 +33,7 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
) {
|
) {
|
||||||
this._default = Default(_schema, {} as any) as any;
|
this._default = Default(_schema, {} as any) as any;
|
||||||
this._value = initial
|
this._value = initial
|
||||||
? parse(_schema, cloneDeep(initial as any), {
|
? parse(_schema, structuredClone(initial as any), {
|
||||||
forceParse: this.isForceParse(),
|
forceParse: this.isForceParse(),
|
||||||
skipMark: this.isForceParse()
|
skipMark: this.isForceParse()
|
||||||
})
|
})
|
||||||
@@ -64,8 +64,12 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
return this._config;
|
return this._config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return structuredClone(this._config);
|
||||||
|
}
|
||||||
|
|
||||||
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> {
|
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> {
|
||||||
const valid = parse(this._schema, cloneDeep(config) as any, {
|
const valid = parse(this._schema, structuredClone(config) as any, {
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
skipMark: this.isForceParse()
|
skipMark: this.isForceParse()
|
||||||
});
|
});
|
||||||
@@ -114,7 +118,7 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
||||||
const current = cloneDeep(this._config);
|
const current = this.clone();
|
||||||
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
||||||
|
|
||||||
this.throwIfRestricted(partial);
|
this.throwIfRestricted(partial);
|
||||||
@@ -122,11 +126,13 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
|
|
||||||
// overwrite arrays and primitives, only deep merge objects
|
// overwrite arrays and primitives, only deep merge objects
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const config = mergeWith(current, partial, (objValue, srcValue) => {
|
//console.log("---alt:new", _jsonp(mergeObject(current, partial)));
|
||||||
|
const config = mergeObjectWith(current, partial, (objValue, srcValue) => {
|
||||||
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
|
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
|
||||||
return srcValue;
|
return srcValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
//console.log("---new", _jsonp(config));
|
||||||
|
|
||||||
//console.log("overwritePaths", this.options?.overwritePaths);
|
//console.log("overwritePaths", this.options?.overwritePaths);
|
||||||
if (this.options?.overwritePaths) {
|
if (this.options?.overwritePaths) {
|
||||||
@@ -164,14 +170,14 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("patch", { path, value, partial, config, current });
|
//console.log("patch", _jsonp({ path, value, partial, config, current }));
|
||||||
|
|
||||||
const newConfig = await this.set(config);
|
const newConfig = await this.set(config);
|
||||||
return [partial, newConfig];
|
return [partial, newConfig];
|
||||||
}
|
}
|
||||||
|
|
||||||
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
|
||||||
const current = cloneDeep(this._config);
|
const current = this.clone();
|
||||||
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
||||||
|
|
||||||
this.throwIfRestricted(partial);
|
this.throwIfRestricted(partial);
|
||||||
@@ -192,7 +198,7 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
if (p.length > 1) {
|
if (p.length > 1) {
|
||||||
const parent = p.slice(0, -1).join(".");
|
const parent = p.slice(0, -1).join(".");
|
||||||
if (!has(this._config, parent)) {
|
if (!has(this._config, parent)) {
|
||||||
console.log("parent", parent, JSON.stringify(this._config, null, 2));
|
//console.log("parent", parent, JSON.stringify(this._config, null, 2));
|
||||||
throw new Error(`Parent path "${parent}" does not exist`);
|
throw new Error(`Parent path "${parent}" does not exist`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +213,7 @@ export class SchemaObject<Schema extends TObject> {
|
|||||||
throw new Error(`Path "${path}" does not exist`);
|
throw new Error(`Path "${path}" does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = cloneDeep(this._config);
|
const current = this.clone();
|
||||||
const removed = get(current, path) as Partial<Static<Schema>>;
|
const removed = get(current, path) as Partial<Static<Schema>>;
|
||||||
const config = omit(current, path);
|
const config = omit(current, path);
|
||||||
const newConfig = await this.set(config);
|
const newConfig = await this.set(config);
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ export function _jsonp(obj: any, space = 2): string {
|
|||||||
return JSON.stringify(obj, null, space);
|
return JSON.stringify(obj, null, space);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Object.prototype.toString.call(value) === "[object Object]";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object";
|
||||||
|
}
|
||||||
|
|
||||||
export function safelyParseObjectValues<T extends { [key: string]: any }>(obj: T): T {
|
export function safelyParseObjectValues<T extends { [key: string]: any }>(obj: T): T {
|
||||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||||
try {
|
try {
|
||||||
@@ -97,15 +105,6 @@ export function objectEach<T extends Record<string, any>, U>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple object check.
|
|
||||||
* @param item
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export function isObject(item) {
|
|
||||||
return item && typeof item === "object" && !Array.isArray(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep merge two objects.
|
* Deep merge two objects.
|
||||||
* @param target
|
* @param target
|
||||||
@@ -197,3 +196,73 @@ export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj):
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as any);
|
}, {} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lodash's merge implementation caused issues in Next.js environments
|
||||||
|
* From: https://thescottyjam.github.io/snap.js/#!/nolodash/merge
|
||||||
|
* NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge.
|
||||||
|
* @param object
|
||||||
|
* @param sources
|
||||||
|
*/
|
||||||
|
export function mergeObject(object, ...sources) {
|
||||||
|
for (const source of sources) {
|
||||||
|
for (const [key, value] of Object.entries(source)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These checks are a week attempt at mimicking the various edge-case behaviors
|
||||||
|
// that Lodash's `_.merge()` exhibits. Feel free to simplify and
|
||||||
|
// remove checks that you don't need.
|
||||||
|
if (!isPlainObject(value) && !Array.isArray(value)) {
|
||||||
|
object[key] = value;
|
||||||
|
} else if (Array.isArray(value) && !Array.isArray(object[key])) {
|
||||||
|
object[key] = value;
|
||||||
|
} else if (!isObject(object[key])) {
|
||||||
|
object[key] = value;
|
||||||
|
} else {
|
||||||
|
mergeObject(object[key], value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lodash's mergeWith implementation caused issues in Next.js environments
|
||||||
|
* From: https://thescottyjam.github.io/snap.js/#!/nolodash/mergeWith
|
||||||
|
* NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge.
|
||||||
|
* @param object
|
||||||
|
* @param sources
|
||||||
|
* @param customizer
|
||||||
|
*/
|
||||||
|
export function mergeObjectWith(object, source, customizer) {
|
||||||
|
for (const [key, value] of Object.entries(source)) {
|
||||||
|
const mergedValue = customizer(object[key], value, key, object, source);
|
||||||
|
if (mergedValue !== undefined) {
|
||||||
|
object[key] = mergedValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Otherwise, fall back to default behavior
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These checks are a week attempt at mimicking the various edge-case behaviors
|
||||||
|
// that Lodash's `_.merge()` exhibits. Feel free to simplify and
|
||||||
|
// remove checks that you don't need.
|
||||||
|
if (!isPlainObject(value) && !Array.isArray(value)) {
|
||||||
|
object[key] = value;
|
||||||
|
} else if (Array.isArray(value) && !Array.isArray(object[key])) {
|
||||||
|
object[key] = value;
|
||||||
|
} else if (!isObject(object[key])) {
|
||||||
|
object[key] = value;
|
||||||
|
} else {
|
||||||
|
mergeObjectWith(object[key], value, customizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import { type AppDataConfig, dataConfigSchema } from "./data-schema";
|
|||||||
|
|
||||||
export class AppData extends Module<typeof dataConfigSchema> {
|
export class AppData extends Module<typeof dataConfigSchema> {
|
||||||
override async build() {
|
override async build() {
|
||||||
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
const {
|
||||||
|
entities: _entities = {},
|
||||||
|
relations: _relations = {},
|
||||||
|
indices: _indices = {}
|
||||||
|
} = this.config;
|
||||||
|
|
||||||
|
this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities));
|
||||||
|
|
||||||
|
const entities = transformObject(_entities, (entityConfig, name) => {
|
||||||
return constructEntity(name, entityConfig);
|
return constructEntity(name, entityConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,14 +29,14 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
|||||||
const name = typeof _e === "string" ? _e : _e.name;
|
const name = typeof _e === "string" ? _e : _e.name;
|
||||||
const entity = entities[name];
|
const entity = entities[name];
|
||||||
if (entity) return entity;
|
if (entity) return entity;
|
||||||
throw new Error(`Entity "${name}" not found`);
|
throw new Error(`[AppData] Entity "${name}" not found`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const relations = transformObject(this.config.relations ?? {}, (relation) =>
|
const relations = transformObject(_relations, (relation) =>
|
||||||
constructRelation(relation, _entity)
|
constructRelation(relation, _entity)
|
||||||
);
|
);
|
||||||
|
|
||||||
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
const indices = transformObject(_indices, (index, name) => {
|
||||||
const entity = _entity(index.entity)!;
|
const entity = _entity(index.entity)!;
|
||||||
const fields = index.fields.map((f) => entity.field(f)!);
|
const fields = index.fields.map((f) => entity.field(f)!);
|
||||||
return new EntityIndex(entity, fields, index.unique, name);
|
return new EntityIndex(entity, fields, index.unique, name);
|
||||||
@@ -52,6 +60,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
|||||||
);
|
);
|
||||||
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
|
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
|
||||||
|
|
||||||
|
this.ctx.logger.clear();
|
||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import type { Guard } from "auth";
|
import type { Guard } from "auth";
|
||||||
import { SchemaObject } from "core";
|
import { type DebugLogger, SchemaObject } from "core";
|
||||||
import type { EventManager } from "core/events";
|
import type { EventManager } from "core/events";
|
||||||
import type { Static, TSchema } from "core/utils";
|
import type { Static, TSchema } from "core/utils";
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +35,7 @@ export type ModuleBuildContext = {
|
|||||||
em: EntityManager;
|
em: EntityManager;
|
||||||
emgr: EventManager<any>;
|
emgr: EventManager<any>;
|
||||||
guard: Guard;
|
guard: Guard;
|
||||||
|
logger: DebugLogger;
|
||||||
flags: (typeof Module)["ctx_flags"];
|
flags: (typeof Module)["ctx_flags"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,8 @@ export class ModuleManager {
|
|||||||
em: this.em,
|
em: this.em,
|
||||||
emgr: this.emgr,
|
emgr: this.emgr,
|
||||||
guard: this.guard,
|
guard: this.guard,
|
||||||
flags: Module.ctx_flags
|
flags: Module.ctx_flags,
|
||||||
|
logger: this.logger
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ export function useBkndData() {
|
|||||||
return {
|
return {
|
||||||
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
|
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
|
||||||
console.log("patch config", entityName, partial);
|
console.log("patch config", entityName, partial);
|
||||||
return await bkndActions.patch("data", `entities.${entityName}.config`, partial);
|
return await bkndActions.overwrite(
|
||||||
|
"data",
|
||||||
|
`entities.${entityName}.config`,
|
||||||
|
partial
|
||||||
|
);
|
||||||
},
|
},
|
||||||
fields: entityFieldActions(bkndActions, entityName)
|
fields: entityFieldActions(bkndActions, entityName)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type JsonSchemaFormProps = any & {
|
|||||||
uiSchema?: any;
|
uiSchema?: any;
|
||||||
direction?: "horizontal" | "vertical";
|
direction?: "horizontal" | "vertical";
|
||||||
onChange?: (value: any, isValid: () => boolean) => void;
|
onChange?: (value: any, isValid: () => boolean) => void;
|
||||||
|
cleanOnChange?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonSchemaFormRef = {
|
export type JsonSchemaFormRef = {
|
||||||
@@ -36,6 +37,7 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
|||||||
templates,
|
templates,
|
||||||
fields,
|
fields,
|
||||||
widgets,
|
widgets,
|
||||||
|
cleanOnChange,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -51,8 +53,8 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const handleChange = ({ formData }: any, e) => {
|
const handleChange = ({ formData }: any, e) => {
|
||||||
const clean = JSON.parse(JSON.stringify(formData));
|
const clean = cleanOnChange !== false ? JSON.parse(JSON.stringify(formData)) : formData;
|
||||||
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
|
console.log("Data changed: ", clean, { cleanOnChange });
|
||||||
setValue(clean);
|
setValue(clean);
|
||||||
onChange?.(clean, () => isValid(clean));
|
onChange?.(clean, () => isValid(clean));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ const EntityContextMenu = ({
|
|||||||
separator,
|
separator,
|
||||||
{
|
{
|
||||||
icon: IconSettings,
|
icon: IconSettings,
|
||||||
label: "Settings",
|
label: "Advanced",
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||||
absolute: true
|
absolute: true
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export function DataEntityList({ params }) {
|
|||||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const search = useSearch(searchSchema, {
|
const search = useSearch(searchSchema, {
|
||||||
select: undefined,
|
select: entity.getSelect(undefined, "form"),
|
||||||
sort: undefined
|
sort: entity.getDefaultSort()
|
||||||
});
|
});
|
||||||
|
|
||||||
const $q = useApiQuery(
|
const $q = useApiQuery(
|
||||||
|
|||||||
Reference in New Issue
Block a user