@alepha/protobuf 0.10.6 → 0.11.0

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.
package/dist/index.d.ts CHANGED
@@ -1,23 +1,26 @@
1
- import { Alepha, Static, TObject, TSchema } from "@alepha/core";
1
+ import * as _alepha_core0 from "@alepha/core";
2
+ import { Alepha, SchemaCodec, StaticDecode, TObject, TSchema } from "@alepha/core";
2
3
  import protobufjs, { Type } from "protobufjs";
4
+ import "@alepha/datetime";
3
5
 
4
6
  //#region src/providers/ProtobufProvider.d.ts
5
7
  declare class ProtobufProvider {
6
8
  protected readonly alepha: Alepha;
7
9
  protected readonly schemas: Map<string | TObject, Type>;
8
10
  protected readonly protobuf: typeof protobufjs;
11
+ protected readonly enumDefinitions: Map<string, string[]>;
9
12
  /**
10
13
  * Encode an object to a Uint8Array.
11
14
  */
12
- encode(schema: TObject, data: any): Uint8Array;
15
+ encode(schema: ProtobufSchema, message: any): Uint8Array;
13
16
  /**
14
17
  * Decode a Uint8Array to an object.
15
18
  */
16
- decode<T extends TObject>(schema: T, data: Uint8Array): Static<T>;
19
+ decode<T = any>(schema: ProtobufSchema, data: Uint8Array): T;
17
20
  /**
18
21
  * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.
19
22
  */
20
- parse(schema: ProtobufSchema | TObject, typeName?: string): Type;
23
+ parse(schema: ProtobufSchema, typeName?: string): Type;
21
24
  /**
22
25
  * Convert a TypeBox schema to a Protobuf schema as a string.
23
26
  */
@@ -33,6 +36,24 @@ declare class ProtobufProvider {
33
36
  * Convert a primitive TypeBox schema type to a Protobuf spec type.
34
37
  */
35
38
  protected convertType(schema: TSchema): string;
39
+ /**
40
+ * Check if a schema is an enum type.
41
+ * TypeBox enums have an "enum" property with an array of values.
42
+ */
43
+ protected isEnum(schema: TSchema): boolean;
44
+ /**
45
+ * Extract enum values from a TypeBox enum schema.
46
+ */
47
+ protected getEnumValues(schema: TSchema): string[];
48
+ /**
49
+ * Register an enum and return its type name.
50
+ * Generates a PascalCase name from the field name.
51
+ */
52
+ protected registerEnum(fieldName: string, values: string[]): string;
53
+ /**
54
+ * Generate a protobuf enum definition.
55
+ */
56
+ protected generateEnumDefinition(enumName: string, values: string[]): string;
36
57
  }
37
58
  type ProtobufSchema = string;
38
59
  interface CreateProtobufSchemaOptions {
@@ -40,5 +61,49 @@ interface CreateProtobufSchemaOptions {
40
61
  mainMessageName?: string;
41
62
  }
42
63
  //#endregion
43
- export { CreateProtobufSchemaOptions, ProtobufProvider, ProtobufSchema };
64
+ //#region src/providers/ProtobufSchemaCodec.d.ts
65
+ /**
66
+ * ProtobufSchemaCodec handles encoding/decoding for Protobuf format.
67
+ *
68
+ * Key differences from JSON codec:
69
+ * - BigInt values are kept as BigInt (not converted to string)
70
+ * - Date values are converted to ISO strings for protobuf compatibility
71
+ * - Binary data (Uint8Array) is kept as-is
72
+ * - Proto3 default values are applied when decoding (to handle omitted fields)
73
+ */
74
+ declare class ProtobufSchemaCodec extends SchemaCodec {
75
+ protected protobufProvider: ProtobufProvider;
76
+ protected decoder: TextDecoder;
77
+ encodeToString(schema: TSchema, value: any): string;
78
+ encodeToBinary(schema: TSchema, value: any): Uint8Array;
79
+ decode<T extends TSchema>(schema: T, value: any): StaticDecode<T>;
80
+ /**
81
+ * Apply proto3 default values for fields that were omitted during encoding.
82
+ * Proto3 omits fields with default values, so we need to restore them.
83
+ * Also converts enum integers back to their string values.
84
+ */
85
+ protected applyProto3Defaults(schema: TSchema, value: any): any;
86
+ /**
87
+ * Check if a schema is an enum type.
88
+ */
89
+ protected isEnum(schema: TSchema): boolean;
90
+ /**
91
+ * Convert an enum value from protobuf integer to TypeBox string.
92
+ */
93
+ protected convertEnumValue(schema: TSchema, value: any): any;
94
+ /**
95
+ * Get the proto3 default value for a schema type.
96
+ */
97
+ protected getProto3Default(schema: TSchema): any;
98
+ /**
99
+ * Transform types for Protobuf compatibility.
100
+ * This method is called for each type in the schema tree.
101
+ */
102
+ protected transformType(schema: TSchema): TSchema | undefined;
103
+ }
104
+ //#endregion
105
+ //#region src/index.d.ts
106
+ declare const AlephaProtobuf: _alepha_core0.Service<_alepha_core0.Module<{}>>;
107
+ //#endregion
108
+ export { AlephaProtobuf, CreateProtobufSchemaOptions, ProtobufProvider, ProtobufSchema, ProtobufSchemaCodec };
44
109
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/providers/ProtobufProvider.ts"],"sourcesContent":[],"mappings":";;;;cAWa,gBAAA;EAAA,mBAAgB,MAAA,EACH,MADG;EAAA,mBAAA,OAAA,EAEA,GAFA,CAAA,MAAA,GAEa,OAFb,EAEsB,IAFtB,CAAA;qBACH,QAAA,EAAA,OAEW,UAFX;;;;QAEW,CAAA,MAAA,EAKd,OALc,EAAA,IAAA,EAAA,GAAA,CAAA,EAKO,UALP;;;;QAYK,CAAA,UAAjB,OAAiB,CAAA,CAAA,MAAA,EAAA,CAAA,EAAA,IAAA,EAAS,UAAT,CAAA,EAAsB,MAAtB,CAA6B,CAA7B,CAAA;;;;OAQhC,CAAA,MAAA,EAAA,cAAA,GAAiB,OAAjB,EAAA,QAAA,CAAA,EAAA,MAAA,CAAA,EAEN,IAFM;;;;sBAmBC,CAAA,MAAA,EADD,OACC,EAAA,OAAA,CAAA,EAAA,2BAAA,CAAA,EAAA,MAAA;;;;EA0KC,UAAA,2BAAc,CAAA,GAAA,EAhJnB,OAgJmB,EAAA,UAAA,EAAA,MAAA,CAAA,EAAA;IAET,OAAA,EAAA,MAAA;;;;;;gCApCc;;KAkCnB,cAAA;UAEK,2BAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/providers/ProtobufProvider.ts","../src/providers/ProtobufSchemaCodec.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;cAIa,gBAAA;6BACc;8BACG,aAAa,SAAS;EAFvC,mBAAgB,QAAA,EAAA,OAGS,UAHT;EACF,mBAAA,eAAA,EAGW,GAHX,CAAA,MAAA,EAAA,MAAA,EAAA,CAAA;EACgB;;;EACL,MAAA,CAAA,MAAA,EAMd,cANc,EAAA,OAAA,EAAA,GAAA,CAAA,EAMiB,UANjB;EACA;;;EAYL,MAAA,CAAA,IAAA,GAAA,CAAA,CAAA,MAAA,EAAA,cAAA,EAAA,IAAA,EAAsB,UAAtB,CAAA,EAAmC,CAAnC;EAAsB;;;EAOW,KAAA,CAAA,MAAA,EAA3C,cAA2C,EAAA,QAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;EAgBtD;;;EA0KoB,oBAAA,CAAA,MAAA,EA1KpB,OA0KoB,EAAA,OAAA,CAAA,EAzKnB,2BAyKmB,CAAA,EAAA,MAAA;EAqCL;;;EAiDf,UAAA,2BAAc,CAAA,GAAA,EA5NjB,OA4NiB,EAAA,UAAA,EAAA,MAAA,CAAA,EAAA;IAET,OAAA,EAAA,MAAA;;;;AC1RjB;;EAEmB,UAAA,WAAA,CAAA,MAAA,EDgMa,OChMb,CAAA,EAAA,MAAA;EAEa;;;;EAUW,UAAA,MAAA,CAAA,MAAA,EDyNhB,OCzNgB,CAAA,EAAA,OAAA;EAA6B;;;EAqD7C,UAAA,aAAA,CAAA,MAAA,ED2KO,OC3KP,CAAA,EAAA,MAAA,EAAA;EAOU;;;;EA1EI,UAAA,YAAA,CAAA,SAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,CAAA,EAAA,MAAA;EAAW;;;;ACZpD;KFoSY,cAAA;UAEK,2BAAA;;;;;;;;;AAzSjB;;;;;;AAIsC,cCWzB,mBAAA,SAA4B,WAAA,CDXH;EAKd,UAAA,gBAAA,ECOI,gBDPJ;EAA+B,UAAA,OAAA,ECQpC,WDRoC;EAOtB,cAAA,CAAA,MAAA,ECGD,ODHC,EAAA,KAAA,EAAA,GAAA,CAAA,EAAA,MAAA;EAAsB,cAAA,CAAA,MAAA,ECQvB,ODRuB,EAAA,KAAA,EAAA,GAAA,CAAA,ECQD,UDRC;EAAa,MAAA,CAAA,UCa1C,ODb0C,CAAA,CAAA,MAAA,ECazB,CDbyB,EAAA,KAAA,EAAA,GAAA,CAAA,ECaT,YDbS,CCaI,CDbJ,CAAA;EAO7C;;;;;EA0LS,UAAA,mBAAA,CAAA,MAAA,ECnKQ,ODmKR,EAAA,KAAA,EAAA,GAAA,CAAA,EAAA,GAAA;EAqCL;;;EAiDf,UAAA,MAAA,CAAc,MAAA,ECrNC,ODqND,CAAA,EAAA,OAAA;EAET;;;qCChNoB;EA1ExB;;;EAImB,UAAA,gBAAA,CAAA,MAAA,EAqFK,OArFL,CAAA,EAAA,GAAA;EAKA;;;;EAKwC,UAAA,aAAA,CAAA,MAAA,EA8GtC,OA9GsC,CAAA,EA8G5B,OA9G4B,GAAA,SAAA;;;;cC1B3D,gBAAc,aAAA,CAAA,QAOzB,aAAA,CAPyB"}
package/dist/index.js CHANGED
@@ -1,22 +1,24 @@
1
- import { $inject, Alepha, t } from "@alepha/core";
1
+ import { $inject, $module, Alepha, SchemaCodec, t } from "@alepha/core";
2
2
  import protobufjs from "protobufjs";
3
+ import "@alepha/datetime";
3
4
 
4
5
  //#region src/providers/ProtobufProvider.ts
5
6
  var ProtobufProvider = class {
6
7
  alepha = $inject(Alepha);
7
8
  schemas = /* @__PURE__ */ new Map();
8
9
  protobuf = protobufjs;
10
+ enumDefinitions = /* @__PURE__ */ new Map();
9
11
  /**
10
12
  * Encode an object to a Uint8Array.
11
13
  */
12
- encode(schema, data) {
13
- return this.parse(schema).encode(this.alepha.parse(schema, data)).finish();
14
+ encode(schema, message) {
15
+ return this.parse(schema).encode(message).finish();
14
16
  }
15
17
  /**
16
18
  * Decode a Uint8Array to an object.
17
19
  */
18
20
  decode(schema, data) {
19
- return this.alepha.parse(schema, this.parse(schema).decode(data));
21
+ return this.parse(schema).decode(data);
20
22
  }
21
23
  /**
22
24
  * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.
@@ -24,9 +26,7 @@ var ProtobufProvider = class {
24
26
  parse(schema, typeName = "root.Target") {
25
27
  const exists = this.schemas.get(schema);
26
28
  if (exists) return exists;
27
- const pbSchema = typeof schema === "string" ? schema : this.createProtobufSchema(schema);
28
- const result = this.protobuf.parse(pbSchema);
29
- const type = result.root.lookupType(typeName);
29
+ const type = this.protobuf.parse(schema).root.lookupType(typeName);
30
30
  this.schemas.set(schema, type);
31
31
  return type;
32
32
  }
@@ -35,12 +35,14 @@ var ProtobufProvider = class {
35
35
  */
36
36
  createProtobufSchema(schema, options = {}) {
37
37
  const { rootName = "root", mainMessageName = "Target" } = options;
38
+ this.enumDefinitions.clear();
38
39
  const context = {
39
40
  proto: `package ${rootName};\nsyntax = "proto3";\n\n`,
40
41
  fieldIndex: 1
41
42
  };
42
43
  if (t.schema.isObject(schema)) {
43
44
  const { message, subMessages } = this.parseObjectWithDependencies(schema, mainMessageName);
45
+ for (const [enumName, values] of this.enumDefinitions) context.proto += this.generateEnumDefinition(enumName, values);
44
46
  context.proto += subMessages.join("");
45
47
  context.proto += message;
46
48
  }
@@ -59,6 +61,12 @@ var ProtobufProvider = class {
59
61
  let fieldIndex = 1;
60
62
  for (const [key, value] of Object.entries(obj.properties)) {
61
63
  if (t.schema.isArray(value)) {
64
+ if (this.isEnum(value.items)) {
65
+ const enumValues = this.getEnumValues(value.items);
66
+ const enumName = this.registerEnum(key, enumValues);
67
+ fields.push(` repeated ${enumName} ${key} = ${fieldIndex++};`);
68
+ continue;
69
+ }
62
70
  if (t.schema.isObject(value.items)) {
63
71
  const subMessageName = "title" in value.items && typeof value.items.title === "string" ? value.items.title : `${parentName}_${key}`;
64
72
  const { message: subMessage, subMessages: nestedSubMessages } = this.parseObjectWithDependencies(value.items, subMessageName);
@@ -82,6 +90,12 @@ var ProtobufProvider = class {
82
90
  if (t.schema.isUnion(value)) {
83
91
  const nonNullType = value.anyOf.find((type) => !t.schema.isNull(type));
84
92
  if (nonNullType) {
93
+ if (this.isEnum(nonNullType)) {
94
+ const enumValues = this.getEnumValues(nonNullType);
95
+ const enumName = this.registerEnum(key, enumValues);
96
+ fields.push(` ${enumName} ${key} = ${fieldIndex++};`);
97
+ continue;
98
+ }
85
99
  if (t.schema.isObject(nonNullType)) {
86
100
  const subMessageName = "title" in nonNullType && typeof nonNullType.title === "string" ? nonNullType.title : `${parentName}_${key}`;
87
101
  const { message: subMessage, subMessages: nestedSubMessages } = this.parseObjectWithDependencies(nonNullType, subMessageName);
@@ -108,12 +122,17 @@ var ProtobufProvider = class {
108
122
  continue;
109
123
  }
110
124
  }
125
+ if (this.isEnum(value)) {
126
+ const enumValues = this.getEnumValues(value);
127
+ const enumName = this.registerEnum(key, enumValues);
128
+ fields.push(` ${enumName} ${key} = ${fieldIndex++};`);
129
+ continue;
130
+ }
111
131
  const fieldType = this.convertType(value);
112
132
  fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);
113
133
  }
114
- const message = `message ${parentName} {\n${fields.join("\n")}\n}\n`;
115
134
  return {
116
- message,
135
+ message: `message ${parentName} {\n${fields.join("\n")}\n}\n`,
117
136
  subMessages
118
137
  };
119
138
  }
@@ -135,8 +154,132 @@ var ProtobufProvider = class {
135
154
  if (t.schema.isUnsafe(schema)) return "string";
136
155
  throw new Error(`Unsupported type: ${JSON.stringify(schema)}`);
137
156
  }
157
+ /**
158
+ * Check if a schema is an enum type.
159
+ * TypeBox enums have an "enum" property with an array of values.
160
+ */
161
+ isEnum(schema) {
162
+ return "enum" in schema && Array.isArray(schema.enum);
163
+ }
164
+ /**
165
+ * Extract enum values from a TypeBox enum schema.
166
+ */
167
+ getEnumValues(schema) {
168
+ if ("enum" in schema && Array.isArray(schema.enum)) return schema.enum.map(String);
169
+ return [];
170
+ }
171
+ /**
172
+ * Register an enum and return its type name.
173
+ * Generates a PascalCase name from the field name.
174
+ */
175
+ registerEnum(fieldName, values) {
176
+ const enumName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
177
+ const valueKey = values.join(",");
178
+ const existingEnum = Array.from(this.enumDefinitions.entries()).find(([_, enumValues]) => enumValues.join(",") === valueKey);
179
+ if (existingEnum) return existingEnum[0];
180
+ this.enumDefinitions.set(enumName, values);
181
+ return enumName;
182
+ }
183
+ /**
184
+ * Generate a protobuf enum definition.
185
+ */
186
+ generateEnumDefinition(enumName, values) {
187
+ return `enum ${enumName} {\n${values.map((value, index) => ` ${value} = ${index};`).join("\n")}\n}\n`;
188
+ }
189
+ };
190
+
191
+ //#endregion
192
+ //#region src/providers/ProtobufSchemaCodec.ts
193
+ /**
194
+ * ProtobufSchemaCodec handles encoding/decoding for Protobuf format.
195
+ *
196
+ * Key differences from JSON codec:
197
+ * - BigInt values are kept as BigInt (not converted to string)
198
+ * - Date values are converted to ISO strings for protobuf compatibility
199
+ * - Binary data (Uint8Array) is kept as-is
200
+ * - Proto3 default values are applied when decoding (to handle omitted fields)
201
+ */
202
+ var ProtobufSchemaCodec = class extends SchemaCodec {
203
+ protobufProvider = $inject(ProtobufProvider);
204
+ decoder = new TextDecoder();
205
+ encodeToString(schema, value) {
206
+ const binary = this.encodeToBinary(schema, value);
207
+ return this.decoder.decode(binary);
208
+ }
209
+ encodeToBinary(schema, value) {
210
+ const proto = this.protobufProvider.createProtobufSchema(schema);
211
+ return this.protobufProvider.encode(proto, this.encode(schema, value));
212
+ }
213
+ decode(schema, value) {
214
+ const proto = this.protobufProvider.createProtobufSchema(schema);
215
+ const decoded = this.protobufProvider.decode(proto, value);
216
+ const withDefaults = this.applyProto3Defaults(schema, decoded);
217
+ return super.decode(schema, withDefaults);
218
+ }
219
+ /**
220
+ * Apply proto3 default values for fields that were omitted during encoding.
221
+ * Proto3 omits fields with default values, so we need to restore them.
222
+ * Also converts enum integers back to their string values.
223
+ */
224
+ applyProto3Defaults(schema, value) {
225
+ if (!value || typeof value !== "object") return value;
226
+ if (t.schema.isObject(schema)) {
227
+ const result = { ...value };
228
+ for (const [key, propSchema] of Object.entries(schema.properties)) if (!(key in result) || result[key] === void 0) result[key] = this.getProto3Default(propSchema);
229
+ else if (this.isEnum(propSchema)) result[key] = this.convertEnumValue(propSchema, result[key]);
230
+ else if (typeof result[key] === "object" && result[key] !== null) result[key] = this.applyProto3Defaults(propSchema, result[key]);
231
+ return result;
232
+ }
233
+ if (t.schema.isArray(schema) && Array.isArray(value)) return value.map((item) => this.applyProto3Defaults(schema.items, item));
234
+ return value;
235
+ }
236
+ /**
237
+ * Check if a schema is an enum type.
238
+ */
239
+ isEnum(schema) {
240
+ return "enum" in schema && Array.isArray(schema.enum);
241
+ }
242
+ /**
243
+ * Convert an enum value from protobuf integer to TypeBox string.
244
+ */
245
+ convertEnumValue(schema, value) {
246
+ if (typeof value === "number" && "enum" in schema && Array.isArray(schema.enum)) return schema.enum[value];
247
+ return value;
248
+ }
249
+ /**
250
+ * Get the proto3 default value for a schema type.
251
+ */
252
+ getProto3Default(schema) {
253
+ if (t.schema.isOptional(schema) || t.schema.isUnion(schema)) return;
254
+ if (t.schema.isArray(schema)) return [];
255
+ if (t.schema.isRecord(schema)) return {};
256
+ if (t.schema.isString(schema)) return "";
257
+ if (t.schema.isNumber(schema)) return 0;
258
+ if (t.schema.isInteger(schema)) return 0;
259
+ if (t.schema.isBigInt(schema)) return BigInt(0);
260
+ if (t.schema.isBoolean(schema)) return false;
261
+ if (t.schema.isObject(schema)) return {};
262
+ }
263
+ /**
264
+ * Transform types for Protobuf compatibility.
265
+ * This method is called for each type in the schema tree.
266
+ */
267
+ transformType(schema) {
268
+ if (t.schema.isBigInt(schema)) return t.bigint();
269
+ }
138
270
  };
139
271
 
140
272
  //#endregion
141
- export { ProtobufProvider };
273
+ //#region src/index.ts
274
+ const AlephaProtobuf = $module({
275
+ name: "alepha.protobuf",
276
+ services: [ProtobufProvider, ProtobufSchemaCodec],
277
+ register: (alepha) => {
278
+ alepha.with(ProtobufProvider);
279
+ alepha.codec.register("protobuf", alepha.inject(ProtobufSchemaCodec));
280
+ }
281
+ });
282
+
283
+ //#endregion
284
+ export { AlephaProtobuf, ProtobufProvider, ProtobufSchemaCodec };
142
285
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["fields: string[]","subMessages: string[]","fieldType","valueSchema: TSchema | undefined"],"sources":["../src/providers/ProtobufProvider.ts"],"sourcesContent":["import {\n\t$inject,\n\tAlepha,\n\ttype Static,\n\ttype TObject,\n\ttype TSchema,\n\tt,\n} from \"@alepha/core\";\nimport type { Type } from \"protobufjs\";\nimport protobufjs from \"protobufjs\";\n\nexport class ProtobufProvider {\n\tprotected readonly alepha = $inject(Alepha);\n\tprotected readonly schemas: Map<string | TObject, Type> = new Map();\n\tprotected readonly protobuf: typeof protobufjs = protobufjs;\n\n\t/**\n\t * Encode an object to a Uint8Array.\n\t */\n\tpublic encode(schema: TObject, data: any): Uint8Array {\n\t\treturn this.parse(schema).encode(this.alepha.parse(schema, data)).finish();\n\t}\n\n\t/**\n\t * Decode a Uint8Array to an object.\n\t */\n\tpublic decode<T extends TObject>(schema: T, data: Uint8Array): Static<T> {\n\t\treturn this.alepha.parse(schema, this.parse(schema).decode(data));\n\t}\n\n\t/**\n\t * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.\n\t */\n\tpublic parse(\n\t\tschema: ProtobufSchema | TObject,\n\t\ttypeName = \"root.Target\",\n\t): Type {\n\t\tconst exists = this.schemas.get(schema);\n\t\tif (exists) return exists;\n\n\t\tconst pbSchema =\n\t\t\ttypeof schema === \"string\" ? schema : this.createProtobufSchema(schema);\n\t\tconst result = this.protobuf.parse(pbSchema);\n\t\tconst type = result.root.lookupType(typeName);\n\t\tthis.schemas.set(schema, type);\n\t\treturn type;\n\t}\n\n\t/**\n\t * Convert a TypeBox schema to a Protobuf schema as a string.\n\t */\n\tpublic createProtobufSchema(\n\t\tschema: TSchema,\n\t\toptions: CreateProtobufSchemaOptions = {},\n\t): string {\n\t\tconst { rootName = \"root\", mainMessageName = \"Target\" } = options;\n\t\tconst context = {\n\t\t\tproto: `package ${rootName};\\nsyntax = \"proto3\";\\n\\n`,\n\t\t\tfieldIndex: 1,\n\t\t};\n\n\t\tif (t.schema.isObject(schema)) {\n\t\t\tconst { message, subMessages } = this.parseObjectWithDependencies(\n\t\t\t\tschema,\n\t\t\t\tmainMessageName,\n\t\t\t);\n\t\t\t// Add all sub-messages first\n\t\t\tcontext.proto += subMessages.join(\"\");\n\t\t\t// Then add the main message\n\t\t\tcontext.proto += message;\n\t\t}\n\n\t\treturn context.proto;\n\t}\n\n\t/**\n\t * Parse an object schema with dependencies (sub-messages).\n\t */\n\tprotected parseObjectWithDependencies(\n\t\tobj: TSchema,\n\t\tparentName: string,\n\t): { message: string; subMessages: string[] } {\n\t\tif (!t.schema.isObject(obj)) {\n\t\t\treturn { message: \"\", subMessages: [] };\n\t\t}\n\n\t\tconst fields: string[] = [];\n\t\tconst subMessages: string[] = [];\n\t\tlet fieldIndex = 1;\n\n\t\tfor (const [key, value] of Object.entries(obj.properties)) {\n\t\t\t// Handle arrays\n\t\t\tif (t.schema.isArray(value)) {\n\t\t\t\tif (t.schema.isObject(value.items)) {\n\t\t\t\t\tconst subMessageName =\n\t\t\t\t\t\t\"title\" in value.items && typeof value.items.title === \"string\"\n\t\t\t\t\t\t\t? value.items.title\n\t\t\t\t\t\t\t: `${parentName}_${key}`;\n\t\t\t\t\tconst { message: subMessage, subMessages: nestedSubMessages } =\n\t\t\t\t\t\tthis.parseObjectWithDependencies(value.items, subMessageName);\n\t\t\t\t\tsubMessages.push(...nestedSubMessages);\n\t\t\t\t\tsubMessages.push(subMessage);\n\t\t\t\t\tfields.push(` repeated ${subMessageName} ${key} = ${fieldIndex++};`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst itemType = this.convertType(value.items);\n\t\t\t\tfields.push(` repeated ${itemType} ${key} = ${fieldIndex++};`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Handle nested objects\n\t\t\tif (t.schema.isObject(value)) {\n\t\t\t\tconst subMessageName =\n\t\t\t\t\t\"title\" in value && typeof value.title === \"string\"\n\t\t\t\t\t\t? value.title\n\t\t\t\t\t\t: `${parentName}_${key}`;\n\t\t\t\tconst { message: subMessage, subMessages: nestedSubMessages } =\n\t\t\t\t\tthis.parseObjectWithDependencies(value, subMessageName);\n\t\t\t\tsubMessages.push(...nestedSubMessages);\n\t\t\t\tsubMessages.push(subMessage);\n\t\t\t\tfields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Handle union types (nullable fields)\n\t\t\tif (t.schema.isUnion(value)) {\n\t\t\t\tconst nonNullType = value.anyOf.find(\n\t\t\t\t\t(type: TSchema) => !t.schema.isNull(type),\n\t\t\t\t);\n\t\t\t\tif (nonNullType) {\n\t\t\t\t\tif (t.schema.isObject(nonNullType)) {\n\t\t\t\t\t\tconst subMessageName =\n\t\t\t\t\t\t\t\"title\" in nonNullType && typeof nonNullType.title === \"string\"\n\t\t\t\t\t\t\t\t? nonNullType.title\n\t\t\t\t\t\t\t\t: `${parentName}_${key}`;\n\t\t\t\t\t\tconst { message: subMessage, subMessages: nestedSubMessages } =\n\t\t\t\t\t\t\tthis.parseObjectWithDependencies(nonNullType, subMessageName);\n\t\t\t\t\t\tsubMessages.push(...nestedSubMessages);\n\t\t\t\t\t\tsubMessages.push(subMessage);\n\t\t\t\t\t\tfields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst fieldType = this.convertType(nonNullType);\n\t\t\t\t\tfields.push(` ${fieldType} ${key} = ${fieldIndex++};`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle records (maps)\n\t\t\tif (t.schema.isRecord(value)) {\n\t\t\t\t// TypeBox records use additionalProperties or patternProperties for the value type\n\t\t\t\tlet valueSchema: TSchema | undefined;\n\t\t\t\tif (\n\t\t\t\t\t\"additionalProperties\" in value &&\n\t\t\t\t\tvalue.additionalProperties &&\n\t\t\t\t\ttypeof value.additionalProperties === \"object\"\n\t\t\t\t) {\n\t\t\t\t\tvalueSchema = value.additionalProperties;\n\t\t\t\t} else if (\n\t\t\t\t\tvalue.patternProperties &&\n\t\t\t\t\ttypeof value.patternProperties === \"object\"\n\t\t\t\t) {\n\t\t\t\t\t// Get the first pattern property (usually \"^(.*)$\" or similar)\n\t\t\t\t\tconst patterns = Object.values(value.patternProperties);\n\t\t\t\t\tif (patterns.length > 0 && typeof patterns[0] === \"object\") {\n\t\t\t\t\t\tvalueSchema = patterns[0] as TSchema;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (valueSchema) {\n\t\t\t\t\tconst valueType = this.convertType(valueSchema);\n\t\t\t\t\tfields.push(` map<string, ${valueType}> ${key} = ${fieldIndex++};`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle regular fields\n\t\t\tconst fieldType = this.convertType(value);\n\t\t\tfields.push(` ${fieldType} ${key} = ${fieldIndex++};`);\n\t\t}\n\n\t\tconst message = `message ${parentName} {\\n${fields.join(\"\\n\")}\\n}\\n`;\n\t\treturn { message, subMessages };\n\t}\n\n\t/**\n\t * Convert a primitive TypeBox schema type to a Protobuf spec type.\n\t */\n\tprotected convertType(schema: TSchema): string {\n\t\tif (t.schema.isBoolean(schema)) return \"bool\";\n\t\tif (t.schema.isNumber(schema) && schema.format === \"int64\") return \"int64\";\n\t\tif (t.schema.isNumber(schema)) return \"double\";\n\t\tif (t.schema.isInteger(schema)) return \"int32\";\n\t\tif (t.schema.isBigInt(schema)) return \"int64\";\n\t\tif (t.schema.isString(schema)) return \"string\";\n\n\t\t// Handle union types (nullable)\n\t\tif (t.schema.isUnion(schema)) {\n\t\t\t// Find the non-null type in the union\n\t\t\tconst nonNullType = schema.anyOf.find(\n\t\t\t\t(type: TSchema) => !t.schema.isNull(type),\n\t\t\t);\n\t\t\tif (nonNullType) {\n\t\t\t\treturn this.convertType(nonNullType);\n\t\t\t}\n\t\t}\n\n\t\t// Handle optional types\n\t\tif (t.schema.isOptional(schema)) {\n\t\t\treturn this.convertType(schema);\n\t\t}\n\n\t\t// Handle unsafe types (like enums)\n\t\tif (t.schema.isUnsafe(schema)) {\n\t\t\t// if it's an enum or other unsafe types, default to string\n\t\t\treturn \"string\";\n\t\t}\n\n\t\tthrow new Error(`Unsupported type: ${JSON.stringify(schema)}`);\n\t}\n}\n\nexport type ProtobufSchema = string;\n\nexport interface CreateProtobufSchemaOptions {\n\trootName?: string;\n\tmainMessageName?: string;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["fields: string[]","subMessages: string[]","fieldType","valueSchema: TSchema | undefined","result: any"],"sources":["../src/providers/ProtobufProvider.ts","../src/providers/ProtobufSchemaCodec.ts","../src/index.ts"],"sourcesContent":["import { $inject, Alepha, type TObject, type TSchema, t } from \"@alepha/core\";\nimport type { Type } from \"protobufjs\";\nimport protobufjs from \"protobufjs\";\n\nexport class ProtobufProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly schemas: Map<string | TObject, Type> = new Map();\n protected readonly protobuf: typeof protobufjs = protobufjs;\n protected readonly enumDefinitions: Map<string, string[]> = new Map();\n\n /**\n * Encode an object to a Uint8Array.\n */\n public encode(schema: ProtobufSchema, message: any): Uint8Array {\n return this.parse(schema).encode(message).finish();\n }\n\n /**\n * Decode a Uint8Array to an object.\n */\n public decode<T = any>(schema: ProtobufSchema, data: Uint8Array): T {\n return this.parse(schema).decode(data) as T;\n }\n\n /**\n * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.\n */\n public parse(schema: ProtobufSchema, typeName = \"root.Target\"): Type {\n const exists = this.schemas.get(schema);\n if (exists) {\n return exists;\n }\n\n const result = this.protobuf.parse(schema);\n const type = result.root.lookupType(typeName);\n this.schemas.set(schema, type);\n return type;\n }\n\n /**\n * Convert a TypeBox schema to a Protobuf schema as a string.\n */\n public createProtobufSchema(\n schema: TSchema,\n options: CreateProtobufSchemaOptions = {},\n ): string {\n const { rootName = \"root\", mainMessageName = \"Target\" } = options;\n // Clear enum definitions for this schema generation\n this.enumDefinitions.clear();\n\n const context = {\n proto: `package ${rootName};\\nsyntax = \"proto3\";\\n\\n`,\n fieldIndex: 1,\n };\n\n if (t.schema.isObject(schema)) {\n const { message, subMessages } = this.parseObjectWithDependencies(\n schema,\n mainMessageName,\n );\n\n // Add all enum definitions first\n for (const [enumName, values] of this.enumDefinitions) {\n context.proto += this.generateEnumDefinition(enumName, values);\n }\n\n // Add all sub-messages\n context.proto += subMessages.join(\"\");\n // Then add the main message\n context.proto += message;\n }\n\n return context.proto;\n }\n\n /**\n * Parse an object schema with dependencies (sub-messages).\n */\n protected parseObjectWithDependencies(\n obj: TSchema,\n parentName: string,\n ): { message: string; subMessages: string[] } {\n if (!t.schema.isObject(obj)) {\n return { message: \"\", subMessages: [] };\n }\n\n const fields: string[] = [];\n const subMessages: string[] = [];\n let fieldIndex = 1;\n\n for (const [key, value] of Object.entries(obj.properties)) {\n // Handle arrays\n if (t.schema.isArray(value)) {\n // Check if array items are enums\n if (this.isEnum(value.items)) {\n const enumValues = this.getEnumValues(value.items);\n const enumName = this.registerEnum(key, enumValues);\n fields.push(` repeated ${enumName} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n if (t.schema.isObject(value.items)) {\n const subMessageName =\n \"title\" in value.items && typeof value.items.title === \"string\"\n ? value.items.title\n : `${parentName}_${key}`;\n const { message: subMessage, subMessages: nestedSubMessages } =\n this.parseObjectWithDependencies(value.items, subMessageName);\n subMessages.push(...nestedSubMessages);\n subMessages.push(subMessage);\n fields.push(` repeated ${subMessageName} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n const itemType = this.convertType(value.items);\n fields.push(` repeated ${itemType} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n // Handle nested objects\n if (t.schema.isObject(value)) {\n const subMessageName =\n \"title\" in value && typeof value.title === \"string\"\n ? value.title\n : `${parentName}_${key}`;\n const { message: subMessage, subMessages: nestedSubMessages } =\n this.parseObjectWithDependencies(value, subMessageName);\n subMessages.push(...nestedSubMessages);\n subMessages.push(subMessage);\n fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n // Handle union types (nullable fields)\n if (t.schema.isUnion(value)) {\n const nonNullType = value.anyOf.find(\n (type: TSchema) => !t.schema.isNull(type),\n );\n if (nonNullType) {\n // Check if it's an enum\n if (this.isEnum(nonNullType)) {\n const enumValues = this.getEnumValues(nonNullType);\n const enumName = this.registerEnum(key, enumValues);\n fields.push(` ${enumName} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n if (t.schema.isObject(nonNullType)) {\n const subMessageName =\n \"title\" in nonNullType && typeof nonNullType.title === \"string\"\n ? nonNullType.title\n : `${parentName}_${key}`;\n const { message: subMessage, subMessages: nestedSubMessages } =\n this.parseObjectWithDependencies(nonNullType, subMessageName);\n subMessages.push(...nestedSubMessages);\n subMessages.push(subMessage);\n fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);\n continue;\n }\n const fieldType = this.convertType(nonNullType);\n fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);\n continue;\n }\n }\n\n // Handle records (maps)\n if (t.schema.isRecord(value)) {\n // TypeBox records use additionalProperties or patternProperties for the value type\n let valueSchema: TSchema | undefined;\n if (\n \"additionalProperties\" in value &&\n value.additionalProperties &&\n typeof value.additionalProperties === \"object\"\n ) {\n valueSchema = value.additionalProperties;\n } else if (\n value.patternProperties &&\n typeof value.patternProperties === \"object\"\n ) {\n // Get the first pattern property (usually \"^(.*)$\" or similar)\n const patterns = Object.values(value.patternProperties);\n if (patterns.length > 0 && typeof patterns[0] === \"object\") {\n valueSchema = patterns[0] as TSchema;\n }\n }\n\n if (valueSchema) {\n const valueType = this.convertType(valueSchema);\n fields.push(` map<string, ${valueType}> ${key} = ${fieldIndex++};`);\n continue;\n }\n }\n\n // Handle enum fields\n if (this.isEnum(value)) {\n const enumValues = this.getEnumValues(value);\n const enumName = this.registerEnum(key, enumValues);\n fields.push(` ${enumName} ${key} = ${fieldIndex++};`);\n continue;\n }\n\n // Handle regular fields\n const fieldType = this.convertType(value);\n fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);\n }\n\n const message = `message ${parentName} {\\n${fields.join(\"\\n\")}\\n}\\n`;\n return { message, subMessages };\n }\n\n /**\n * Convert a primitive TypeBox schema type to a Protobuf spec type.\n */\n protected convertType(schema: TSchema): string {\n if (t.schema.isBoolean(schema)) return \"bool\";\n if (t.schema.isNumber(schema) && schema.format === \"int64\") return \"int64\";\n if (t.schema.isNumber(schema)) return \"double\";\n if (t.schema.isInteger(schema)) return \"int32\";\n if (t.schema.isBigInt(schema)) return \"int64\";\n if (t.schema.isString(schema)) return \"string\";\n\n // Handle union types (nullable)\n if (t.schema.isUnion(schema)) {\n // Find the non-null type in the union\n const nonNullType = schema.anyOf.find(\n (type: TSchema) => !t.schema.isNull(type),\n );\n if (nonNullType) {\n return this.convertType(nonNullType);\n }\n }\n\n // Handle optional types\n if (t.schema.isOptional(schema)) {\n return this.convertType(schema);\n }\n\n // Handle unsafe types (like enums)\n if (t.schema.isUnsafe(schema)) {\n // if it's an enum or other unsafe types, default to string\n return \"string\";\n }\n\n throw new Error(`Unsupported type: ${JSON.stringify(schema)}`);\n }\n\n /**\n * Check if a schema is an enum type.\n * TypeBox enums have an \"enum\" property with an array of values.\n */\n protected isEnum(schema: TSchema): boolean {\n return \"enum\" in schema && Array.isArray(schema.enum);\n }\n\n /**\n * Extract enum values from a TypeBox enum schema.\n */\n protected getEnumValues(schema: TSchema): string[] {\n if (\"enum\" in schema && Array.isArray(schema.enum)) {\n return schema.enum.map(String);\n }\n return [];\n }\n\n /**\n * Register an enum and return its type name.\n * Generates a PascalCase name from the field name.\n */\n protected registerEnum(fieldName: string, values: string[]): string {\n // Capitalize first letter of field name for enum type name\n const enumName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);\n\n // Check if we already have this exact enum registered\n const valueKey = values.join(\",\");\n const existingEnum = Array.from(this.enumDefinitions.entries()).find(\n ([_, enumValues]) => enumValues.join(\",\") === valueKey,\n );\n\n if (existingEnum) {\n // Reuse existing enum with same values\n return existingEnum[0];\n }\n\n // Register new enum\n this.enumDefinitions.set(enumName, values);\n return enumName;\n }\n\n /**\n * Generate a protobuf enum definition.\n */\n protected generateEnumDefinition(enumName: string, values: string[]): string {\n const enumValues = values\n .map((value, index) => ` ${value} = ${index};`)\n .join(\"\\n\");\n return `enum ${enumName} {\\n${enumValues}\\n}\\n`;\n }\n}\n\nexport type ProtobufSchema = string;\n\nexport interface CreateProtobufSchemaOptions {\n rootName?: string;\n mainMessageName?: string;\n}\n","import {\n $inject,\n SchemaCodec,\n type StaticDecode,\n type TSchema,\n t,\n} from \"@alepha/core\";\nimport \"@alepha/datetime\";\nimport { ProtobufProvider } from \"./ProtobufProvider.ts\";\n\n/**\n * ProtobufSchemaCodec handles encoding/decoding for Protobuf format.\n *\n * Key differences from JSON codec:\n * - BigInt values are kept as BigInt (not converted to string)\n * - Date values are converted to ISO strings for protobuf compatibility\n * - Binary data (Uint8Array) is kept as-is\n * - Proto3 default values are applied when decoding (to handle omitted fields)\n */\nexport class ProtobufSchemaCodec extends SchemaCodec {\n protected protobufProvider = $inject(ProtobufProvider);\n protected decoder = new TextDecoder();\n\n public encodeToString(schema: TSchema, value: any): string {\n const binary = this.encodeToBinary(schema, value);\n return this.decoder.decode(binary);\n }\n\n public encodeToBinary(schema: TSchema, value: any): Uint8Array {\n const proto = this.protobufProvider.createProtobufSchema(schema);\n return this.protobufProvider.encode(proto, this.encode(schema, value));\n }\n\n public decode<T extends TSchema>(schema: T, value: any): StaticDecode<T> {\n // First decode from protobuf binary to object\n const proto = this.protobufProvider.createProtobufSchema(schema);\n const decoded = this.protobufProvider.decode(proto, value);\n\n // Apply proto3 default values for missing fields\n const withDefaults = this.applyProto3Defaults(schema, decoded);\n\n // Then use the parent decode to validate and transform\n return super.decode(schema, withDefaults);\n }\n\n /**\n * Apply proto3 default values for fields that were omitted during encoding.\n * Proto3 omits fields with default values, so we need to restore them.\n * Also converts enum integers back to their string values.\n */\n protected applyProto3Defaults(schema: TSchema, value: any): any {\n if (!value || typeof value !== \"object\") {\n return value;\n }\n\n if (t.schema.isObject(schema)) {\n const result: any = { ...value };\n\n for (const [key, propSchema] of Object.entries(schema.properties)) {\n if (!(key in result) || result[key] === undefined) {\n // Apply proto3 default values based on type\n result[key] = this.getProto3Default(propSchema);\n } else {\n // Convert enum integers to strings\n if (this.isEnum(propSchema)) {\n result[key] = this.convertEnumValue(propSchema, result[key]);\n } else if (typeof result[key] === \"object\" && result[key] !== null) {\n // Recursively apply defaults to nested objects\n result[key] = this.applyProto3Defaults(propSchema, result[key]);\n }\n }\n }\n\n return result;\n }\n\n if (t.schema.isArray(schema) && Array.isArray(value)) {\n return value.map((item) => this.applyProto3Defaults(schema.items, item));\n }\n\n return value;\n }\n\n /**\n * Check if a schema is an enum type.\n */\n protected isEnum(schema: TSchema): boolean {\n return \"enum\" in schema && Array.isArray(schema.enum);\n }\n\n /**\n * Convert an enum value from protobuf integer to TypeBox string.\n */\n protected convertEnumValue(schema: TSchema, value: any): any {\n if (\n typeof value === \"number\" &&\n \"enum\" in schema &&\n Array.isArray(schema.enum)\n ) {\n // Protobuf encodes enums as integers, convert back to string\n return schema.enum[value];\n }\n return value;\n }\n\n /**\n * Get the proto3 default value for a schema type.\n */\n protected getProto3Default(schema: TSchema): any {\n // Handle nullable/optional types - they can be undefined\n if (t.schema.isOptional(schema) || t.schema.isUnion(schema)) {\n return undefined;\n }\n\n // Handle arrays - default is empty array\n if (t.schema.isArray(schema)) {\n return [];\n }\n\n // Handle records (maps) - default is empty object\n if (t.schema.isRecord(schema)) {\n return {};\n }\n\n // Handle primitive types\n if (t.schema.isString(schema)) return \"\";\n if (t.schema.isNumber(schema)) return 0;\n if (t.schema.isInteger(schema)) return 0;\n if (t.schema.isBigInt(schema)) return BigInt(0);\n if (t.schema.isBoolean(schema)) return false;\n\n // For objects, return empty object (will be filled in recursively)\n if (t.schema.isObject(schema)) {\n return {};\n }\n\n return undefined;\n }\n\n /**\n * Transform types for Protobuf compatibility.\n * This method is called for each type in the schema tree.\n */\n protected transformType(schema: TSchema): TSchema | undefined {\n // For bigint: keep as-is, don't convert to string\n // The schema is still a string type with format \"int64\", but we override encode/decode\n if (t.schema.isBigInt(schema)) {\n return t.bigint();\n }\n\n // For other types, use default behavior\n return undefined;\n }\n}\n","import { $module } from \"@alepha/core\";\nimport { ProtobufProvider } from \"./providers/ProtobufProvider.ts\";\nimport { ProtobufSchemaCodec } from \"./providers/ProtobufSchemaCodec.ts\";\n\nexport * from \"./providers/ProtobufProvider.ts\";\nexport * from \"./providers/ProtobufSchemaCodec.ts\";\n\nexport const AlephaProtobuf = $module({\n name: \"alepha.protobuf\",\n services: [ProtobufProvider, ProtobufSchemaCodec],\n register: (alepha) => {\n alepha.with(ProtobufProvider);\n alepha.codec.register(\"protobuf\", alepha.inject(ProtobufSchemaCodec));\n },\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alepha/protobuf",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -13,12 +13,13 @@
13
13
  "src"
14
14
  ],
15
15
  "dependencies": {
16
- "@alepha/core": "0.10.6",
16
+ "@alepha/core": "0.11.0",
17
+ "@alepha/datetime": "0.11.0",
17
18
  "protobufjs": "^7.5.4"
18
19
  },
19
20
  "devDependencies": {
20
- "@biomejs/biome": "^2.2.6",
21
- "tsdown": "^0.15.7",
21
+ "@biomejs/biome": "^2.3.2",
22
+ "tsdown": "^0.15.11",
22
23
  "typescript": "^5.9.3",
23
24
  "vitest": "^3.2.4"
24
25
  },
package/src/index.ts CHANGED
@@ -1 +1,15 @@
1
+ import { $module } from "@alepha/core";
2
+ import { ProtobufProvider } from "./providers/ProtobufProvider.ts";
3
+ import { ProtobufSchemaCodec } from "./providers/ProtobufSchemaCodec.ts";
4
+
1
5
  export * from "./providers/ProtobufProvider.ts";
6
+ export * from "./providers/ProtobufSchemaCodec.ts";
7
+
8
+ export const AlephaProtobuf = $module({
9
+ name: "alepha.protobuf",
10
+ services: [ProtobufProvider, ProtobufSchemaCodec],
11
+ register: (alepha) => {
12
+ alepha.with(ProtobufProvider);
13
+ alepha.codec.register("protobuf", alepha.inject(ProtobufSchemaCodec));
14
+ },
15
+ });
@@ -1,229 +1,305 @@
1
- import {
2
- $inject,
3
- Alepha,
4
- type Static,
5
- type TObject,
6
- type TSchema,
7
- t,
8
- } from "@alepha/core";
1
+ import { $inject, Alepha, type TObject, type TSchema, t } from "@alepha/core";
9
2
  import type { Type } from "protobufjs";
10
3
  import protobufjs from "protobufjs";
11
4
 
12
5
  export class ProtobufProvider {
13
- protected readonly alepha = $inject(Alepha);
14
- protected readonly schemas: Map<string | TObject, Type> = new Map();
15
- protected readonly protobuf: typeof protobufjs = protobufjs;
16
-
17
- /**
18
- * Encode an object to a Uint8Array.
19
- */
20
- public encode(schema: TObject, data: any): Uint8Array {
21
- return this.parse(schema).encode(this.alepha.parse(schema, data)).finish();
22
- }
23
-
24
- /**
25
- * Decode a Uint8Array to an object.
26
- */
27
- public decode<T extends TObject>(schema: T, data: Uint8Array): Static<T> {
28
- return this.alepha.parse(schema, this.parse(schema).decode(data));
29
- }
30
-
31
- /**
32
- * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.
33
- */
34
- public parse(
35
- schema: ProtobufSchema | TObject,
36
- typeName = "root.Target",
37
- ): Type {
38
- const exists = this.schemas.get(schema);
39
- if (exists) return exists;
40
-
41
- const pbSchema =
42
- typeof schema === "string" ? schema : this.createProtobufSchema(schema);
43
- const result = this.protobuf.parse(pbSchema);
44
- const type = result.root.lookupType(typeName);
45
- this.schemas.set(schema, type);
46
- return type;
47
- }
48
-
49
- /**
50
- * Convert a TypeBox schema to a Protobuf schema as a string.
51
- */
52
- public createProtobufSchema(
53
- schema: TSchema,
54
- options: CreateProtobufSchemaOptions = {},
55
- ): string {
56
- const { rootName = "root", mainMessageName = "Target" } = options;
57
- const context = {
58
- proto: `package ${rootName};\nsyntax = "proto3";\n\n`,
59
- fieldIndex: 1,
60
- };
61
-
62
- if (t.schema.isObject(schema)) {
63
- const { message, subMessages } = this.parseObjectWithDependencies(
64
- schema,
65
- mainMessageName,
66
- );
67
- // Add all sub-messages first
68
- context.proto += subMessages.join("");
69
- // Then add the main message
70
- context.proto += message;
71
- }
72
-
73
- return context.proto;
74
- }
75
-
76
- /**
77
- * Parse an object schema with dependencies (sub-messages).
78
- */
79
- protected parseObjectWithDependencies(
80
- obj: TSchema,
81
- parentName: string,
82
- ): { message: string; subMessages: string[] } {
83
- if (!t.schema.isObject(obj)) {
84
- return { message: "", subMessages: [] };
85
- }
86
-
87
- const fields: string[] = [];
88
- const subMessages: string[] = [];
89
- let fieldIndex = 1;
90
-
91
- for (const [key, value] of Object.entries(obj.properties)) {
92
- // Handle arrays
93
- if (t.schema.isArray(value)) {
94
- if (t.schema.isObject(value.items)) {
95
- const subMessageName =
96
- "title" in value.items && typeof value.items.title === "string"
97
- ? value.items.title
98
- : `${parentName}_${key}`;
99
- const { message: subMessage, subMessages: nestedSubMessages } =
100
- this.parseObjectWithDependencies(value.items, subMessageName);
101
- subMessages.push(...nestedSubMessages);
102
- subMessages.push(subMessage);
103
- fields.push(` repeated ${subMessageName} ${key} = ${fieldIndex++};`);
104
- continue;
105
- }
106
-
107
- const itemType = this.convertType(value.items);
108
- fields.push(` repeated ${itemType} ${key} = ${fieldIndex++};`);
109
- continue;
110
- }
111
-
112
- // Handle nested objects
113
- if (t.schema.isObject(value)) {
114
- const subMessageName =
115
- "title" in value && typeof value.title === "string"
116
- ? value.title
117
- : `${parentName}_${key}`;
118
- const { message: subMessage, subMessages: nestedSubMessages } =
119
- this.parseObjectWithDependencies(value, subMessageName);
120
- subMessages.push(...nestedSubMessages);
121
- subMessages.push(subMessage);
122
- fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);
123
- continue;
124
- }
125
-
126
- // Handle union types (nullable fields)
127
- if (t.schema.isUnion(value)) {
128
- const nonNullType = value.anyOf.find(
129
- (type: TSchema) => !t.schema.isNull(type),
130
- );
131
- if (nonNullType) {
132
- if (t.schema.isObject(nonNullType)) {
133
- const subMessageName =
134
- "title" in nonNullType && typeof nonNullType.title === "string"
135
- ? nonNullType.title
136
- : `${parentName}_${key}`;
137
- const { message: subMessage, subMessages: nestedSubMessages } =
138
- this.parseObjectWithDependencies(nonNullType, subMessageName);
139
- subMessages.push(...nestedSubMessages);
140
- subMessages.push(subMessage);
141
- fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);
142
- continue;
143
- }
144
- const fieldType = this.convertType(nonNullType);
145
- fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);
146
- continue;
147
- }
148
- }
149
-
150
- // Handle records (maps)
151
- if (t.schema.isRecord(value)) {
152
- // TypeBox records use additionalProperties or patternProperties for the value type
153
- let valueSchema: TSchema | undefined;
154
- if (
155
- "additionalProperties" in value &&
156
- value.additionalProperties &&
157
- typeof value.additionalProperties === "object"
158
- ) {
159
- valueSchema = value.additionalProperties;
160
- } else if (
161
- value.patternProperties &&
162
- typeof value.patternProperties === "object"
163
- ) {
164
- // Get the first pattern property (usually "^(.*)$" or similar)
165
- const patterns = Object.values(value.patternProperties);
166
- if (patterns.length > 0 && typeof patterns[0] === "object") {
167
- valueSchema = patterns[0] as TSchema;
168
- }
169
- }
170
-
171
- if (valueSchema) {
172
- const valueType = this.convertType(valueSchema);
173
- fields.push(` map<string, ${valueType}> ${key} = ${fieldIndex++};`);
174
- continue;
175
- }
176
- }
177
-
178
- // Handle regular fields
179
- const fieldType = this.convertType(value);
180
- fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);
181
- }
182
-
183
- const message = `message ${parentName} {\n${fields.join("\n")}\n}\n`;
184
- return { message, subMessages };
185
- }
186
-
187
- /**
188
- * Convert a primitive TypeBox schema type to a Protobuf spec type.
189
- */
190
- protected convertType(schema: TSchema): string {
191
- if (t.schema.isBoolean(schema)) return "bool";
192
- if (t.schema.isNumber(schema) && schema.format === "int64") return "int64";
193
- if (t.schema.isNumber(schema)) return "double";
194
- if (t.schema.isInteger(schema)) return "int32";
195
- if (t.schema.isBigInt(schema)) return "int64";
196
- if (t.schema.isString(schema)) return "string";
197
-
198
- // Handle union types (nullable)
199
- if (t.schema.isUnion(schema)) {
200
- // Find the non-null type in the union
201
- const nonNullType = schema.anyOf.find(
202
- (type: TSchema) => !t.schema.isNull(type),
203
- );
204
- if (nonNullType) {
205
- return this.convertType(nonNullType);
206
- }
207
- }
208
-
209
- // Handle optional types
210
- if (t.schema.isOptional(schema)) {
211
- return this.convertType(schema);
212
- }
213
-
214
- // Handle unsafe types (like enums)
215
- if (t.schema.isUnsafe(schema)) {
216
- // if it's an enum or other unsafe types, default to string
217
- return "string";
218
- }
219
-
220
- throw new Error(`Unsupported type: ${JSON.stringify(schema)}`);
221
- }
6
+ protected readonly alepha = $inject(Alepha);
7
+ protected readonly schemas: Map<string | TObject, Type> = new Map();
8
+ protected readonly protobuf: typeof protobufjs = protobufjs;
9
+ protected readonly enumDefinitions: Map<string, string[]> = new Map();
10
+
11
+ /**
12
+ * Encode an object to a Uint8Array.
13
+ */
14
+ public encode(schema: ProtobufSchema, message: any): Uint8Array {
15
+ return this.parse(schema).encode(message).finish();
16
+ }
17
+
18
+ /**
19
+ * Decode a Uint8Array to an object.
20
+ */
21
+ public decode<T = any>(schema: ProtobufSchema, data: Uint8Array): T {
22
+ return this.parse(schema).decode(data) as T;
23
+ }
24
+
25
+ /**
26
+ * Parse a TypeBox schema to a Protobuf Type schema ready for encoding/decoding.
27
+ */
28
+ public parse(schema: ProtobufSchema, typeName = "root.Target"): Type {
29
+ const exists = this.schemas.get(schema);
30
+ if (exists) {
31
+ return exists;
32
+ }
33
+
34
+ const result = this.protobuf.parse(schema);
35
+ const type = result.root.lookupType(typeName);
36
+ this.schemas.set(schema, type);
37
+ return type;
38
+ }
39
+
40
+ /**
41
+ * Convert a TypeBox schema to a Protobuf schema as a string.
42
+ */
43
+ public createProtobufSchema(
44
+ schema: TSchema,
45
+ options: CreateProtobufSchemaOptions = {},
46
+ ): string {
47
+ const { rootName = "root", mainMessageName = "Target" } = options;
48
+ // Clear enum definitions for this schema generation
49
+ this.enumDefinitions.clear();
50
+
51
+ const context = {
52
+ proto: `package ${rootName};\nsyntax = "proto3";\n\n`,
53
+ fieldIndex: 1,
54
+ };
55
+
56
+ if (t.schema.isObject(schema)) {
57
+ const { message, subMessages } = this.parseObjectWithDependencies(
58
+ schema,
59
+ mainMessageName,
60
+ );
61
+
62
+ // Add all enum definitions first
63
+ for (const [enumName, values] of this.enumDefinitions) {
64
+ context.proto += this.generateEnumDefinition(enumName, values);
65
+ }
66
+
67
+ // Add all sub-messages
68
+ context.proto += subMessages.join("");
69
+ // Then add the main message
70
+ context.proto += message;
71
+ }
72
+
73
+ return context.proto;
74
+ }
75
+
76
+ /**
77
+ * Parse an object schema with dependencies (sub-messages).
78
+ */
79
+ protected parseObjectWithDependencies(
80
+ obj: TSchema,
81
+ parentName: string,
82
+ ): { message: string; subMessages: string[] } {
83
+ if (!t.schema.isObject(obj)) {
84
+ return { message: "", subMessages: [] };
85
+ }
86
+
87
+ const fields: string[] = [];
88
+ const subMessages: string[] = [];
89
+ let fieldIndex = 1;
90
+
91
+ for (const [key, value] of Object.entries(obj.properties)) {
92
+ // Handle arrays
93
+ if (t.schema.isArray(value)) {
94
+ // Check if array items are enums
95
+ if (this.isEnum(value.items)) {
96
+ const enumValues = this.getEnumValues(value.items);
97
+ const enumName = this.registerEnum(key, enumValues);
98
+ fields.push(` repeated ${enumName} ${key} = ${fieldIndex++};`);
99
+ continue;
100
+ }
101
+
102
+ if (t.schema.isObject(value.items)) {
103
+ const subMessageName =
104
+ "title" in value.items && typeof value.items.title === "string"
105
+ ? value.items.title
106
+ : `${parentName}_${key}`;
107
+ const { message: subMessage, subMessages: nestedSubMessages } =
108
+ this.parseObjectWithDependencies(value.items, subMessageName);
109
+ subMessages.push(...nestedSubMessages);
110
+ subMessages.push(subMessage);
111
+ fields.push(` repeated ${subMessageName} ${key} = ${fieldIndex++};`);
112
+ continue;
113
+ }
114
+
115
+ const itemType = this.convertType(value.items);
116
+ fields.push(` repeated ${itemType} ${key} = ${fieldIndex++};`);
117
+ continue;
118
+ }
119
+
120
+ // Handle nested objects
121
+ if (t.schema.isObject(value)) {
122
+ const subMessageName =
123
+ "title" in value && typeof value.title === "string"
124
+ ? value.title
125
+ : `${parentName}_${key}`;
126
+ const { message: subMessage, subMessages: nestedSubMessages } =
127
+ this.parseObjectWithDependencies(value, subMessageName);
128
+ subMessages.push(...nestedSubMessages);
129
+ subMessages.push(subMessage);
130
+ fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);
131
+ continue;
132
+ }
133
+
134
+ // Handle union types (nullable fields)
135
+ if (t.schema.isUnion(value)) {
136
+ const nonNullType = value.anyOf.find(
137
+ (type: TSchema) => !t.schema.isNull(type),
138
+ );
139
+ if (nonNullType) {
140
+ // Check if it's an enum
141
+ if (this.isEnum(nonNullType)) {
142
+ const enumValues = this.getEnumValues(nonNullType);
143
+ const enumName = this.registerEnum(key, enumValues);
144
+ fields.push(` ${enumName} ${key} = ${fieldIndex++};`);
145
+ continue;
146
+ }
147
+
148
+ if (t.schema.isObject(nonNullType)) {
149
+ const subMessageName =
150
+ "title" in nonNullType && typeof nonNullType.title === "string"
151
+ ? nonNullType.title
152
+ : `${parentName}_${key}`;
153
+ const { message: subMessage, subMessages: nestedSubMessages } =
154
+ this.parseObjectWithDependencies(nonNullType, subMessageName);
155
+ subMessages.push(...nestedSubMessages);
156
+ subMessages.push(subMessage);
157
+ fields.push(` ${subMessageName} ${key} = ${fieldIndex++};`);
158
+ continue;
159
+ }
160
+ const fieldType = this.convertType(nonNullType);
161
+ fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);
162
+ continue;
163
+ }
164
+ }
165
+
166
+ // Handle records (maps)
167
+ if (t.schema.isRecord(value)) {
168
+ // TypeBox records use additionalProperties or patternProperties for the value type
169
+ let valueSchema: TSchema | undefined;
170
+ if (
171
+ "additionalProperties" in value &&
172
+ value.additionalProperties &&
173
+ typeof value.additionalProperties === "object"
174
+ ) {
175
+ valueSchema = value.additionalProperties;
176
+ } else if (
177
+ value.patternProperties &&
178
+ typeof value.patternProperties === "object"
179
+ ) {
180
+ // Get the first pattern property (usually "^(.*)$" or similar)
181
+ const patterns = Object.values(value.patternProperties);
182
+ if (patterns.length > 0 && typeof patterns[0] === "object") {
183
+ valueSchema = patterns[0] as TSchema;
184
+ }
185
+ }
186
+
187
+ if (valueSchema) {
188
+ const valueType = this.convertType(valueSchema);
189
+ fields.push(` map<string, ${valueType}> ${key} = ${fieldIndex++};`);
190
+ continue;
191
+ }
192
+ }
193
+
194
+ // Handle enum fields
195
+ if (this.isEnum(value)) {
196
+ const enumValues = this.getEnumValues(value);
197
+ const enumName = this.registerEnum(key, enumValues);
198
+ fields.push(` ${enumName} ${key} = ${fieldIndex++};`);
199
+ continue;
200
+ }
201
+
202
+ // Handle regular fields
203
+ const fieldType = this.convertType(value);
204
+ fields.push(` ${fieldType} ${key} = ${fieldIndex++};`);
205
+ }
206
+
207
+ const message = `message ${parentName} {\n${fields.join("\n")}\n}\n`;
208
+ return { message, subMessages };
209
+ }
210
+
211
+ /**
212
+ * Convert a primitive TypeBox schema type to a Protobuf spec type.
213
+ */
214
+ protected convertType(schema: TSchema): string {
215
+ if (t.schema.isBoolean(schema)) return "bool";
216
+ if (t.schema.isNumber(schema) && schema.format === "int64") return "int64";
217
+ if (t.schema.isNumber(schema)) return "double";
218
+ if (t.schema.isInteger(schema)) return "int32";
219
+ if (t.schema.isBigInt(schema)) return "int64";
220
+ if (t.schema.isString(schema)) return "string";
221
+
222
+ // Handle union types (nullable)
223
+ if (t.schema.isUnion(schema)) {
224
+ // Find the non-null type in the union
225
+ const nonNullType = schema.anyOf.find(
226
+ (type: TSchema) => !t.schema.isNull(type),
227
+ );
228
+ if (nonNullType) {
229
+ return this.convertType(nonNullType);
230
+ }
231
+ }
232
+
233
+ // Handle optional types
234
+ if (t.schema.isOptional(schema)) {
235
+ return this.convertType(schema);
236
+ }
237
+
238
+ // Handle unsafe types (like enums)
239
+ if (t.schema.isUnsafe(schema)) {
240
+ // if it's an enum or other unsafe types, default to string
241
+ return "string";
242
+ }
243
+
244
+ throw new Error(`Unsupported type: ${JSON.stringify(schema)}`);
245
+ }
246
+
247
+ /**
248
+ * Check if a schema is an enum type.
249
+ * TypeBox enums have an "enum" property with an array of values.
250
+ */
251
+ protected isEnum(schema: TSchema): boolean {
252
+ return "enum" in schema && Array.isArray(schema.enum);
253
+ }
254
+
255
+ /**
256
+ * Extract enum values from a TypeBox enum schema.
257
+ */
258
+ protected getEnumValues(schema: TSchema): string[] {
259
+ if ("enum" in schema && Array.isArray(schema.enum)) {
260
+ return schema.enum.map(String);
261
+ }
262
+ return [];
263
+ }
264
+
265
+ /**
266
+ * Register an enum and return its type name.
267
+ * Generates a PascalCase name from the field name.
268
+ */
269
+ protected registerEnum(fieldName: string, values: string[]): string {
270
+ // Capitalize first letter of field name for enum type name
271
+ const enumName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
272
+
273
+ // Check if we already have this exact enum registered
274
+ const valueKey = values.join(",");
275
+ const existingEnum = Array.from(this.enumDefinitions.entries()).find(
276
+ ([_, enumValues]) => enumValues.join(",") === valueKey,
277
+ );
278
+
279
+ if (existingEnum) {
280
+ // Reuse existing enum with same values
281
+ return existingEnum[0];
282
+ }
283
+
284
+ // Register new enum
285
+ this.enumDefinitions.set(enumName, values);
286
+ return enumName;
287
+ }
288
+
289
+ /**
290
+ * Generate a protobuf enum definition.
291
+ */
292
+ protected generateEnumDefinition(enumName: string, values: string[]): string {
293
+ const enumValues = values
294
+ .map((value, index) => ` ${value} = ${index};`)
295
+ .join("\n");
296
+ return `enum ${enumName} {\n${enumValues}\n}\n`;
297
+ }
222
298
  }
223
299
 
224
300
  export type ProtobufSchema = string;
225
301
 
226
302
  export interface CreateProtobufSchemaOptions {
227
- rootName?: string;
228
- mainMessageName?: string;
303
+ rootName?: string;
304
+ mainMessageName?: string;
229
305
  }
@@ -0,0 +1,154 @@
1
+ import {
2
+ $inject,
3
+ SchemaCodec,
4
+ type StaticDecode,
5
+ type TSchema,
6
+ t,
7
+ } from "@alepha/core";
8
+ import "@alepha/datetime";
9
+ import { ProtobufProvider } from "./ProtobufProvider.ts";
10
+
11
+ /**
12
+ * ProtobufSchemaCodec handles encoding/decoding for Protobuf format.
13
+ *
14
+ * Key differences from JSON codec:
15
+ * - BigInt values are kept as BigInt (not converted to string)
16
+ * - Date values are converted to ISO strings for protobuf compatibility
17
+ * - Binary data (Uint8Array) is kept as-is
18
+ * - Proto3 default values are applied when decoding (to handle omitted fields)
19
+ */
20
+ export class ProtobufSchemaCodec extends SchemaCodec {
21
+ protected protobufProvider = $inject(ProtobufProvider);
22
+ protected decoder = new TextDecoder();
23
+
24
+ public encodeToString(schema: TSchema, value: any): string {
25
+ const binary = this.encodeToBinary(schema, value);
26
+ return this.decoder.decode(binary);
27
+ }
28
+
29
+ public encodeToBinary(schema: TSchema, value: any): Uint8Array {
30
+ const proto = this.protobufProvider.createProtobufSchema(schema);
31
+ return this.protobufProvider.encode(proto, this.encode(schema, value));
32
+ }
33
+
34
+ public decode<T extends TSchema>(schema: T, value: any): StaticDecode<T> {
35
+ // First decode from protobuf binary to object
36
+ const proto = this.protobufProvider.createProtobufSchema(schema);
37
+ const decoded = this.protobufProvider.decode(proto, value);
38
+
39
+ // Apply proto3 default values for missing fields
40
+ const withDefaults = this.applyProto3Defaults(schema, decoded);
41
+
42
+ // Then use the parent decode to validate and transform
43
+ return super.decode(schema, withDefaults);
44
+ }
45
+
46
+ /**
47
+ * Apply proto3 default values for fields that were omitted during encoding.
48
+ * Proto3 omits fields with default values, so we need to restore them.
49
+ * Also converts enum integers back to their string values.
50
+ */
51
+ protected applyProto3Defaults(schema: TSchema, value: any): any {
52
+ if (!value || typeof value !== "object") {
53
+ return value;
54
+ }
55
+
56
+ if (t.schema.isObject(schema)) {
57
+ const result: any = { ...value };
58
+
59
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
60
+ if (!(key in result) || result[key] === undefined) {
61
+ // Apply proto3 default values based on type
62
+ result[key] = this.getProto3Default(propSchema);
63
+ } else {
64
+ // Convert enum integers to strings
65
+ if (this.isEnum(propSchema)) {
66
+ result[key] = this.convertEnumValue(propSchema, result[key]);
67
+ } else if (typeof result[key] === "object" && result[key] !== null) {
68
+ // Recursively apply defaults to nested objects
69
+ result[key] = this.applyProto3Defaults(propSchema, result[key]);
70
+ }
71
+ }
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ if (t.schema.isArray(schema) && Array.isArray(value)) {
78
+ return value.map((item) => this.applyProto3Defaults(schema.items, item));
79
+ }
80
+
81
+ return value;
82
+ }
83
+
84
+ /**
85
+ * Check if a schema is an enum type.
86
+ */
87
+ protected isEnum(schema: TSchema): boolean {
88
+ return "enum" in schema && Array.isArray(schema.enum);
89
+ }
90
+
91
+ /**
92
+ * Convert an enum value from protobuf integer to TypeBox string.
93
+ */
94
+ protected convertEnumValue(schema: TSchema, value: any): any {
95
+ if (
96
+ typeof value === "number" &&
97
+ "enum" in schema &&
98
+ Array.isArray(schema.enum)
99
+ ) {
100
+ // Protobuf encodes enums as integers, convert back to string
101
+ return schema.enum[value];
102
+ }
103
+ return value;
104
+ }
105
+
106
+ /**
107
+ * Get the proto3 default value for a schema type.
108
+ */
109
+ protected getProto3Default(schema: TSchema): any {
110
+ // Handle nullable/optional types - they can be undefined
111
+ if (t.schema.isOptional(schema) || t.schema.isUnion(schema)) {
112
+ return undefined;
113
+ }
114
+
115
+ // Handle arrays - default is empty array
116
+ if (t.schema.isArray(schema)) {
117
+ return [];
118
+ }
119
+
120
+ // Handle records (maps) - default is empty object
121
+ if (t.schema.isRecord(schema)) {
122
+ return {};
123
+ }
124
+
125
+ // Handle primitive types
126
+ if (t.schema.isString(schema)) return "";
127
+ if (t.schema.isNumber(schema)) return 0;
128
+ if (t.schema.isInteger(schema)) return 0;
129
+ if (t.schema.isBigInt(schema)) return BigInt(0);
130
+ if (t.schema.isBoolean(schema)) return false;
131
+
132
+ // For objects, return empty object (will be filled in recursively)
133
+ if (t.schema.isObject(schema)) {
134
+ return {};
135
+ }
136
+
137
+ return undefined;
138
+ }
139
+
140
+ /**
141
+ * Transform types for Protobuf compatibility.
142
+ * This method is called for each type in the schema tree.
143
+ */
144
+ protected transformType(schema: TSchema): TSchema | undefined {
145
+ // For bigint: keep as-is, don't convert to string
146
+ // The schema is still a string type with format "int64", but we override encode/decode
147
+ if (t.schema.isBigInt(schema)) {
148
+ return t.bigint();
149
+ }
150
+
151
+ // For other types, use default behavior
152
+ return undefined;
153
+ }
154
+ }