diff --git a/src/emitter.ts b/src/emitter.ts index 6897c6d..1b2f356 100644 --- a/src/emitter.ts +++ b/src/emitter.ts @@ -8,6 +8,7 @@ import type { Scalar, Type, Union, + Value, } from "@typespec/compiler"; import { type EmitContext, @@ -21,6 +22,29 @@ import * as ts from "typescript"; import { StateKeys } from "./lib.js"; import { RawCode, stringifyObject } from "./stringify.js"; +/** + * Extracts a primitive default value from a TypeSpec Value. + * Returns undefined if the value cannot be converted to a simple default. + */ +function extractDefaultValue( + value: Value, +): string | number | boolean | undefined { + switch (value.valueKind) { + case "StringValue": + return value.value; + case "NumericValue": + return Number(value.value.asNumber()); + case "BooleanValue": + return value.value; + case "EnumValue": + // For enum values, use the value if specified, otherwise use the member name + return value.value.value ?? value.value.name; + default: + // Complex values (objects, arrays) are not supported as simple defaults + return undefined; + } +} + function emitIntrinsincScalar(type: Scalar) { switch (type.name) { case "boolean": @@ -225,10 +249,21 @@ function emitType(type: Type): Attribute { } function emitModelProperty(prop: ModelProperty): Attribute { - return { + const attr: Attribute = { ...emitType(prop.type), required: !prop.optional, }; + + // Add default value if present + if (prop.defaultValue) { + const defaultValue = extractDefaultValue(prop.defaultValue); + if (defaultValue !== undefined) { + // @ts-expect-error - default is a valid ElectroDB attribute property + attr.default = defaultValue; + } + } + + return attr; } const getLabel = (ctx: EmitContext, prop: ModelProperty) => @@ -274,6 +309,15 @@ function emitAttribute(ctx: EmitContext, prop: ModelProperty): Attribute { attr.label = label; } + // Add default value if present + if (prop.defaultValue) { + const defaultValue = extractDefaultValue(prop.defaultValue); + if (defaultValue !== undefined) { + // @ts-expect-error - default is a valid ElectroDB attribute property + attr.default = defaultValue; + } + } + return attr; } diff --git a/test/entities.test.js b/test/entities.test.js index b50d555..c034cc9 100644 --- a/test/entities.test.js +++ b/test/entities.test.js @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { suite, test } from "node:test"; -import { Job, Person } from "../build/entities/index.js"; +import { Job, Person, Task } from "../build/entities/index.js"; suite("Job Entity", () => { test("Job entity has correct model configuration", () => { @@ -53,6 +53,86 @@ suite("Job Entity", () => { }); }); +suite("Task Entity - Default Values", () => { + test("Task entity has correct model configuration", () => { + assert.deepEqual(Task.model, { + entity: "task", + service: "org", + version: "1", + }); + }); + + suite("String default values", () => { + test("optional description has string default", () => { + assert.deepEqual(Task.attributes.description, { + type: "string", + required: false, + default: "No description provided", + }); + }); + }); + + suite("Enum default values", () => { + test("priority has enum default value", () => { + assert.deepEqual(Task.attributes.priority, { + type: ["LOW", "MEDIUM", "HIGH"], + required: true, + default: "MEDIUM", + }); + }); + }); + + suite("Number default values", () => { + test("count has number default value", () => { + assert.deepEqual(Task.attributes.count, { + type: "number", + required: true, + default: 0, + }); + }); + }); + + suite("Boolean default values", () => { + test("active has boolean default value", () => { + assert.deepEqual(Task.attributes.active, { + type: "boolean", + required: true, + default: true, + }); + }); + }); + + suite("Nested model default values", () => { + test("settings is a list type", () => { + assert.equal(Task.attributes.settings.type, "list"); + assert.equal(Task.attributes.settings.items.type, "map"); + }); + + test("settings item value property has string default", () => { + assert.deepEqual(Task.attributes.settings.items.properties.value, { + type: "string", + required: true, + default: "default", + }); + }); + + test("settings item enabled property has boolean default", () => { + assert.deepEqual(Task.attributes.settings.items.properties.enabled, { + type: "boolean", + required: true, + default: true, + }); + }); + + test("settings item key property has no default", () => { + assert.deepEqual(Task.attributes.settings.items.properties.key, { + type: "string", + required: true, + }); + }); + }); +}); + suite("Person Entity", () => { test("Person entity has correct model configuration", () => { assert.deepEqual(Person.model, { @@ -195,13 +275,10 @@ suite("Person Entity", () => { }); test("contact item has description property", () => { - assert.deepEqual( - Person.attributes.contact.items.properties.description, - { - type: "string", - required: true, - }, - ); + assert.deepEqual(Person.attributes.contact.items.properties.description, { + type: "string", + required: true, + }); }); }); @@ -239,13 +316,10 @@ suite("Person Entity", () => { }); test("additionalInfo item has name property as string", () => { - assert.deepEqual( - Person.attributes.additionalInfo.items.properties.name, - { - type: "string", - required: true, - }, - ); + assert.deepEqual(Person.attributes.additionalInfo.items.properties.name, { + type: "string", + required: true, + }); }); test("additionalInfo item value property uses CustomAttributeType for union", () => { diff --git a/test/main.tsp b/test/main.tsp index 81a3c3b..26c1255 100644 --- a/test/main.tsp +++ b/test/main.tsp @@ -67,6 +67,35 @@ model Job { description: string; } +enum Priority { + LOW, + MEDIUM, + HIGH, +} + +model SettingsItem { + key: string; + value: string = "default"; + enabled: boolean = true; +} + +@entity("task", "org") +@index( + "tasks", + { + pk: [Task.pk], + } +) +model Task { + pk: UUID; + title: string; + description?: string = "No description provided"; + priority: Priority = Priority.MEDIUM; + count: int32 = 0; + active: boolean = true; + settings: SettingsItem[]; +} + @entity("person", "org") @index( "persons",