Skip to content

Commit 2692ce1

Browse files
committed
Update schema decorators and fix lint issue
1 parent 9be2ed4 commit 2692ce1

3 files changed

Lines changed: 165 additions & 18 deletions

File tree

src/sdk/worker/decorators/worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export function worker(options: WorkerOptions) {
230230
| string
231231
| { kind: string; name: string | symbol },
232232
descriptor?: PropertyDescriptor
233-
): T | PropertyDescriptor | void {
233+
): T | PropertyDescriptor | undefined {
234234
// Detect decorator API: new (Stage 3) vs legacy (experimentalDecorators)
235235
let executeFunction: (task: Task) => Promise<Omit<TaskResult, "workflowInstanceId" | "taskId">>;
236236
let isNewApi = false;
@@ -343,6 +343,6 @@ export function worker(options: WorkerOptions) {
343343
descriptor.value = dualModeFunction;
344344
return descriptor;
345345
}
346-
return dualModeFunction;
346+
return dualModeFunction as unknown as T;
347347
};
348348
}

src/sdk/worker/schema/__tests__/decorators.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,89 @@ describe("@schemaField() decorator", () => {
174174
expect(address.required).toEqual(["street"]);
175175
});
176176
});
177+
178+
describe("Stage 3 (TypeScript 5.0+) decorator API", () => {
179+
it("should register fields when called with new decorator signature (value, context)", () => {
180+
class TestClass {
181+
name = "";
182+
}
183+
// Simulate new decorator API: decorator(value, context) returns initializer
184+
const decorator = schemaField({ type: "string" });
185+
const initializer = decorator(undefined, {
186+
kind: "field",
187+
name: "name",
188+
}) as (initialValue: unknown) => unknown;
189+
expect(typeof initializer).toBe("function");
190+
191+
// Initializer runs when instance is created; bind instance as `this`
192+
const instance = new TestClass();
193+
initializer.call(instance, "");
194+
195+
const schema = generateSchemaFromClass(TestClass);
196+
expect(schema.properties).toEqual({ name: { type: "string" } });
197+
});
198+
199+
it("should support required fields with new API", () => {
200+
class TestClass {
201+
id = "";
202+
count = 0;
203+
}
204+
const initId = schemaField({ type: "string", required: true })(
205+
undefined,
206+
{ kind: "field", name: "id" }
207+
) as (v: unknown) => unknown;
208+
const initCount = schemaField({ type: "number" })(
209+
undefined,
210+
{ kind: "field", name: "count" }
211+
) as (v: unknown) => unknown;
212+
213+
const instance = new TestClass();
214+
initId.call(instance, "");
215+
initCount.call(instance, 0);
216+
217+
const schema = generateSchemaFromClass(TestClass);
218+
expect(schema.properties).toEqual({
219+
id: { type: "string" },
220+
count: { type: "number" },
221+
});
222+
expect(schema.required).toEqual(["id"]);
223+
});
224+
225+
it("should not duplicate metadata when initializer runs multiple times", () => {
226+
class TestClass {
227+
value = "";
228+
}
229+
const initializer = schemaField({ type: "string" })(
230+
undefined,
231+
{ kind: "field", name: "value" }
232+
) as (v: unknown) => unknown;
233+
234+
const i1 = new TestClass();
235+
const i2 = new TestClass();
236+
const i3 = new TestClass();
237+
initializer.call(i1, "");
238+
initializer.call(i2, "");
239+
initializer.call(i3, "");
240+
241+
const schema = generateSchemaFromClass(TestClass);
242+
expect(schema.properties).toEqual({ value: { type: "string" } });
243+
expect(Object.keys(schema.properties)).toHaveLength(1);
244+
});
245+
246+
it("should work with explicit type when design:type unavailable (new API)", () => {
247+
class TestClass {
248+
count = 0;
249+
}
250+
const initializer = schemaField({
251+
type: "integer",
252+
description: "Count",
253+
})(undefined, { kind: "field", name: "count" }) as (v: unknown) => unknown;
254+
initializer.call(new TestClass(), 0);
255+
256+
const schema = generateSchemaFromClass(TestClass);
257+
expect(schema.properties).toEqual({
258+
count: { type: "integer", description: "Count" },
259+
});
260+
});
261+
});
177262
});

src/sdk/worker/schema/decorators.ts

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,57 @@ interface StoredFieldMeta extends SchemaFieldOptions {
3838
designType?: unknown;
3939
}
4040

41+
/**
42+
* Type guard for Stage 3 (TypeScript 5.0+) decorator context.
43+
* New decorators pass (value, context) where context has a `kind` property.
44+
*/
45+
function isNewDecoratorContext(
46+
arg: unknown
47+
): arg is { kind: string; name: string | symbol } {
48+
return (
49+
typeof arg === "object" &&
50+
arg !== null &&
51+
"kind" in arg &&
52+
typeof (arg as { kind: string }).kind === "string"
53+
);
54+
}
55+
56+
/**
57+
* Track (class, propertyKey) pairs already stored to avoid duplicates when
58+
* the initializer runs on each instance (Stage 3 decorator API).
59+
*/
60+
const schemaFieldProcessed = new WeakMap<object, Set<string>>();
61+
62+
function storeSchemaFieldMetadata(
63+
cls: object,
64+
propertyKey: string,
65+
options: SchemaFieldOptions,
66+
designType?: unknown
67+
): void {
68+
const existing: StoredFieldMeta[] =
69+
(Reflect.getOwnMetadata(SCHEMA_METADATA_KEY, cls) as StoredFieldMeta[] | undefined) ?? [];
70+
71+
existing.push({
72+
...options,
73+
propertyKey,
74+
designType,
75+
});
76+
77+
Reflect.defineMetadata(SCHEMA_METADATA_KEY, existing, cls);
78+
}
79+
4180
/**
4281
* Property decorator to define JSON Schema metadata on a class.
4382
*
4483
* When used with `generateSchemaFromClass()`, produces a JSON Schema draft-07
4584
* object from the decorated properties.
4685
*
47-
* If `emitDecoratorMetadata` is enabled in tsconfig.json, the TypeScript type
48-
* is automatically inferred for `string`, `number`, `boolean` — no need to
49-
* specify `type` explicitly for those.
86+
* Supports both TypeScript 5.0+ (Stage 3) and legacy (experimentalDecorators)
87+
* decorator APIs.
88+
*
89+
* If `emitDecoratorMetadata` is enabled in tsconfig.json (legacy mode), the
90+
* TypeScript type is automatically inferred for `string`, `number`, `boolean` —
91+
* no need to specify `type` explicitly for those.
5092
*
5193
* @example
5294
* ```typescript
@@ -65,26 +107,46 @@ interface StoredFieldMeta extends SchemaFieldOptions {
65107
* ```
66108
*/
67109
export function schemaField(options: SchemaFieldOptions = {}) {
68-
return function (target: object, propertyKey: string) {
69-
// Read existing field metadata for this class
70-
const existing: StoredFieldMeta[] =
71-
(Reflect.getOwnMetadata(SCHEMA_METADATA_KEY, target.constructor) as StoredFieldMeta[] | undefined) ?? [];
110+
return function (
111+
targetOrValue: object | undefined,
112+
propertyKeyOrContext?: string | { kind: string; name: string | symbol }
113+
): ((initialValue: unknown) => unknown) | undefined {
114+
if (isNewDecoratorContext(propertyKeyOrContext)) {
115+
// Stage 3 (TypeScript 5.0+) API: (value, context)
116+
// Return initializer that runs when instance is created; `this` = instance
117+
const propertyKey = String(propertyKeyOrContext.name);
118+
return function (this: unknown, initialValue: unknown) {
119+
const cls = (this as object).constructor as object;
120+
const processed = schemaFieldProcessed.get(cls) ?? new Set<string>();
121+
if (!processed.has(propertyKey)) {
122+
processed.add(propertyKey);
123+
schemaFieldProcessed.set(cls, processed);
124+
let designType: unknown;
125+
try {
126+
designType = Reflect.getMetadata(
127+
"design:type",
128+
this as object,
129+
propertyKey
130+
);
131+
} catch {
132+
// reflect-metadata may not emit design:type for Stage 3 decorators
133+
}
134+
storeSchemaFieldMetadata(cls, propertyKey, options, designType);
135+
}
136+
return initialValue;
137+
};
138+
}
72139

73-
// Try to infer type from TypeScript metadata
140+
// Legacy (experimentalDecorators) API: (target, propertyKey)
141+
const target = targetOrValue as object;
142+
const propertyKey = propertyKeyOrContext as string;
74143
let designType: unknown;
75144
try {
76145
designType = Reflect.getMetadata("design:type", target, propertyKey);
77146
} catch {
78147
// reflect-metadata not available — user must provide type explicitly
79148
}
80-
81-
existing.push({
82-
...options,
83-
propertyKey,
84-
designType,
85-
});
86-
87-
Reflect.defineMetadata(SCHEMA_METADATA_KEY, existing, target.constructor);
149+
storeSchemaFieldMetadata(target.constructor as object, propertyKey, options, designType);
88150
};
89151
}
90152

0 commit comments

Comments
 (0)