@eventferry/schema-registry 3.2.3 → 3.4.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 +29 -0
- package/README.md +89 -0
- package/dist/index.cjs +93 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -6
- package/dist/index.d.ts +136 -6
- package/dist/index.js +91 -12
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @eventferry/schema-registry
|
|
2
2
|
|
|
3
|
+
## 3.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0f328d2: Typed authentication for the Schema Registry HTTP API. New `auth?: SchemaRegistryAuth` option on `SchemaRegistrySerializer` with two shapes:
|
|
8
|
+
|
|
9
|
+
- `{ type: "basic", username, password }` — HTTP Basic, forwarded straight to the underlying client's `auth` config (the conventional Confluent Cloud + commercial-registry shape).
|
|
10
|
+
- `{ type: "bearer", token: string | () => string | Promise<string> }` — adds an `Authorization: Bearer <token>` header via a small middleware on the upstream client. Callable tokens are resolved on **every** request, so rotation logic lives in the caller's provider (cache inside your callable if rotation cost matters).
|
|
11
|
+
|
|
12
|
+
Ignored when an already-constructed `registry` client is injected — configure auth there yourself. mTLS to the registry stays out of scope: it's handled by a custom `https.Agent` on a self-constructed client (registry TLS is independent of broker TLS, and we don't want to fold an unrelated knob into this surface).
|
|
13
|
+
|
|
14
|
+
The middleware factory is exported as `bearerAuthMiddleware` for testing only — not part of the supported public API and may move in a future version.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [715523f]
|
|
19
|
+
- Updated dependencies [fb0549d]
|
|
20
|
+
- @eventferry/core@3.4.0
|
|
21
|
+
|
|
22
|
+
## 3.3.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- d983d90: Schema Registry serializer gains three production-grade controls without breaking the existing API:
|
|
27
|
+
|
|
28
|
+
- **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).
|
|
29
|
+
- **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.
|
|
30
|
+
- **`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.
|
|
31
|
+
|
|
3
32
|
## 3.2.3
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -32,6 +32,95 @@ 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
|
+
## Authentication
|
|
36
|
+
|
|
37
|
+
The serializer accepts the two HTTP auth shapes Confluent Schema Registry installations use:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// HTTP Basic — Confluent Cloud + most commercial registries.
|
|
41
|
+
new SchemaRegistrySerializer({
|
|
42
|
+
host,
|
|
43
|
+
auth: { type: "basic", username: "<api-key>", password: "<api-secret>" },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Bearer token — OIDC, custom SR proxies, etc.
|
|
47
|
+
new SchemaRegistrySerializer({
|
|
48
|
+
host,
|
|
49
|
+
auth: { type: "bearer", token: "eyJhbGc..." },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Rotating bearer token (refresh per request — cache inside your provider).
|
|
53
|
+
new SchemaRegistrySerializer({
|
|
54
|
+
host,
|
|
55
|
+
auth: {
|
|
56
|
+
type: "bearer",
|
|
57
|
+
token: async () => await getCachedAccessToken(),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Bearer tokens are injected via a small middleware on the underlying client — the provider is invoked on **every** request, so you control rotation. `auth` is ignored when you pass an already-constructed `registry` client (configure auth there yourself).
|
|
63
|
+
|
|
64
|
+
For **mTLS** to the registry, supply a custom `https.Agent` on a self-constructed client and pass it via the `registry` option. The serializer does not surface a separate `tls` block — registry TLS is independent of broker TLS and `https.Agent` is the standard Node entry point.
|
|
65
|
+
|
|
66
|
+
## Subject naming strategies
|
|
67
|
+
|
|
68
|
+
Pick one of Confluent's three built-in strategies (default `TopicNameStrategy`):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
new SchemaRegistrySerializer({
|
|
72
|
+
host,
|
|
73
|
+
subjectStrategy: "RecordNameStrategy", // subject = recordName
|
|
74
|
+
// or: "TopicRecordNameStrategy" // subject = `${topic}-${recordName}`
|
|
75
|
+
recordName: (record) => `com.example.${record.aggregateType}.Created`,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`recordName` is required for the `RecordName` and `TopicRecordName` strategies — typical implementation reads `${namespace}.${name}` from your avsc.
|
|
80
|
+
|
|
81
|
+
Need full control? Skip the preset and pass an explicit `subject` function:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
new SchemaRegistrySerializer({
|
|
85
|
+
host,
|
|
86
|
+
subject: (topic, isKey, record) => `acme.${topic}.${isKey ? "key" : "value"}`,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Avro key serialization
|
|
91
|
+
|
|
92
|
+
Configure per-topic key schemas and call `serializeKey` from your publish path:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const serializer = new SchemaRegistrySerializer({
|
|
96
|
+
host,
|
|
97
|
+
schemas: { "orders.created": { type: "AVRO", schema: valueAvsc } },
|
|
98
|
+
keySchemas: { "orders.created": { type: "AVRO", schema: keyAvsc } },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Inside your custom publish glue:
|
|
102
|
+
const encodedValue = await serializer.serialize(record);
|
|
103
|
+
const encodedKey = await serializer.serializeKey(record); // null when record.key is null
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
Key and value subjects cache their schema ids independently — registering one doesn't affect the other.
|
|
109
|
+
|
|
110
|
+
## Auto-register toggle
|
|
111
|
+
|
|
112
|
+
For production clusters where schemas are managed out-of-band (Confluent Cloud, regulated environments), turn auto-registration off:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
new SchemaRegistrySerializer({
|
|
116
|
+
host,
|
|
117
|
+
schemas: { /* ignored when autoRegister is false */ },
|
|
118
|
+
autoRegister: false,
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
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`.
|
|
123
|
+
|
|
35
124
|
📖 **Full documentation:** [github.com/SametGoktepe/eventferry](https://github.com/SametGoktepe/eventferry#readme)
|
|
36
125
|
|
|
37
126
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
SchemaRegistrySerializer: () => SchemaRegistrySerializer
|
|
33
|
+
SchemaRegistrySerializer: () => SchemaRegistrySerializer,
|
|
34
|
+
bearerAuthMiddleware: () => bearerAuthMiddleware
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
36
37
|
|
|
@@ -39,8 +40,14 @@ var DEFAULT_CONTENT_TYPE = "application/vnd.confluent.avro";
|
|
|
39
40
|
var SchemaRegistrySerializer = class {
|
|
40
41
|
contentType;
|
|
41
42
|
schemas;
|
|
42
|
-
|
|
43
|
+
keySchemas;
|
|
44
|
+
subjectStrategy;
|
|
45
|
+
recordName;
|
|
46
|
+
subjectFn;
|
|
43
47
|
host;
|
|
48
|
+
auth;
|
|
49
|
+
autoRegister;
|
|
50
|
+
// Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.
|
|
44
51
|
idCache = /* @__PURE__ */ new Map();
|
|
45
52
|
registry;
|
|
46
53
|
constructor(opts) {
|
|
@@ -51,35 +58,107 @@ var SchemaRegistrySerializer = class {
|
|
|
51
58
|
}
|
|
52
59
|
this.registry = opts.registry ?? null;
|
|
53
60
|
this.host = opts.host ?? null;
|
|
61
|
+
this.auth = opts.auth ?? null;
|
|
54
62
|
this.schemas = opts.schemas ?? {};
|
|
55
|
-
this.
|
|
63
|
+
this.keySchemas = opts.keySchemas ?? {};
|
|
64
|
+
this.subjectStrategy = opts.subjectStrategy ?? "TopicNameStrategy";
|
|
65
|
+
this.recordName = opts.recordName ?? null;
|
|
66
|
+
this.subjectFn = opts.subject ?? null;
|
|
56
67
|
this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
68
|
+
this.autoRegister = opts.autoRegister ?? true;
|
|
57
69
|
}
|
|
58
70
|
async serialize(record) {
|
|
59
71
|
const registry = await this.getRegistry();
|
|
60
|
-
const
|
|
72
|
+
const subject = this.resolveSubject(record, false);
|
|
73
|
+
const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);
|
|
61
74
|
return registry.encode(id, record.payload);
|
|
62
75
|
}
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
78
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
79
|
+
* as the producer-side "no key" signal).
|
|
80
|
+
*
|
|
81
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
82
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
83
|
+
* UTF-8 strings.
|
|
84
|
+
*/
|
|
85
|
+
async serializeKey(record) {
|
|
86
|
+
if (record.key === null || record.key === void 0) return null;
|
|
87
|
+
const registry = await this.getRegistry();
|
|
88
|
+
const subject = this.resolveSubject(record, true);
|
|
89
|
+
const id = await this.schemaId(
|
|
90
|
+
registry,
|
|
91
|
+
subject,
|
|
92
|
+
this.keySchemas[record.topic],
|
|
93
|
+
true,
|
|
94
|
+
record.topic
|
|
95
|
+
);
|
|
96
|
+
return registry.encode(id, record.key);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
100
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
101
|
+
*/
|
|
102
|
+
resolveSubject(record, isKey) {
|
|
103
|
+
if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);
|
|
104
|
+
switch (this.subjectStrategy) {
|
|
105
|
+
case "TopicNameStrategy":
|
|
106
|
+
return `${record.topic}-${isKey ? "key" : "value"}`;
|
|
107
|
+
case "RecordNameStrategy":
|
|
108
|
+
return this.recordNameFor(record, isKey);
|
|
109
|
+
case "TopicRecordNameStrategy":
|
|
110
|
+
return `${record.topic}-${this.recordNameFor(record, isKey)}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
recordNameFor(record, isKey) {
|
|
114
|
+
if (!this.recordName) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`SchemaRegistrySerializer: subjectStrategy "${this.subjectStrategy}" requires a \`recordName\` resolver.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return this.recordName(record, isKey);
|
|
120
|
+
}
|
|
121
|
+
schemaId(registry, subject, spec, isKey, topic) {
|
|
122
|
+
const cacheKey = `${topic}:${isKey ? "key" : "value"}`;
|
|
123
|
+
const cached = this.idCache.get(cacheKey);
|
|
65
124
|
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);
|
|
125
|
+
const lookup = spec && this.autoRegister ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
69
126
|
const guarded = lookup.catch((err) => {
|
|
70
|
-
this.idCache.delete(
|
|
127
|
+
this.idCache.delete(cacheKey);
|
|
71
128
|
throw err;
|
|
72
129
|
});
|
|
73
|
-
this.idCache.set(
|
|
130
|
+
this.idCache.set(cacheKey, guarded);
|
|
74
131
|
return guarded;
|
|
75
132
|
}
|
|
76
133
|
async getRegistry() {
|
|
77
134
|
if (this.registry) return this.registry;
|
|
78
135
|
const mod = await importSchemaRegistry();
|
|
79
|
-
|
|
136
|
+
const cfg = {
|
|
137
|
+
host: this.host
|
|
138
|
+
};
|
|
139
|
+
if (this.auth) {
|
|
140
|
+
if (this.auth.type === "basic") {
|
|
141
|
+
cfg.auth = {
|
|
142
|
+
username: this.auth.username,
|
|
143
|
+
password: this.auth.password
|
|
144
|
+
};
|
|
145
|
+
} else {
|
|
146
|
+
cfg.middlewares = [bearerAuthMiddleware(this.auth.token)];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.registry = new mod.SchemaRegistry(cfg);
|
|
80
150
|
return this.registry;
|
|
81
151
|
}
|
|
82
152
|
};
|
|
153
|
+
function bearerAuthMiddleware(token) {
|
|
154
|
+
return () => ({
|
|
155
|
+
async prepareRequest(next) {
|
|
156
|
+
const request = await next();
|
|
157
|
+
const value = typeof token === "function" ? await token() : token;
|
|
158
|
+
return request.enhance({ headers: { Authorization: `Bearer ${value}` } });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
83
162
|
async function importSchemaRegistry() {
|
|
84
163
|
try {
|
|
85
164
|
return await import("@kafkajs/confluent-schema-registry");
|
|
@@ -91,6 +170,7 @@ async function importSchemaRegistry() {
|
|
|
91
170
|
}
|
|
92
171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
93
172
|
0 && (module.exports = {
|
|
94
|
-
SchemaRegistrySerializer
|
|
173
|
+
SchemaRegistrySerializer,
|
|
174
|
+
bearerAuthMiddleware
|
|
95
175
|
});
|
|
96
176
|
//# sourceMappingURL=index.cjs.map
|
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 * Authentication for the Schema Registry HTTP API.\n *\n * - `\"basic\"` — HTTP Basic Auth. The shape Confluent Cloud and most\n * commercial registries use; passed straight through to the underlying\n * client's `auth` config.\n * - `\"bearer\"` — `Authorization: Bearer <token>` header. The `token`\n * field accepts either a static string OR a callable that resolves a\n * fresh token on every request (cache inside your callable to amortise\n * cost; we don't memoize for you, so OAuth refresh-loop logic lives\n * on your side).\n *\n * mTLS for the registry connection itself is handled by Node's `tls`\n * stack — supply a custom `https.Agent` via the upstream client's\n * `agent` option (use the `registry` injection here and configure it\n * yourself), separate from the broker TLS the publisher uses.\n */\nexport type SchemaRegistryAuth =\n | { type: \"basic\"; username: string; password: string }\n | {\n type: \"bearer\";\n token: string | (() => string | Promise<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 /**\n * Optional authentication for the Schema Registry HTTP API. See\n * {@link SchemaRegistryAuth} for the two supported shapes. Ignored\n * when `registry` is provided (configure auth on the injected client\n * yourself in that case).\n */\n auth?: SchemaRegistryAuth;\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 auth: SchemaRegistryAuth | 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.auth = opts.auth ?? 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 const cfg: SchemaRegistryConstructorConfig = {\n host: this.host as string,\n };\n if (this.auth) {\n if (this.auth.type === \"basic\") {\n // Confluent SR client accepts `auth: { username, password }`\n // and the mappersmith basic-auth middleware builds the header.\n cfg.auth = {\n username: this.auth.username,\n password: this.auth.password,\n };\n } else {\n // Bearer: SR doesn't ship a built-in middleware. Inject our own\n // so every API call carries `Authorization: Bearer <token>`.\n cfg.middlewares = [bearerAuthMiddleware(this.auth.token)];\n }\n }\n this.registry = new mod.SchemaRegistry(cfg);\n return this.registry;\n }\n}\n\n/**\n * Mappersmith middleware shape — the structural subset the Confluent\n * SR client passes through. We don't depend on mappersmith types\n * directly so the package compiles without the optional peer installed.\n */\ninterface MmRequest {\n enhance(args: { headers?: Record<string, string> }): MmRequest;\n}\n\n/**\n * Build a mappersmith middleware that adds `Authorization: Bearer <token>`.\n *\n * Exported for testing only — not part of the public API surface. The\n * middleware resolves the token on EVERY request, so callable token\n * providers can rotate without re-constructing the serializer. Cache\n * inside your provider if rotation cost matters.\n */\nexport function bearerAuthMiddleware(\n token: string | (() => string | Promise<string>),\n) {\n return () => ({\n async prepareRequest(next: () => Promise<MmRequest>): Promise<MmRequest> {\n const request = await next();\n const value = typeof token === \"function\" ? await token() : token;\n return request.enhance({ headers: { Authorization: `Bearer ${value}` } });\n },\n });\n}\n\n/**\n * Structural shape the SR client's constructor accepts. Mirrors\n * `SchemaRegistryAPIClientArgs` from the upstream package without\n * importing the type directly (optional peer).\n */\ninterface SchemaRegistryConstructorConfig {\n host: string;\n auth?: { username: string; password: string };\n middlewares?: Array<() => unknown>;\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (\n cfg: SchemaRegistryConstructorConfig,\n ) => 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,44 @@ 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";
|
|
22
|
+
/**
|
|
23
|
+
* Authentication for the Schema Registry HTTP API.
|
|
24
|
+
*
|
|
25
|
+
* - `"basic"` — HTTP Basic Auth. The shape Confluent Cloud and most
|
|
26
|
+
* commercial registries use; passed straight through to the underlying
|
|
27
|
+
* client's `auth` config.
|
|
28
|
+
* - `"bearer"` — `Authorization: Bearer <token>` header. The `token`
|
|
29
|
+
* field accepts either a static string OR a callable that resolves a
|
|
30
|
+
* fresh token on every request (cache inside your callable to amortise
|
|
31
|
+
* cost; we don't memoize for you, so OAuth refresh-loop logic lives
|
|
32
|
+
* on your side).
|
|
33
|
+
*
|
|
34
|
+
* mTLS for the registry connection itself is handled by Node's `tls`
|
|
35
|
+
* stack — supply a custom `https.Agent` via the upstream client's
|
|
36
|
+
* `agent` option (use the `registry` injection here and configure it
|
|
37
|
+
* yourself), separate from the broker TLS the publisher uses.
|
|
38
|
+
*/
|
|
39
|
+
type SchemaRegistryAuth = {
|
|
40
|
+
type: "basic";
|
|
41
|
+
username: string;
|
|
42
|
+
password: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "bearer";
|
|
45
|
+
token: string | (() => string | Promise<string>);
|
|
46
|
+
};
|
|
9
47
|
/**
|
|
10
48
|
* The subset of a Confluent Schema Registry client this serializer uses. The
|
|
11
49
|
* `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.
|
|
@@ -27,29 +65,121 @@ interface SchemaRegistrySerializerOptions {
|
|
|
27
65
|
registry?: SchemaRegistryClient;
|
|
28
66
|
/** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */
|
|
29
67
|
host?: string;
|
|
30
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Optional authentication for the Schema Registry HTTP API. See
|
|
70
|
+
* {@link SchemaRegistryAuth} for the two supported shapes. Ignored
|
|
71
|
+
* when `registry` is provided (configure auth on the injected client
|
|
72
|
+
* yourself in that case).
|
|
73
|
+
*/
|
|
74
|
+
auth?: SchemaRegistryAuth;
|
|
75
|
+
/** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */
|
|
31
76
|
schemas?: Record<string, SchemaSpec>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes
|
|
79
|
+
* the record key for the matching topic. Topics omitted here fall back
|
|
80
|
+
* to the subject's latest (or, with `autoRegister: false`, ALWAYS the
|
|
81
|
+
* subject's latest).
|
|
82
|
+
*/
|
|
83
|
+
keySchemas?: Record<string, SchemaSpec>;
|
|
84
|
+
/**
|
|
85
|
+
* Subject naming strategy preset. Default `"TopicNameStrategy"`.
|
|
86
|
+
* Setting `subject` (function) overrides this entirely.
|
|
87
|
+
*/
|
|
88
|
+
subjectStrategy?: SubjectNameStrategy;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the schema's record name (used by `RecordNameStrategy` and
|
|
91
|
+
* `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to
|
|
92
|
+
* one of those — throws on first serialize if absent.
|
|
93
|
+
*
|
|
94
|
+
* Typical implementation: read `${namespace}.${name}` from the avsc you
|
|
95
|
+
* already supply via `schemas` / `keySchemas`.
|
|
96
|
+
*/
|
|
97
|
+
recordName?: (record: OutboxRecord, isKey: boolean) => string;
|
|
98
|
+
/**
|
|
99
|
+
* Custom subject function — overrides BOTH `subjectStrategy` and
|
|
100
|
+
* `recordName`. Receives `(topic, isKey, record)` for full flexibility.
|
|
101
|
+
*
|
|
102
|
+
* Backwards-compatible with the single-argument legacy form
|
|
103
|
+
* `(topic) => string` — extra args are ignored by JavaScript.
|
|
104
|
+
*/
|
|
105
|
+
subject?: (topic: string, isKey?: boolean, record?: OutboxRecord) => string;
|
|
34
106
|
/** content-type header value. Default "application/vnd.confluent.avro". */
|
|
35
107
|
contentType?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Auto-register schemas when one is supplied via `schemas` / `keySchemas`.
|
|
110
|
+
* Default `true` — matches Confluent client behavior.
|
|
111
|
+
*
|
|
112
|
+
* Set to `false` for production clusters where schemas are managed
|
|
113
|
+
* out-of-band (Confluent Cloud, regulated environments). With
|
|
114
|
+
* autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`
|
|
115
|
+
* on the computed subject — and the locally-supplied schema bytes are
|
|
116
|
+
* ignored.
|
|
117
|
+
*/
|
|
118
|
+
autoRegister?: boolean;
|
|
36
119
|
}
|
|
37
120
|
/**
|
|
38
121
|
* A core {@link Serializer} that encodes payloads with a Confluent Schema Registry
|
|
39
122
|
* (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s
|
|
40
|
-
* `serializer` option. The schema id per topic is resolved once
|
|
123
|
+
* `serializer` option. The schema id per (topic, isKey) tuple is resolved once
|
|
124
|
+
* and cached.
|
|
125
|
+
*
|
|
126
|
+
* Also exposes `serializeKey(record)` for users who want Avro-encoded message
|
|
127
|
+
* keys — call it manually when building the publish path; the relay does NOT
|
|
128
|
+
* call it automatically (key encoding is application-level by convention).
|
|
41
129
|
*/
|
|
42
130
|
declare class SchemaRegistrySerializer implements Serializer {
|
|
43
131
|
readonly contentType: string;
|
|
44
132
|
private readonly schemas;
|
|
45
|
-
private readonly
|
|
133
|
+
private readonly keySchemas;
|
|
134
|
+
private readonly subjectStrategy;
|
|
135
|
+
private readonly recordName;
|
|
136
|
+
private readonly subjectFn;
|
|
46
137
|
private readonly host;
|
|
138
|
+
private readonly auth;
|
|
139
|
+
private readonly autoRegister;
|
|
47
140
|
private readonly idCache;
|
|
48
141
|
private registry;
|
|
49
142
|
constructor(opts: SchemaRegistrySerializerOptions);
|
|
50
143
|
serialize(record: OutboxRecord): Promise<Buffer>;
|
|
144
|
+
/**
|
|
145
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
146
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
147
|
+
* as the producer-side "no key" signal).
|
|
148
|
+
*
|
|
149
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
150
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
151
|
+
* UTF-8 strings.
|
|
152
|
+
*/
|
|
153
|
+
serializeKey(record: OutboxRecord): Promise<Buffer | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
156
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
157
|
+
*/
|
|
158
|
+
private resolveSubject;
|
|
159
|
+
private recordNameFor;
|
|
51
160
|
private schemaId;
|
|
52
161
|
private getRegistry;
|
|
53
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Mappersmith middleware shape — the structural subset the Confluent
|
|
165
|
+
* SR client passes through. We don't depend on mappersmith types
|
|
166
|
+
* directly so the package compiles without the optional peer installed.
|
|
167
|
+
*/
|
|
168
|
+
interface MmRequest {
|
|
169
|
+
enhance(args: {
|
|
170
|
+
headers?: Record<string, string>;
|
|
171
|
+
}): MmRequest;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build a mappersmith middleware that adds `Authorization: Bearer <token>`.
|
|
175
|
+
*
|
|
176
|
+
* Exported for testing only — not part of the public API surface. The
|
|
177
|
+
* middleware resolves the token on EVERY request, so callable token
|
|
178
|
+
* providers can rotate without re-constructing the serializer. Cache
|
|
179
|
+
* inside your provider if rotation cost matters.
|
|
180
|
+
*/
|
|
181
|
+
declare function bearerAuthMiddleware(token: string | (() => string | Promise<string>)): () => {
|
|
182
|
+
prepareRequest(next: () => Promise<MmRequest>): Promise<MmRequest>;
|
|
183
|
+
};
|
|
54
184
|
|
|
55
|
-
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType };
|
|
185
|
+
export { type SchemaRegistryAuth, type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType, type SubjectNameStrategy, bearerAuthMiddleware };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,44 @@ 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";
|
|
22
|
+
/**
|
|
23
|
+
* Authentication for the Schema Registry HTTP API.
|
|
24
|
+
*
|
|
25
|
+
* - `"basic"` — HTTP Basic Auth. The shape Confluent Cloud and most
|
|
26
|
+
* commercial registries use; passed straight through to the underlying
|
|
27
|
+
* client's `auth` config.
|
|
28
|
+
* - `"bearer"` — `Authorization: Bearer <token>` header. The `token`
|
|
29
|
+
* field accepts either a static string OR a callable that resolves a
|
|
30
|
+
* fresh token on every request (cache inside your callable to amortise
|
|
31
|
+
* cost; we don't memoize for you, so OAuth refresh-loop logic lives
|
|
32
|
+
* on your side).
|
|
33
|
+
*
|
|
34
|
+
* mTLS for the registry connection itself is handled by Node's `tls`
|
|
35
|
+
* stack — supply a custom `https.Agent` via the upstream client's
|
|
36
|
+
* `agent` option (use the `registry` injection here and configure it
|
|
37
|
+
* yourself), separate from the broker TLS the publisher uses.
|
|
38
|
+
*/
|
|
39
|
+
type SchemaRegistryAuth = {
|
|
40
|
+
type: "basic";
|
|
41
|
+
username: string;
|
|
42
|
+
password: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "bearer";
|
|
45
|
+
token: string | (() => string | Promise<string>);
|
|
46
|
+
};
|
|
9
47
|
/**
|
|
10
48
|
* The subset of a Confluent Schema Registry client this serializer uses. The
|
|
11
49
|
* `@kafkajs/confluent-schema-registry` `SchemaRegistry` satisfies it structurally.
|
|
@@ -27,29 +65,121 @@ interface SchemaRegistrySerializerOptions {
|
|
|
27
65
|
registry?: SchemaRegistryClient;
|
|
28
66
|
/** Or construct one from a host (requires @kafkajs/confluent-schema-registry). */
|
|
29
67
|
host?: string;
|
|
30
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Optional authentication for the Schema Registry HTTP API. See
|
|
70
|
+
* {@link SchemaRegistryAuth} for the two supported shapes. Ignored
|
|
71
|
+
* when `registry` is provided (configure auth on the injected client
|
|
72
|
+
* yourself in that case).
|
|
73
|
+
*/
|
|
74
|
+
auth?: SchemaRegistryAuth;
|
|
75
|
+
/** Per-topic VALUE schema to register. Topics omitted here use the subject's latest. */
|
|
31
76
|
schemas?: Record<string, SchemaSpec>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Per-topic KEY schema. When set, `serializeKey(record)` Avro-encodes
|
|
79
|
+
* the record key for the matching topic. Topics omitted here fall back
|
|
80
|
+
* to the subject's latest (or, with `autoRegister: false`, ALWAYS the
|
|
81
|
+
* subject's latest).
|
|
82
|
+
*/
|
|
83
|
+
keySchemas?: Record<string, SchemaSpec>;
|
|
84
|
+
/**
|
|
85
|
+
* Subject naming strategy preset. Default `"TopicNameStrategy"`.
|
|
86
|
+
* Setting `subject` (function) overrides this entirely.
|
|
87
|
+
*/
|
|
88
|
+
subjectStrategy?: SubjectNameStrategy;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the schema's record name (used by `RecordNameStrategy` and
|
|
91
|
+
* `TopicRecordNameStrategy`). REQUIRED when `subjectStrategy` is set to
|
|
92
|
+
* one of those — throws on first serialize if absent.
|
|
93
|
+
*
|
|
94
|
+
* Typical implementation: read `${namespace}.${name}` from the avsc you
|
|
95
|
+
* already supply via `schemas` / `keySchemas`.
|
|
96
|
+
*/
|
|
97
|
+
recordName?: (record: OutboxRecord, isKey: boolean) => string;
|
|
98
|
+
/**
|
|
99
|
+
* Custom subject function — overrides BOTH `subjectStrategy` and
|
|
100
|
+
* `recordName`. Receives `(topic, isKey, record)` for full flexibility.
|
|
101
|
+
*
|
|
102
|
+
* Backwards-compatible with the single-argument legacy form
|
|
103
|
+
* `(topic) => string` — extra args are ignored by JavaScript.
|
|
104
|
+
*/
|
|
105
|
+
subject?: (topic: string, isKey?: boolean, record?: OutboxRecord) => string;
|
|
34
106
|
/** content-type header value. Default "application/vnd.confluent.avro". */
|
|
35
107
|
contentType?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Auto-register schemas when one is supplied via `schemas` / `keySchemas`.
|
|
110
|
+
* Default `true` — matches Confluent client behavior.
|
|
111
|
+
*
|
|
112
|
+
* Set to `false` for production clusters where schemas are managed
|
|
113
|
+
* out-of-band (Confluent Cloud, regulated environments). With
|
|
114
|
+
* autoRegister off, the serializer ALWAYS resolves by `getLatestSchemaId`
|
|
115
|
+
* on the computed subject — and the locally-supplied schema bytes are
|
|
116
|
+
* ignored.
|
|
117
|
+
*/
|
|
118
|
+
autoRegister?: boolean;
|
|
36
119
|
}
|
|
37
120
|
/**
|
|
38
121
|
* A core {@link Serializer} that encodes payloads with a Confluent Schema Registry
|
|
39
122
|
* (Avro / Protobuf / JSON Schema). Drop it into `Relay`/`PostgresStreamingRelay`'s
|
|
40
|
-
* `serializer` option. The schema id per topic is resolved once
|
|
123
|
+
* `serializer` option. The schema id per (topic, isKey) tuple is resolved once
|
|
124
|
+
* and cached.
|
|
125
|
+
*
|
|
126
|
+
* Also exposes `serializeKey(record)` for users who want Avro-encoded message
|
|
127
|
+
* keys — call it manually when building the publish path; the relay does NOT
|
|
128
|
+
* call it automatically (key encoding is application-level by convention).
|
|
41
129
|
*/
|
|
42
130
|
declare class SchemaRegistrySerializer implements Serializer {
|
|
43
131
|
readonly contentType: string;
|
|
44
132
|
private readonly schemas;
|
|
45
|
-
private readonly
|
|
133
|
+
private readonly keySchemas;
|
|
134
|
+
private readonly subjectStrategy;
|
|
135
|
+
private readonly recordName;
|
|
136
|
+
private readonly subjectFn;
|
|
46
137
|
private readonly host;
|
|
138
|
+
private readonly auth;
|
|
139
|
+
private readonly autoRegister;
|
|
47
140
|
private readonly idCache;
|
|
48
141
|
private registry;
|
|
49
142
|
constructor(opts: SchemaRegistrySerializerOptions);
|
|
50
143
|
serialize(record: OutboxRecord): Promise<Buffer>;
|
|
144
|
+
/**
|
|
145
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
146
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
147
|
+
* as the producer-side "no key" signal).
|
|
148
|
+
*
|
|
149
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
150
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
151
|
+
* UTF-8 strings.
|
|
152
|
+
*/
|
|
153
|
+
serializeKey(record: OutboxRecord): Promise<Buffer | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
156
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
157
|
+
*/
|
|
158
|
+
private resolveSubject;
|
|
159
|
+
private recordNameFor;
|
|
51
160
|
private schemaId;
|
|
52
161
|
private getRegistry;
|
|
53
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Mappersmith middleware shape — the structural subset the Confluent
|
|
165
|
+
* SR client passes through. We don't depend on mappersmith types
|
|
166
|
+
* directly so the package compiles without the optional peer installed.
|
|
167
|
+
*/
|
|
168
|
+
interface MmRequest {
|
|
169
|
+
enhance(args: {
|
|
170
|
+
headers?: Record<string, string>;
|
|
171
|
+
}): MmRequest;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build a mappersmith middleware that adds `Authorization: Bearer <token>`.
|
|
175
|
+
*
|
|
176
|
+
* Exported for testing only — not part of the public API surface. The
|
|
177
|
+
* middleware resolves the token on EVERY request, so callable token
|
|
178
|
+
* providers can rotate without re-constructing the serializer. Cache
|
|
179
|
+
* inside your provider if rotation cost matters.
|
|
180
|
+
*/
|
|
181
|
+
declare function bearerAuthMiddleware(token: string | (() => string | Promise<string>)): () => {
|
|
182
|
+
prepareRequest(next: () => Promise<MmRequest>): Promise<MmRequest>;
|
|
183
|
+
};
|
|
54
184
|
|
|
55
|
-
export { type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType };
|
|
185
|
+
export { type SchemaRegistryAuth, type SchemaRegistryClient, SchemaRegistrySerializer, type SchemaRegistrySerializerOptions, type SchemaSpec, type SchemaType, type SubjectNameStrategy, bearerAuthMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,14 @@ 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
|
+
auth;
|
|
12
|
+
autoRegister;
|
|
13
|
+
// Keyed by `${topic}:${isKey}` to keep value- and key-subject ids distinct.
|
|
8
14
|
idCache = /* @__PURE__ */ new Map();
|
|
9
15
|
registry;
|
|
10
16
|
constructor(opts) {
|
|
@@ -15,35 +21,107 @@ var SchemaRegistrySerializer = class {
|
|
|
15
21
|
}
|
|
16
22
|
this.registry = opts.registry ?? null;
|
|
17
23
|
this.host = opts.host ?? null;
|
|
24
|
+
this.auth = opts.auth ?? null;
|
|
18
25
|
this.schemas = opts.schemas ?? {};
|
|
19
|
-
this.
|
|
26
|
+
this.keySchemas = opts.keySchemas ?? {};
|
|
27
|
+
this.subjectStrategy = opts.subjectStrategy ?? "TopicNameStrategy";
|
|
28
|
+
this.recordName = opts.recordName ?? null;
|
|
29
|
+
this.subjectFn = opts.subject ?? null;
|
|
20
30
|
this.contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
31
|
+
this.autoRegister = opts.autoRegister ?? true;
|
|
21
32
|
}
|
|
22
33
|
async serialize(record) {
|
|
23
34
|
const registry = await this.getRegistry();
|
|
24
|
-
const
|
|
35
|
+
const subject = this.resolveSubject(record, false);
|
|
36
|
+
const id = await this.schemaId(registry, subject, this.schemas[record.topic], false, record.topic);
|
|
25
37
|
return registry.encode(id, record.payload);
|
|
26
38
|
}
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Avro-encode the record's KEY using the registered key schema. Returns
|
|
41
|
+
* `null` when the record has no key (kafkajs/confluent treat null keys
|
|
42
|
+
* as the producer-side "no key" signal).
|
|
43
|
+
*
|
|
44
|
+
* Not part of the core `Serializer` interface — callers wire it into
|
|
45
|
+
* their publish path manually when they want Avro keys instead of raw
|
|
46
|
+
* UTF-8 strings.
|
|
47
|
+
*/
|
|
48
|
+
async serializeKey(record) {
|
|
49
|
+
if (record.key === null || record.key === void 0) return null;
|
|
50
|
+
const registry = await this.getRegistry();
|
|
51
|
+
const subject = this.resolveSubject(record, true);
|
|
52
|
+
const id = await this.schemaId(
|
|
53
|
+
registry,
|
|
54
|
+
subject,
|
|
55
|
+
this.keySchemas[record.topic],
|
|
56
|
+
true,
|
|
57
|
+
record.topic
|
|
58
|
+
);
|
|
59
|
+
return registry.encode(id, record.key);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the subject for this (record, isKey) tuple. Order of
|
|
63
|
+
* precedence: explicit `subject` function → `subjectStrategy` preset.
|
|
64
|
+
*/
|
|
65
|
+
resolveSubject(record, isKey) {
|
|
66
|
+
if (this.subjectFn) return this.subjectFn(record.topic, isKey, record);
|
|
67
|
+
switch (this.subjectStrategy) {
|
|
68
|
+
case "TopicNameStrategy":
|
|
69
|
+
return `${record.topic}-${isKey ? "key" : "value"}`;
|
|
70
|
+
case "RecordNameStrategy":
|
|
71
|
+
return this.recordNameFor(record, isKey);
|
|
72
|
+
case "TopicRecordNameStrategy":
|
|
73
|
+
return `${record.topic}-${this.recordNameFor(record, isKey)}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
recordNameFor(record, isKey) {
|
|
77
|
+
if (!this.recordName) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`SchemaRegistrySerializer: subjectStrategy "${this.subjectStrategy}" requires a \`recordName\` resolver.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return this.recordName(record, isKey);
|
|
83
|
+
}
|
|
84
|
+
schemaId(registry, subject, spec, isKey, topic) {
|
|
85
|
+
const cacheKey = `${topic}:${isKey ? "key" : "value"}`;
|
|
86
|
+
const cached = this.idCache.get(cacheKey);
|
|
29
87
|
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);
|
|
88
|
+
const lookup = spec && this.autoRegister ? registry.register({ type: spec.type, schema: spec.schema }, { subject }).then((r) => r.id) : registry.getLatestSchemaId(subject);
|
|
33
89
|
const guarded = lookup.catch((err) => {
|
|
34
|
-
this.idCache.delete(
|
|
90
|
+
this.idCache.delete(cacheKey);
|
|
35
91
|
throw err;
|
|
36
92
|
});
|
|
37
|
-
this.idCache.set(
|
|
93
|
+
this.idCache.set(cacheKey, guarded);
|
|
38
94
|
return guarded;
|
|
39
95
|
}
|
|
40
96
|
async getRegistry() {
|
|
41
97
|
if (this.registry) return this.registry;
|
|
42
98
|
const mod = await importSchemaRegistry();
|
|
43
|
-
|
|
99
|
+
const cfg = {
|
|
100
|
+
host: this.host
|
|
101
|
+
};
|
|
102
|
+
if (this.auth) {
|
|
103
|
+
if (this.auth.type === "basic") {
|
|
104
|
+
cfg.auth = {
|
|
105
|
+
username: this.auth.username,
|
|
106
|
+
password: this.auth.password
|
|
107
|
+
};
|
|
108
|
+
} else {
|
|
109
|
+
cfg.middlewares = [bearerAuthMiddleware(this.auth.token)];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.registry = new mod.SchemaRegistry(cfg);
|
|
44
113
|
return this.registry;
|
|
45
114
|
}
|
|
46
115
|
};
|
|
116
|
+
function bearerAuthMiddleware(token) {
|
|
117
|
+
return () => ({
|
|
118
|
+
async prepareRequest(next) {
|
|
119
|
+
const request = await next();
|
|
120
|
+
const value = typeof token === "function" ? await token() : token;
|
|
121
|
+
return request.enhance({ headers: { Authorization: `Bearer ${value}` } });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
47
125
|
async function importSchemaRegistry() {
|
|
48
126
|
try {
|
|
49
127
|
return await import("@kafkajs/confluent-schema-registry");
|
|
@@ -54,6 +132,7 @@ async function importSchemaRegistry() {
|
|
|
54
132
|
}
|
|
55
133
|
}
|
|
56
134
|
export {
|
|
57
|
-
SchemaRegistrySerializer
|
|
135
|
+
SchemaRegistrySerializer,
|
|
136
|
+
bearerAuthMiddleware
|
|
58
137
|
};
|
|
59
138
|
//# sourceMappingURL=index.js.map
|
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 * Authentication for the Schema Registry HTTP API.\n *\n * - `\"basic\"` — HTTP Basic Auth. The shape Confluent Cloud and most\n * commercial registries use; passed straight through to the underlying\n * client's `auth` config.\n * - `\"bearer\"` — `Authorization: Bearer <token>` header. The `token`\n * field accepts either a static string OR a callable that resolves a\n * fresh token on every request (cache inside your callable to amortise\n * cost; we don't memoize for you, so OAuth refresh-loop logic lives\n * on your side).\n *\n * mTLS for the registry connection itself is handled by Node's `tls`\n * stack — supply a custom `https.Agent` via the upstream client's\n * `agent` option (use the `registry` injection here and configure it\n * yourself), separate from the broker TLS the publisher uses.\n */\nexport type SchemaRegistryAuth =\n | { type: \"basic\"; username: string; password: string }\n | {\n type: \"bearer\";\n token: string | (() => string | Promise<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 /**\n * Optional authentication for the Schema Registry HTTP API. See\n * {@link SchemaRegistryAuth} for the two supported shapes. Ignored\n * when `registry` is provided (configure auth on the injected client\n * yourself in that case).\n */\n auth?: SchemaRegistryAuth;\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 auth: SchemaRegistryAuth | 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.auth = opts.auth ?? 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 const cfg: SchemaRegistryConstructorConfig = {\n host: this.host as string,\n };\n if (this.auth) {\n if (this.auth.type === \"basic\") {\n // Confluent SR client accepts `auth: { username, password }`\n // and the mappersmith basic-auth middleware builds the header.\n cfg.auth = {\n username: this.auth.username,\n password: this.auth.password,\n };\n } else {\n // Bearer: SR doesn't ship a built-in middleware. Inject our own\n // so every API call carries `Authorization: Bearer <token>`.\n cfg.middlewares = [bearerAuthMiddleware(this.auth.token)];\n }\n }\n this.registry = new mod.SchemaRegistry(cfg);\n return this.registry;\n }\n}\n\n/**\n * Mappersmith middleware shape — the structural subset the Confluent\n * SR client passes through. We don't depend on mappersmith types\n * directly so the package compiles without the optional peer installed.\n */\ninterface MmRequest {\n enhance(args: { headers?: Record<string, string> }): MmRequest;\n}\n\n/**\n * Build a mappersmith middleware that adds `Authorization: Bearer <token>`.\n *\n * Exported for testing only — not part of the public API surface. The\n * middleware resolves the token on EVERY request, so callable token\n * providers can rotate without re-constructing the serializer. Cache\n * inside your provider if rotation cost matters.\n */\nexport function bearerAuthMiddleware(\n token: string | (() => string | Promise<string>),\n) {\n return () => ({\n async prepareRequest(next: () => Promise<MmRequest>): Promise<MmRequest> {\n const request = await next();\n const value = typeof token === \"function\" ? await token() : token;\n return request.enhance({ headers: { Authorization: `Bearer ${value}` } });\n },\n });\n}\n\n/**\n * Structural shape the SR client's constructor accepts. Mirrors\n * `SchemaRegistryAPIClientArgs` from the upstream package without\n * importing the type directly (optional peer).\n */\ninterface SchemaRegistryConstructorConfig {\n host: string;\n auth?: { username: string; password: string };\n middlewares?: Array<() => unknown>;\n}\n\nasync function importSchemaRegistry(): Promise<{\n SchemaRegistry: new (\n cfg: SchemaRegistryConstructorConfig,\n ) => 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventferry/schema-registry",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Confluent Schema Registry serializer for @eventferry (Avro/Protobuf/JSON Schema)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"CHANGELOG.md"
|
|
19
19
|
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"outbox",
|
|
22
25
|
"transactional-outbox",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"node": ">=18"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@eventferry/core": "3.
|
|
51
|
+
"@eventferry/core": "3.4.0"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
54
|
"@kafkajs/confluent-schema-registry": "^3.0.0"
|
|
@@ -59,8 +62,8 @@
|
|
|
59
62
|
"@kafkajs/confluent-schema-registry": "^3.0.0",
|
|
60
63
|
"tsup": "^8.3.5",
|
|
61
64
|
"typescript": "^5.7.2",
|
|
62
|
-
"vitest": "^2.
|
|
63
|
-
"@eventferry/core": "3.
|
|
65
|
+
"vitest": "^3.2.6",
|
|
66
|
+
"@eventferry/core": "3.4.0"
|
|
64
67
|
},
|
|
65
68
|
"scripts": {
|
|
66
69
|
"build": "tsup",
|