@platformatic/kafka 1.27.0-alpha.1 → 1.27.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/clients/consumer/consumer.js +24 -1
- package/dist/clients/consumer/messages-stream.js +66 -10
- package/dist/clients/consumer/options.d.ts +24 -0
- package/dist/clients/consumer/options.js +3 -1
- package/dist/clients/consumer/types.d.ts +4 -1
- package/dist/clients/producer/options.d.ts +2 -18
- package/dist/clients/producer/options.js +3 -1
- package/dist/clients/producer/producer.js +75 -15
- package/dist/clients/producer/types.d.ts +4 -1
- package/dist/clients/serde.d.ts +11 -6
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/network/connection.d.ts +7 -6
- package/dist/protocol/compression.js +45 -6
- package/dist/protocol/crc32c.js +4 -3
- package/dist/protocol/definitions.js +1 -1
- package/dist/protocol/reader.js +1 -1
- package/dist/protocol/records.d.ts +7 -18
- package/dist/protocol/records.js +2 -6
- package/dist/protocol/sasl/oauth-bearer.d.ts +3 -3
- package/dist/protocol/sasl/plain.d.ts +3 -3
- package/dist/protocol/sasl/scram-sha.d.ts +3 -3
- package/dist/protocol/sasl/utils.d.ts +3 -3
- package/dist/protocol/writer.js +1 -1
- package/dist/registries/abstract.d.ts +22 -0
- package/dist/registries/abstract.js +38 -0
- package/dist/registries/confluent-schema-registry.d.ts +41 -0
- package/dist/registries/confluent-schema-registry.js +222 -0
- package/dist/registries/index.d.ts +2 -0
- package/dist/registries/index.js +2 -0
- package/dist/typescript-4/dist/clients/consumer/options.d.ts +24 -0
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +4 -1
- package/dist/typescript-4/dist/clients/producer/options.d.ts +2 -18
- package/dist/typescript-4/dist/clients/producer/types.d.ts +4 -1
- package/dist/typescript-4/dist/clients/serde.d.ts +11 -6
- package/dist/typescript-4/dist/errors.d.ts +5 -1
- package/dist/typescript-4/dist/index.d.ts +2 -1
- package/dist/typescript-4/dist/network/connection.d.ts +7 -6
- package/dist/typescript-4/dist/protocol/records.d.ts +7 -18
- package/dist/typescript-4/dist/protocol/sasl/oauth-bearer.d.ts +3 -3
- package/dist/typescript-4/dist/protocol/sasl/plain.d.ts +3 -3
- package/dist/typescript-4/dist/protocol/sasl/scram-sha.d.ts +3 -3
- package/dist/typescript-4/dist/protocol/sasl/utils.d.ts +3 -3
- package/dist/typescript-4/dist/registries/abstract.d.ts +22 -0
- package/dist/typescript-4/dist/registries/confluent-schema-registry.d.ts +41 -0
- package/dist/typescript-4/dist/registries/index.d.ts +2 -0
- package/dist/version.js +1 -1
- package/package.json +12 -12
- package/dist/native.wasm +0 -0
- package/dist/protocol/native.d.ts +0 -8
- package/dist/protocol/native.js +0 -48
- package/dist/typescript-4/dist/protocol/native.d.ts +0 -8
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import zlib from 'node:zlib';
|
|
2
3
|
import { UnsupportedCompressionError } from "../errors.js";
|
|
3
4
|
import { DynamicBuffer } from "./dynamic-buffer.js";
|
|
4
|
-
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
5
6
|
const { zstdCompressSync, zstdDecompressSync, gzipSync, gunzipSync } = zlib;
|
|
6
7
|
export const CompressionAlgorithms = {
|
|
7
8
|
NONE: 'none',
|
|
@@ -12,12 +13,34 @@ export const CompressionAlgorithms = {
|
|
|
12
13
|
};
|
|
13
14
|
export const allowedCompressionsAlgorithms = Object.values(CompressionAlgorithms);
|
|
14
15
|
function ensureBuffer(data) {
|
|
15
|
-
return DynamicBuffer.isDynamicBuffer(data) ? data.
|
|
16
|
+
return DynamicBuffer.isDynamicBuffer(data) ? data.slice() : data;
|
|
17
|
+
}
|
|
18
|
+
let snappyCompressSync;
|
|
19
|
+
let snappyDecompressSync;
|
|
20
|
+
let lz4CompressFrameSync;
|
|
21
|
+
let lz4DecompressFrameSync;
|
|
22
|
+
function loadSnappy() {
|
|
23
|
+
try {
|
|
24
|
+
const snappy = require('snappy');
|
|
25
|
+
snappyCompressSync = snappy.compressSync;
|
|
26
|
+
snappyDecompressSync = snappy.uncompressSync;
|
|
27
|
+
/* c8 ignore next 5 - In tests snappy is always available */
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw new UnsupportedCompressionError('Cannot load snappy module, which is an optionalDependency. Please check your local installation.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadLZ4() {
|
|
34
|
+
try {
|
|
35
|
+
const lz4 = require('lz4-napi');
|
|
36
|
+
lz4CompressFrameSync = lz4.compressFrameSync;
|
|
37
|
+
lz4DecompressFrameSync = lz4.decompressFrameSync;
|
|
38
|
+
/* c8 ignore next 5 - In tests lz4-napi is always available */
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
throw new UnsupportedCompressionError('Cannot load lz4-napi module, which is an optionalDependency. Please check your local installation.');
|
|
42
|
+
}
|
|
16
43
|
}
|
|
17
|
-
const snappyCompressSync = snappyCompress;
|
|
18
|
-
const snappyDecompressSync = snappyDecompress;
|
|
19
|
-
const lz4CompressFrameSync = lz4Compress;
|
|
20
|
-
const lz4DecompressFrameSync = lz4Decompress;
|
|
21
44
|
export const compressionsAlgorithms = {
|
|
22
45
|
/* c8 ignore next 8 - 'none' is actually never used but this is to please Typescript */
|
|
23
46
|
none: {
|
|
@@ -42,9 +65,17 @@ export const compressionsAlgorithms = {
|
|
|
42
65
|
},
|
|
43
66
|
snappy: {
|
|
44
67
|
compressSync(data) {
|
|
68
|
+
/* c8 ignore next 4 - In tests snappy is always available */
|
|
69
|
+
if (!snappyCompressSync) {
|
|
70
|
+
loadSnappy();
|
|
71
|
+
}
|
|
45
72
|
return snappyCompressSync(ensureBuffer(data));
|
|
46
73
|
},
|
|
47
74
|
decompressSync(data) {
|
|
75
|
+
/* c8 ignore next 4 - In tests snappy is always available */
|
|
76
|
+
if (!snappyDecompressSync) {
|
|
77
|
+
loadSnappy();
|
|
78
|
+
}
|
|
48
79
|
return snappyDecompressSync(ensureBuffer(data));
|
|
49
80
|
},
|
|
50
81
|
bitmask: 2,
|
|
@@ -52,9 +83,17 @@ export const compressionsAlgorithms = {
|
|
|
52
83
|
},
|
|
53
84
|
lz4: {
|
|
54
85
|
compressSync(data) {
|
|
86
|
+
/* c8 ignore next 4 - In tests lz4-napi is always available */
|
|
87
|
+
if (!lz4CompressFrameSync) {
|
|
88
|
+
loadLZ4();
|
|
89
|
+
}
|
|
55
90
|
return lz4CompressFrameSync(ensureBuffer(data));
|
|
56
91
|
},
|
|
57
92
|
decompressSync(data) {
|
|
93
|
+
/* c8 ignore next 4 - In tests lz4-napi is always available */
|
|
94
|
+
if (!lz4DecompressFrameSync) {
|
|
95
|
+
loadLZ4();
|
|
96
|
+
}
|
|
58
97
|
return lz4DecompressFrameSync(ensureBuffer(data));
|
|
59
98
|
},
|
|
60
99
|
bitmask: 3,
|
package/dist/protocol/crc32c.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Based on the work from: https://github.com/tulios/kafkajs/blob/master/src/protocol/recordBatch/crc32C/crc32C.js
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { DynamicBuffer } from "./dynamic-buffer.js";
|
|
4
|
-
import { crc32c as wasmCRC32C } from "./native.js";
|
|
5
4
|
/* prettier-ignore */
|
|
6
5
|
const CRC = [
|
|
7
6
|
0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4,
|
|
@@ -86,7 +85,9 @@ export function loadNativeCRC32C() {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
export function jsCRC32C(data) {
|
|
89
|
-
const bytes = DynamicBuffer.isDynamicBuffer(data)
|
|
88
|
+
const bytes = DynamicBuffer.isDynamicBuffer(data)
|
|
89
|
+
? data.buffer
|
|
90
|
+
: new Uint8Array(data);
|
|
90
91
|
let crc = 0xffffffff;
|
|
91
92
|
for (let i = 0, len = bytes.length; i < len; ++i) {
|
|
92
93
|
crc = CRC[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
@@ -94,4 +95,4 @@ export function jsCRC32C(data) {
|
|
|
94
95
|
return (crc ^ 0xffffffff) >>> 0;
|
|
95
96
|
}
|
|
96
97
|
/* c8 ignore next - Hard to test */
|
|
97
|
-
export const crc32c = loadNativeCRC32C() ??
|
|
98
|
+
export const crc32c = loadNativeCRC32C() ?? jsCRC32C;
|
|
@@ -7,5 +7,5 @@ export const EMPTY_BUFFER = Buffer.alloc(0);
|
|
|
7
7
|
export const EMPTY_UUID = Buffer.alloc(UUID_SIZE);
|
|
8
8
|
// Since it is serialized at either 0 (for nullable) or 1 (since length is stored as length + 1), it always uses a single byte
|
|
9
9
|
export const EMPTY_OR_SINGLE_COMPACT_LENGTH_SIZE = INT8_SIZE;
|
|
10
|
-
// TODO
|
|
10
|
+
// TODO: Tagged fields are not supported yet
|
|
11
11
|
export const EMPTY_TAGGED_FIELDS_BUFFER = Buffer.from([0]);
|
package/dist/protocol/reader.js
CHANGED
|
@@ -17,6 +17,7 @@ export interface MessageBase<Key = Buffer, Value = Buffer> {
|
|
|
17
17
|
}
|
|
18
18
|
export interface MessageToProduce<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends MessageBase<Key, Value> {
|
|
19
19
|
headers?: Map<HeaderKey, HeaderValue> | Record<string, HeaderValue>;
|
|
20
|
+
metadata?: unknown;
|
|
20
21
|
}
|
|
21
22
|
export interface MessageConsumerMetadata {
|
|
22
23
|
coordinatorId: number;
|
|
@@ -67,6 +68,10 @@ export interface KafkaRecord {
|
|
|
67
68
|
value: Buffer;
|
|
68
69
|
headers: [Buffer, Buffer][];
|
|
69
70
|
}
|
|
71
|
+
export interface MessageToConsume extends KafkaRecord {
|
|
72
|
+
topic: string;
|
|
73
|
+
partition: number;
|
|
74
|
+
}
|
|
70
75
|
export interface RecordsBatch {
|
|
71
76
|
firstOffset: bigint;
|
|
72
77
|
length: number;
|
|
@@ -85,24 +90,8 @@ export interface RecordsBatch {
|
|
|
85
90
|
export declare const messageSchema: {
|
|
86
91
|
type: string;
|
|
87
92
|
properties: {
|
|
88
|
-
key:
|
|
89
|
-
|
|
90
|
-
type: string;
|
|
91
|
-
buffer?: undefined;
|
|
92
|
-
} | {
|
|
93
|
-
buffer: boolean;
|
|
94
|
-
type?: undefined;
|
|
95
|
-
})[];
|
|
96
|
-
};
|
|
97
|
-
value: {
|
|
98
|
-
oneOf: ({
|
|
99
|
-
type: string;
|
|
100
|
-
buffer?: undefined;
|
|
101
|
-
} | {
|
|
102
|
-
buffer: boolean;
|
|
103
|
-
type?: undefined;
|
|
104
|
-
})[];
|
|
105
|
-
};
|
|
93
|
+
key: boolean;
|
|
94
|
+
value: boolean;
|
|
106
95
|
headers: {
|
|
107
96
|
anyOf: ({
|
|
108
97
|
map: boolean;
|
package/dist/protocol/records.js
CHANGED
|
@@ -14,12 +14,8 @@ export const BATCH_HEAD = INT64_SIZE + INT32_SIZE; // FirstOffset + Length
|
|
|
14
14
|
export const messageSchema = {
|
|
15
15
|
type: 'object',
|
|
16
16
|
properties: {
|
|
17
|
-
key:
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
value: {
|
|
21
|
-
oneOf: [{ type: 'string' }, { buffer: true }]
|
|
22
|
-
},
|
|
17
|
+
key: true,
|
|
18
|
+
value: true,
|
|
23
19
|
headers: {
|
|
24
20
|
// Note: we can't use oneOf here since a Map is also a 'object'. Thanks JS.
|
|
25
21
|
anyOf: [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
2
2
|
import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
|
|
3
|
-
import { type Connection, type
|
|
3
|
+
import { type Connection, type CredentialProvider } from '../../network/connection.ts';
|
|
4
4
|
export declare function jwtValidateAuthenticationBytes(authBytes: Buffer, callback: CallbackWithPromise<Buffer>): void;
|
|
5
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string |
|
|
6
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string |
|
|
5
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string | CredentialProvider, extensions: Record<string, string> | CredentialProvider<Record<string, string>>, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
|
|
6
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string | CredentialProvider, extensions: Record<string, string> | CredentialProvider<Record<string, string>>): Promise<SaslAuthenticateResponse>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
2
2
|
import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
|
|
3
|
-
import { type Connection, type
|
|
4
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string |
|
|
5
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string |
|
|
3
|
+
import { type Connection, type CredentialProvider } from '../../network/connection.ts';
|
|
4
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string | CredentialProvider, passwordProvider: string | CredentialProvider, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
|
|
5
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string | CredentialProvider, passwordProvider: string | CredentialProvider): Promise<SaslAuthenticateResponse>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
2
2
|
import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
|
|
3
|
-
import { type Connection, type
|
|
3
|
+
import { type Connection, type CredentialProvider } from '../../network/connection.ts';
|
|
4
4
|
export interface ScramAlgorithmDefinition {
|
|
5
5
|
keyLength: number;
|
|
6
6
|
algorithm: string;
|
|
@@ -35,5 +35,5 @@ export declare function hi(definition: ScramAlgorithmDefinition, password: strin
|
|
|
35
35
|
export declare function hmac(definition: ScramAlgorithmDefinition, key: Buffer, data: string | Buffer): Buffer;
|
|
36
36
|
export declare function xor(a: Buffer, b: Buffer): Buffer;
|
|
37
37
|
export declare const defaultCrypto: ScramCryptoModule;
|
|
38
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string |
|
|
39
|
-
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string |
|
|
38
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string | CredentialProvider, passwordProvider: string | CredentialProvider, crypto: ScramCryptoModule, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
|
|
39
|
+
export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string | CredentialProvider, passwordProvider: string | CredentialProvider, crypto?: ScramCryptoModule): Promise<SaslAuthenticateResponse>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from '../../apis/index.ts';
|
|
2
|
-
import { type
|
|
3
|
-
export declare function getCredential<T>(label: string, credentialOrProvider: T |
|
|
4
|
-
export declare function getCredential<T>(label: string, credentialOrProvider: T |
|
|
2
|
+
import { type CredentialProvider } from '../../network/connection.ts';
|
|
3
|
+
export declare function getCredential<T>(label: string, credentialOrProvider: T | CredentialProvider<T>, callback: CallbackWithPromise<T>): void;
|
|
4
|
+
export declare function getCredential<T>(label: string, credentialOrProvider: T | CredentialProvider<T>): Promise<T>;
|
package/dist/protocol/writer.js
CHANGED
|
@@ -210,7 +210,7 @@ export class Writer {
|
|
|
210
210
|
}
|
|
211
211
|
return this;
|
|
212
212
|
}
|
|
213
|
-
// TODO
|
|
213
|
+
// TODO: Tagged fields are not supported yet
|
|
214
214
|
appendTaggedFields(_ = []) {
|
|
215
215
|
return this.append(EMPTY_TAGGED_FIELDS_BUFFER);
|
|
216
216
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Callback } from '../apis/definitions.ts';
|
|
2
|
+
import { type BeforeDeserializationHook, type BeforeHookPayloadType, type BeforeSerializationHook, type Deserializers, type Serializers } from '../clients/serde.ts';
|
|
3
|
+
import { type MessageToProduce } from '../protocol/records.ts';
|
|
4
|
+
export interface SchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> {
|
|
5
|
+
get(id: Id): Schema | undefined;
|
|
6
|
+
fetch(id: Id, callback?: (error?: Error) => void): void | Promise<void>;
|
|
7
|
+
getSchemaId(payload: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): Id;
|
|
8
|
+
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
9
|
+
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
10
|
+
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
11
|
+
getBeforeDeserializationHook(): BeforeDeserializationHook;
|
|
12
|
+
}
|
|
13
|
+
export declare function runAsyncSeries<V>(operation: (item: V, cb: Callback<void>) => void | Promise<void>, collection: V[], index: number, callback: Callback<void>): void;
|
|
14
|
+
export declare class AbstractSchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> implements SchemaRegistry<Id, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
15
|
+
get(_: Id): Schema | undefined;
|
|
16
|
+
fetch(_i: unknown, _c?: (error?: Error) => void): void | Promise<void>;
|
|
17
|
+
getSchemaId(_p: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _t?: BeforeHookPayloadType): Id;
|
|
18
|
+
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
19
|
+
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
20
|
+
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
21
|
+
getBeforeDeserializationHook(): BeforeDeserializationHook;
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function runAsyncSeries(operation, collection, index, callback) {
|
|
2
|
+
operation(collection[index], error => {
|
|
3
|
+
if (error) {
|
|
4
|
+
callback(error);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
else if (index === collection.length - 1) {
|
|
8
|
+
callback(null);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
runAsyncSeries(operation, collection, index + 1, callback);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/* c8 ignore start */
|
|
15
|
+
export class AbstractSchemaRegistry {
|
|
16
|
+
get(_) {
|
|
17
|
+
throw new Error('AbstractSchemaRegistry.get() should be implemented in subclasses.');
|
|
18
|
+
}
|
|
19
|
+
fetch(_i, _c) {
|
|
20
|
+
throw new Error('AbstractSchemaRegistry.fetch() should be implemented in subclasses.');
|
|
21
|
+
}
|
|
22
|
+
getSchemaId(_p, _t) {
|
|
23
|
+
throw new Error('AbstractSchemaRegistry.getSchemaId() should be implemented in subclasses.');
|
|
24
|
+
}
|
|
25
|
+
getSerializers() {
|
|
26
|
+
throw new Error('AbstractSchemaRegistry.getSerializers() should be implemented in subclasses.');
|
|
27
|
+
}
|
|
28
|
+
getDeserializers() {
|
|
29
|
+
throw new Error('AbstractSchemaRegistry.getDeserializers() should be implemented in subclasses.');
|
|
30
|
+
}
|
|
31
|
+
getBeforeSerializationHook() {
|
|
32
|
+
throw new Error('AbstractSchemaRegistry.getBeforeSerializationHook() should be implemented in subclasses.');
|
|
33
|
+
}
|
|
34
|
+
getBeforeDeserializationHook() {
|
|
35
|
+
throw new Error('AbstractSchemaRegistry.getBeforeDeserializationHook() should be implemented in subclasses.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/* c8 ignore end */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ValidateFunction } from 'ajv';
|
|
2
|
+
import { type Type } from 'avsc';
|
|
3
|
+
import { type Root } from 'protobufjs';
|
|
4
|
+
import { type Callback } from '../apis/definitions.ts';
|
|
5
|
+
import { type BeforeDeserializationHook, type BeforeHookPayloadType, type BeforeSerializationHook, type Deserializers, type Serializers } from '../clients/serde.ts';
|
|
6
|
+
import { type CredentialProvider } from '../index.ts';
|
|
7
|
+
import { type MessageToConsume, type MessageToProduce } from '../protocol/records.ts';
|
|
8
|
+
import { AbstractSchemaRegistry } from './abstract.ts';
|
|
9
|
+
type ConfluentSchemaRegistryMessageToProduce = MessageToProduce<unknown, unknown, unknown, unknown>;
|
|
10
|
+
export interface ConfluentSchemaRegistryMetadata {
|
|
11
|
+
schemas?: Record<BeforeHookPayloadType, number>;
|
|
12
|
+
}
|
|
13
|
+
export type ConfluentSchemaRegistryProtobufTypeMapper = (id: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume) => string;
|
|
14
|
+
export interface ConfluentSchemaRegistryOptions {
|
|
15
|
+
url: string;
|
|
16
|
+
auth?: {
|
|
17
|
+
username?: string | CredentialProvider;
|
|
18
|
+
password?: string | CredentialProvider;
|
|
19
|
+
token?: string | CredentialProvider;
|
|
20
|
+
};
|
|
21
|
+
protobufTypeMapper?: ConfluentSchemaRegistryProtobufTypeMapper;
|
|
22
|
+
jsonValidateSend?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface Schema {
|
|
25
|
+
id: number;
|
|
26
|
+
type: 'avro' | 'protobuf' | 'json';
|
|
27
|
+
schema: Type | Root | ValidateFunction;
|
|
28
|
+
}
|
|
29
|
+
export declare function defaultProtobufTypeMapper(_: number, type: BeforeHookPayloadType, context: ConfluentSchemaRegistryMessageToProduce | MessageToConsume): string;
|
|
30
|
+
export declare class ConfluentSchemaRegistry<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends AbstractSchemaRegistry<number | undefined, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options: ConfluentSchemaRegistryOptions);
|
|
33
|
+
getSchemaId(message: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): number | undefined;
|
|
34
|
+
get(id: number): Schema | undefined;
|
|
35
|
+
fetchSchema(id: number, callback: Callback<void>): Promise<void>;
|
|
36
|
+
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
37
|
+
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
38
|
+
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
39
|
+
getBeforeDeserializationHook(): BeforeDeserializationHook;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import avro from 'avsc';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { jsonDeserializer, jsonSerializer, stringDeserializer, stringSerializer } from "../clients/serde.js";
|
|
4
|
+
import { ajv, EMPTY_BUFFER, UnsupportedFormatError, UserError } from "../index.js";
|
|
5
|
+
import { getCredential } from "../protocol/sasl/utils.js";
|
|
6
|
+
import { AbstractSchemaRegistry } from "./abstract.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
/* c8 ignore next 8 */
|
|
9
|
+
export function defaultProtobufTypeMapper(_, type, context) {
|
|
10
|
+
// Confluent Schema Registry convention
|
|
11
|
+
return `${context.topic}-${type}`;
|
|
12
|
+
}
|
|
13
|
+
// TODO(ShogunPanda): Authentication support
|
|
14
|
+
export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
|
|
15
|
+
#url;
|
|
16
|
+
#schemas;
|
|
17
|
+
#protobufParse;
|
|
18
|
+
#protobufTypeMapper;
|
|
19
|
+
#jsonValidateSend;
|
|
20
|
+
#auth;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super();
|
|
23
|
+
this.#url = options.url;
|
|
24
|
+
this.#schemas = new Map();
|
|
25
|
+
this.#protobufTypeMapper = options.protobufTypeMapper ?? defaultProtobufTypeMapper;
|
|
26
|
+
this.#jsonValidateSend = options.jsonValidateSend ?? false;
|
|
27
|
+
this.#auth = options.auth;
|
|
28
|
+
}
|
|
29
|
+
getSchemaId(message, type) {
|
|
30
|
+
if (Buffer.isBuffer(message)) {
|
|
31
|
+
if (type !== 'value') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return message.readInt32BE(1);
|
|
35
|
+
}
|
|
36
|
+
return message.metadata?.schemas?.[type];
|
|
37
|
+
}
|
|
38
|
+
get(id) {
|
|
39
|
+
return this.#schemas.get(id);
|
|
40
|
+
}
|
|
41
|
+
async fetchSchema(id, callback) {
|
|
42
|
+
try {
|
|
43
|
+
const requestInit = {};
|
|
44
|
+
if (this.#auth) {
|
|
45
|
+
if (this.#auth.token) {
|
|
46
|
+
const token = await getCredential('token', this.#auth.token);
|
|
47
|
+
requestInit.headers = {
|
|
48
|
+
Authorization: `Bearer ${token}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const username = await getCredential('username', this.#auth.username);
|
|
53
|
+
const password = await getCredential('password', this.#auth.password);
|
|
54
|
+
requestInit.headers = {
|
|
55
|
+
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const response = await fetch(`${this.#url}/schemas/ids/${id}`, requestInit);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new UserError(`Failed to fetch a schema: [HTTP ${response.status}]`, { response: await response.text() });
|
|
62
|
+
}
|
|
63
|
+
const responseBody = await response.json();
|
|
64
|
+
const { schema, schemaType } = responseBody;
|
|
65
|
+
switch (schemaType) {
|
|
66
|
+
case 'AVRO':
|
|
67
|
+
this.#schemas.set(id, { id, type: 'avro', schema: avro.Type.forSchema(JSON.parse(schema)) });
|
|
68
|
+
break;
|
|
69
|
+
case 'PROTOBUF':
|
|
70
|
+
this.#protobufParse ??= this.#loadProtobuf();
|
|
71
|
+
this.#schemas.set(id, { id, type: 'protobuf', schema: this.#protobufParse(schema).root });
|
|
72
|
+
break;
|
|
73
|
+
case 'JSON':
|
|
74
|
+
this.#schemas.set(id, { id, type: 'json', schema: ajv.compile(JSON.parse(schema)) });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
process.nextTick(callback);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
process.nextTick(() => callback(err));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
getSerializers() {
|
|
84
|
+
return {
|
|
85
|
+
key: this.#schemaSerializer.bind(this, 'key', stringSerializer),
|
|
86
|
+
value: this.#schemaSerializer.bind(this, 'value', jsonSerializer),
|
|
87
|
+
headerKey: this.#schemaSerializer.bind(this, 'headerKey', stringSerializer),
|
|
88
|
+
headerValue: this.#schemaSerializer.bind(this, 'headerValue', jsonSerializer)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
getDeserializers() {
|
|
92
|
+
return {
|
|
93
|
+
key: this.#schemaDeserializer.bind(this, 'key', stringDeserializer),
|
|
94
|
+
value: this.#schemaDeserializer.bind(this, 'value', jsonDeserializer),
|
|
95
|
+
headerKey: this.#schemaDeserializer.bind(this, 'headerKey', stringDeserializer),
|
|
96
|
+
headerValue: this.#schemaDeserializer.bind(this, 'headerValue', jsonDeserializer)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
getBeforeSerializationHook() {
|
|
100
|
+
const registry = this;
|
|
101
|
+
return function beforeSerialization(_, type, message, callback) {
|
|
102
|
+
// Extract the schema ID from the message metadata
|
|
103
|
+
const schemaId = registry.getSchemaId(message, type);
|
|
104
|
+
// When no schema ID is found, nothing to do
|
|
105
|
+
if (!schemaId) {
|
|
106
|
+
callback(null);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// The schema is already fetch
|
|
110
|
+
if (registry.get(schemaId)) {
|
|
111
|
+
callback(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
registry.fetchSchema(schemaId, callback);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
getBeforeDeserializationHook() {
|
|
118
|
+
const registry = this;
|
|
119
|
+
return function beforeDeserialization(payload, type, _message, callback) {
|
|
120
|
+
// Extract the schema ID from the message metadata
|
|
121
|
+
const schemaId = registry.getSchemaId(payload, type);
|
|
122
|
+
// When no schema ID is found, nothing to do
|
|
123
|
+
if (!schemaId) {
|
|
124
|
+
callback(null);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// The schema is already fetch
|
|
128
|
+
if (registry.get(schemaId)) {
|
|
129
|
+
callback(null);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
registry.fetchSchema(schemaId, callback);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
#schemaSerializer(type, fallbackSerializer, data, headers, message) {
|
|
136
|
+
/* c8 ignore next 3 - Hard to test */
|
|
137
|
+
if (typeof data === 'undefined') {
|
|
138
|
+
return EMPTY_BUFFER;
|
|
139
|
+
}
|
|
140
|
+
if (type === 'headerKey' || type === 'headerValue') {
|
|
141
|
+
message = headers;
|
|
142
|
+
}
|
|
143
|
+
const schemaId = message?.metadata?.schemas?.[type];
|
|
144
|
+
if (!schemaId) {
|
|
145
|
+
return fallbackSerializer(data);
|
|
146
|
+
}
|
|
147
|
+
const schema = this.#schemas.get(schemaId);
|
|
148
|
+
if (!schema) {
|
|
149
|
+
throw new UserError(`Schema with ID ${schemaId} not found.`, { missingSchema: schemaId });
|
|
150
|
+
}
|
|
151
|
+
let encodedMessage;
|
|
152
|
+
switch (schema.type) {
|
|
153
|
+
case 'avro':
|
|
154
|
+
encodedMessage = schema.schema.toBuffer(data);
|
|
155
|
+
break;
|
|
156
|
+
case 'protobuf':
|
|
157
|
+
{
|
|
158
|
+
const typeName = this.#protobufTypeMapper(schemaId, type, message);
|
|
159
|
+
const Type = schema.schema.lookupType(typeName);
|
|
160
|
+
encodedMessage = Buffer.from(Type.encode(Type.create(data)).finish());
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case 'json':
|
|
164
|
+
if (this.#jsonValidateSend) {
|
|
165
|
+
const validate = schema.schema;
|
|
166
|
+
const valid = validate(data);
|
|
167
|
+
if (!valid) {
|
|
168
|
+
throw new UserError(`JSON Schema validation failed before serialization: ${ajv.errorsText(validate.errors)}`, { type, data, headers, validationErrors: validate.errors });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
encodedMessage = Buffer.from(JSON.stringify(data));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const header = Buffer.alloc(5);
|
|
175
|
+
header.writeInt32BE(schemaId, 1);
|
|
176
|
+
return Buffer.concat([header, encodedMessage]);
|
|
177
|
+
}
|
|
178
|
+
#schemaDeserializer(type, fallbackDeserializer, data, headers, message) {
|
|
179
|
+
/* c8 ignore next 3 - Hard to test */
|
|
180
|
+
if (typeof data === 'undefined' || data.length === 0) {
|
|
181
|
+
return EMPTY_BUFFER;
|
|
182
|
+
}
|
|
183
|
+
if (type === 'headerKey' || type === 'headerValue') {
|
|
184
|
+
message = headers;
|
|
185
|
+
}
|
|
186
|
+
const schemaId = this.getSchemaId(data, type);
|
|
187
|
+
if (!schemaId) {
|
|
188
|
+
return fallbackDeserializer(data);
|
|
189
|
+
}
|
|
190
|
+
const schema = this.#schemas.get(schemaId);
|
|
191
|
+
if (!schema) {
|
|
192
|
+
throw new UserError(`Schema with ID ${schemaId} not found.`, { missingSchema: schemaId });
|
|
193
|
+
}
|
|
194
|
+
switch (schema.type) {
|
|
195
|
+
case 'avro':
|
|
196
|
+
return schema.schema.fromBuffer(data.subarray(5));
|
|
197
|
+
case 'protobuf': {
|
|
198
|
+
const typeName = this.#protobufTypeMapper(schemaId, type, message);
|
|
199
|
+
const Type = schema.schema.lookupType(typeName);
|
|
200
|
+
return Type.decode(data.subarray(5));
|
|
201
|
+
}
|
|
202
|
+
case 'json': {
|
|
203
|
+
const parsed = JSON.parse(data.subarray(5).toString('utf-8'));
|
|
204
|
+
const validate = schema.schema;
|
|
205
|
+
const valid = validate(parsed);
|
|
206
|
+
if (!valid) {
|
|
207
|
+
throw new UserError(`JSON Schema validation failed before deserialization: ${ajv.errorsText(validate.errors)}`, { type, data: parsed, headers, validationErrors: validate.errors });
|
|
208
|
+
}
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
#loadProtobuf() {
|
|
214
|
+
try {
|
|
215
|
+
return require('protobufjs').parse;
|
|
216
|
+
/* c8 ignore next 5 - In tests protobufjs is always available */
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
throw new UnsupportedFormatError('Cannot load protobufjs module, which is an optionalDependency. Please check your local installation.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -127,6 +127,12 @@ export declare const consumeOptionsProperties: {
|
|
|
127
127
|
type: string;
|
|
128
128
|
minimum: number;
|
|
129
129
|
};
|
|
130
|
+
beforeDeserialization: {
|
|
131
|
+
function: boolean;
|
|
132
|
+
};
|
|
133
|
+
registry: {
|
|
134
|
+
type: string;
|
|
135
|
+
};
|
|
130
136
|
};
|
|
131
137
|
export declare const groupOptionsSchema: {
|
|
132
138
|
type: string;
|
|
@@ -241,6 +247,12 @@ export declare const consumeOptionsSchema: {
|
|
|
241
247
|
type: string;
|
|
242
248
|
minimum: number;
|
|
243
249
|
};
|
|
250
|
+
beforeDeserialization: {
|
|
251
|
+
function: boolean;
|
|
252
|
+
};
|
|
253
|
+
registry: {
|
|
254
|
+
type: string;
|
|
255
|
+
};
|
|
244
256
|
groupInstanceId: {
|
|
245
257
|
type: string;
|
|
246
258
|
pattern: string;
|
|
@@ -396,6 +408,12 @@ export declare const consumerOptionsSchema: {
|
|
|
396
408
|
type: string;
|
|
397
409
|
minimum: number;
|
|
398
410
|
};
|
|
411
|
+
beforeDeserialization: {
|
|
412
|
+
function: boolean;
|
|
413
|
+
};
|
|
414
|
+
registry: {
|
|
415
|
+
type: string;
|
|
416
|
+
};
|
|
399
417
|
groupInstanceId: {
|
|
400
418
|
type: string;
|
|
401
419
|
pattern: string;
|
|
@@ -511,6 +529,12 @@ export declare const fetchOptionsSchema: {
|
|
|
511
529
|
type: string;
|
|
512
530
|
minimum: number;
|
|
513
531
|
};
|
|
532
|
+
beforeDeserialization: {
|
|
533
|
+
function: boolean;
|
|
534
|
+
};
|
|
535
|
+
registry: {
|
|
536
|
+
type: string;
|
|
537
|
+
};
|
|
514
538
|
groupInstanceId: {
|
|
515
539
|
type: string;
|
|
516
540
|
pattern: string;
|
|
@@ -2,8 +2,9 @@ import { type FetchRequestTopic } from "../../apis/consumer/fetch-v17";
|
|
|
2
2
|
import { type GroupProtocols } from "../../apis/enumerations";
|
|
3
3
|
import { type ConnectionPool } from "../../network/connection-pool";
|
|
4
4
|
import { type KafkaRecord, type Message } from "../../protocol/records";
|
|
5
|
+
import { type SchemaRegistry } from "../../registries/abstract";
|
|
5
6
|
import { type BaseOptions, type ClusterMetadata, type TopicWithPartitionAndOffset } from "../base/types";
|
|
6
|
-
import { type Deserializers } from "../serde";
|
|
7
|
+
import { type BeforeDeserializationHook, type Deserializers } from "../serde";
|
|
7
8
|
export interface GroupProtocolSubscription {
|
|
8
9
|
name: string;
|
|
9
10
|
version: number;
|
|
@@ -68,6 +69,8 @@ export interface ConsumeBaseOptions<Key, Value, HeaderKey, HeaderValue> {
|
|
|
68
69
|
isolationLevel?: number;
|
|
69
70
|
deserializers?: Partial<Deserializers<Key, Value, HeaderKey, HeaderValue>>;
|
|
70
71
|
highWaterMark?: number;
|
|
72
|
+
beforeDeserialization?: BeforeDeserializationHook;
|
|
73
|
+
registry?: SchemaRegistry<unknown, unknown, Key, Value, HeaderKey, HeaderValue>;
|
|
71
74
|
}
|
|
72
75
|
export interface StreamOptions {
|
|
73
76
|
topics: string[];
|