From 292e4595ea1243a67f77c4d283d0aac44ffe1c92 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 24 Oct 2025 12:49:39 +0200 Subject: [PATCH 1/3] feat: add endpoint/tool to retrieve TypeScript definitions for data entities Implemented a new endpoint at "/types" in the DataController to return TypeScript definitions for data entities, enhancing type safety and developer experience. --- app/src/data/api/DataController.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 163f0af..e2608d2 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -15,6 +15,7 @@ import type { AppDataConfig } from "../data-schema"; import type { EntityManager, EntityData } from "data/entities"; import * as DataPermissions from "data/permissions"; import { repoQuery, type RepoQuery } from "data/server/query"; +import { EntityTypescript } from "data/entities/EntityTypescript"; export class DataController extends Controller { constructor( @@ -153,6 +154,20 @@ export class DataController extends Controller { }, ); + hono.get( + "/types", + permission(DataPermissions.entityRead), + describeRoute({ + summary: "Retrieve data typescript definitions", + tags: ["data"], + }), + mcpTool("data_types"), + async (c) => { + const et = new EntityTypescript(this.em); + return c.text(et.toString()); + }, + ); + // entity endpoints hono.route("/entity", this.getEntityRoutes()); From f2aad9caacac14c28d3d099859fb25275fabc1dd Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 24 Oct 2025 14:07:37 +0200 Subject: [PATCH 2/3] make non-fillable fields visible but disabled in UI --- .../modules/migrations/migrations.spec.ts | 16 +- .../modules/migrations/samples/v10.json | 270 ++++++++++++++++++ app/src/data/entities/Entity.ts | 22 +- app/src/data/entities/mutation/Mutator.ts | 6 +- app/src/data/fields/Field.ts | 22 +- app/src/data/fields/field-test-suite.ts | 7 +- app/src/data/schema/SchemaManager.ts | 2 +- app/src/modules/db/migrations.ts | 24 ++ .../ui/components/form/Formy/components.tsx | 11 +- .../ui/modules/data/components/EntityForm.tsx | 8 +- .../ui/modules/data/hooks/useEntityForm.tsx | 2 +- 11 files changed, 353 insertions(+), 37 deletions(-) create mode 100644 app/__test__/modules/migrations/samples/v10.json diff --git a/app/__test__/modules/migrations/migrations.spec.ts b/app/__test__/modules/migrations/migrations.spec.ts index 1266746..f68b462 100644 --- a/app/__test__/modules/migrations/migrations.spec.ts +++ b/app/__test__/modules/migrations/migrations.spec.ts @@ -7,7 +7,9 @@ import v7 from "./samples/v7.json"; import v8 from "./samples/v8.json"; import v8_2 from "./samples/v8-2.json"; import v9 from "./samples/v9.json"; +import v10 from "./samples/v10.json"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; +import { CURRENT_VERSION } from "modules/db/migrations"; beforeAll(() => disableConsoleLog()); afterAll(enableConsoleLog); @@ -61,7 +63,7 @@ async function getRawConfig( return await db .selectFrom("__bknd") .selectAll() - .$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version)) + .where("version", "=", opts?.version ?? CURRENT_VERSION) .$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types)) .execute(); } @@ -115,7 +117,6 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); const [config, secrets] = (await getRawConfig(app, { - version: 10, types: ["config", "secrets"], })) as any; @@ -129,4 +130,15 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); }); + + test("migration from 10 to 11", async () => { + expect(v10.version).toBe(10); + expect(v10.data.entities.test.fields.title.config.fillable).toEqual(["read", "update"]); + + const app = await createVersionedApp(v10); + + expect(app.version()).toBeGreaterThan(10); + const [config] = (await getRawConfig(app, { types: ["config"] })) as any; + expect(config.json.data.entities.test.fields.title.config.fillable).toEqual(true); + }); }); diff --git a/app/__test__/modules/migrations/samples/v10.json b/app/__test__/modules/migrations/samples/v10.json new file mode 100644 index 0000000..022ef2f --- /dev/null +++ b/app/__test__/modules/migrations/samples/v10.json @@ -0,0 +1,270 @@ +{ + "version": 10, + "server": { + "cors": { + "origin": "*", + "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"], + "allow_headers": [ + "Content-Type", + "Content-Length", + "Authorization", + "Accept" + ], + "allow_credentials": true + }, + "mcp": { + "enabled": true, + "path": "/api/system/mcp", + "logLevel": "warning" + } + }, + "data": { + "basepath": "/api/data", + "default_primary_format": "integer", + "entities": { + "test": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": ["read", "update"] + } + }, + "status": { + "type": "enum", + "config": { + "default_value": "INACTIVE", + "options": { + "type": "strings", + "values": ["INACTIVE", "SUBSCRIBED", "UNSUBSCRIBED"] + }, + "required": true, + "fillable": true + } + }, + "created_at": { + "type": "date", + "config": { + "type": "datetime", + "required": true, + "fillable": true + } + }, + "schema": { + "type": "jsonschema", + "config": { + "default_from_schema": true, + "schema": { + "type": "object", + "properties": { + "one": { + "type": "number", + "default": 1 + } + } + }, + "required": true, + "fillable": true + } + }, + "text": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "items": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "media": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "path": { + "type": "text", + "config": { + "required": true, + "fillable": true + } + }, + "folder": { + "type": "boolean", + "config": { + "default_value": false, + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "mime_type": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "size": { + "type": "number", + "config": { + "required": false, + "fillable": true + } + }, + "scope": { + "type": "text", + "config": { + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "etag": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "modified_at": { + "type": "date", + "config": { + "type": "datetime", + "required": false, + "fillable": true + } + }, + "reference": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "entity_id": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "metadata": { + "type": "json", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + } + }, + "relations": {}, + "indices": { + "idx_unique_media_path": { + "entity": "media", + "fields": ["path"], + "unique": true + }, + "idx_media_reference": { + "entity": "media", + "fields": ["reference"], + "unique": false + }, + "idx_media_entity_id": { + "entity": "media", + "fields": ["entity_id"], + "unique": false + } + } + }, + "auth": { + "enabled": false, + "basepath": "/api/auth", + "entity_name": "users", + "allow_register": true, + "jwt": { + "secret": "", + "alg": "HS256", + "fields": ["id", "email", "role"] + }, + "cookie": { + "path": "/", + "sameSite": "lax", + "secure": true, + "httpOnly": true, + "expires": 604800, + "partitioned": false, + "renew": true, + "pathSuccess": "/", + "pathLoggedOut": "/" + }, + "strategies": { + "password": { + "type": "password", + "enabled": true, + "config": { + "hashing": "sha256" + } + } + }, + "guard": { + "enabled": false + }, + "roles": {} + }, + "media": { + "enabled": false + }, + "flows": { + "basepath": "/api/flows", + "flows": {} + } +} diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index fcbe092..db7e6f4 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -136,8 +136,10 @@ export class Entity< .map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name)); } - getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] { - return this.getFields(include_virtual).filter((field) => field.isFillable(context)); + getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] { + return this.getFields({ virtual: include_virtual }).filter((field) => + field.isFillable(context), + ); } getRequiredFields(): Field[] { @@ -189,9 +191,15 @@ export class Entity< return this.fields.findIndex((field) => field.name === name) !== -1; } - getFields(include_virtual: boolean = false): Field[] { - if (include_virtual) return this.fields; - return this.fields.filter((f) => !f.isVirtual()); + getFields({ + virtual = false, + primary = true, + }: { virtual?: boolean; primary?: boolean } = {}): Field[] { + return this.fields.filter((f) => { + if (!virtual && f.isVirtual()) return false; + if (!primary && f instanceof PrimaryField) return false; + return true; + }); } addField(field: Field) { @@ -231,7 +239,7 @@ export class Entity< } } - const fields = this.getFillableFields(context, false); + const fields = this.getFillableFields(context as any, false); if (options?.ignoreUnknown !== true) { const field_names = fields.map((f) => f.name); @@ -275,7 +283,7 @@ export class Entity< fields = this.getFillableFields(options.context); break; default: - fields = this.getFields(true); + fields = this.getFields({ virtual: true }); } const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); diff --git a/app/src/data/entities/mutation/Mutator.ts b/app/src/data/entities/mutation/Mutator.ts index 84389c3..e4bb6f0 100644 --- a/app/src/data/entities/mutation/Mutator.ts +++ b/app/src/data/entities/mutation/Mutator.ts @@ -83,8 +83,10 @@ export class Mutator< } // we should never get here, but just to be sure (why?) - if (!field.isFillable(context)) { - throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`); + if (!field.isFillable(context as any)) { + throw new Error( + `Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`, + ); } // transform from field diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index 98a1f45..8451d1d 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -26,11 +26,19 @@ export const baseFieldConfigSchema = s .strictObject({ label: s.string(), description: s.string(), - required: s.boolean({ default: false }), - fillable: s.anyOf([ - s.boolean({ title: "Boolean" }), - s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }), - ]), + required: s.boolean({ default: DEFAULT_REQUIRED }), + fillable: s.anyOf( + [ + s.boolean({ title: "Boolean" }), + s.array(s.string({ enum: ["create", "update"] }), { + title: "Context", + uniqueItems: true, + }), + ], + { + default: DEFAULT_FILLABLE, + }, + ), hidden: s.anyOf([ s.boolean({ title: "Boolean" }), // @todo: tmp workaround @@ -103,7 +111,7 @@ export abstract class Field< return this.config?.default_value; } - isFillable(context?: TActionContext): boolean { + isFillable(context?: "create" | "update"): boolean { if (Array.isArray(this.config.fillable)) { return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE; } @@ -165,7 +173,7 @@ export abstract class Field< // @todo: add field level validation isValid(value: any, context: TActionContext): boolean { if (typeof value !== "undefined") { - return this.isFillable(context); + return this.isFillable(context as any); } else if (context === "create") { return !this.isRequired(); } diff --git a/app/src/data/fields/field-test-suite.ts b/app/src/data/fields/field-test-suite.ts index 369d41b..bf81fbd 100644 --- a/app/src/data/fields/field-test-suite.ts +++ b/app/src/data/fields/field-test-suite.ts @@ -99,6 +99,7 @@ export function fieldTestSuite( const _config = { ..._requiredConfig, required: false, + fillable: true, }; function fieldJson(field: Field) { @@ -116,10 +117,7 @@ export function fieldTestSuite( expect(fieldJson(fillable)).toEqual({ type: noConfigField.type, - config: { - ..._config, - fillable: true, - }, + config: _config, }); expect(fieldJson(required)).toEqual({ @@ -150,7 +148,6 @@ export function fieldTestSuite( type: requiredAndDefault.type, config: { ..._config, - fillable: true, required: true, default_value: config.defaultValue, }, diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index 78708d6..8a06957 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -77,7 +77,7 @@ export class SchemaManager { } getIntrospectionFromEntity(entity: Entity): IntrospectedTable { - const fields = entity.getFields(false); + const fields = entity.getFields({ virtual: false }); const indices = this.em.getIndicesOf(entity); // this is intentionally setting values to defaults, like "nullable" and "default" diff --git a/app/src/modules/db/migrations.ts b/app/src/modules/db/migrations.ts index 13f39ee..e97071b 100644 --- a/app/src/modules/db/migrations.ts +++ b/app/src/modules/db/migrations.ts @@ -1,6 +1,7 @@ import { transformObject } from "bknd/utils"; import type { Kysely } from "kysely"; import { set } from "lodash-es"; +import type { InitialModuleConfigs } from "modules/ModuleManager"; export type MigrationContext = { db: Kysely; @@ -107,6 +108,29 @@ export const migrations: Migration[] = [ return config; }, }, + { + // change field.config.fillable to only "create" and "update" + version: 11, + up: async (config: InitialModuleConfigs) => { + const { data, ...rest } = config; + return { + ...rest, + data: { + ...data, + entities: transformObject(data?.entities ?? {}, (entity) => { + return { + ...entity, + fields: transformObject(entity?.fields ?? {}, (field) => { + const fillable = field!.config?.fillable; + if (!fillable || typeof fillable === "boolean") return field; + return { ...field, config: { ...field!.config, fillable: true } }; + }), + }; + }), + }, + }; + }, + }, ]; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 502a844..cd85aa4 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -29,7 +29,7 @@ export const Group = ({ > ref={ref} className={twMerge( "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", - disabledOrReadonly && "bg-muted/50 text-primary/50", + disabledOrReadonly && "bg-muted/50 text-primary/50 cursor-not-allowed", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", props.className, @@ -153,7 +153,7 @@ export const Textarea = forwardRef @@ -213,7 +213,7 @@ export const BooleanInput = forwardRef { - console.log("setting", bool); props.onChange?.({ target: { value: bool } }); }} {...(props as any)} @@ -293,7 +292,7 @@ export const Select = forwardRef< {...props} ref={ref} className={twMerge( - "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50", + "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 disabled:cursor-not-allowed", "appearance-none h-11 w-full", !props.multiple && "border-r-8 border-r-transparent", props.className, diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index ff778a1..9943aba 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -44,7 +44,7 @@ export function EntityForm({ className, action, }: EntityFormProps) { - const fields = entity.getFillableFields(action, true); + const fields = entity.getFields({ virtual: true, primary: false }); const options = useEntityAdminOptions(entity, action); return ( @@ -92,10 +92,6 @@ export function EntityForm({ ); } - if (!field.isFillable(action)) { - return; - } - const _key = `${entity.name}-${field.name}-${key}`; return ( @@ -127,7 +123,7 @@ export function EntityForm({ Date: Fri, 24 Oct 2025 14:08:32 +0200 Subject: [PATCH 3/3] Revert "make non-fillable fields visible but disabled in UI" This reverts commit f2aad9caacac14c28d3d099859fb25275fabc1dd. --- .../modules/migrations/migrations.spec.ts | 16 +- .../modules/migrations/samples/v10.json | 270 ------------------ app/src/data/entities/Entity.ts | 22 +- app/src/data/entities/mutation/Mutator.ts | 6 +- app/src/data/fields/Field.ts | 22 +- app/src/data/fields/field-test-suite.ts | 7 +- app/src/data/schema/SchemaManager.ts | 2 +- app/src/modules/db/migrations.ts | 24 -- .../ui/components/form/Formy/components.tsx | 11 +- .../ui/modules/data/components/EntityForm.tsx | 8 +- .../ui/modules/data/hooks/useEntityForm.tsx | 2 +- 11 files changed, 37 insertions(+), 353 deletions(-) delete mode 100644 app/__test__/modules/migrations/samples/v10.json diff --git a/app/__test__/modules/migrations/migrations.spec.ts b/app/__test__/modules/migrations/migrations.spec.ts index f68b462..1266746 100644 --- a/app/__test__/modules/migrations/migrations.spec.ts +++ b/app/__test__/modules/migrations/migrations.spec.ts @@ -7,9 +7,7 @@ import v7 from "./samples/v7.json"; import v8 from "./samples/v8.json"; import v8_2 from "./samples/v8-2.json"; import v9 from "./samples/v9.json"; -import v10 from "./samples/v10.json"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; -import { CURRENT_VERSION } from "modules/db/migrations"; beforeAll(() => disableConsoleLog()); afterAll(enableConsoleLog); @@ -63,7 +61,7 @@ async function getRawConfig( return await db .selectFrom("__bknd") .selectAll() - .where("version", "=", opts?.version ?? CURRENT_VERSION) + .$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version)) .$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types)) .execute(); } @@ -117,6 +115,7 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); const [config, secrets] = (await getRawConfig(app, { + version: 10, types: ["config", "secrets"], })) as any; @@ -130,15 +129,4 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); }); - - test("migration from 10 to 11", async () => { - expect(v10.version).toBe(10); - expect(v10.data.entities.test.fields.title.config.fillable).toEqual(["read", "update"]); - - const app = await createVersionedApp(v10); - - expect(app.version()).toBeGreaterThan(10); - const [config] = (await getRawConfig(app, { types: ["config"] })) as any; - expect(config.json.data.entities.test.fields.title.config.fillable).toEqual(true); - }); }); diff --git a/app/__test__/modules/migrations/samples/v10.json b/app/__test__/modules/migrations/samples/v10.json deleted file mode 100644 index 022ef2f..0000000 --- a/app/__test__/modules/migrations/samples/v10.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "version": 10, - "server": { - "cors": { - "origin": "*", - "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"], - "allow_headers": [ - "Content-Type", - "Content-Length", - "Authorization", - "Accept" - ], - "allow_credentials": true - }, - "mcp": { - "enabled": true, - "path": "/api/system/mcp", - "logLevel": "warning" - } - }, - "data": { - "basepath": "/api/data", - "default_primary_format": "integer", - "entities": { - "test": { - "type": "regular", - "fields": { - "id": { - "type": "primary", - "config": { - "format": "integer", - "fillable": false, - "required": false - } - }, - "title": { - "type": "text", - "config": { - "required": false, - "fillable": ["read", "update"] - } - }, - "status": { - "type": "enum", - "config": { - "default_value": "INACTIVE", - "options": { - "type": "strings", - "values": ["INACTIVE", "SUBSCRIBED", "UNSUBSCRIBED"] - }, - "required": true, - "fillable": true - } - }, - "created_at": { - "type": "date", - "config": { - "type": "datetime", - "required": true, - "fillable": true - } - }, - "schema": { - "type": "jsonschema", - "config": { - "default_from_schema": true, - "schema": { - "type": "object", - "properties": { - "one": { - "type": "number", - "default": 1 - } - } - }, - "required": true, - "fillable": true - } - }, - "text": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - } - }, - "config": { - "sort_field": "id", - "sort_dir": "asc" - } - }, - "items": { - "type": "regular", - "fields": { - "id": { - "type": "primary", - "config": { - "format": "integer", - "fillable": false, - "required": false - } - }, - "title": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - } - }, - "config": { - "sort_field": "id", - "sort_dir": "asc" - } - }, - "media": { - "type": "system", - "fields": { - "id": { - "type": "primary", - "config": { - "format": "integer", - "fillable": false, - "required": false - } - }, - "path": { - "type": "text", - "config": { - "required": true, - "fillable": true - } - }, - "folder": { - "type": "boolean", - "config": { - "default_value": false, - "hidden": true, - "fillable": ["create"], - "required": false - } - }, - "mime_type": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - }, - "size": { - "type": "number", - "config": { - "required": false, - "fillable": true - } - }, - "scope": { - "type": "text", - "config": { - "hidden": true, - "fillable": ["create"], - "required": false - } - }, - "etag": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - }, - "modified_at": { - "type": "date", - "config": { - "type": "datetime", - "required": false, - "fillable": true - } - }, - "reference": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - }, - "entity_id": { - "type": "text", - "config": { - "required": false, - "fillable": true - } - }, - "metadata": { - "type": "json", - "config": { - "required": false, - "fillable": true - } - } - }, - "config": { - "sort_field": "id", - "sort_dir": "asc" - } - } - }, - "relations": {}, - "indices": { - "idx_unique_media_path": { - "entity": "media", - "fields": ["path"], - "unique": true - }, - "idx_media_reference": { - "entity": "media", - "fields": ["reference"], - "unique": false - }, - "idx_media_entity_id": { - "entity": "media", - "fields": ["entity_id"], - "unique": false - } - } - }, - "auth": { - "enabled": false, - "basepath": "/api/auth", - "entity_name": "users", - "allow_register": true, - "jwt": { - "secret": "", - "alg": "HS256", - "fields": ["id", "email", "role"] - }, - "cookie": { - "path": "/", - "sameSite": "lax", - "secure": true, - "httpOnly": true, - "expires": 604800, - "partitioned": false, - "renew": true, - "pathSuccess": "/", - "pathLoggedOut": "/" - }, - "strategies": { - "password": { - "type": "password", - "enabled": true, - "config": { - "hashing": "sha256" - } - } - }, - "guard": { - "enabled": false - }, - "roles": {} - }, - "media": { - "enabled": false - }, - "flows": { - "basepath": "/api/flows", - "flows": {} - } -} diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index db7e6f4..fcbe092 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -136,10 +136,8 @@ export class Entity< .map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name)); } - getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] { - return this.getFields({ virtual: include_virtual }).filter((field) => - field.isFillable(context), - ); + getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] { + return this.getFields(include_virtual).filter((field) => field.isFillable(context)); } getRequiredFields(): Field[] { @@ -191,15 +189,9 @@ export class Entity< return this.fields.findIndex((field) => field.name === name) !== -1; } - getFields({ - virtual = false, - primary = true, - }: { virtual?: boolean; primary?: boolean } = {}): Field[] { - return this.fields.filter((f) => { - if (!virtual && f.isVirtual()) return false; - if (!primary && f instanceof PrimaryField) return false; - return true; - }); + getFields(include_virtual: boolean = false): Field[] { + if (include_virtual) return this.fields; + return this.fields.filter((f) => !f.isVirtual()); } addField(field: Field) { @@ -239,7 +231,7 @@ export class Entity< } } - const fields = this.getFillableFields(context as any, false); + const fields = this.getFillableFields(context, false); if (options?.ignoreUnknown !== true) { const field_names = fields.map((f) => f.name); @@ -283,7 +275,7 @@ export class Entity< fields = this.getFillableFields(options.context); break; default: - fields = this.getFields({ virtual: true }); + fields = this.getFields(true); } const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); diff --git a/app/src/data/entities/mutation/Mutator.ts b/app/src/data/entities/mutation/Mutator.ts index e4bb6f0..84389c3 100644 --- a/app/src/data/entities/mutation/Mutator.ts +++ b/app/src/data/entities/mutation/Mutator.ts @@ -83,10 +83,8 @@ export class Mutator< } // we should never get here, but just to be sure (why?) - if (!field.isFillable(context as any)) { - throw new Error( - `Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`, - ); + if (!field.isFillable(context)) { + throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`); } // transform from field diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index 8451d1d..98a1f45 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -26,19 +26,11 @@ export const baseFieldConfigSchema = s .strictObject({ label: s.string(), description: s.string(), - required: s.boolean({ default: DEFAULT_REQUIRED }), - fillable: s.anyOf( - [ - s.boolean({ title: "Boolean" }), - s.array(s.string({ enum: ["create", "update"] }), { - title: "Context", - uniqueItems: true, - }), - ], - { - default: DEFAULT_FILLABLE, - }, - ), + required: s.boolean({ default: false }), + fillable: s.anyOf([ + s.boolean({ title: "Boolean" }), + s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }), + ]), hidden: s.anyOf([ s.boolean({ title: "Boolean" }), // @todo: tmp workaround @@ -111,7 +103,7 @@ export abstract class Field< return this.config?.default_value; } - isFillable(context?: "create" | "update"): boolean { + isFillable(context?: TActionContext): boolean { if (Array.isArray(this.config.fillable)) { return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE; } @@ -173,7 +165,7 @@ export abstract class Field< // @todo: add field level validation isValid(value: any, context: TActionContext): boolean { if (typeof value !== "undefined") { - return this.isFillable(context as any); + return this.isFillable(context); } else if (context === "create") { return !this.isRequired(); } diff --git a/app/src/data/fields/field-test-suite.ts b/app/src/data/fields/field-test-suite.ts index bf81fbd..369d41b 100644 --- a/app/src/data/fields/field-test-suite.ts +++ b/app/src/data/fields/field-test-suite.ts @@ -99,7 +99,6 @@ export function fieldTestSuite( const _config = { ..._requiredConfig, required: false, - fillable: true, }; function fieldJson(field: Field) { @@ -117,7 +116,10 @@ export function fieldTestSuite( expect(fieldJson(fillable)).toEqual({ type: noConfigField.type, - config: _config, + config: { + ..._config, + fillable: true, + }, }); expect(fieldJson(required)).toEqual({ @@ -148,6 +150,7 @@ export function fieldTestSuite( type: requiredAndDefault.type, config: { ..._config, + fillable: true, required: true, default_value: config.defaultValue, }, diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index 8a06957..78708d6 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -77,7 +77,7 @@ export class SchemaManager { } getIntrospectionFromEntity(entity: Entity): IntrospectedTable { - const fields = entity.getFields({ virtual: false }); + const fields = entity.getFields(false); const indices = this.em.getIndicesOf(entity); // this is intentionally setting values to defaults, like "nullable" and "default" diff --git a/app/src/modules/db/migrations.ts b/app/src/modules/db/migrations.ts index e97071b..13f39ee 100644 --- a/app/src/modules/db/migrations.ts +++ b/app/src/modules/db/migrations.ts @@ -1,7 +1,6 @@ import { transformObject } from "bknd/utils"; import type { Kysely } from "kysely"; import { set } from "lodash-es"; -import type { InitialModuleConfigs } from "modules/ModuleManager"; export type MigrationContext = { db: Kysely; @@ -108,29 +107,6 @@ export const migrations: Migration[] = [ return config; }, }, - { - // change field.config.fillable to only "create" and "update" - version: 11, - up: async (config: InitialModuleConfigs) => { - const { data, ...rest } = config; - return { - ...rest, - data: { - ...data, - entities: transformObject(data?.entities ?? {}, (entity) => { - return { - ...entity, - fields: transformObject(entity?.fields ?? {}, (field) => { - const fillable = field!.config?.fillable; - if (!fillable || typeof fillable === "boolean") return field; - return { ...field, config: { ...field!.config, fillable: true } }; - }), - }; - }), - }, - }; - }, - }, ]; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index cd85aa4..502a844 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -29,7 +29,7 @@ export const Group = ({ > ref={ref} className={twMerge( "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", - disabledOrReadonly && "bg-muted/50 text-primary/50 cursor-not-allowed", + disabledOrReadonly && "bg-muted/50 text-primary/50", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", props.className, @@ -153,7 +153,7 @@ export const Textarea = forwardRef @@ -213,7 +213,7 @@ export const BooleanInput = forwardRef { + console.log("setting", bool); props.onChange?.({ target: { value: bool } }); }} {...(props as any)} @@ -292,7 +293,7 @@ export const Select = forwardRef< {...props} ref={ref} className={twMerge( - "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 disabled:cursor-not-allowed", + "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50", "appearance-none h-11 w-full", !props.multiple && "border-r-8 border-r-transparent", props.className, diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index 9943aba..ff778a1 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -44,7 +44,7 @@ export function EntityForm({ className, action, }: EntityFormProps) { - const fields = entity.getFields({ virtual: true, primary: false }); + const fields = entity.getFillableFields(action, true); const options = useEntityAdminOptions(entity, action); return ( @@ -92,6 +92,10 @@ export function EntityForm({ ); } + if (!field.isFillable(action)) { + return; + } + const _key = `${entity.name}-${field.name}-${key}`; return ( @@ -123,7 +127,7 @@ export function EntityForm({