diff --git a/packages/cli/src/core/resources/entity/schema.ts b/packages/cli/src/core/resources/entity/schema.ts index ea0c6c2a..61ff52e6 100644 --- a/packages/cli/src/core/resources/entity/schema.ts +++ b/packages/cli/src/core/resources/entity/schema.ts @@ -138,7 +138,10 @@ export const EntitySchema = z.looseObject({ name: z .string() .min(1) - .regex(/^[a-zA-Z0-9]+$/, "Entity name must be alphanumeric only"), + .regex( + /^[a-zA-Z0-9_]+$/, + "Entity name can only contain letters, numbers, and underscores", + ), title: z.string().optional(), description: z.string().optional(), properties: z.record(z.string(), PropertyDefinitionSchema).default({}), diff --git a/packages/cli/tests/cli/entities_push.spec.ts b/packages/cli/tests/cli/entities_push.spec.ts index 57e4bfad..f9da41a8 100644 --- a/packages/cli/tests/cli/entities_push.spec.ts +++ b/packages/cli/tests/cli/entities_push.spec.ts @@ -53,6 +53,20 @@ describe("entities push command", () => { t.expectResult(result).toContain("Updated: Product"); }); + it("pushes entities with snake_case names", async () => { + await t.givenLoggedInWithProject(fixture("with-snake-case-entities")); + t.api.mockEntitiesPush({ + created: ["line_items"], + updated: [], + deleted: [], + }); + + const result = await t.run("entities", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Created: line_items"); + }); + it("fails with helpful error when entity is missing required fields", async () => { await t.givenLoggedInWithProject(fixture("invalid-entity")); diff --git a/packages/cli/tests/core/entity-schema.spec.ts b/packages/cli/tests/core/entity-schema.spec.ts new file mode 100644 index 00000000..69aeea40 --- /dev/null +++ b/packages/cli/tests/core/entity-schema.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { EntitySchema } from "@/core/resources/entity/schema.js"; + +describe("Entity name validation", () => { + it("accepts alphanumeric names", () => { + const result = EntitySchema.safeParse({ name: "Customer" }); + + expect(result.success).toBe(true); + }); + + it("accepts snake_case names", () => { + const result = EntitySchema.safeParse({ name: "line_items" }); + + expect(result.success).toBe(true); + }); + + it("accepts names with a leading underscore", () => { + const result = EntitySchema.safeParse({ name: "_internal" }); + + expect(result.success).toBe(true); + }); + + it("rejects names with dashes", () => { + const result = EntitySchema.safeParse({ name: "line-items" }); + + expect(result.success).toBe(false); + expect(result.error?.issues[0]?.message).toBe( + "Entity name can only contain letters, numbers, and underscores", + ); + }); + + it("rejects names with spaces", () => { + const result = EntitySchema.safeParse({ name: "line items" }); + + expect(result.success).toBe(false); + }); + + it("rejects names with dots", () => { + const result = EntitySchema.safeParse({ name: "line.items" }); + + expect(result.success).toBe(false); + }); + + it("rejects empty names", () => { + const result = EntitySchema.safeParse({ name: "" }); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/cli/tests/core/project.spec.ts b/packages/cli/tests/core/project.spec.ts index f9b4da04..18babc2c 100644 --- a/packages/cli/tests/core/project.spec.ts +++ b/packages/cli/tests/core/project.spec.ts @@ -35,6 +35,15 @@ describe("readProjectConfig", () => { expect(result.agents).toEqual([]); }); + it("reads project with snake_case entity names", async () => { + const result = await readProjectConfig( + resolve(FIXTURES_DIR, "with-snake-case-entities"), + ); + + expect(result.entities).toHaveLength(1); + expect(result.entities[0].name).toBe("line_items"); + }); + it("reads project with functions and entities", async () => { const result = await readProjectConfig( resolve(FIXTURES_DIR, "with-functions-and-entities"), diff --git a/packages/cli/tests/fixtures/with-snake-case-entities/base44/.app.jsonc b/packages/cli/tests/fixtures/with-snake-case-entities/base44/.app.jsonc new file mode 100644 index 00000000..fc774263 --- /dev/null +++ b/packages/cli/tests/fixtures/with-snake-case-entities/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} \ No newline at end of file diff --git a/packages/cli/tests/fixtures/with-snake-case-entities/base44/config.jsonc b/packages/cli/tests/fixtures/with-snake-case-entities/base44/config.jsonc new file mode 100644 index 00000000..738b7f27 --- /dev/null +++ b/packages/cli/tests/fixtures/with-snake-case-entities/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Snake Case Entities Test Project" +} diff --git a/packages/cli/tests/fixtures/with-snake-case-entities/base44/entities/line-items.json b/packages/cli/tests/fixtures/with-snake-case-entities/base44/entities/line-items.json new file mode 100644 index 00000000..bc8293ad --- /dev/null +++ b/packages/cli/tests/fixtures/with-snake-case-entities/base44/entities/line-items.json @@ -0,0 +1,15 @@ +{ + "name": "line_items", + "type": "object", + "properties": { + "sku": { + "type": "string", + "description": "Stock keeping unit" + }, + "quantity": { + "type": "number", + "description": "Quantity ordered" + } + }, + "required": ["sku"] +}