@rocketmq/schema 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,23 @@
1
1
 
2
2
 
3
- > @rocketmq/schema@0.1.0 build /home/edilson/learnspace/rocketmq-broker/client-ts/packages/schema
3
+ > @rocketmq/schema@0.1.2 build /home/edilson/learnspace/rocketmq-broker/rocketmq.js/packages/schema
4
4
  > tsup
5
5
 
6
6
  CLI Building entry: src/index.ts
7
7
  CLI Using tsconfig: tsconfig.json
8
8
  CLI tsup v8.5.1
9
- CLI Using tsup config: /home/edilson/learnspace/rocketmq-broker/client-ts/packages/schema/tsup.config.ts
9
+ CLI Using tsup config: /home/edilson/learnspace/rocketmq-broker/rocketmq.js/packages/schema/tsup.config.ts
10
10
  CLI Target: es2022
11
11
  CLI Cleaning output folder
12
12
  ESM Build start
13
13
  CJS Build start
14
- CJS dist/index.cjs 3.21 KB
15
- CJS ⚡️ Build success in 10ms
16
- ESM dist/index.js 2.11 KB
17
- ESM ⚡️ Build success in 10ms
14
+ CJS dist/index.cjs 6.92 KB
15
+ CJS dist/index.cjs.map 13.75 KB
16
+ CJS ⚡️ Build success in 24ms
17
+ ESM dist/index.js 5.83 KB
18
+ ESM dist/index.js.map 13.45 KB
19
+ ESM ⚡️ Build success in 25ms
18
20
  DTS Build start
19
- DTS ⚡️ Build success in 932ms
20
- DTS dist/index.d.ts 4.01 KB
21
- DTS dist/index.d.cts 4.01 KB
21
+ DTS ⚡️ Build success in 558ms
22
+ DTS dist/index.d.ts 4.39 KB
23
+ DTS dist/index.d.cts 4.39 KB
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @rocketmq/schema
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - docs: comprehensive README and API docs like Python SDK
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @rocketmq/schema
2
+
3
+ Decorator-based schema definition system for RocketMQ.
4
+
5
+ Provides the `@Schema()` and `@Field()` decorators used to define queue payload structures. This package automatically collects metadata that the `@rocketmq/core` SDK uses to generate Protobuf schemas for broker-side validation.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @rocketmq/schema
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { Schema, Field } from '@rocketmq/schema';
17
+
18
+ @Schema()
19
+ export class User {
20
+ @Field()
21
+ id!: string;
22
+
23
+ @Field()
24
+ name!: string;
25
+
26
+ @Field({ type: 'int32' })
27
+ age!: number;
28
+ }
29
+ ```
30
+
31
+ This package is re-exported by `@rocketmq/core`, so you can just import from there in most applications.
32
+
33
+ ## License
34
+
35
+ Apache 2.0
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -29,12 +30,34 @@ module.exports = __toCommonJS(index_exports);
29
30
 
30
31
  // src/registry.ts
31
32
  var SchemaRegistry = class {
33
+ static {
34
+ __name(this, "SchemaRegistry");
35
+ }
32
36
  /** Queue name → schema entry. */
33
37
  byQueue = /* @__PURE__ */ new Map();
34
38
  /** Class constructor → field metadata (populated by decorators). */
35
39
  fieldStore = /* @__PURE__ */ new Map();
36
40
  /** Class constructor → subject prefix (populated by @Schema("subject")). */
37
41
  subjectStore = /* @__PURE__ */ new Map();
42
+ /** Set of all decorated schema constructors. */
43
+ allSchemas = /* @__PURE__ */ new Set();
44
+ /** Tracks a constructor as a decorated schema. */
45
+ registerSchema(ctor) {
46
+ this.allSchemas.add(ctor);
47
+ }
48
+ /** Returns true if a constructor is a registered schema. */
49
+ isSchema(ctor) {
50
+ return this.allSchemas.has(ctor);
51
+ }
52
+ /** Looks up a schema constructor by its class name. */
53
+ getSchemaByName(name) {
54
+ for (const ctor of this.allSchemas) {
55
+ if (ctor.name === name) {
56
+ return ctor;
57
+ }
58
+ }
59
+ return void 0;
60
+ }
38
61
  /** Binds a queue name to a fully resolved schema entry. */
39
62
  register(queueName, entry) {
40
63
  this.byQueue.set(queueName, entry);
@@ -45,7 +68,9 @@ var SchemaRegistry = class {
45
68
  }
46
69
  /** Returns all registered schema entries. */
47
70
  listAll() {
48
- return [...this.byQueue.values()];
71
+ return [
72
+ ...this.byQueue.values()
73
+ ];
49
74
  }
50
75
  /** Stores the subject prefix set by @Schema("subject"). */
51
76
  setSubject(ctor, subject) {
@@ -71,9 +96,59 @@ var SchemaRegistry = class {
71
96
  };
72
97
  var defaultRegistry = new SchemaRegistry();
73
98
 
99
+ // src/inference.ts
100
+ function inferFromReflectMetadata(target, key) {
101
+ const R = Reflect;
102
+ if (!R.getMetadata) return "string";
103
+ const designType = R.getMetadata("design:type", target, key);
104
+ if (!designType) return "string";
105
+ return inferFromConstructor(designType);
106
+ }
107
+ __name(inferFromReflectMetadata, "inferFromReflectMetadata");
108
+ function inferFromConstructor(ctor) {
109
+ if (ctor === Number) return "double";
110
+ if (ctor === Boolean) return "bool";
111
+ if (ctor === Date) return "google.protobuf.Timestamp";
112
+ if (ctor === Array || ctor === Object) return "bytes";
113
+ if (defaultRegistry.isSchema(ctor)) return ctor.name;
114
+ return "string";
115
+ }
116
+ __name(inferFromConstructor, "inferFromConstructor");
117
+ function inferFromValue(value) {
118
+ if (value instanceof Date) return "google.protobuf.Timestamp";
119
+ if (value && typeof value === "object") {
120
+ const proto = Object.getPrototypeOf(value);
121
+ if (proto && proto.constructor && defaultRegistry.isSchema(proto.constructor)) {
122
+ return proto.constructor.name;
123
+ }
124
+ return "bytes";
125
+ }
126
+ switch (typeof value) {
127
+ case "number":
128
+ return "double";
129
+ case "boolean":
130
+ return "bool";
131
+ default:
132
+ return "string";
133
+ }
134
+ }
135
+ __name(inferFromValue, "inferFromValue");
136
+
74
137
  // src/decorators.ts
138
+ var import_reflect_metadata = require("reflect-metadata");
139
+ var SCHEMA_SYMBOL = /* @__PURE__ */ Symbol.for("rocketmq:schema");
75
140
  function Schema(subject) {
76
- return function(target, _ctx) {
141
+ return function(target, _ctxOrUndefined) {
142
+ Object.defineProperty(target, SCHEMA_SYMBOL, {
143
+ value: true,
144
+ enumerable: false,
145
+ writable: false
146
+ });
147
+ const R = Reflect;
148
+ if (R.defineMetadata) {
149
+ R.defineMetadata(SCHEMA_SYMBOL, true, target);
150
+ }
151
+ defaultRegistry.registerSchema(target);
77
152
  defaultRegistry.getOrCreateFields(target);
78
153
  if (subject) {
79
154
  defaultRegistry.setSubject(target, subject);
@@ -81,21 +156,61 @@ function Schema(subject) {
81
156
  return target;
82
157
  };
83
158
  }
159
+ __name(Schema, "Schema");
84
160
  function Field(opts) {
85
- return function(_value, ctx) {
86
- const name = String(ctx.name);
161
+ return function(targetOrValue, keyOrCtx) {
162
+ if (targetOrValue === void 0 || typeof keyOrCtx === "object") {
163
+ return handleTC39Field(opts, keyOrCtx);
164
+ }
165
+ handleExperimentalField(opts, targetOrValue, keyOrCtx);
166
+ };
167
+ }
168
+ __name(Field, "Field");
169
+ function handleExperimentalField(opts, target, propertyKey) {
170
+ const ctor = target.constructor;
171
+ const name = String(propertyKey);
172
+ const fields = defaultRegistry.getOrCreateFields(ctor);
173
+ if (fields.some((f) => f.name === name)) return;
174
+ const R = Reflect;
175
+ const reflectedType = R.getMetadata ? R.getMetadata("design:type", target, propertyKey) : void 0;
176
+ const protoType = opts?.type ?? inferFromReflectMetadata(target, propertyKey);
177
+ fields.push({
178
+ name,
179
+ protoType,
180
+ number: fields.length + 1,
181
+ reflectedType,
182
+ ...opts
183
+ });
184
+ }
185
+ __name(handleExperimentalField, "handleExperimentalField");
186
+ function handleTC39Field(opts, ctx) {
187
+ const name = String(ctx.name);
188
+ if (opts?.type) {
87
189
  ctx.addInitializer(function() {
88
- const ctor = this.constructor;
89
- const fields = defaultRegistry.getOrCreateFields(ctor);
90
- if (fields.some((f) => f.name === name)) return;
91
- fields.push({
92
- name,
93
- protoType: opts?.type ?? "string",
94
- number: fields.length + 1
95
- });
190
+ registerTC39Field(this, name, opts.type, opts);
96
191
  });
192
+ return;
193
+ }
194
+ return function(initialValue) {
195
+ registerTC39Field(this, name, inferFromValue(initialValue), opts, initialValue);
196
+ return initialValue;
97
197
  };
98
198
  }
199
+ __name(handleTC39Field, "handleTC39Field");
200
+ function registerTC39Field(instance, name, protoType, opts, initialValue) {
201
+ const ctor = instance.constructor;
202
+ const fields = defaultRegistry.getOrCreateFields(ctor);
203
+ if (fields.some((f) => f.name === name)) return;
204
+ const reflectedType = initialValue && typeof initialValue === "object" ? Object.getPrototypeOf(initialValue).constructor : void 0;
205
+ fields.push({
206
+ name,
207
+ protoType,
208
+ number: fields.length + 1,
209
+ reflectedType,
210
+ ...opts
211
+ });
212
+ }
213
+ __name(registerTC39Field, "registerTC39Field");
99
214
  // Annotate the CommonJS export names for ESM import in node:
100
215
  0 && (module.exports = {
101
216
  Field,
@@ -103,3 +218,4 @@ function Field(opts) {
103
218
  SchemaRegistry,
104
219
  defaultRegistry
105
220
  });
221
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/registry.ts","../src/inference.ts","../src/decorators.ts"],"sourcesContent":["export type { ProtoType, FieldOptions, FieldMeta, SchemaEntry } from './metadata.js';\nexport { SchemaRegistry, defaultRegistry } from './registry.js';\nexport { Schema, Field } from './decorators.js';\nexport type { Constructor } from './types.js';\n","/**\n * In-memory schema registry.\n *\n * Replaces the module-level Map globals from the original schema.ts.\n * Injectable via constructor so tests can use isolated instances.\n */\n\nimport type { FieldMeta, SchemaEntry } from './metadata.js';\nimport type { Constructor } from './types.js';\n\nexport class SchemaRegistry {\n /** Queue name → schema entry. */\n private byQueue = new Map<string, SchemaEntry>();\n\n /** Class constructor → field metadata (populated by decorators). */\n private fieldStore = new Map<Constructor, FieldMeta[]>();\n\n /** Class constructor → subject prefix (populated by @Schema(\"subject\")). */\n private subjectStore = new Map<Constructor, string>();\n\n /** Set of all decorated schema constructors. */\n private allSchemas = new Set<Constructor>();\n\n /** Tracks a constructor as a decorated schema. */\n registerSchema(ctor: Constructor): void {\n this.allSchemas.add(ctor);\n }\n\n /** Returns true if a constructor is a registered schema. */\n isSchema(ctor: Constructor): boolean {\n return this.allSchemas.has(ctor);\n }\n\n /** Looks up a schema constructor by its class name. */\n getSchemaByName(name: string): Constructor | undefined {\n for (const ctor of this.allSchemas) {\n if (ctor.name === name) {\n return ctor;\n }\n }\n return undefined;\n }\n\n /** Binds a queue name to a fully resolved schema entry. */\n register(queueName: string, entry: SchemaEntry): void {\n this.byQueue.set(queueName, entry);\n }\n\n /** Returns the schema entry for a queue, or undefined if unregistered. */\n lookup(queueName: string): SchemaEntry | undefined {\n return this.byQueue.get(queueName);\n }\n\n /** Returns all registered schema entries. */\n listAll(): SchemaEntry[] {\n return [...this.byQueue.values()];\n }\n\n /** Stores the subject prefix set by @Schema(\"subject\"). */\n setSubject(ctor: Constructor, subject: string): void {\n this.subjectStore.set(ctor, subject);\n }\n\n /** Returns the subject prefix for a class, if any. */\n getSubject(ctor: Constructor): string | undefined {\n return this.subjectStore.get(ctor);\n }\n\n /** Returns the mutable field list for a class, creating it if absent. */\n getOrCreateFields(ctor: Constructor): FieldMeta[] {\n let fields = this.fieldStore.get(ctor);\n if (!fields) {\n fields = [];\n this.fieldStore.set(ctor, fields);\n }\n return fields;\n }\n\n /** Returns the field metadata for a class (read-only snapshot). */\n getFields(ctor: Constructor): readonly FieldMeta[] {\n return this.fieldStore.get(ctor) ?? [];\n }\n}\n\n/**\n * Default singleton registry used by decorators.\n *\n * Exported so @Schema() and @Field() can register metadata at class\n * definition time without requiring explicit wiring. The core package\n * reads from this same instance during publish/consume.\n */\nexport const defaultRegistry = new SchemaRegistry();\n","import { defaultRegistry } from './registry.js';\nimport type { Constructor } from './types.js';\n\n/** Infers proto type from reflect metadata. */\nexport function inferFromReflectMetadata(target: object, key: string | symbol): string {\n const R = Reflect as unknown as {\n getMetadata?: (key: string, target: object, prop: string | symbol) => Constructor | undefined;\n };\n if (!R.getMetadata) return 'string';\n\n const designType = R.getMetadata('design:type', target, key);\n if (!designType) return 'string';\n\n return inferFromConstructor(designType);\n}\n\n/** Infers type from primitive or schema constructors. */\nexport function inferFromConstructor(ctor: Constructor): string {\n if (ctor === Number) return 'double';\n if (ctor === Boolean) return 'bool';\n if (ctor === Date) return 'google.protobuf.Timestamp';\n if (ctor === Array || ctor === Object) return 'bytes';\n if (defaultRegistry.isSchema(ctor)) return ctor.name;\n return 'string';\n}\n\n/** Infers type from runtime value (TC39). */\nexport function inferFromValue(value: unknown): string {\n if (value instanceof Date) return 'google.protobuf.Timestamp';\n if (value && typeof value === 'object') {\n const proto = Object.getPrototypeOf(value);\n if (proto && proto.constructor && defaultRegistry.isSchema(proto.constructor)) {\n return proto.constructor.name;\n }\n return 'bytes';\n }\n switch (typeof value) {\n case 'number':\n return 'double';\n case 'boolean':\n return 'bool';\n default:\n return 'string';\n }\n}\n","/**\n * Dual-mode decorators: works with BOTH experimental decorators\n * (reflect-metadata) AND TC39 stage 3 decorators.\n */\n\nimport { inferFromReflectMetadata, inferFromValue } from './inference.js';\nimport type { FieldOptions } from './metadata.js';\nimport { defaultRegistry } from './registry.js';\nimport type { Constructor } from './types.js';\n\nimport 'reflect-metadata';\n\nconst SCHEMA_SYMBOL = Symbol.for('rocketmq:schema');\n\n/** Checks if a constructor is decorated as a schema class. */\nexport function isSchemaClass(ctor: unknown): boolean {\n if (typeof ctor !== 'function') return false;\n const R = Reflect as unknown as {\n hasOwnMetadata?: (k: unknown, t: unknown) => boolean;\n };\n return (\n SCHEMA_SYMBOL in ctor ||\n (R.hasOwnMetadata !== undefined && R.hasOwnMetadata(SCHEMA_SYMBOL, ctor))\n );\n}\n\n/** Marks a class as a schema definition. */\nexport function Schema(subject?: string) {\n return function <T extends Constructor>(target: T, _ctxOrUndefined?: ClassDecoratorContext): T {\n Object.defineProperty(target, SCHEMA_SYMBOL, {\n value: true,\n enumerable: false,\n writable: false,\n });\n const R = Reflect as unknown as {\n defineMetadata?: (k: unknown, v: unknown, t: unknown) => void;\n };\n if (R.defineMetadata) {\n R.defineMetadata(SCHEMA_SYMBOL, true, target);\n }\n defaultRegistry.registerSchema(target);\n defaultRegistry.getOrCreateFields(target);\n if (subject) {\n defaultRegistry.setSubject(target, subject);\n }\n return target;\n };\n}\n\n/** Marks a property as a schema field. */\nexport function Field(opts?: FieldOptions) {\n return function (\n targetOrValue: object | undefined,\n keyOrCtx: string | symbol | ClassFieldDecoratorContext,\n // WHY void: Node16 moduleResolution enforces TS1271 — decorator return must be void|any.\n // The TC39 initializer return is consumed by the runtime, not the decorator call site.\n ): void {\n if (targetOrValue === undefined || typeof keyOrCtx === 'object') {\n // WHY cast: TC39 path returns an initializer function at runtime,\n // but the .d.ts must declare void to satisfy experimental decorator constraints.\n return handleTC39Field(opts, keyOrCtx as ClassFieldDecoratorContext) as void;\n }\n handleExperimentalField(opts, targetOrValue, keyOrCtx as string | symbol);\n };\n}\n\n/** Experimental decorator path. */\nfunction handleExperimentalField(\n opts: FieldOptions | undefined,\n target: object,\n propertyKey: string | symbol,\n): void {\n const ctor = target.constructor as Constructor;\n const name = String(propertyKey);\n const fields = defaultRegistry.getOrCreateFields(ctor);\n\n if (fields.some((f) => f.name === name)) return;\n\n const R = Reflect as unknown as {\n getMetadata?: (k: string, t: object, p: string | symbol) => Constructor | undefined;\n };\n const reflectedType = R.getMetadata\n ? R.getMetadata('design:type', target, propertyKey)\n : undefined;\n const protoType = opts?.type ?? inferFromReflectMetadata(target, propertyKey);\n\n fields.push({\n name,\n protoType,\n number: fields.length + 1,\n reflectedType,\n ...opts,\n });\n}\n\n/* v8 ignore start */\n/** TC39 stage 3 decorator path. */\nfunction handleTC39Field(\n opts: FieldOptions | undefined,\n ctx: ClassFieldDecoratorContext,\n): ((this: unknown, initialValue: unknown) => unknown) | void {\n const name = String(ctx.name);\n\n if (opts?.type) {\n ctx.addInitializer(function (this: unknown) {\n registerTC39Field(this, name, opts.type!, opts);\n });\n return;\n }\n\n return function (this: unknown, initialValue: unknown): unknown {\n registerTC39Field(this, name, inferFromValue(initialValue), opts, initialValue);\n return initialValue;\n };\n}\n\n/** Internal TC39 helper to register field inside initializer. */\nfunction registerTC39Field(\n instance: unknown,\n name: string,\n protoType: string,\n opts: FieldOptions | undefined,\n initialValue?: unknown,\n): void {\n const ctor = (instance as Record<string, unknown>).constructor as Constructor;\n const fields = defaultRegistry.getOrCreateFields(ctor);\n if (fields.some((f) => f.name === name)) return;\n\n const reflectedType =\n initialValue && typeof initialValue === 'object'\n ? (Object.getPrototypeOf(initialValue).constructor as Constructor)\n : undefined;\n\n fields.push({\n name,\n protoType,\n number: fields.length + 1,\n reflectedType,\n ...opts,\n });\n}\n"],"mappings":"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","names":["SchemaRegistry","byQueue","Map","fieldStore","subjectStore","allSchemas","Set","registerSchema","ctor","add","isSchema","has","getSchemaByName","name","undefined","register","queueName","entry","set","lookup","get","listAll","values","setSubject","subject","getSubject","getOrCreateFields","fields","getFields","defaultRegistry","inferFromReflectMetadata","target","key","R","Reflect","getMetadata","designType","inferFromConstructor","ctor","Number","Boolean","Date","Array","Object","defaultRegistry","isSchema","name","inferFromValue","value","proto","getPrototypeOf","SCHEMA_SYMBOL","Symbol","for","Schema","subject","target","_ctxOrUndefined","Object","defineProperty","SCHEMA_SYMBOL","value","enumerable","writable","R","Reflect","defineMetadata","defaultRegistry","registerSchema","getOrCreateFields","setSubject","Field","opts","targetOrValue","keyOrCtx","undefined","handleTC39Field","handleExperimentalField","propertyKey","ctor","name","String","fields","some","f","reflectedType","getMetadata","protoType","type","inferFromReflectMetadata","push","number","length","ctx","addInitializer","registerTC39Field","initialValue","inferFromValue","instance","getPrototypeOf"]}
package/dist/index.d.cts CHANGED
@@ -1,26 +1,45 @@
1
+ /**
2
+ * Core type definitions for the schema package.
3
+ */
4
+ /**
5
+ * Represents a class constructor.
6
+ * Used to replace the generic 'Function' type for stricter type safety.
7
+ */
8
+ type Constructor<T = object> = new (...args: any[]) => T;
9
+
1
10
  /**
2
11
  * Protobuf wire types supported by the schema decorator system.
3
12
  *
4
13
  * Maps 1:1 to proto3 scalar types so the SDK can generate
5
14
  * `.proto` definitions without requiring protobuf knowledge.
6
- *
7
- * Usage:
8
- * @Field({ type: "int32" }) qty!: number;
9
15
  */
10
16
  type ProtoType = 'string' | 'int32' | 'int64' | 'uint32' | 'uint64' | 'float' | 'double' | 'bool' | 'bytes';
17
+ /** Additional configuration options passed to the @Field() decorator. */
18
+ interface FieldOptions {
19
+ /** Explicit proto3 type override (scalar or custom message name). */
20
+ type?: string;
21
+ /** Emits 'repeated' prefix for arrays. */
22
+ repeated?: boolean;
23
+ /** Emits 'optional' prefix for optional presence. */
24
+ optional?: boolean;
25
+ /** Inline comment written above the field. */
26
+ comment?: string;
27
+ }
11
28
  /** Metadata captured by the @Field() decorator for a single class property. */
12
- interface FieldMeta {
29
+ interface FieldMeta extends FieldOptions {
13
30
  /** Property name on the decorated class. */
14
31
  name: string;
15
- /** Proto3 scalar type. Defaults to "string" when omitted. */
16
- protoType: ProtoType;
32
+ /** Proto3 type. Defaults to "string" when omitted. */
33
+ protoType: string;
17
34
  /** 1-based field number for proto ordering. */
18
35
  number: number;
36
+ /** Constructor captured by reflect-metadata (String, Number, Boolean, Date, etc.). */
37
+ reflectedType?: Constructor;
19
38
  }
20
39
  /** Collected metadata for a decorated schema class. */
21
40
  interface SchemaEntry {
22
41
  /** Class constructor reference. */
23
- ctor: Function;
42
+ ctor: Constructor;
24
43
  /** Human-readable schema name (defaults to class name). */
25
44
  name: string;
26
45
  /** Optional subject prefix for registry-based validation. */
@@ -34,11 +53,6 @@ interface SchemaEntry {
34
53
  *
35
54
  * Replaces the module-level Map globals from the original schema.ts.
36
55
  * Injectable via constructor so tests can use isolated instances.
37
- *
38
- * Usage:
39
- * const registry = new SchemaRegistry();
40
- * registry.register("orders", entry);
41
- * const entry = registry.lookup("orders");
42
56
  */
43
57
 
44
58
  declare class SchemaRegistry {
@@ -48,6 +62,14 @@ declare class SchemaRegistry {
48
62
  private fieldStore;
49
63
  /** Class constructor → subject prefix (populated by @Schema("subject")). */
50
64
  private subjectStore;
65
+ /** Set of all decorated schema constructors. */
66
+ private allSchemas;
67
+ /** Tracks a constructor as a decorated schema. */
68
+ registerSchema(ctor: Constructor): void;
69
+ /** Returns true if a constructor is a registered schema. */
70
+ isSchema(ctor: Constructor): boolean;
71
+ /** Looks up a schema constructor by its class name. */
72
+ getSchemaByName(name: string): Constructor | undefined;
51
73
  /** Binds a queue name to a fully resolved schema entry. */
52
74
  register(queueName: string, entry: SchemaEntry): void;
53
75
  /** Returns the schema entry for a queue, or undefined if unregistered. */
@@ -55,13 +77,13 @@ declare class SchemaRegistry {
55
77
  /** Returns all registered schema entries. */
56
78
  listAll(): SchemaEntry[];
57
79
  /** Stores the subject prefix set by @Schema("subject"). */
58
- setSubject(ctor: Function, subject: string): void;
80
+ setSubject(ctor: Constructor, subject: string): void;
59
81
  /** Returns the subject prefix for a class, if any. */
60
- getSubject(ctor: Function): string | undefined;
82
+ getSubject(ctor: Constructor): string | undefined;
61
83
  /** Returns the mutable field list for a class, creating it if absent. */
62
- getOrCreateFields(ctor: Function): FieldMeta[];
84
+ getOrCreateFields(ctor: Constructor): FieldMeta[];
63
85
  /** Returns the field metadata for a class (read-only snapshot). */
64
- getFields(ctor: Function): readonly FieldMeta[];
86
+ getFields(ctor: Constructor): readonly FieldMeta[];
65
87
  }
66
88
  /**
67
89
  * Default singleton registry used by decorators.
@@ -73,37 +95,13 @@ declare class SchemaRegistry {
73
95
  declare const defaultRegistry: SchemaRegistry;
74
96
 
75
97
  /**
76
- * TC39 stage 3 decorators for schema definition.
77
- *
78
- * Collects field names and types from decorated classes into the
79
- * default SchemaRegistry so the SDK can generate proto3 definitions
80
- * and validate payloads at runtime.
81
- *
82
- * Usage:
83
- * @Schema("notifications")
84
- * class Notification {
85
- * @Field() id!: string;
86
- * @Field({ type: "int64" }) timestamp!: number;
87
- * }
98
+ * Dual-mode decorators: works with BOTH experimental decorators
99
+ * (reflect-metadata) AND TC39 stage 3 decorators.
88
100
  */
89
101
 
90
- /**
91
- * Marks a class as a schema definition.
92
- *
93
- * @param subject - Optional subject prefix for registry-based validation
94
- * (e.g. "notifications" → "notifications-value").
95
- */
96
- declare function Schema(subject?: string): <T extends new (...args: unknown[]) => object>(target: T, _ctx: ClassDecoratorContext) => T;
97
- interface FieldOptions {
98
- /** Proto3 scalar type. Defaults to "string". */
99
- type?: ProtoType;
100
- }
101
- /**
102
- * Marks a property as a schema field (TC39 stage 3 field decorator).
103
- *
104
- * Defers registration to `addInitializer` so the class constructor is
105
- * fully defined before we read `this.constructor`.
106
- */
107
- declare function Field(opts?: FieldOptions): (_value: undefined, ctx: ClassFieldDecoratorContext) => void;
102
+ /** Marks a class as a schema definition. */
103
+ declare function Schema(subject?: string): <T extends Constructor>(target: T, _ctxOrUndefined?: ClassDecoratorContext) => T;
104
+ /** Marks a property as a schema field. */
105
+ declare function Field(opts?: FieldOptions): (targetOrValue: object | undefined, keyOrCtx: string | symbol | ClassFieldDecoratorContext) => void;
108
106
 
109
- export { Field, type FieldMeta, type ProtoType, Schema, type SchemaEntry, SchemaRegistry, defaultRegistry };
107
+ export { type Constructor, Field, type FieldMeta, type FieldOptions, type ProtoType, Schema, type SchemaEntry, SchemaRegistry, defaultRegistry };
package/dist/index.d.ts CHANGED
@@ -1,26 +1,45 @@
1
+ /**
2
+ * Core type definitions for the schema package.
3
+ */
4
+ /**
5
+ * Represents a class constructor.
6
+ * Used to replace the generic 'Function' type for stricter type safety.
7
+ */
8
+ type Constructor<T = object> = new (...args: any[]) => T;
9
+
1
10
  /**
2
11
  * Protobuf wire types supported by the schema decorator system.
3
12
  *
4
13
  * Maps 1:1 to proto3 scalar types so the SDK can generate
5
14
  * `.proto` definitions without requiring protobuf knowledge.
6
- *
7
- * Usage:
8
- * @Field({ type: "int32" }) qty!: number;
9
15
  */
10
16
  type ProtoType = 'string' | 'int32' | 'int64' | 'uint32' | 'uint64' | 'float' | 'double' | 'bool' | 'bytes';
17
+ /** Additional configuration options passed to the @Field() decorator. */
18
+ interface FieldOptions {
19
+ /** Explicit proto3 type override (scalar or custom message name). */
20
+ type?: string;
21
+ /** Emits 'repeated' prefix for arrays. */
22
+ repeated?: boolean;
23
+ /** Emits 'optional' prefix for optional presence. */
24
+ optional?: boolean;
25
+ /** Inline comment written above the field. */
26
+ comment?: string;
27
+ }
11
28
  /** Metadata captured by the @Field() decorator for a single class property. */
12
- interface FieldMeta {
29
+ interface FieldMeta extends FieldOptions {
13
30
  /** Property name on the decorated class. */
14
31
  name: string;
15
- /** Proto3 scalar type. Defaults to "string" when omitted. */
16
- protoType: ProtoType;
32
+ /** Proto3 type. Defaults to "string" when omitted. */
33
+ protoType: string;
17
34
  /** 1-based field number for proto ordering. */
18
35
  number: number;
36
+ /** Constructor captured by reflect-metadata (String, Number, Boolean, Date, etc.). */
37
+ reflectedType?: Constructor;
19
38
  }
20
39
  /** Collected metadata for a decorated schema class. */
21
40
  interface SchemaEntry {
22
41
  /** Class constructor reference. */
23
- ctor: Function;
42
+ ctor: Constructor;
24
43
  /** Human-readable schema name (defaults to class name). */
25
44
  name: string;
26
45
  /** Optional subject prefix for registry-based validation. */
@@ -34,11 +53,6 @@ interface SchemaEntry {
34
53
  *
35
54
  * Replaces the module-level Map globals from the original schema.ts.
36
55
  * Injectable via constructor so tests can use isolated instances.
37
- *
38
- * Usage:
39
- * const registry = new SchemaRegistry();
40
- * registry.register("orders", entry);
41
- * const entry = registry.lookup("orders");
42
56
  */
43
57
 
44
58
  declare class SchemaRegistry {
@@ -48,6 +62,14 @@ declare class SchemaRegistry {
48
62
  private fieldStore;
49
63
  /** Class constructor → subject prefix (populated by @Schema("subject")). */
50
64
  private subjectStore;
65
+ /** Set of all decorated schema constructors. */
66
+ private allSchemas;
67
+ /** Tracks a constructor as a decorated schema. */
68
+ registerSchema(ctor: Constructor): void;
69
+ /** Returns true if a constructor is a registered schema. */
70
+ isSchema(ctor: Constructor): boolean;
71
+ /** Looks up a schema constructor by its class name. */
72
+ getSchemaByName(name: string): Constructor | undefined;
51
73
  /** Binds a queue name to a fully resolved schema entry. */
52
74
  register(queueName: string, entry: SchemaEntry): void;
53
75
  /** Returns the schema entry for a queue, or undefined if unregistered. */
@@ -55,13 +77,13 @@ declare class SchemaRegistry {
55
77
  /** Returns all registered schema entries. */
56
78
  listAll(): SchemaEntry[];
57
79
  /** Stores the subject prefix set by @Schema("subject"). */
58
- setSubject(ctor: Function, subject: string): void;
80
+ setSubject(ctor: Constructor, subject: string): void;
59
81
  /** Returns the subject prefix for a class, if any. */
60
- getSubject(ctor: Function): string | undefined;
82
+ getSubject(ctor: Constructor): string | undefined;
61
83
  /** Returns the mutable field list for a class, creating it if absent. */
62
- getOrCreateFields(ctor: Function): FieldMeta[];
84
+ getOrCreateFields(ctor: Constructor): FieldMeta[];
63
85
  /** Returns the field metadata for a class (read-only snapshot). */
64
- getFields(ctor: Function): readonly FieldMeta[];
86
+ getFields(ctor: Constructor): readonly FieldMeta[];
65
87
  }
66
88
  /**
67
89
  * Default singleton registry used by decorators.
@@ -73,37 +95,13 @@ declare class SchemaRegistry {
73
95
  declare const defaultRegistry: SchemaRegistry;
74
96
 
75
97
  /**
76
- * TC39 stage 3 decorators for schema definition.
77
- *
78
- * Collects field names and types from decorated classes into the
79
- * default SchemaRegistry so the SDK can generate proto3 definitions
80
- * and validate payloads at runtime.
81
- *
82
- * Usage:
83
- * @Schema("notifications")
84
- * class Notification {
85
- * @Field() id!: string;
86
- * @Field({ type: "int64" }) timestamp!: number;
87
- * }
98
+ * Dual-mode decorators: works with BOTH experimental decorators
99
+ * (reflect-metadata) AND TC39 stage 3 decorators.
88
100
  */
89
101
 
90
- /**
91
- * Marks a class as a schema definition.
92
- *
93
- * @param subject - Optional subject prefix for registry-based validation
94
- * (e.g. "notifications" → "notifications-value").
95
- */
96
- declare function Schema(subject?: string): <T extends new (...args: unknown[]) => object>(target: T, _ctx: ClassDecoratorContext) => T;
97
- interface FieldOptions {
98
- /** Proto3 scalar type. Defaults to "string". */
99
- type?: ProtoType;
100
- }
101
- /**
102
- * Marks a property as a schema field (TC39 stage 3 field decorator).
103
- *
104
- * Defers registration to `addInitializer` so the class constructor is
105
- * fully defined before we read `this.constructor`.
106
- */
107
- declare function Field(opts?: FieldOptions): (_value: undefined, ctx: ClassFieldDecoratorContext) => void;
102
+ /** Marks a class as a schema definition. */
103
+ declare function Schema(subject?: string): <T extends Constructor>(target: T, _ctxOrUndefined?: ClassDecoratorContext) => T;
104
+ /** Marks a property as a schema field. */
105
+ declare function Field(opts?: FieldOptions): (targetOrValue: object | undefined, keyOrCtx: string | symbol | ClassFieldDecoratorContext) => void;
108
106
 
109
- export { Field, type FieldMeta, type ProtoType, Schema, type SchemaEntry, SchemaRegistry, defaultRegistry };
107
+ export { type Constructor, Field, type FieldMeta, type FieldOptions, type ProtoType, Schema, type SchemaEntry, SchemaRegistry, defaultRegistry };
package/dist/index.js CHANGED
@@ -1,11 +1,36 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
1
4
  // src/registry.ts
2
5
  var SchemaRegistry = class {
6
+ static {
7
+ __name(this, "SchemaRegistry");
8
+ }
3
9
  /** Queue name → schema entry. */
4
10
  byQueue = /* @__PURE__ */ new Map();
5
11
  /** Class constructor → field metadata (populated by decorators). */
6
12
  fieldStore = /* @__PURE__ */ new Map();
7
13
  /** Class constructor → subject prefix (populated by @Schema("subject")). */
8
14
  subjectStore = /* @__PURE__ */ new Map();
15
+ /** Set of all decorated schema constructors. */
16
+ allSchemas = /* @__PURE__ */ new Set();
17
+ /** Tracks a constructor as a decorated schema. */
18
+ registerSchema(ctor) {
19
+ this.allSchemas.add(ctor);
20
+ }
21
+ /** Returns true if a constructor is a registered schema. */
22
+ isSchema(ctor) {
23
+ return this.allSchemas.has(ctor);
24
+ }
25
+ /** Looks up a schema constructor by its class name. */
26
+ getSchemaByName(name) {
27
+ for (const ctor of this.allSchemas) {
28
+ if (ctor.name === name) {
29
+ return ctor;
30
+ }
31
+ }
32
+ return void 0;
33
+ }
9
34
  /** Binds a queue name to a fully resolved schema entry. */
10
35
  register(queueName, entry) {
11
36
  this.byQueue.set(queueName, entry);
@@ -16,7 +41,9 @@ var SchemaRegistry = class {
16
41
  }
17
42
  /** Returns all registered schema entries. */
18
43
  listAll() {
19
- return [...this.byQueue.values()];
44
+ return [
45
+ ...this.byQueue.values()
46
+ ];
20
47
  }
21
48
  /** Stores the subject prefix set by @Schema("subject"). */
22
49
  setSubject(ctor, subject) {
@@ -42,9 +69,59 @@ var SchemaRegistry = class {
42
69
  };
43
70
  var defaultRegistry = new SchemaRegistry();
44
71
 
72
+ // src/inference.ts
73
+ function inferFromReflectMetadata(target, key) {
74
+ const R = Reflect;
75
+ if (!R.getMetadata) return "string";
76
+ const designType = R.getMetadata("design:type", target, key);
77
+ if (!designType) return "string";
78
+ return inferFromConstructor(designType);
79
+ }
80
+ __name(inferFromReflectMetadata, "inferFromReflectMetadata");
81
+ function inferFromConstructor(ctor) {
82
+ if (ctor === Number) return "double";
83
+ if (ctor === Boolean) return "bool";
84
+ if (ctor === Date) return "google.protobuf.Timestamp";
85
+ if (ctor === Array || ctor === Object) return "bytes";
86
+ if (defaultRegistry.isSchema(ctor)) return ctor.name;
87
+ return "string";
88
+ }
89
+ __name(inferFromConstructor, "inferFromConstructor");
90
+ function inferFromValue(value) {
91
+ if (value instanceof Date) return "google.protobuf.Timestamp";
92
+ if (value && typeof value === "object") {
93
+ const proto = Object.getPrototypeOf(value);
94
+ if (proto && proto.constructor && defaultRegistry.isSchema(proto.constructor)) {
95
+ return proto.constructor.name;
96
+ }
97
+ return "bytes";
98
+ }
99
+ switch (typeof value) {
100
+ case "number":
101
+ return "double";
102
+ case "boolean":
103
+ return "bool";
104
+ default:
105
+ return "string";
106
+ }
107
+ }
108
+ __name(inferFromValue, "inferFromValue");
109
+
45
110
  // src/decorators.ts
111
+ import "reflect-metadata";
112
+ var SCHEMA_SYMBOL = /* @__PURE__ */ Symbol.for("rocketmq:schema");
46
113
  function Schema(subject) {
47
- return function(target, _ctx) {
114
+ return function(target, _ctxOrUndefined) {
115
+ Object.defineProperty(target, SCHEMA_SYMBOL, {
116
+ value: true,
117
+ enumerable: false,
118
+ writable: false
119
+ });
120
+ const R = Reflect;
121
+ if (R.defineMetadata) {
122
+ R.defineMetadata(SCHEMA_SYMBOL, true, target);
123
+ }
124
+ defaultRegistry.registerSchema(target);
48
125
  defaultRegistry.getOrCreateFields(target);
49
126
  if (subject) {
50
127
  defaultRegistry.setSubject(target, subject);
@@ -52,24 +129,65 @@ function Schema(subject) {
52
129
  return target;
53
130
  };
54
131
  }
132
+ __name(Schema, "Schema");
55
133
  function Field(opts) {
56
- return function(_value, ctx) {
57
- const name = String(ctx.name);
134
+ return function(targetOrValue, keyOrCtx) {
135
+ if (targetOrValue === void 0 || typeof keyOrCtx === "object") {
136
+ return handleTC39Field(opts, keyOrCtx);
137
+ }
138
+ handleExperimentalField(opts, targetOrValue, keyOrCtx);
139
+ };
140
+ }
141
+ __name(Field, "Field");
142
+ function handleExperimentalField(opts, target, propertyKey) {
143
+ const ctor = target.constructor;
144
+ const name = String(propertyKey);
145
+ const fields = defaultRegistry.getOrCreateFields(ctor);
146
+ if (fields.some((f) => f.name === name)) return;
147
+ const R = Reflect;
148
+ const reflectedType = R.getMetadata ? R.getMetadata("design:type", target, propertyKey) : void 0;
149
+ const protoType = opts?.type ?? inferFromReflectMetadata(target, propertyKey);
150
+ fields.push({
151
+ name,
152
+ protoType,
153
+ number: fields.length + 1,
154
+ reflectedType,
155
+ ...opts
156
+ });
157
+ }
158
+ __name(handleExperimentalField, "handleExperimentalField");
159
+ function handleTC39Field(opts, ctx) {
160
+ const name = String(ctx.name);
161
+ if (opts?.type) {
58
162
  ctx.addInitializer(function() {
59
- const ctor = this.constructor;
60
- const fields = defaultRegistry.getOrCreateFields(ctor);
61
- if (fields.some((f) => f.name === name)) return;
62
- fields.push({
63
- name,
64
- protoType: opts?.type ?? "string",
65
- number: fields.length + 1
66
- });
163
+ registerTC39Field(this, name, opts.type, opts);
67
164
  });
165
+ return;
166
+ }
167
+ return function(initialValue) {
168
+ registerTC39Field(this, name, inferFromValue(initialValue), opts, initialValue);
169
+ return initialValue;
68
170
  };
69
171
  }
172
+ __name(handleTC39Field, "handleTC39Field");
173
+ function registerTC39Field(instance, name, protoType, opts, initialValue) {
174
+ const ctor = instance.constructor;
175
+ const fields = defaultRegistry.getOrCreateFields(ctor);
176
+ if (fields.some((f) => f.name === name)) return;
177
+ const reflectedType = initialValue && typeof initialValue === "object" ? Object.getPrototypeOf(initialValue).constructor : void 0;
178
+ fields.push({
179
+ name,
180
+ protoType,
181
+ number: fields.length + 1,
182
+ reflectedType,
183
+ ...opts
184
+ });
185
+ }
186
+ __name(registerTC39Field, "registerTC39Field");
70
187
  export {
71
188
  Field,
72
189
  Schema,
73
190
  SchemaRegistry,
74
191
  defaultRegistry
75
192
  };
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts","../src/inference.ts","../src/decorators.ts"],"sourcesContent":["/**\n * In-memory schema registry.\n *\n * Replaces the module-level Map globals from the original schema.ts.\n * Injectable via constructor so tests can use isolated instances.\n */\n\nimport type { FieldMeta, SchemaEntry } from './metadata.js';\nimport type { Constructor } from './types.js';\n\nexport class SchemaRegistry {\n /** Queue name → schema entry. */\n private byQueue = new Map<string, SchemaEntry>();\n\n /** Class constructor → field metadata (populated by decorators). */\n private fieldStore = new Map<Constructor, FieldMeta[]>();\n\n /** Class constructor → subject prefix (populated by @Schema(\"subject\")). */\n private subjectStore = new Map<Constructor, string>();\n\n /** Set of all decorated schema constructors. */\n private allSchemas = new Set<Constructor>();\n\n /** Tracks a constructor as a decorated schema. */\n registerSchema(ctor: Constructor): void {\n this.allSchemas.add(ctor);\n }\n\n /** Returns true if a constructor is a registered schema. */\n isSchema(ctor: Constructor): boolean {\n return this.allSchemas.has(ctor);\n }\n\n /** Looks up a schema constructor by its class name. */\n getSchemaByName(name: string): Constructor | undefined {\n for (const ctor of this.allSchemas) {\n if (ctor.name === name) {\n return ctor;\n }\n }\n return undefined;\n }\n\n /** Binds a queue name to a fully resolved schema entry. */\n register(queueName: string, entry: SchemaEntry): void {\n this.byQueue.set(queueName, entry);\n }\n\n /** Returns the schema entry for a queue, or undefined if unregistered. */\n lookup(queueName: string): SchemaEntry | undefined {\n return this.byQueue.get(queueName);\n }\n\n /** Returns all registered schema entries. */\n listAll(): SchemaEntry[] {\n return [...this.byQueue.values()];\n }\n\n /** Stores the subject prefix set by @Schema(\"subject\"). */\n setSubject(ctor: Constructor, subject: string): void {\n this.subjectStore.set(ctor, subject);\n }\n\n /** Returns the subject prefix for a class, if any. */\n getSubject(ctor: Constructor): string | undefined {\n return this.subjectStore.get(ctor);\n }\n\n /** Returns the mutable field list for a class, creating it if absent. */\n getOrCreateFields(ctor: Constructor): FieldMeta[] {\n let fields = this.fieldStore.get(ctor);\n if (!fields) {\n fields = [];\n this.fieldStore.set(ctor, fields);\n }\n return fields;\n }\n\n /** Returns the field metadata for a class (read-only snapshot). */\n getFields(ctor: Constructor): readonly FieldMeta[] {\n return this.fieldStore.get(ctor) ?? [];\n }\n}\n\n/**\n * Default singleton registry used by decorators.\n *\n * Exported so @Schema() and @Field() can register metadata at class\n * definition time without requiring explicit wiring. The core package\n * reads from this same instance during publish/consume.\n */\nexport const defaultRegistry = new SchemaRegistry();\n","import { defaultRegistry } from './registry.js';\nimport type { Constructor } from './types.js';\n\n/** Infers proto type from reflect metadata. */\nexport function inferFromReflectMetadata(target: object, key: string | symbol): string {\n const R = Reflect as unknown as {\n getMetadata?: (key: string, target: object, prop: string | symbol) => Constructor | undefined;\n };\n if (!R.getMetadata) return 'string';\n\n const designType = R.getMetadata('design:type', target, key);\n if (!designType) return 'string';\n\n return inferFromConstructor(designType);\n}\n\n/** Infers type from primitive or schema constructors. */\nexport function inferFromConstructor(ctor: Constructor): string {\n if (ctor === Number) return 'double';\n if (ctor === Boolean) return 'bool';\n if (ctor === Date) return 'google.protobuf.Timestamp';\n if (ctor === Array || ctor === Object) return 'bytes';\n if (defaultRegistry.isSchema(ctor)) return ctor.name;\n return 'string';\n}\n\n/** Infers type from runtime value (TC39). */\nexport function inferFromValue(value: unknown): string {\n if (value instanceof Date) return 'google.protobuf.Timestamp';\n if (value && typeof value === 'object') {\n const proto = Object.getPrototypeOf(value);\n if (proto && proto.constructor && defaultRegistry.isSchema(proto.constructor)) {\n return proto.constructor.name;\n }\n return 'bytes';\n }\n switch (typeof value) {\n case 'number':\n return 'double';\n case 'boolean':\n return 'bool';\n default:\n return 'string';\n }\n}\n","/**\n * Dual-mode decorators: works with BOTH experimental decorators\n * (reflect-metadata) AND TC39 stage 3 decorators.\n */\n\nimport { inferFromReflectMetadata, inferFromValue } from './inference.js';\nimport type { FieldOptions } from './metadata.js';\nimport { defaultRegistry } from './registry.js';\nimport type { Constructor } from './types.js';\n\nimport 'reflect-metadata';\n\nconst SCHEMA_SYMBOL = Symbol.for('rocketmq:schema');\n\n/** Checks if a constructor is decorated as a schema class. */\nexport function isSchemaClass(ctor: unknown): boolean {\n if (typeof ctor !== 'function') return false;\n const R = Reflect as unknown as {\n hasOwnMetadata?: (k: unknown, t: unknown) => boolean;\n };\n return (\n SCHEMA_SYMBOL in ctor ||\n (R.hasOwnMetadata !== undefined && R.hasOwnMetadata(SCHEMA_SYMBOL, ctor))\n );\n}\n\n/** Marks a class as a schema definition. */\nexport function Schema(subject?: string) {\n return function <T extends Constructor>(target: T, _ctxOrUndefined?: ClassDecoratorContext): T {\n Object.defineProperty(target, SCHEMA_SYMBOL, {\n value: true,\n enumerable: false,\n writable: false,\n });\n const R = Reflect as unknown as {\n defineMetadata?: (k: unknown, v: unknown, t: unknown) => void;\n };\n if (R.defineMetadata) {\n R.defineMetadata(SCHEMA_SYMBOL, true, target);\n }\n defaultRegistry.registerSchema(target);\n defaultRegistry.getOrCreateFields(target);\n if (subject) {\n defaultRegistry.setSubject(target, subject);\n }\n return target;\n };\n}\n\n/** Marks a property as a schema field. */\nexport function Field(opts?: FieldOptions) {\n return function (\n targetOrValue: object | undefined,\n keyOrCtx: string | symbol | ClassFieldDecoratorContext,\n // WHY void: Node16 moduleResolution enforces TS1271 — decorator return must be void|any.\n // The TC39 initializer return is consumed by the runtime, not the decorator call site.\n ): void {\n if (targetOrValue === undefined || typeof keyOrCtx === 'object') {\n // WHY cast: TC39 path returns an initializer function at runtime,\n // but the .d.ts must declare void to satisfy experimental decorator constraints.\n return handleTC39Field(opts, keyOrCtx as ClassFieldDecoratorContext) as void;\n }\n handleExperimentalField(opts, targetOrValue, keyOrCtx as string | symbol);\n };\n}\n\n/** Experimental decorator path. */\nfunction handleExperimentalField(\n opts: FieldOptions | undefined,\n target: object,\n propertyKey: string | symbol,\n): void {\n const ctor = target.constructor as Constructor;\n const name = String(propertyKey);\n const fields = defaultRegistry.getOrCreateFields(ctor);\n\n if (fields.some((f) => f.name === name)) return;\n\n const R = Reflect as unknown as {\n getMetadata?: (k: string, t: object, p: string | symbol) => Constructor | undefined;\n };\n const reflectedType = R.getMetadata\n ? R.getMetadata('design:type', target, propertyKey)\n : undefined;\n const protoType = opts?.type ?? inferFromReflectMetadata(target, propertyKey);\n\n fields.push({\n name,\n protoType,\n number: fields.length + 1,\n reflectedType,\n ...opts,\n });\n}\n\n/* v8 ignore start */\n/** TC39 stage 3 decorator path. */\nfunction handleTC39Field(\n opts: FieldOptions | undefined,\n ctx: ClassFieldDecoratorContext,\n): ((this: unknown, initialValue: unknown) => unknown) | void {\n const name = String(ctx.name);\n\n if (opts?.type) {\n ctx.addInitializer(function (this: unknown) {\n registerTC39Field(this, name, opts.type!, opts);\n });\n return;\n }\n\n return function (this: unknown, initialValue: unknown): unknown {\n registerTC39Field(this, name, inferFromValue(initialValue), opts, initialValue);\n return initialValue;\n };\n}\n\n/** Internal TC39 helper to register field inside initializer. */\nfunction registerTC39Field(\n instance: unknown,\n name: string,\n protoType: string,\n opts: FieldOptions | undefined,\n initialValue?: unknown,\n): void {\n const ctor = (instance as Record<string, unknown>).constructor as Constructor;\n const fields = defaultRegistry.getOrCreateFields(ctor);\n if (fields.some((f) => f.name === name)) return;\n\n const reflectedType =\n initialValue && typeof initialValue === 'object'\n ? (Object.getPrototypeOf(initialValue).constructor as Constructor)\n : undefined;\n\n fields.push({\n name,\n protoType,\n number: fields.length + 1,\n reflectedType,\n ...opts,\n });\n}\n"],"mappings":"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","names":["SchemaRegistry","byQueue","Map","fieldStore","subjectStore","allSchemas","Set","registerSchema","ctor","add","isSchema","has","getSchemaByName","name","undefined","register","queueName","entry","set","lookup","get","listAll","values","setSubject","subject","getSubject","getOrCreateFields","fields","getFields","defaultRegistry","inferFromReflectMetadata","target","key","R","Reflect","getMetadata","designType","inferFromConstructor","ctor","Number","Boolean","Date","Array","Object","defaultRegistry","isSchema","name","inferFromValue","value","proto","getPrototypeOf","SCHEMA_SYMBOL","Symbol","for","Schema","subject","target","_ctxOrUndefined","Object","defineProperty","SCHEMA_SYMBOL","value","enumerable","writable","R","Reflect","defineMetadata","defaultRegistry","registerSchema","getOrCreateFields","setSubject","Field","opts","targetOrValue","keyOrCtx","undefined","handleTC39Field","handleExperimentalField","propertyKey","ctor","name","String","fields","some","f","reflectedType","getMetadata","protoType","type","inferFromReflectMetadata","push","number","length","ctx","addInitializer","registerTC39Field","initialValue","inferFromValue","instance","getPrototypeOf"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocketmq/schema",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -8,6 +8,9 @@
8
8
  "types": "./dist/index.d.ts"
9
9
  }
10
10
  },
11
+ "dependencies": {
12
+ "reflect-metadata": "^0.2.2"
13
+ },
11
14
  "scripts": {
12
15
  "build": "tsup",
13
16
  "test": "vitest run"
@@ -7,9 +7,22 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
- import { Schema, Field } from './decorators.js';
10
+ import { Schema, Field, isSchemaClass } from './decorators.js';
11
11
  import { defaultRegistry } from './registry.js';
12
12
 
13
+ describe('isSchemaClass', () => {
14
+ it('detects valid and invalid schemas', () => {
15
+ @Schema()
16
+ class A {}
17
+ expect(isSchemaClass(A)).toBe(true);
18
+
19
+ class B {}
20
+ expect(isSchemaClass(B)).toBe(false);
21
+ expect(isSchemaClass(null)).toBe(false);
22
+ expect(isSchemaClass(123)).toBe(false);
23
+ });
24
+ });
25
+
13
26
  describe('@Schema() decorator', () => {
14
27
  it('registers field list for a decorated class', () => {
15
28
  @Schema()
package/src/decorators.ts CHANGED
@@ -1,35 +1,45 @@
1
1
  /**
2
- * TC39 stage 3 decorators for schema definition.
3
- *
4
- * Collects field names and types from decorated classes into the
5
- * default SchemaRegistry so the SDK can generate proto3 definitions
6
- * and validate payloads at runtime.
7
- *
8
- * Usage:
9
- * @Schema("notifications")
10
- * class Notification {
11
- * @Field() id!: string;
12
- * @Field({ type: "int64" }) timestamp!: number;
13
- * }
2
+ * Dual-mode decorators: works with BOTH experimental decorators
3
+ * (reflect-metadata) AND TC39 stage 3 decorators.
14
4
  */
15
5
 
16
- import type { ProtoType } from './metadata.js';
6
+ import { inferFromReflectMetadata, inferFromValue } from './inference.js';
7
+ import type { FieldOptions } from './metadata.js';
17
8
  import { defaultRegistry } from './registry.js';
9
+ import type { Constructor } from './types.js';
18
10
 
19
- /**
20
- * Marks a class as a schema definition.
21
- *
22
- * @param subject - Optional subject prefix for registry-based validation
23
- * (e.g. "notifications" "notifications-value").
24
- */
11
+ import 'reflect-metadata';
12
+
13
+ const SCHEMA_SYMBOL = Symbol.for('rocketmq:schema');
14
+
15
+ /** Checks if a constructor is decorated as a schema class. */
16
+ export function isSchemaClass(ctor: unknown): boolean {
17
+ if (typeof ctor !== 'function') return false;
18
+ const R = Reflect as unknown as {
19
+ hasOwnMetadata?: (k: unknown, t: unknown) => boolean;
20
+ };
21
+ return (
22
+ SCHEMA_SYMBOL in ctor ||
23
+ (R.hasOwnMetadata !== undefined && R.hasOwnMetadata(SCHEMA_SYMBOL, ctor))
24
+ );
25
+ }
26
+
27
+ /** Marks a class as a schema definition. */
25
28
  export function Schema(subject?: string) {
26
- return function <T extends new (...args: unknown[]) => object>(
27
- target: T,
28
- _ctx: ClassDecoratorContext,
29
- ): T {
30
- // Ensure the field list exists even if no @Field() was used
29
+ return function <T extends Constructor>(target: T, _ctxOrUndefined?: ClassDecoratorContext): T {
30
+ Object.defineProperty(target, SCHEMA_SYMBOL, {
31
+ value: true,
32
+ enumerable: false,
33
+ writable: false,
34
+ });
35
+ const R = Reflect as unknown as {
36
+ defineMetadata?: (k: unknown, v: unknown, t: unknown) => void;
37
+ };
38
+ if (R.defineMetadata) {
39
+ R.defineMetadata(SCHEMA_SYMBOL, true, target);
40
+ }
41
+ defaultRegistry.registerSchema(target);
31
42
  defaultRegistry.getOrCreateFields(target);
32
-
33
43
  if (subject) {
34
44
  defaultRegistry.setSubject(target, subject);
35
45
  }
@@ -37,33 +47,95 @@ export function Schema(subject?: string) {
37
47
  };
38
48
  }
39
49
 
40
- interface FieldOptions {
41
- /** Proto3 scalar type. Defaults to "string". */
42
- type?: ProtoType;
50
+ /** Marks a property as a schema field. */
51
+ export function Field(opts?: FieldOptions) {
52
+ return function (
53
+ targetOrValue: object | undefined,
54
+ keyOrCtx: string | symbol | ClassFieldDecoratorContext,
55
+ // WHY void: Node16 moduleResolution enforces TS1271 — decorator return must be void|any.
56
+ // The TC39 initializer return is consumed by the runtime, not the decorator call site.
57
+ ): void {
58
+ if (targetOrValue === undefined || typeof keyOrCtx === 'object') {
59
+ // WHY cast: TC39 path returns an initializer function at runtime,
60
+ // but the .d.ts must declare void to satisfy experimental decorator constraints.
61
+ return handleTC39Field(opts, keyOrCtx as ClassFieldDecoratorContext) as void;
62
+ }
63
+ handleExperimentalField(opts, targetOrValue, keyOrCtx as string | symbol);
64
+ };
43
65
  }
44
66
 
45
- /**
46
- * Marks a property as a schema field (TC39 stage 3 field decorator).
47
- *
48
- * Defers registration to `addInitializer` so the class constructor is
49
- * fully defined before we read `this.constructor`.
50
- */
51
- export function Field(opts?: FieldOptions) {
52
- return function (_value: undefined, ctx: ClassFieldDecoratorContext) {
53
- const name = String(ctx.name);
67
+ /** Experimental decorator path. */
68
+ function handleExperimentalField(
69
+ opts: FieldOptions | undefined,
70
+ target: object,
71
+ propertyKey: string | symbol,
72
+ ): void {
73
+ const ctor = target.constructor as Constructor;
74
+ const name = String(propertyKey);
75
+ const fields = defaultRegistry.getOrCreateFields(ctor);
54
76
 
55
- ctx.addInitializer(function (this: unknown) {
56
- const ctor = (this as Record<string, unknown>).constructor as Function;
57
- const fields = defaultRegistry.getOrCreateFields(ctor);
77
+ if (fields.some((f) => f.name === name)) return;
78
+
79
+ const R = Reflect as unknown as {
80
+ getMetadata?: (k: string, t: object, p: string | symbol) => Constructor | undefined;
81
+ };
82
+ const reflectedType = R.getMetadata
83
+ ? R.getMetadata('design:type', target, propertyKey)
84
+ : undefined;
85
+ const protoType = opts?.type ?? inferFromReflectMetadata(target, propertyKey);
86
+
87
+ fields.push({
88
+ name,
89
+ protoType,
90
+ number: fields.length + 1,
91
+ reflectedType,
92
+ ...opts,
93
+ });
94
+ }
58
95
 
59
- // Prevent duplicate registrations from multiple instantiations
60
- if (fields.some((f) => f.name === name)) return;
96
+ /* v8 ignore start */
97
+ /** TC39 stage 3 decorator path. */
98
+ function handleTC39Field(
99
+ opts: FieldOptions | undefined,
100
+ ctx: ClassFieldDecoratorContext,
101
+ ): ((this: unknown, initialValue: unknown) => unknown) | void {
102
+ const name = String(ctx.name);
61
103
 
62
- fields.push({
63
- name,
64
- protoType: opts?.type ?? 'string',
65
- number: fields.length + 1,
66
- });
104
+ if (opts?.type) {
105
+ ctx.addInitializer(function (this: unknown) {
106
+ registerTC39Field(this, name, opts.type!, opts);
67
107
  });
108
+ return;
109
+ }
110
+
111
+ return function (this: unknown, initialValue: unknown): unknown {
112
+ registerTC39Field(this, name, inferFromValue(initialValue), opts, initialValue);
113
+ return initialValue;
68
114
  };
69
115
  }
116
+
117
+ /** Internal TC39 helper to register field inside initializer. */
118
+ function registerTC39Field(
119
+ instance: unknown,
120
+ name: string,
121
+ protoType: string,
122
+ opts: FieldOptions | undefined,
123
+ initialValue?: unknown,
124
+ ): void {
125
+ const ctor = (instance as Record<string, unknown>).constructor as Constructor;
126
+ const fields = defaultRegistry.getOrCreateFields(ctor);
127
+ if (fields.some((f) => f.name === name)) return;
128
+
129
+ const reflectedType =
130
+ initialValue && typeof initialValue === 'object'
131
+ ? (Object.getPrototypeOf(initialValue).constructor as Constructor)
132
+ : undefined;
133
+
134
+ fields.push({
135
+ name,
136
+ protoType,
137
+ number: fields.length + 1,
138
+ reflectedType,
139
+ ...opts,
140
+ });
141
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export type { ProtoType, FieldMeta, SchemaEntry } from './metadata.js';
1
+ export type { ProtoType, FieldOptions, FieldMeta, SchemaEntry } from './metadata.js';
2
2
  export { SchemaRegistry, defaultRegistry } from './registry.js';
3
3
  export { Schema, Field } from './decorators.js';
4
+ export type { Constructor } from './types.js';
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { inferFromConstructor, inferFromValue, inferFromReflectMetadata } from './inference.js';
3
+ import { defaultRegistry } from './registry.js';
4
+
5
+ describe('inference', () => {
6
+ describe('inferFromConstructor', () => {
7
+ it('infers proto types from JS constructors', () => {
8
+ expect(inferFromConstructor(Number)).toBe('double');
9
+ expect(inferFromConstructor(Boolean)).toBe('bool');
10
+ expect(inferFromConstructor(Date)).toBe('google.protobuf.Timestamp');
11
+ expect(inferFromConstructor(Array)).toBe('bytes');
12
+ expect(inferFromConstructor(Object)).toBe('bytes');
13
+ expect(inferFromConstructor(String)).toBe('string');
14
+ });
15
+
16
+ it('infers from schema constructors', () => {
17
+ class NestedSchema {}
18
+ defaultRegistry.registerSchema(NestedSchema);
19
+ expect(inferFromConstructor(NestedSchema)).toBe('NestedSchema');
20
+ });
21
+ });
22
+
23
+ describe('inferFromValue', () => {
24
+ it('infers proto types from JS values', () => {
25
+ expect(inferFromValue(1)).toBe('double');
26
+ expect(inferFromValue(true)).toBe('bool');
27
+ expect(inferFromValue(new Date())).toBe('google.protobuf.Timestamp');
28
+ expect(inferFromValue([])).toBe('bytes');
29
+ expect(inferFromValue({})).toBe('bytes');
30
+ expect(inferFromValue('hello')).toBe('string');
31
+ expect(inferFromValue(undefined)).toBe('string');
32
+ });
33
+
34
+ it('infers from schema instances', () => {
35
+ class NestedSchema {}
36
+ defaultRegistry.registerSchema(NestedSchema);
37
+ expect(inferFromValue(new NestedSchema())).toBe('NestedSchema');
38
+ });
39
+ });
40
+
41
+ describe('inferFromReflectMetadata', () => {
42
+ it('falls back to string if design:type is missing', () => {
43
+ expect(inferFromReflectMetadata({}, 'key')).toBe('string');
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,45 @@
1
+ import { defaultRegistry } from './registry.js';
2
+ import type { Constructor } from './types.js';
3
+
4
+ /** Infers proto type from reflect metadata. */
5
+ export function inferFromReflectMetadata(target: object, key: string | symbol): string {
6
+ const R = Reflect as unknown as {
7
+ getMetadata?: (key: string, target: object, prop: string | symbol) => Constructor | undefined;
8
+ };
9
+ if (!R.getMetadata) return 'string';
10
+
11
+ const designType = R.getMetadata('design:type', target, key);
12
+ if (!designType) return 'string';
13
+
14
+ return inferFromConstructor(designType);
15
+ }
16
+
17
+ /** Infers type from primitive or schema constructors. */
18
+ export function inferFromConstructor(ctor: Constructor): string {
19
+ if (ctor === Number) return 'double';
20
+ if (ctor === Boolean) return 'bool';
21
+ if (ctor === Date) return 'google.protobuf.Timestamp';
22
+ if (ctor === Array || ctor === Object) return 'bytes';
23
+ if (defaultRegistry.isSchema(ctor)) return ctor.name;
24
+ return 'string';
25
+ }
26
+
27
+ /** Infers type from runtime value (TC39). */
28
+ export function inferFromValue(value: unknown): string {
29
+ if (value instanceof Date) return 'google.protobuf.Timestamp';
30
+ if (value && typeof value === 'object') {
31
+ const proto = Object.getPrototypeOf(value);
32
+ if (proto && proto.constructor && defaultRegistry.isSchema(proto.constructor)) {
33
+ return proto.constructor.name;
34
+ }
35
+ return 'bytes';
36
+ }
37
+ switch (typeof value) {
38
+ case 'number':
39
+ return 'double';
40
+ case 'boolean':
41
+ return 'bool';
42
+ default:
43
+ return 'string';
44
+ }
45
+ }
package/src/metadata.ts CHANGED
@@ -1,11 +1,10 @@
1
+ import type { Constructor } from './types.js';
2
+
1
3
  /**
2
4
  * Protobuf wire types supported by the schema decorator system.
3
5
  *
4
6
  * Maps 1:1 to proto3 scalar types so the SDK can generate
5
7
  * `.proto` definitions without requiring protobuf knowledge.
6
- *
7
- * Usage:
8
- * @Field({ type: "int32" }) qty!: number;
9
8
  */
10
9
  export type ProtoType =
11
10
  | 'string'
@@ -18,20 +17,34 @@ export type ProtoType =
18
17
  | 'bool'
19
18
  | 'bytes';
20
19
 
20
+ /** Additional configuration options passed to the @Field() decorator. */
21
+ export interface FieldOptions {
22
+ /** Explicit proto3 type override (scalar or custom message name). */
23
+ type?: string;
24
+ /** Emits 'repeated' prefix for arrays. */
25
+ repeated?: boolean;
26
+ /** Emits 'optional' prefix for optional presence. */
27
+ optional?: boolean;
28
+ /** Inline comment written above the field. */
29
+ comment?: string;
30
+ }
31
+
21
32
  /** Metadata captured by the @Field() decorator for a single class property. */
22
- export interface FieldMeta {
33
+ export interface FieldMeta extends FieldOptions {
23
34
  /** Property name on the decorated class. */
24
35
  name: string;
25
- /** Proto3 scalar type. Defaults to "string" when omitted. */
26
- protoType: ProtoType;
36
+ /** Proto3 type. Defaults to "string" when omitted. */
37
+ protoType: string;
27
38
  /** 1-based field number for proto ordering. */
28
39
  number: number;
40
+ /** Constructor captured by reflect-metadata (String, Number, Boolean, Date, etc.). */
41
+ reflectedType?: Constructor;
29
42
  }
30
43
 
31
44
  /** Collected metadata for a decorated schema class. */
32
45
  export interface SchemaEntry {
33
46
  /** Class constructor reference. */
34
- ctor: Function;
47
+ ctor: Constructor;
35
48
  /** Human-readable schema name (defaults to class name). */
36
49
  name: string;
37
50
  /** Optional subject prefix for registry-based validation. */
@@ -43,6 +43,22 @@ describe('SchemaRegistry', () => {
43
43
  });
44
44
  });
45
45
 
46
+ describe('schema constructors', () => {
47
+ it('tracks decorated schemas via isSchema', () => {
48
+ class A {}
49
+ registry.registerSchema(A);
50
+ expect(registry.isSchema(A)).toBe(true);
51
+ expect(registry.isSchema(class B {})).toBe(false);
52
+ });
53
+
54
+ it('looks up schema constructor by name', () => {
55
+ class FooSchema {}
56
+ registry.registerSchema(FooSchema);
57
+ expect(registry.getSchemaByName('FooSchema')).toBe(FooSchema);
58
+ expect(registry.getSchemaByName('BarSchema')).toBeUndefined();
59
+ });
60
+ });
61
+
46
62
  describe('listAll', () => {
47
63
  it('returns empty array when nothing registered', () => {
48
64
  expect(registry.listAll()).toEqual([]);
package/src/registry.ts CHANGED
@@ -3,24 +3,43 @@
3
3
  *
4
4
  * Replaces the module-level Map globals from the original schema.ts.
5
5
  * Injectable via constructor so tests can use isolated instances.
6
- *
7
- * Usage:
8
- * const registry = new SchemaRegistry();
9
- * registry.register("orders", entry);
10
- * const entry = registry.lookup("orders");
11
6
  */
12
7
 
13
8
  import type { FieldMeta, SchemaEntry } from './metadata.js';
9
+ import type { Constructor } from './types.js';
14
10
 
15
11
  export class SchemaRegistry {
16
12
  /** Queue name → schema entry. */
17
13
  private byQueue = new Map<string, SchemaEntry>();
18
14
 
19
15
  /** Class constructor → field metadata (populated by decorators). */
20
- private fieldStore = new Map<Function, FieldMeta[]>();
16
+ private fieldStore = new Map<Constructor, FieldMeta[]>();
21
17
 
22
18
  /** Class constructor → subject prefix (populated by @Schema("subject")). */
23
- private subjectStore = new Map<Function, string>();
19
+ private subjectStore = new Map<Constructor, string>();
20
+
21
+ /** Set of all decorated schema constructors. */
22
+ private allSchemas = new Set<Constructor>();
23
+
24
+ /** Tracks a constructor as a decorated schema. */
25
+ registerSchema(ctor: Constructor): void {
26
+ this.allSchemas.add(ctor);
27
+ }
28
+
29
+ /** Returns true if a constructor is a registered schema. */
30
+ isSchema(ctor: Constructor): boolean {
31
+ return this.allSchemas.has(ctor);
32
+ }
33
+
34
+ /** Looks up a schema constructor by its class name. */
35
+ getSchemaByName(name: string): Constructor | undefined {
36
+ for (const ctor of this.allSchemas) {
37
+ if (ctor.name === name) {
38
+ return ctor;
39
+ }
40
+ }
41
+ return undefined;
42
+ }
24
43
 
25
44
  /** Binds a queue name to a fully resolved schema entry. */
26
45
  register(queueName: string, entry: SchemaEntry): void {
@@ -38,17 +57,17 @@ export class SchemaRegistry {
38
57
  }
39
58
 
40
59
  /** Stores the subject prefix set by @Schema("subject"). */
41
- setSubject(ctor: Function, subject: string): void {
60
+ setSubject(ctor: Constructor, subject: string): void {
42
61
  this.subjectStore.set(ctor, subject);
43
62
  }
44
63
 
45
64
  /** Returns the subject prefix for a class, if any. */
46
- getSubject(ctor: Function): string | undefined {
65
+ getSubject(ctor: Constructor): string | undefined {
47
66
  return this.subjectStore.get(ctor);
48
67
  }
49
68
 
50
69
  /** Returns the mutable field list for a class, creating it if absent. */
51
- getOrCreateFields(ctor: Function): FieldMeta[] {
70
+ getOrCreateFields(ctor: Constructor): FieldMeta[] {
52
71
  let fields = this.fieldStore.get(ctor);
53
72
  if (!fields) {
54
73
  fields = [];
@@ -58,7 +77,7 @@ export class SchemaRegistry {
58
77
  }
59
78
 
60
79
  /** Returns the field metadata for a class (read-only snapshot). */
61
- getFields(ctor: Function): readonly FieldMeta[] {
80
+ getFields(ctor: Constructor): readonly FieldMeta[] {
62
81
  return this.fieldStore.get(ctor) ?? [];
63
82
  }
64
83
  }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Core type definitions for the schema package.
3
+ */
4
+
5
+ /**
6
+ * Represents a class constructor.
7
+ * Used to replace the generic 'Function' type for stricter type safety.
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export type Constructor<T = object> = new (...args: any[]) => T;
package/tsup.config.ts CHANGED
@@ -5,4 +5,5 @@ export default defineConfig({
5
5
  format: ["esm", "cjs"],
6
6
  dts: true,
7
7
  clean: true,
8
+ sourcemap: true,
8
9
  });