@eventferry/schema-registry 3.2.3 → 3.3.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/CHANGELOG.md +10 -0
- package/README.md +58 -0
- package/dist/index.cjs +64 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -6
- package/dist/index.d.ts +82 -6
- package/dist/index.js +64 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @eventferry/schema-registry
|
|
2
2
|
|
|
3
|
+
## 3.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d983d90: Schema Registry serializer gains three production-grade controls without breaking the existing API:
|
|
8
|
+
|
|
9
|
+
- **Subject naming strategy** — `subjectStrategy: "TopicNameStrategy" | "RecordNameStrategy" | "TopicRecordNameStrategy"` mirrors Confluent's three built-ins. The two record-based strategies take a `recordName: (record, isKey) => string` resolver. Default stays `TopicNameStrategy`; the existing `subject` callable still wins when set (now optionally receiving `(topic, isKey, record)` — the single-arg legacy form keeps working).
|
|
10
|
+
- **Avro key serialization** — new `keySchemas` option + `serializeKey(record): Promise<Buffer | null>` method. Returns `null` for keyless records (matching the kafka "no key" semantics) and uses the `-key` subject by default. Key and value subject ids cache independently. Not wired into the relay automatically — call it from your publish glue when you want Avro-encoded keys instead of UTF-8 strings.
|
|
11
|
+
- **`autoRegister: false`** — never call `register()`, always resolve schemas via `getLatestSchemaId` on the computed subject. Locally supplied `schemas` / `keySchemas` bytes become docs-only in this mode. Matches `auto.register.schemas=false` on every Confluent client, for production clusters where schemas are managed out-of-band.
|
|
12
|
+
|
|
3
13
|
## 3.2.3
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -32,6 +32,64 @@ Topics without a configured schema use the subject's latest registered schema
|
|
|
32
32
|
(default subject: `${topic}-value`). On the consumer, decode with the same client:
|
|
33
33
|
`await registry.decode(message.value)`.
|
|
34
34
|
|
|
35
|
+
## Subject naming strategies
|
|
36
|
+
|
|
37
|
+
Pick one of Confluent's three built-in strategies (default `TopicNameStrategy`):
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
new SchemaRegistrySerializer({
|
|
41
|
+
host,
|
|
42
|
+
subjectStrategy: "RecordNameStrategy", // subject = recordName
|
|
43
|
+
// or: "TopicRecordNameStrategy" // subject = `${topic}-${recordName}`
|
|
44
|
+
recordName: (record) => `com.example.${record.aggregateType}.Created`,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`recordName` is required for the `RecordName` and `TopicRecordName` strategies — typical implementation reads `${namespace}.${name}` from your avsc.
|
|
49
|
+
|
|
50
|
+
Need full control? Skip the preset and pass an explicit `subject` function:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
new SchemaRegistrySerializer({
|
|
54
|
+
host,
|
|
55
|
+
subject: (topic, isKey, record) => `acme.${topic}.${isKey ? "key" : "value"}`,
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Avro key serialization
|
|
60
|
+
|
|
61
|
+
Configure per-topic key schemas and call `serializeKey` from your publish path:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const serializer = new SchemaRegistrySerializer({
|
|
65
|
+
host,
|
|
66
|
+
schemas: { "orders.created": { type: "AVRO", schema: valueAvsc } },
|
|
67
|
+
keySchemas: { "orders.created": { type: "AVRO", schema: keyAvsc } },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Inside your custom publish glue:
|
|
71
|
+
const encodedValue = await serializer.serialize(record);
|
|
72
|
+
const encodedKey = await serializer.serializeKey(record); // null when record.key is null
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The relay does NOT call `serializeKey` automatically — Avro keys are an application-level convention, and adding them silently would break consumers expecting UTF-8 string keys. Wire it in your publish path explicitly.
|
|
76
|
+
|
|
77
|
+
Key and value subjects cache their schema ids independently — registering one doesn't affect the other.
|
|
78
|
+
|
|
79
|
+
## Auto-register toggle
|
|
80
|
+
|
|
81
|
+
For production clusters where schemas are managed out-of-band (Confluent Cloud, regulated environments), turn auto-registration off:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
new SchemaRegistrySerializer({
|
|
85
|
+
host,
|
|
86
|
+
schemas: { /* ignored when autoRegister is false */ },
|
|
87
|
+
autoRegister: false,
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With `autoRegister: false`, the serializer ALWAYS resolves by `getLatestSchemaId` on the computed subject — locally supplied `schemas` / `keySchemas` bytes are ignored. Matches the Confluent client's `auto.register.schemas=false`.
|
|
92
|
+
|
|
35
93
|
📖 **Full documentation:** [github.com/SametGoktepe/eventferry](https://github.com/SametGoktepe/eventferry#readme)
|
|
36
94
|
|
|
37
95
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -39,8 +39,13 @@ var DEFAULT_CONTENT_TYPE = "application/vnd.confluent.avro";
|
|
|
39
39
|
var SchemaRegistrySerializer = class {
|
|
40
40
|
contentType;
|
|
41
41
|
schemas;
|
|
42
|
-
|
|
42
|
+
keySchemas;
|
|
43
|
+
subjectStrategy;
|
|
44
|
+
recordName;
|
|
45
|
+
subjectFn;
|
|
43
46
|
host;
|
|
47
|
+
autoRegister;
|
|
48
|
+
// Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.
|
|
44
49
|
idCache = /* @__PURE__ */ new Map();
|
|
45
50
|
registry;
|
|
46
51
|
constructor(opts) {
|
|
@@ -52,25 +57,74 @@ var SchemaRegistrySerializer = class {
|
|
|
52
57
|
this.registry = opts.registry ?? null;
|
|
53
58
|
this.host = opts.host ?? null;
|
|
54
59
|
this.schemas = opts.schemas ?? {};
|
|
55
|
-
this.
|
|
60
|
+
this.keySchemas = opts.keySchemas ?? {};
|
|
61
|
+
this.subjectStrategy = opts.subjectStrategy ?? "TopicNameStrategy";
|
|
62
|
+
this.recordName = opts.recordName ?? null;
|
|
63
|
+
this.subjectFn = opts.subject ?? null;
|
|
56
64
|
this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
65
|
+
this.autoRegister = opts.autoRegister ?? true;
|
|
57
66
|
}
|
|
58
67
|
async serialize(record) {
|
|
59
68
|
const registry = await this.getRegistry();
|
|
60
|
-
const
|
|
69
|
+
const subject = this.resolveSubject(record, false);
|
|
70
|
+
const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);
|
|
61
71
|
return registry.encode(id, record.payload);
|
|
62
72
|
}
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
75
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
76
|
+
* as the producer-side "no key" signal).
|
|
77
|
+
*
|
|
78
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
79
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
80
|
+
* UTF-8 strings.
|
|
81
|
+
*/
|
|
82
|
+
async serializeKey(record) {
|
|
83
|
+
if (record.key === null || record.key === void 0) return null;
|
|
84
|
+
const registry = await this.getRegistry();
|
|
85
|
+
const subject = this.resolveSubject(record, true);
|
|
86
|
+
const id = await this.schemaId(
|
|
87
|
+
registry,
|
|
88
|
+
subject,
|
|
89
|
+
this.keySchemas[record.topic],
|
|
90
|
+
true,
|
|
91
|
+
record.topic
|
|
92
|
+
);
|
|
93
|
+
return registry.encode(id, record.key);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
97
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
98
|
+
*/
|
|
99
|
+
resolveSubject(record, isKey) {
|
|
100
|
+
if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);
|
|
101
|
+
switch (this.subjectStrategy) {
|
|
102
|
+
case "TopicNameStrategy":
|
|
103
|
+
return `${record.topic}-${isKey ? "key" : "value"}`;
|
|
104
|
+
case "RecordNameStrategy":
|
|
105
|
+
return this.recordNameFor(record, isKey);
|
|
106
|
+
case "TopicRecordNameStrategy":
|
|
107
|
+
return `${record.topic}-${this.recordNameFor(record, isKey)}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
recordNameFor(record, isKey) {
|
|
111
|
+
if (!this.recordName) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`SchemaRegistrySerializer: subjectStrategy "${this.subjectStrategy}" requires a \`recordName\` resolver.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return this.recordName(record, isKey);
|
|
117
|
+
}
|
|
118
|
+
schemaId(registry, subject, spec, isKey, topic) {
|
|
119
|
+
const cacheKey = `${topic}:${isKey ? "key" : "value"}`;
|
|
120
|
+
const cached = this.idCache.get(cacheKey);
|
|
65
121
|
if (cached) return cached;
|
|
66
|
-
const
|
|
67
|
-
const spec = this.schemas[topic];
|
|
68
|
-
const lookup = spec ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
122
|
+
const lookup = spec && this.autoRegister ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
69
123
|
const guarded = lookup.catch((err) => {
|
|
70
|
-
this.idCache.delete(
|
|
124
|
+
this.idCache.delete(cacheKey);
|
|
71
125
|
throw err;
|
|
72
126
|
});
|
|
73
|
-
this.idCache.set(
|
|
127
|
+
this.idCache.set(cacheKey, guarded);
|
|
74
128
|
return guarded;
|
|
75
129
|
}
|
|
76
130
|
async getRegistry() {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/serializer.ts"],"sourcesContent":["export * from \"./serializer.js\";\n","import type { OutboxRecord, Serializer } from \"@eventferry/core\";\n\nexport type SchemaType = \"AVRO\" | \"PROTOBUF\" | \"JSON\";\n\nexport interface SchemaSpec {\n type: SchemaType;\n /** Schema definition string (avsc JSON / .proto / JSON Schema). */\n schema: string;\n}\n\n/**\n * The subset of a Confluent Schema Registry client this serializer uses. The\n * `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.\n */\nexport interface SchemaRegistryClient {\n register(\n schema: { type: string; schema: string },\n opts?: { subject: string },\n ): Promise<{ id: number }>;\n getLatestSchemaId(subject: string): Promise<number>;\n encode(registryId: number, payload: unknown): Promise<Buffer>;\n}\n\nexport interface SchemaRegistrySerializerOptions {\n /** Inject a ready client (tests, custom config). */\n registry?: SchemaRegistryClient;\n /** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */\n host?: string;\n /** Per-topic schema to register. Topics omitted here use the subject's latest. */\n schemas?: Record<string, SchemaSpec>;\n /** Subject naming. Default TopicNameStrategy: `${topic}-value`. */\n subject?: (topic: string) => string;\n /** content-type header value. Default \"application/vnd.confluent.avro\". */\n contentType?: string;\n}\n\nconst DEFAULT_CONTENT_TYPE = \"application/vnd.confluent.avro\";\n\n/**\n * A core {@link Serializer} that encodes payloads with a Confluent Schema Registry\n * (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s\n * `serializer` option. The schema id per topic is resolved once and cached.\n */\nexport class SchemaRegistrySerializer implements Serializer {\n readonly contentType: string;\n private readonly schemas: Record<string, SchemaSpec>;\n private readonly subject: (topic: string) => string;\n private readonly host: string | null;\n private readonly idCache = new Map<string, Promise<number>>();\n private registry: SchemaRegistryClient | null;\n\n constructor(opts: SchemaRegistrySerializerOptions) {\n if (!opts.registry && !opts.host) {\n throw new Error(\n \"SchemaRegistrySerializer requires either a `registry` client or a `host`.\",\n );\n }\n this.registry = opts.registry ?? null;\n this.host = opts.host ?? null;\n this.schemas = opts.schemas ?? {};\n this.subject = opts.subject ?? ((topic) => `${topic}-value`);\n this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;\n }\n\n async serialize(record: OutboxRecord): Promise<Buffer> {\n const registry = await this.getRegistry();\n const id = await this.schemaId(registry, record.topic);\n return registry.encode(id, record.payload);\n }\n\n private schemaId(\n registry: SchemaRegistryClient,\n topic: string,\n ): Promise<number> {\n const cached = this.idCache.get(topic);\n if (cached) return cached;\n\n const subject = this.subject(topic);\n const spec = this.schemas[topic];\n const lookup = spec\n ? registry\n .register({ type: spec.type, schema: spec.schema }, { subject })\n .then((r) => r.id)\n : registry.getLatestSchemaId(subject);\n\n // Cache the in-flight promise so concurrent first calls don't double-register;\n // drop it on failure so a transient error can be retried.\n const guarded = lookup.catch((err) => {\n this.idCache.delete(topic);\n throw err;\n });\n this.idCache.set(topic, guarded);\n return guarded;\n }\n\n private async getRegistry(): Promise<SchemaRegistryClient> {\n if (this.registry) return this.registry;\n const mod = await importSchemaRegistry();\n this.registry = new mod.SchemaRegistry({ host: this.host as string });\n return this.registry;\n }\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (cfg: { host: string }) => SchemaRegistryClient;\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@kafkajs/confluent-schema-registry\")) as any;\n } catch {\n throw new Error(\n 'SchemaRegistrySerializer with `host` needs the \"@kafkajs/confluent-schema-registry\" package. Run: npm i @kafkajs/confluent-schema-registry',\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACoCA,IAAM,uBAAuB;AAOtB,IAAM,2BAAN,MAAqD;AAAA,EACjD;AAAA,EACQ;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,oBAAI,IAA6B;AAAA,EACpD;AAAA,EAER,YAAY,MAAuC;AACjD,QAAI,CAAC,KAAK,YAAY,CAAC,KAAK,MAAM;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,WAAW,KAAK,YAAY;AACjC,SAAK,OAAO,KAAK,QAAQ;AACzB,SAAK,UAAU,KAAK,WAAW,CAAC;AAChC,SAAK,UAAU,KAAK,YAAY,CAAC,UAAU,GAAG,KAAK;AACnD,SAAK,cAAc,KAAK,eAAe;AAAA,EACzC;AAAA,EAEA,MAAM,UAAU,QAAuC;AACrD,UAAM,WAAW,MAAM,KAAK,YAAY;AACxC,UAAM,KAAK,MAAM,KAAK,SAAS,UAAU,OAAO,KAAK;AACrD,WAAO,SAAS,OAAO,IAAI,OAAO,OAAO;AAAA,EAC3C;AAAA,EAEQ,SACN,UACA,OACiB;AACjB,UAAM,SAAS,KAAK,QAAQ,IAAI,KAAK;AACrC,QAAI,OAAQ,QAAO;AAEnB,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,UAAM,SAAS,OACX,SACG,SAAS,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,OAAO,GAAG,EAAE,QAAQ,CAAC,EAC9D,KAAK,CAAC,MAAM,EAAE,EAAE,IACnB,SAAS,kBAAkB,OAAO;AAItC,UAAM,UAAU,OAAO,MAAM,CAAC,QAAQ;AACpC,WAAK,QAAQ,OAAO,KAAK;AACzB,YAAM;AAAA,IACR,CAAC;AACD,SAAK,QAAQ,IAAI,OAAO,OAAO;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,cAA6C;AACzD,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,UAAM,MAAM,MAAM,qBAAqB;AACvC,SAAK,WAAW,IAAI,IAAI,eAAe,EAAE,MAAM,KAAK,KAAe,CAAC;AACpE,WAAO,KAAK;AAAA,EACd;AACF;AAEA,eAAe,uBAEZ;AACD,MAAI;AAEF,WAAQ,MAAM,OAAO,oCAAoC;AAAA,EAC3D,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/serializer.ts"],"sourcesContent":["export * from \"./serializer.js\";\n","import type { OutboxRecord, Serializer } from \"@eventferry/core\";\n\nexport type SchemaType = \"AVRO\" | \"PROTOBUF\" | \"JSON\";\n\nexport interface SchemaSpec {\n type: SchemaType;\n /** Schema definition string (avsc JSON / .proto / JSON Schema). */\n schema: string;\n}\n\n/**\n * Subject naming strategy. Mirrors Confluent's three built-ins:\n *\n * - `\"TopicNameStrategy\"` (default) — `${topic}-value` / `${topic}-key`.\n * The conventional default; one schema per (topic, isKey) tuple.\n * - `\"RecordNameStrategy\"` — `${recordName}`. Same record type can flow\n * on multiple topics. Requires a `recordName` resolver.\n * - `\"TopicRecordNameStrategy\"` — `${topic}-${recordName}`. Multiple record\n * types per topic. Requires a `recordName` resolver.\n *\n * Set `subject` (function form) to override entirely.\n */\nexport type SubjectNameStrategy =\n | \"TopicNameStrategy\"\n | \"RecordNameStrategy\"\n | \"TopicRecordNameStrategy\";\n\n/**\n * The subset of a Confluent Schema Registry client this serializer uses. The\n * `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.\n */\nexport interface SchemaRegistryClient {\n register(\n schema: { type: string; schema: string },\n opts?: { subject: string },\n ): Promise<{ id: number }>;\n getLatestSchemaId(subject: string): Promise<number>;\n encode(registryId: number, payload: unknown): Promise<Buffer>;\n}\n\nexport interface SchemaRegistrySerializerOptions {\n /** Inject a ready client (tests, custom config). */\n registry?: SchemaRegistryClient;\n /** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */\n host?: string;\n /** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */\n schemas?: Record<string, SchemaSpec>;\n /**\n * Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes\n * the record key for the matching topic. Topics omitted here fall back\n * to the subject's latest (or, with `autoRegister: false`, ALWAYS the\n * subject's latest).\n */\n keySchemas?: Record<string, SchemaSpec>;\n /**\n * Subject naming strategy preset. Default `\"TopicNameStrategy\"`.\n * Setting `subject` (function) overrides this entirely.\n */\n subjectStrategy?: SubjectNameStrategy;\n /**\n * Resolve the schema's record name (used by `RecordNameStrategy` and\n * `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to\n * one of those — throws on first serialize if absent.\n *\n * Typical implementation: read `${namespace}.${name}` from the avsc you\n * already supply via `schemas` / `keySchemas`.\n */\n recordName?: (record: OutboxRecord, isKey: boolean) => string;\n /**\n * Custom subject function — overrides BOTH `subjectStrategy` and\n * `recordName`. Receives `(topic, isKey, record)` for full flexibility.\n *\n * Backwards-compatible with the single-argument legacy form\n * `(topic) => string` — extra args are ignored by JavaScript.\n */\n subject?: (\n topic: string,\n isKey?: boolean,\n record?: OutboxRecord,\n ) => string;\n /** content-type header value. Default \"application/vnd.confluent.avro\". */\n contentType?: string;\n /**\n * Auto-register schemas when one is supplied via `schemas` / `keySchemas`.\n * Default `true` — matches Confluent client behavior.\n *\n * Set to `false` for production clusters where schemas are managed\n * out-of-band (Confluent Cloud, regulated environments). With\n * autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`\n * on the computed subject — and the locally-supplied schema bytes are\n * ignored.\n */\n autoRegister?: boolean;\n}\n\nconst DEFAULT_CONTENT_TYPE = \"application/vnd.confluent.avro\";\n\n/**\n * A core {@link Serializer} that encodes payloads with a Confluent Schema Registry\n * (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s\n * `serializer` option. The schema id per (topic, isKey) tuple is resolved once\n * and cached.\n *\n * Also exposes `serializeKey(record)` for users who want Avro-encoded message\n * keys — call it manually when building the publish path; the relay does NOT\n * call it automatically (key encoding is application-level by convention).\n */\nexport class SchemaRegistrySerializer implements Serializer {\n readonly contentType: string;\n private readonly schemas: Record<string, SchemaSpec>;\n private readonly keySchemas: Record<string, SchemaSpec>;\n private readonly subjectStrategy: SubjectNameStrategy;\n private readonly recordName:\n | ((record: OutboxRecord, isKey: boolean) => string)\n | null;\n private readonly subjectFn:\n | ((topic: string, isKey?: boolean, record?: OutboxRecord) => string)\n | null;\n private readonly host: string | null;\n private readonly autoRegister: boolean;\n // Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.\n private readonly idCache = new Map<string, Promise<number>>();\n private registry: SchemaRegistryClient | null;\n\n constructor(opts: SchemaRegistrySerializerOptions) {\n if (!opts.registry && !opts.host) {\n throw new Error(\n \"SchemaRegistrySerializer requires either a `registry` client or a `host`.\",\n );\n }\n this.registry = opts.registry ?? null;\n this.host = opts.host ?? null;\n this.schemas = opts.schemas ?? {};\n this.keySchemas = opts.keySchemas ?? {};\n this.subjectStrategy = opts.subjectStrategy ?? \"TopicNameStrategy\";\n this.recordName = opts.recordName ?? null;\n this.subjectFn = opts.subject ?? null;\n this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;\n this.autoRegister = opts.autoRegister ?? true;\n }\n\n async serialize(record: OutboxRecord): Promise<Buffer> {\n const registry = await this.getRegistry();\n const subject = this.resolveSubject(record, false);\n const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);\n return registry.encode(id, record.payload);\n }\n\n /**\n * Avro-encode the record's KEY using the registered key schema. Returns\n * `null` when the record has no key (kafkajs/confluent treat null keys\n * as the producer-side \"no key\" signal).\n *\n * Not part of the core `Serializer` interface — callers wire it into\n * their publish path manually when they want Avro keys instead of raw\n * UTF-8 strings.\n */\n async serializeKey(record: OutboxRecord): Promise<Buffer | null> {\n if (record.key === null || record.key === undefined) return null;\n const registry = await this.getRegistry();\n const subject = this.resolveSubject(record, true);\n const id = await this.schemaId(\n registry,\n subject,\n this.keySchemas[record.topic],\n true,\n record.topic,\n );\n return registry.encode(id, record.key);\n }\n\n /**\n * Resolve the subject for this (record, isKey) tuple. Order of\n * precedence: explicit `subject` function → `subjectStrategy` preset.\n */\n private resolveSubject(record: OutboxRecord, isKey: boolean): string {\n if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);\n switch (this.subjectStrategy) {\n case \"TopicNameStrategy\":\n return `${record.topic}-${isKey ? \"key\" : \"value\"}`;\n case \"RecordNameStrategy\":\n return this.recordNameFor(record, isKey);\n case \"TopicRecordNameStrategy\":\n return `${record.topic}-${this.recordNameFor(record, isKey)}`;\n }\n }\n\n private recordNameFor(record: OutboxRecord, isKey: boolean): string {\n if (!this.recordName) {\n throw new Error(\n `SchemaRegistrySerializer: subjectStrategy \"${this.subjectStrategy}\" requires a \\`recordName\\` resolver.`,\n );\n }\n return this.recordName(record, isKey);\n }\n\n private schemaId(\n registry: SchemaRegistryClient,\n subject: string,\n spec: SchemaSpec | undefined,\n isKey: boolean,\n topic: string,\n ): Promise<number> {\n const cacheKey = `${topic}:${isKey ? \"key\" : \"value\"}`;\n const cached = this.idCache.get(cacheKey);\n if (cached) return cached;\n\n // autoRegister=false → ALWAYS resolve by latest; the local spec\n // (if any) is ignored. Matches Confluent's auto.register.schemas=false.\n const lookup =\n spec && this.autoRegister\n ? registry\n .register({ type: spec.type, schema: spec.schema }, { subject })\n .then((r) => r.id)\n : registry.getLatestSchemaId(subject);\n\n // Cache the in-flight promise so concurrent first calls don't double-register;\n // drop it on failure so a transient error can be retried.\n const guarded = lookup.catch((err) => {\n this.idCache.delete(cacheKey);\n throw err;\n });\n this.idCache.set(cacheKey, guarded);\n return guarded;\n }\n\n private async getRegistry(): Promise<SchemaRegistryClient> {\n if (this.registry) return this.registry;\n const mod = await importSchemaRegistry();\n this.registry = new mod.SchemaRegistry({ host: this.host as string });\n return this.registry;\n }\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (cfg: { host: string }) => SchemaRegistryClient;\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@kafkajs/confluent-schema-registry\")) as any;\n } catch {\n throw new Error(\n 'SchemaRegistrySerializer with `host` needs the \"@kafkajs/confluent-schema-registry\" package. Run: npm i @kafkajs/confluent-schema-registry',\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,19 @@ interface SchemaSpec {
|
|
|
6
6
|
/** Schema definition string (avsc JSON / .proto / JSON Schema). */
|
|
7
7
|
schema: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Subject naming strategy. Mirrors Confluent's three built-ins:
|
|
11
|
+
*
|
|
12
|
+
* - `"TopicNameStrategy"` (default) — `${topic}-value` / `${topic}-key`.
|
|
13
|
+
* The conventional default; one schema per (topic, isKey) tuple.
|
|
14
|
+
* - `"RecordNameStrategy"` — `${recordName}`. Same record type can flow
|
|
15
|
+
* on multiple topics. Requires a `recordName` resolver.
|
|
16
|
+
* - `"TopicRecordNameStrategy"` — `${topic}-${recordName}`. Multiple record
|
|
17
|
+
* types per topic. Requires a `recordName` resolver.
|
|
18
|
+
*
|
|
19
|
+
* Set `subject` (function form) to override entirely.
|
|
20
|
+
*/
|
|
21
|
+
type SubjectNameStrategy = "TopicNameStrategy" | "RecordNameStrategy" | "TopicRecordNameStrategy";
|
|
9
22
|
/**
|
|
10
23
|
* The subset of a Confluent Schema Registry client this serializer uses. The
|
|
11
24
|
* `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.
|
|
@@ -27,29 +40,92 @@ interface SchemaRegistrySerializerOptions {
|
|
|
27
40
|
registry?: SchemaRegistryClient;
|
|
28
41
|
/** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */
|
|
29
42
|
host?: string;
|
|
30
|
-
/** Per-topic schema to register. Topics omitted here use the subject's latest. */
|
|
43
|
+
/** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */
|
|
31
44
|
schemas?: Record<string, SchemaSpec>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes
|
|
47
|
+
* the record key for the matching topic. Topics omitted here fall back
|
|
48
|
+
* to the subject's latest (or, with `autoRegister: false`, ALWAYS the
|
|
49
|
+
* subject's latest).
|
|
50
|
+
*/
|
|
51
|
+
keySchemas?: Record<string, SchemaSpec>;
|
|
52
|
+
/**
|
|
53
|
+
* Subject naming strategy preset. Default `"TopicNameStrategy"`.
|
|
54
|
+
* Setting `subject` (function) overrides this entirely.
|
|
55
|
+
*/
|
|
56
|
+
subjectStrategy?: SubjectNameStrategy;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the schema's record name (used by `RecordNameStrategy` and
|
|
59
|
+
* `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to
|
|
60
|
+
* one of those — throws on first serialize if absent.
|
|
61
|
+
*
|
|
62
|
+
* Typical implementation: read `${namespace}.${name}` from the avsc you
|
|
63
|
+
* already supply via `schemas` / `keySchemas`.
|
|
64
|
+
*/
|
|
65
|
+
recordName?: (record: OutboxRecord, isKey: boolean) => string;
|
|
66
|
+
/**
|
|
67
|
+
* Custom subject function — overrides BOTH `subjectStrategy` and
|
|
68
|
+
* `recordName`. Receives `(topic, isKey, record)` for full flexibility.
|
|
69
|
+
*
|
|
70
|
+
* Backwards-compatible with the single-argument legacy form
|
|
71
|
+
* `(topic) => string` — extra args are ignored by JavaScript.
|
|
72
|
+
*/
|
|
73
|
+
subject?: (topic: string, isKey?: boolean, record?: OutboxRecord) => string;
|
|
34
74
|
/** content-type header value. Default "application/vnd.confluent.avro". */
|
|
35
75
|
contentType?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Auto-register schemas when one is supplied via `schemas` / `keySchemas`.
|
|
78
|
+
* Default `true` — matches Confluent client behavior.
|
|
79
|
+
*
|
|
80
|
+
* Set to `false` for production clusters where schemas are managed
|
|
81
|
+
* out-of-band (Confluent Cloud, regulated environments). With
|
|
82
|
+
* autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`
|
|
83
|
+
* on the computed subject — and the locally-supplied schema bytes are
|
|
84
|
+
* ignored.
|
|
85
|
+
*/
|
|
86
|
+
autoRegister?: boolean;
|
|
36
87
|
}
|
|
37
88
|
/**
|
|
38
89
|
* A core {@link Serializer} that encodes payloads with a Confluent Schema Registry
|
|
39
90
|
* (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s
|
|
40
|
-
* `serializer` option. The schema id per topic is resolved once
|
|
91
|
+
* `serializer` option. The schema id per (topic, isKey) tuple is resolved once
|
|
92
|
+
* and cached.
|
|
93
|
+
*
|
|
94
|
+
* Also exposes `serializeKey(record)` for users who want Avro-encoded message
|
|
95
|
+
* keys — call it manually when building the publish path; the relay does NOT
|
|
96
|
+
* call it automatically (key encoding is application-level by convention).
|
|
41
97
|
*/
|
|
42
98
|
declare class SchemaRegistrySerializer implements Serializer {
|
|
43
99
|
readonly contentType: string;
|
|
44
100
|
private readonly schemas;
|
|
45
|
-
private readonly
|
|
101
|
+
private readonly keySchemas;
|
|
102
|
+
private readonly subjectStrategy;
|
|
103
|
+
private readonly recordName;
|
|
104
|
+
private readonly subjectFn;
|
|
46
105
|
private readonly host;
|
|
106
|
+
private readonly autoRegister;
|
|
47
107
|
private readonly idCache;
|
|
48
108
|
private registry;
|
|
49
109
|
constructor(opts: SchemaRegistrySerializerOptions);
|
|
50
110
|
serialize(record: OutboxRecord): Promise<Buffer>;
|
|
111
|
+
/**
|
|
112
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
113
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
114
|
+
* as the producer-side "no key" signal).
|
|
115
|
+
*
|
|
116
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
117
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
118
|
+
* UTF-8 strings.
|
|
119
|
+
*/
|
|
120
|
+
serializeKey(record: OutboxRecord): Promise<Buffer | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
123
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
124
|
+
*/
|
|
125
|
+
private resolveSubject;
|
|
126
|
+
private recordNameFor;
|
|
51
127
|
private schemaId;
|
|
52
128
|
private getRegistry;
|
|
53
129
|
}
|
|
54
130
|
|
|
55
|
-
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType };
|
|
131
|
+
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType, type SubjectNameStrategy };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,19 @@ interface SchemaSpec {
|
|
|
6
6
|
/** Schema definition string (avsc JSON / .proto / JSON Schema). */
|
|
7
7
|
schema: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Subject naming strategy. Mirrors Confluent's three built-ins:
|
|
11
|
+
*
|
|
12
|
+
* - `"TopicNameStrategy"` (default) — `${topic}-value` / `${topic}-key`.
|
|
13
|
+
* The conventional default; one schema per (topic, isKey) tuple.
|
|
14
|
+
* - `"RecordNameStrategy"` — `${recordName}`. Same record type can flow
|
|
15
|
+
* on multiple topics. Requires a `recordName` resolver.
|
|
16
|
+
* - `"TopicRecordNameStrategy"` — `${topic}-${recordName}`. Multiple record
|
|
17
|
+
* types per topic. Requires a `recordName` resolver.
|
|
18
|
+
*
|
|
19
|
+
* Set `subject` (function form) to override entirely.
|
|
20
|
+
*/
|
|
21
|
+
type SubjectNameStrategy = "TopicNameStrategy" | "RecordNameStrategy" | "TopicRecordNameStrategy";
|
|
9
22
|
/**
|
|
10
23
|
* The subset of a Confluent Schema Registry client this serializer uses. The
|
|
11
24
|
* `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.
|
|
@@ -27,29 +40,92 @@ interface SchemaRegistrySerializerOptions {
|
|
|
27
40
|
registry?: SchemaRegistryClient;
|
|
28
41
|
/** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */
|
|
29
42
|
host?: string;
|
|
30
|
-
/** Per-topic schema to register. Topics omitted here use the subject's latest. */
|
|
43
|
+
/** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */
|
|
31
44
|
schemas?: Record<string, SchemaSpec>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes
|
|
47
|
+
* the record key for the matching topic. Topics omitted here fall back
|
|
48
|
+
* to the subject's latest (or, with `autoRegister: false`, ALWAYS the
|
|
49
|
+
* subject's latest).
|
|
50
|
+
*/
|
|
51
|
+
keySchemas?: Record<string, SchemaSpec>;
|
|
52
|
+
/**
|
|
53
|
+
* Subject naming strategy preset. Default `"TopicNameStrategy"`.
|
|
54
|
+
* Setting `subject` (function) overrides this entirely.
|
|
55
|
+
*/
|
|
56
|
+
subjectStrategy?: SubjectNameStrategy;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the schema's record name (used by `RecordNameStrategy` and
|
|
59
|
+
* `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to
|
|
60
|
+
* one of those — throws on first serialize if absent.
|
|
61
|
+
*
|
|
62
|
+
* Typical implementation: read `${namespace}.${name}` from the avsc you
|
|
63
|
+
* already supply via `schemas` / `keySchemas`.
|
|
64
|
+
*/
|
|
65
|
+
recordName?: (record: OutboxRecord, isKey: boolean) => string;
|
|
66
|
+
/**
|
|
67
|
+
* Custom subject function — overrides BOTH `subjectStrategy` and
|
|
68
|
+
* `recordName`. Receives `(topic, isKey, record)` for full flexibility.
|
|
69
|
+
*
|
|
70
|
+
* Backwards-compatible with the single-argument legacy form
|
|
71
|
+
* `(topic) => string` — extra args are ignored by JavaScript.
|
|
72
|
+
*/
|
|
73
|
+
subject?: (topic: string, isKey?: boolean, record?: OutboxRecord) => string;
|
|
34
74
|
/** content-type header value. Default "application/vnd.confluent.avro". */
|
|
35
75
|
contentType?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Auto-register schemas when one is supplied via `schemas` / `keySchemas`.
|
|
78
|
+
* Default `true` — matches Confluent client behavior.
|
|
79
|
+
*
|
|
80
|
+
* Set to `false` for production clusters where schemas are managed
|
|
81
|
+
* out-of-band (Confluent Cloud, regulated environments). With
|
|
82
|
+
* autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`
|
|
83
|
+
* on the computed subject — and the locally-supplied schema bytes are
|
|
84
|
+
* ignored.
|
|
85
|
+
*/
|
|
86
|
+
autoRegister?: boolean;
|
|
36
87
|
}
|
|
37
88
|
/**
|
|
38
89
|
* A core {@link Serializer} that encodes payloads with a Confluent Schema Registry
|
|
39
90
|
* (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s
|
|
40
|
-
* `serializer` option. The schema id per topic is resolved once
|
|
91
|
+
* `serializer` option. The schema id per (topic, isKey) tuple is resolved once
|
|
92
|
+
* and cached.
|
|
93
|
+
*
|
|
94
|
+
* Also exposes `serializeKey(record)` for users who want Avro-encoded message
|
|
95
|
+
* keys — call it manually when building the publish path; the relay does NOT
|
|
96
|
+
* call it automatically (key encoding is application-level by convention).
|
|
41
97
|
*/
|
|
42
98
|
declare class SchemaRegistrySerializer implements Serializer {
|
|
43
99
|
readonly contentType: string;
|
|
44
100
|
private readonly schemas;
|
|
45
|
-
private readonly
|
|
101
|
+
private readonly keySchemas;
|
|
102
|
+
private readonly subjectStrategy;
|
|
103
|
+
private readonly recordName;
|
|
104
|
+
private readonly subjectFn;
|
|
46
105
|
private readonly host;
|
|
106
|
+
private readonly autoRegister;
|
|
47
107
|
private readonly idCache;
|
|
48
108
|
private registry;
|
|
49
109
|
constructor(opts: SchemaRegistrySerializerOptions);
|
|
50
110
|
serialize(record: OutboxRecord): Promise<Buffer>;
|
|
111
|
+
/**
|
|
112
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
113
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
114
|
+
* as the producer-side "no key" signal).
|
|
115
|
+
*
|
|
116
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
117
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
118
|
+
* UTF-8 strings.
|
|
119
|
+
*/
|
|
120
|
+
serializeKey(record: OutboxRecord): Promise<Buffer | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
123
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
124
|
+
*/
|
|
125
|
+
private resolveSubject;
|
|
126
|
+
private recordNameFor;
|
|
51
127
|
private schemaId;
|
|
52
128
|
private getRegistry;
|
|
53
129
|
}
|
|
54
130
|
|
|
55
|
-
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType };
|
|
131
|
+
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType, type SubjectNameStrategy };
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,13 @@ var DEFAULT_CONTENT_TYPE = "application/vnd.confluent.avro";
|
|
|
3
3
|
var SchemaRegistrySerializer = class {
|
|
4
4
|
contentType;
|
|
5
5
|
schemas;
|
|
6
|
-
|
|
6
|
+
keySchemas;
|
|
7
|
+
subjectStrategy;
|
|
8
|
+
recordName;
|
|
9
|
+
subjectFn;
|
|
7
10
|
host;
|
|
11
|
+
autoRegister;
|
|
12
|
+
// Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.
|
|
8
13
|
idCache = /* @__PURE__ */ new Map();
|
|
9
14
|
registry;
|
|
10
15
|
constructor(opts) {
|
|
@@ -16,25 +21,74 @@ var SchemaRegistrySerializer = class {
|
|
|
16
21
|
this.registry = opts.registry ?? null;
|
|
17
22
|
this.host = opts.host ?? null;
|
|
18
23
|
this.schemas = opts.schemas ?? {};
|
|
19
|
-
this.
|
|
24
|
+
this.keySchemas = opts.keySchemas ?? {};
|
|
25
|
+
this.subjectStrategy = opts.subjectStrategy ?? "TopicNameStrategy";
|
|
26
|
+
this.recordName = opts.recordName ?? null;
|
|
27
|
+
this.subjectFn = opts.subject ?? null;
|
|
20
28
|
this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
29
|
+
this.autoRegister = opts.autoRegister ?? true;
|
|
21
30
|
}
|
|
22
31
|
async serialize(record) {
|
|
23
32
|
const registry = await this.getRegistry();
|
|
24
|
-
const
|
|
33
|
+
const subject = this.resolveSubject(record, false);
|
|
34
|
+
const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);
|
|
25
35
|
return registry.encode(id, record.payload);
|
|
26
36
|
}
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
39
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
40
|
+
* as the producer-side "no key" signal).
|
|
41
|
+
*
|
|
42
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
43
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
44
|
+
* UTF-8 strings.
|
|
45
|
+
*/
|
|
46
|
+
async serializeKey(record) {
|
|
47
|
+
if (record.key === null || record.key === void 0) return null;
|
|
48
|
+
const registry = await this.getRegistry();
|
|
49
|
+
const subject = this.resolveSubject(record, true);
|
|
50
|
+
const id = await this.schemaId(
|
|
51
|
+
registry,
|
|
52
|
+
subject,
|
|
53
|
+
this.keySchemas[record.topic],
|
|
54
|
+
true,
|
|
55
|
+
record.topic
|
|
56
|
+
);
|
|
57
|
+
return registry.encode(id, record.key);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
61
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
62
|
+
*/
|
|
63
|
+
resolveSubject(record, isKey) {
|
|
64
|
+
if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);
|
|
65
|
+
switch (this.subjectStrategy) {
|
|
66
|
+
case "TopicNameStrategy":
|
|
67
|
+
return `${record.topic}-${isKey ? "key" : "value"}`;
|
|
68
|
+
case "RecordNameStrategy":
|
|
69
|
+
return this.recordNameFor(record, isKey);
|
|
70
|
+
case "TopicRecordNameStrategy":
|
|
71
|
+
return `${record.topic}-${this.recordNameFor(record, isKey)}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
recordNameFor(record, isKey) {
|
|
75
|
+
if (!this.recordName) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`SchemaRegistrySerializer: subjectStrategy "${this.subjectStrategy}" requires a \`recordName\` resolver.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return this.recordName(record, isKey);
|
|
81
|
+
}
|
|
82
|
+
schemaId(registry, subject, spec, isKey, topic) {
|
|
83
|
+
const cacheKey = `${topic}:${isKey ? "key" : "value"}`;
|
|
84
|
+
const cached = this.idCache.get(cacheKey);
|
|
29
85
|
if (cached) return cached;
|
|
30
|
-
const
|
|
31
|
-
const spec = this.schemas[topic];
|
|
32
|
-
const lookup = spec ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
86
|
+
const lookup = spec && this.autoRegister ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
33
87
|
const guarded = lookup.catch((err) => {
|
|
34
|
-
this.idCache.delete(
|
|
88
|
+
this.idCache.delete(cacheKey);
|
|
35
89
|
throw err;
|
|
36
90
|
});
|
|
37
|
-
this.idCache.set(
|
|
91
|
+
this.idCache.set(cacheKey, guarded);
|
|
38
92
|
return guarded;
|
|
39
93
|
}
|
|
40
94
|
async getRegistry() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serializer.ts"],"sourcesContent":["import type { OutboxRecord, Serializer } from \"@eventferry/core\";\n\nexport type SchemaType = \"AVRO\" | \"PROTOBUF\" | \"JSON\";\n\nexport interface SchemaSpec {\n type: SchemaType;\n /** Schema definition string (avsc JSON / .proto / JSON Schema). */\n schema: string;\n}\n\n/**\n * The subset of a Confluent Schema Registry client this serializer uses. The\n * `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.\n */\nexport interface SchemaRegistryClient {\n register(\n schema: { type: string; schema: string },\n opts?: { subject: string },\n ): Promise<{ id: number }>;\n getLatestSchemaId(subject: string): Promise<number>;\n encode(registryId: number, payload: unknown): Promise<Buffer>;\n}\n\nexport interface SchemaRegistrySerializerOptions {\n /** Inject a ready client (tests, custom config). */\n registry?: SchemaRegistryClient;\n /** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */\n host?: string;\n /** Per-topic schema to register. Topics omitted here use the subject's latest. */\n schemas?: Record<string, SchemaSpec>;\n /** Subject naming. Default TopicNameStrategy: `${topic}-value`. */\n subject?: (topic: string) => string;\n /** content-type header value. Default \"application/vnd.confluent.avro\". */\n contentType?: string;\n}\n\nconst DEFAULT_CONTENT_TYPE = \"application/vnd.confluent.avro\";\n\n/**\n * A core {@link Serializer} that encodes payloads with a Confluent Schema Registry\n * (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s\n * `serializer` option. The schema id per topic is resolved once and cached.\n */\nexport class SchemaRegistrySerializer implements Serializer {\n readonly contentType: string;\n private readonly schemas: Record<string, SchemaSpec>;\n private readonly subject: (topic: string) => string;\n private readonly host: string | null;\n private readonly idCache = new Map<string, Promise<number>>();\n private registry: SchemaRegistryClient | null;\n\n constructor(opts: SchemaRegistrySerializerOptions) {\n if (!opts.registry && !opts.host) {\n throw new Error(\n \"SchemaRegistrySerializer requires either a `registry` client or a `host`.\",\n );\n }\n this.registry = opts.registry ?? null;\n this.host = opts.host ?? null;\n this.schemas = opts.schemas ?? {};\n this.subject = opts.subject ?? ((topic) => `${topic}-value`);\n this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;\n }\n\n async serialize(record: OutboxRecord): Promise<Buffer> {\n const registry = await this.getRegistry();\n const id = await this.schemaId(registry, record.topic);\n return registry.encode(id, record.payload);\n }\n\n private schemaId(\n registry: SchemaRegistryClient,\n topic: string,\n ): Promise<number> {\n const cached = this.idCache.get(topic);\n if (cached) return cached;\n\n const subject = this.subject(topic);\n const spec = this.schemas[topic];\n const lookup = spec\n ? registry\n .register({ type: spec.type, schema: spec.schema }, { subject })\n .then((r) => r.id)\n : registry.getLatestSchemaId(subject);\n\n // Cache the in-flight promise so concurrent first calls don't double-register;\n // drop it on failure so a transient error can be retried.\n const guarded = lookup.catch((err) => {\n this.idCache.delete(topic);\n throw err;\n });\n this.idCache.set(topic, guarded);\n return guarded;\n }\n\n private async getRegistry(): Promise<SchemaRegistryClient> {\n if (this.registry) return this.registry;\n const mod = await importSchemaRegistry();\n this.registry = new mod.SchemaRegistry({ host: this.host as string });\n return this.registry;\n }\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (cfg: { host: string }) => SchemaRegistryClient;\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@kafkajs/confluent-schema-registry\")) as any;\n } catch {\n throw new Error(\n 'SchemaRegistrySerializer with `host` needs the \"@kafkajs/confluent-schema-registry\" package. Run: npm i @kafkajs/confluent-schema-registry',\n );\n }\n}\n"],"mappings":";AAoCA,IAAM,uBAAuB;AAOtB,IAAM,2BAAN,MAAqD;AAAA,EACjD;AAAA,EACQ;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,oBAAI,IAA6B;AAAA,EACpD;AAAA,EAER,YAAY,MAAuC;AACjD,QAAI,CAAC,KAAK,YAAY,CAAC,KAAK,MAAM;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,WAAW,KAAK,YAAY;AACjC,SAAK,OAAO,KAAK,QAAQ;AACzB,SAAK,UAAU,KAAK,WAAW,CAAC;AAChC,SAAK,UAAU,KAAK,YAAY,CAAC,UAAU,GAAG,KAAK;AACnD,SAAK,cAAc,KAAK,eAAe;AAAA,EACzC;AAAA,EAEA,MAAM,UAAU,QAAuC;AACrD,UAAM,WAAW,MAAM,KAAK,YAAY;AACxC,UAAM,KAAK,MAAM,KAAK,SAAS,UAAU,OAAO,KAAK;AACrD,WAAO,SAAS,OAAO,IAAI,OAAO,OAAO;AAAA,EAC3C;AAAA,EAEQ,SACN,UACA,OACiB;AACjB,UAAM,SAAS,KAAK,QAAQ,IAAI,KAAK;AACrC,QAAI,OAAQ,QAAO;AAEnB,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,UAAM,SAAS,OACX,SACG,SAAS,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,OAAO,GAAG,EAAE,QAAQ,CAAC,EAC9D,KAAK,CAAC,MAAM,EAAE,EAAE,IACnB,SAAS,kBAAkB,OAAO;AAItC,UAAM,UAAU,OAAO,MAAM,CAAC,QAAQ;AACpC,WAAK,QAAQ,OAAO,KAAK;AACzB,YAAM;AAAA,IACR,CAAC;AACD,SAAK,QAAQ,IAAI,OAAO,OAAO;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,cAA6C;AACzD,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,UAAM,MAAM,MAAM,qBAAqB;AACvC,SAAK,WAAW,IAAI,IAAI,eAAe,EAAE,MAAM,KAAK,KAAe,CAAC;AACpE,WAAO,KAAK;AAAA,EACd;AACF;AAEA,eAAe,uBAEZ;AACD,MAAI;AAEF,WAAQ,MAAM,OAAO,oCAAoC;AAAA,EAC3D,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/serializer.ts"],"sourcesContent":["import type { OutboxRecord, Serializer } from \"@eventferry/core\";\n\nexport type SchemaType = \"AVRO\" | \"PROTOBUF\" | \"JSON\";\n\nexport interface SchemaSpec {\n type: SchemaType;\n /** Schema definition string (avsc JSON / .proto / JSON Schema). */\n schema: string;\n}\n\n/**\n * Subject naming strategy. Mirrors Confluent's three built-ins:\n *\n * - `\"TopicNameStrategy\"` (default) — `${topic}-value` / `${topic}-key`.\n * The conventional default; one schema per (topic, isKey) tuple.\n * - `\"RecordNameStrategy\"` — `${recordName}`. Same record type can flow\n * on multiple topics. Requires a `recordName` resolver.\n * - `\"TopicRecordNameStrategy\"` — `${topic}-${recordName}`. Multiple record\n * types per topic. Requires a `recordName` resolver.\n *\n * Set `subject` (function form) to override entirely.\n */\nexport type SubjectNameStrategy =\n | \"TopicNameStrategy\"\n | \"RecordNameStrategy\"\n | \"TopicRecordNameStrategy\";\n\n/**\n * The subset of a Confluent Schema Registry client this serializer uses. The\n * `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.\n */\nexport interface SchemaRegistryClient {\n register(\n schema: { type: string; schema: string },\n opts?: { subject: string },\n ): Promise<{ id: number }>;\n getLatestSchemaId(subject: string): Promise<number>;\n encode(registryId: number, payload: unknown): Promise<Buffer>;\n}\n\nexport interface SchemaRegistrySerializerOptions {\n /** Inject a ready client (tests, custom config). */\n registry?: SchemaRegistryClient;\n /** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */\n host?: string;\n /** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */\n schemas?: Record<string, SchemaSpec>;\n /**\n * Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes\n * the record key for the matching topic. Topics omitted here fall back\n * to the subject's latest (or, with `autoRegister: false`, ALWAYS the\n * subject's latest).\n */\n keySchemas?: Record<string, SchemaSpec>;\n /**\n * Subject naming strategy preset. Default `\"TopicNameStrategy\"`.\n * Setting `subject` (function) overrides this entirely.\n */\n subjectStrategy?: SubjectNameStrategy;\n /**\n * Resolve the schema's record name (used by `RecordNameStrategy` and\n * `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to\n * one of those — throws on first serialize if absent.\n *\n * Typical implementation: read `${namespace}.${name}` from the avsc you\n * already supply via `schemas` / `keySchemas`.\n */\n recordName?: (record: OutboxRecord, isKey: boolean) => string;\n /**\n * Custom subject function — overrides BOTH `subjectStrategy` and\n * `recordName`. Receives `(topic, isKey, record)` for full flexibility.\n *\n * Backwards-compatible with the single-argument legacy form\n * `(topic) => string` — extra args are ignored by JavaScript.\n */\n subject?: (\n topic: string,\n isKey?: boolean,\n record?: OutboxRecord,\n ) => string;\n /** content-type header value. Default \"application/vnd.confluent.avro\". */\n contentType?: string;\n /**\n * Auto-register schemas when one is supplied via `schemas` / `keySchemas`.\n * Default `true` — matches Confluent client behavior.\n *\n * Set to `false` for production clusters where schemas are managed\n * out-of-band (Confluent Cloud, regulated environments). With\n * autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`\n * on the computed subject — and the locally-supplied schema bytes are\n * ignored.\n */\n autoRegister?: boolean;\n}\n\nconst DEFAULT_CONTENT_TYPE = \"application/vnd.confluent.avro\";\n\n/**\n * A core {@link Serializer} that encodes payloads with a Confluent Schema Registry\n * (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s\n * `serializer` option. The schema id per (topic, isKey) tuple is resolved once\n * and cached.\n *\n * Also exposes `serializeKey(record)` for users who want Avro-encoded message\n * keys — call it manually when building the publish path; the relay does NOT\n * call it automatically (key encoding is application-level by convention).\n */\nexport class SchemaRegistrySerializer implements Serializer {\n readonly contentType: string;\n private readonly schemas: Record<string, SchemaSpec>;\n private readonly keySchemas: Record<string, SchemaSpec>;\n private readonly subjectStrategy: SubjectNameStrategy;\n private readonly recordName:\n | ((record: OutboxRecord, isKey: boolean) => string)\n | null;\n private readonly subjectFn:\n | ((topic: string, isKey?: boolean, record?: OutboxRecord) => string)\n | null;\n private readonly host: string | null;\n private readonly autoRegister: boolean;\n // Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.\n private readonly idCache = new Map<string, Promise<number>>();\n private registry: SchemaRegistryClient | null;\n\n constructor(opts: SchemaRegistrySerializerOptions) {\n if (!opts.registry && !opts.host) {\n throw new Error(\n \"SchemaRegistrySerializer requires either a `registry` client or a `host`.\",\n );\n }\n this.registry = opts.registry ?? null;\n this.host = opts.host ?? null;\n this.schemas = opts.schemas ?? {};\n this.keySchemas = opts.keySchemas ?? {};\n this.subjectStrategy = opts.subjectStrategy ?? \"TopicNameStrategy\";\n this.recordName = opts.recordName ?? null;\n this.subjectFn = opts.subject ?? null;\n this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;\n this.autoRegister = opts.autoRegister ?? true;\n }\n\n async serialize(record: OutboxRecord): Promise<Buffer> {\n const registry = await this.getRegistry();\n const subject = this.resolveSubject(record, false);\n const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);\n return registry.encode(id, record.payload);\n }\n\n /**\n * Avro-encode the record's KEY using the registered key schema. Returns\n * `null` when the record has no key (kafkajs/confluent treat null keys\n * as the producer-side \"no key\" signal).\n *\n * Not part of the core `Serializer` interface — callers wire it into\n * their publish path manually when they want Avro keys instead of raw\n * UTF-8 strings.\n */\n async serializeKey(record: OutboxRecord): Promise<Buffer | null> {\n if (record.key === null || record.key === undefined) return null;\n const registry = await this.getRegistry();\n const subject = this.resolveSubject(record, true);\n const id = await this.schemaId(\n registry,\n subject,\n this.keySchemas[record.topic],\n true,\n record.topic,\n );\n return registry.encode(id, record.key);\n }\n\n /**\n * Resolve the subject for this (record, isKey) tuple. Order of\n * precedence: explicit `subject` function → `subjectStrategy` preset.\n */\n private resolveSubject(record: OutboxRecord, isKey: boolean): string {\n if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);\n switch (this.subjectStrategy) {\n case \"TopicNameStrategy\":\n return `${record.topic}-${isKey ? \"key\" : \"value\"}`;\n case \"RecordNameStrategy\":\n return this.recordNameFor(record, isKey);\n case \"TopicRecordNameStrategy\":\n return `${record.topic}-${this.recordNameFor(record, isKey)}`;\n }\n }\n\n private recordNameFor(record: OutboxRecord, isKey: boolean): string {\n if (!this.recordName) {\n throw new Error(\n `SchemaRegistrySerializer: subjectStrategy \"${this.subjectStrategy}\" requires a \\`recordName\\` resolver.`,\n );\n }\n return this.recordName(record, isKey);\n }\n\n private schemaId(\n registry: SchemaRegistryClient,\n subject: string,\n spec: SchemaSpec | undefined,\n isKey: boolean,\n topic: string,\n ): Promise<number> {\n const cacheKey = `${topic}:${isKey ? \"key\" : \"value\"}`;\n const cached = this.idCache.get(cacheKey);\n if (cached) return cached;\n\n // autoRegister=false → ALWAYS resolve by latest; the local spec\n // (if any) is ignored. Matches Confluent's auto.register.schemas=false.\n const lookup =\n spec && this.autoRegister\n ? registry\n .register({ type: spec.type, schema: spec.schema }, { subject })\n .then((r) => r.id)\n : registry.getLatestSchemaId(subject);\n\n // Cache the in-flight promise so concurrent first calls don't double-register;\n // drop it on failure so a transient error can be retried.\n const guarded = lookup.catch((err) => {\n this.idCache.delete(cacheKey);\n throw err;\n });\n this.idCache.set(cacheKey, guarded);\n return guarded;\n }\n\n private async getRegistry(): Promise<SchemaRegistryClient> {\n if (this.registry) return this.registry;\n const mod = await importSchemaRegistry();\n this.registry = new mod.SchemaRegistry({ host: this.host as string });\n return this.registry;\n }\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (cfg: { host: string }) => SchemaRegistryClient;\n}> {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (await import(\"@kafkajs/confluent-schema-registry\")) as any;\n } catch {\n throw new Error(\n 'SchemaRegistrySerializer with `host` needs the \"@kafkajs/confluent-schema-registry\" package. Run: npm i @kafkajs/confluent-schema-registry',\n );\n }\n}\n"],"mappings":"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","names":[]}
|