mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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 { Guard } from "../../src/auth";
|
||||
import { DebugLogger } from "../../src/core";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { Default, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager } from "../../src/data";
|
||||
@@ -17,6 +18,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
emgr: new EventManager(),
|
||||
guard: new Guard(),
|
||||
flags: Module.ctx_flags,
|
||||
logger: new DebugLogger(false),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { cloneDeep, get, has, mergeWith, omit, set } from "lodash-es";
|
||||
import { get, has, omit, set } from "lodash-es";
|
||||
import {
|
||||
Default,
|
||||
type Static,
|
||||
type TObject,
|
||||
getFullPathKeys,
|
||||
mark,
|
||||
mergeObjectWith,
|
||||
parse,
|
||||
stripMark
|
||||
} from "../utils";
|
||||
@@ -33,7 +33,7 @@ export class SchemaObject<Schema extends TObject> {
|
||||
) {
|
||||
this._default = Default(_schema, {} as any) as any;
|
||||
this._value = initial
|
||||
? parse(_schema, cloneDeep(initial as any), {
|
||||
? parse(_schema, structuredClone(initial as any), {
|
||||
forceParse: this.isForceParse(),
|
||||
skipMark: this.isForceParse()
|
||||
})
|
||||
@@ -64,8 +64,12 @@ export class SchemaObject<Schema extends TObject> {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return structuredClone(this._config);
|
||||
}
|
||||
|
||||
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,
|
||||
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>]> {
|
||||
const current = cloneDeep(this._config);
|
||||
const current = this.clone();
|
||||
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
|
||||
|
||||
this.throwIfRestricted(partial);
|
||||
@@ -122,11 +126,13 @@ export class SchemaObject<Schema extends TObject> {
|
||||
|
||||
// overwrite arrays and primitives, only deep merge objects
|
||||
// @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)) {
|
||||
return srcValue;
|
||||
}
|
||||
});
|
||||
//console.log("---new", _jsonp(config));
|
||||
|
||||
//console.log("overwritePaths", 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);
|
||||
return [partial, newConfig];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.throwIfRestricted(partial);
|
||||
@@ -192,7 +198,7 @@ export class SchemaObject<Schema extends TObject> {
|
||||
if (p.length > 1) {
|
||||
const parent = p.slice(0, -1).join(".");
|
||||
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`);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +213,7 @@ export class SchemaObject<Schema extends TObject> {
|
||||
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 config = omit(current, path);
|
||||
const newConfig = await this.set(config);
|
||||
|
||||
@@ -4,6 +4,14 @@ export function _jsonp(obj: any, space = 2): string {
|
||||
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 {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
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.
|
||||
* @param target
|
||||
@@ -197,3 +196,73 @@ export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj):
|
||||
return acc;
|
||||
}, {} 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> {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -21,14 +29,14 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
||||
const name = typeof _e === "string" ? _e : _e.name;
|
||||
const entity = entities[name];
|
||||
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)
|
||||
);
|
||||
|
||||
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
||||
const indices = transformObject(_indices, (index, name) => {
|
||||
const entity = _entity(index.entity)!;
|
||||
const fields = index.fields.map((f) => entity.field(f)!);
|
||||
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.logger.clear();
|
||||
this.setBuilt();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { App } from "App";
|
||||
import type { Guard } from "auth";
|
||||
import { SchemaObject } from "core";
|
||||
import { type DebugLogger, SchemaObject } from "core";
|
||||
import type { EventManager } from "core/events";
|
||||
import type { Static, TSchema } from "core/utils";
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ export type ModuleBuildContext = {
|
||||
em: EntityManager;
|
||||
emgr: EventManager<any>;
|
||||
guard: Guard;
|
||||
logger: DebugLogger;
|
||||
flags: (typeof Module)["ctx_flags"];
|
||||
};
|
||||
|
||||
|
||||
@@ -231,7 +231,8 @@ export class ModuleManager {
|
||||
em: this.em,
|
||||
emgr: this.emgr,
|
||||
guard: this.guard,
|
||||
flags: Module.ctx_flags
|
||||
flags: Module.ctx_flags,
|
||||
logger: this.logger
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,11 @@ export function useBkndData() {
|
||||
return {
|
||||
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export type JsonSchemaFormProps = any & {
|
||||
uiSchema?: any;
|
||||
direction?: "horizontal" | "vertical";
|
||||
onChange?: (value: any, isValid: () => boolean) => void;
|
||||
cleanOnChange?: boolean;
|
||||
};
|
||||
|
||||
export type JsonSchemaFormRef = {
|
||||
@@ -36,6 +37,7 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
templates,
|
||||
fields,
|
||||
widgets,
|
||||
cleanOnChange,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -51,8 +53,8 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
return false;
|
||||
};
|
||||
const handleChange = ({ formData }: any, e) => {
|
||||
const clean = JSON.parse(JSON.stringify(formData));
|
||||
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
|
||||
const clean = cleanOnChange !== false ? JSON.parse(JSON.stringify(formData)) : formData;
|
||||
console.log("Data changed: ", clean, { cleanOnChange });
|
||||
setValue(clean);
|
||||
onChange?.(clean, () => isValid(clean));
|
||||
};
|
||||
|
||||
@@ -201,7 +201,7 @@ const EntityContextMenu = ({
|
||||
separator,
|
||||
{
|
||||
icon: IconSettings,
|
||||
label: "Settings",
|
||||
label: "Advanced",
|
||||
onClick: () =>
|
||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||
absolute: true
|
||||
|
||||
@@ -40,8 +40,8 @@ export function DataEntityList({ params }) {
|
||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||
const [navigate] = useNavigate();
|
||||
const search = useSearch(searchSchema, {
|
||||
select: undefined,
|
||||
sort: undefined
|
||||
select: entity.getSelect(undefined, "form"),
|
||||
sort: entity.getDefaultSort()
|
||||
});
|
||||
|
||||
const $q = useApiQuery(
|
||||
|
||||
Reference in New Issue
Block a user