diff --git a/app/__test__/modules/module-test-suite.ts b/app/__test__/modules/module-test-suite.ts index cf4926a..d4f0f95 100644 --- a/app/__test__/modules/module-test-suite.ts +++ b/app/__test__/modules/module-test-suite.ts @@ -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): ModuleBuildCon emgr: new EventManager(), guard: new Guard(), flags: Module.ctx_flags, + logger: new DebugLogger(false), ...overrides }; } diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index 7c2e926..c8584bf 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -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 { ) { 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 { return this._config; } + clone() { + return structuredClone(this._config); + } + async set(config: Static, noEmit?: boolean): Promise> { - 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 { } async patch(path: string, value: any): Promise<[Partial>, Static]> { - const current = cloneDeep(this._config); + const current = this.clone(); const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -122,11 +126,13 @@ export class SchemaObject { // 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 { } } - //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]> { - const current = cloneDeep(this._config); + const current = this.clone(); const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -192,7 +198,7 @@ export class SchemaObject { 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 { throw new Error(`Path "${path}" does not exist`); } - const current = cloneDeep(this._config); + const current = this.clone(); const removed = get(current, path) as Partial>; const config = omit(current, path); const newConfig = await this.set(config); diff --git a/app/src/core/utils/objects.ts b/app/src/core/utils/objects.ts index f8ae7a0..ab5b807 100644 --- a/app/src/core/utils/objects.ts +++ b/app/src/core/utils/objects.ts @@ -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 { + return Object.prototype.toString.call(value) === "[object Object]"; +} + +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object"; +} + export function safelyParseObjectValues(obj: T): T { return Object.entries(obj).reduce((acc, [key, value]) => { try { @@ -97,15 +105,6 @@ export function objectEach, 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: 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; +} diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 210c834..45edb15 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -13,7 +13,15 @@ import { type AppDataConfig, dataConfigSchema } from "./data-schema"; export class AppData extends Module { 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 { 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 { ); this.ctx.guard.registerPermissions(Object.values(DataPermissions)); + this.ctx.logger.clear(); this.setBuilt(); } diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 0d5b8bf..ef0bc81 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -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; guard: Guard; + logger: DebugLogger; flags: (typeof Module)["ctx_flags"]; }; diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 9a09bf9..efe7a09 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -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 }; } diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index d096731..135595e 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -40,7 +40,7 @@ export function DataEntityList({ params }) { useBrowserTitle(["Data", entity?.label ?? params.entity]); const [navigate] = useNavigate(); const search = useSearch(searchSchema, { - select: undefined, + select: entity.getSelect(undefined, "form"), sort: undefined }); diff --git a/bun.lockb b/bun.lockb index 19836ad..1b049f9 100755 Binary files a/bun.lockb and b/bun.lockb differ