Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
Scalar,
Type,
Union,
Value,
} from "@typespec/compiler";
import {
type EmitContext,
Expand All @@ -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":
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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;
}
}
Comment on lines +312 to +319
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for adding default values is duplicated between emitModelProperty (lines 257-264) and emitAttribute (lines 312-319). Consider refactoring to avoid duplication. Since emitAttribute calls emitType which may eventually call emitModelProperty for nested models, you could rely on emitModelProperty to handle default values for nested properties and only handle them once in emitAttribute for top-level properties.

Suggested change
// 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;
}
}

Copilot uses AI. Check for mistakes.

return attr;
}

Expand Down
104 changes: 89 additions & 15 deletions test/entities.test.js
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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,
});
});
});

Expand Down Expand Up @@ -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", () => {
Expand Down
29 changes: 29 additions & 0 deletions test/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading