confluent-schema-registry 3.3.2
Sign up to get free protection for your applications and to get access to all the features.
- package/.dockerignore +2 -0
- package/.prettierrc.js +8 -0
- package/CHANGELOG.md +166 -0
- package/Dockerfile +10 -0
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/avdlToAVSC.sh +9 -0
- package/dist/@types.d.ts +93 -0
- package/dist/@types.js +10 -0
- package/dist/@types.js.map +1 -0
- package/dist/AvroHelper.d.ts +12 -0
- package/dist/AvroHelper.js +67 -0
- package/dist/AvroHelper.js.map +1 -0
- package/dist/JsonHelper.d.ts +7 -0
- package/dist/JsonHelper.js +20 -0
- package/dist/JsonHelper.js.map +1 -0
- package/dist/JsonSchema.d.ts +31 -0
- package/dist/JsonSchema.js +58 -0
- package/dist/JsonSchema.js.map +1 -0
- package/dist/ProtoHelper.d.ts +7 -0
- package/dist/ProtoHelper.js +23 -0
- package/dist/ProtoHelper.js.map +1 -0
- package/dist/ProtoSchema.d.ts +14 -0
- package/dist/ProtoSchema.js +66 -0
- package/dist/ProtoSchema.js.map +1 -0
- package/dist/SchemaRegistry.d.ts +48 -0
- package/dist/SchemaRegistry.js +250 -0
- package/dist/SchemaRegistry.js.map +1 -0
- package/dist/api/index.d.ts +43 -0
- package/dist/api/index.js +90 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/middleware/confluentEncoderMiddleware.d.ts +3 -0
- package/dist/api/middleware/confluentEncoderMiddleware.js +31 -0
- package/dist/api/middleware/confluentEncoderMiddleware.js.map +1 -0
- package/dist/api/middleware/errorMiddleware.d.ts +3 -0
- package/dist/api/middleware/errorMiddleware.js +20 -0
- package/dist/api/middleware/errorMiddleware.js.map +1 -0
- package/dist/api/middleware/userAgent.d.ts +3 -0
- package/dist/api/middleware/userAgent.js +18 -0
- package/dist/api/middleware/userAgent.js.map +1 -0
- package/dist/cache.d.ts +20 -0
- package/dist/cache.js +24 -0
- package/dist/cache.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/schemaTypeResolver.d.ts +4 -0
- package/dist/schemaTypeResolver.js +80 -0
- package/dist/schemaTypeResolver.js.map +1 -0
- package/dist/utils/avdlToAVSC.d.ts +2 -0
- package/dist/utils/avdlToAVSC.js +85 -0
- package/dist/utils/avdlToAVSC.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/readAVSC.d.ts +3 -0
- package/dist/utils/readAVSC.js +33 -0
- package/dist/utils/readAVSC.js.map +1 -0
- package/dist/wireDecoder.d.ts +7 -0
- package/dist/wireDecoder.js +8 -0
- package/dist/wireDecoder.js.map +1 -0
- package/dist/wireEncoder.d.ts +3 -0
- package/dist/wireEncoder.js +10 -0
- package/dist/wireEncoder.js.map +1 -0
- package/dockest-error.json +11 -0
- package/dockest.ts +30 -0
- package/jest.setup.ts +60 -0
- package/package.json +56 -0
- package/release/CHANGELOG.md +166 -0
- package/release/LICENSE +21 -0
- package/release/README.md +44 -0
- package/release/dist/@types.d.ts +93 -0
- package/release/dist/@types.js +10 -0
- package/release/dist/@types.js.map +1 -0
- package/release/dist/AvroHelper.d.ts +12 -0
- package/release/dist/AvroHelper.js +67 -0
- package/release/dist/AvroHelper.js.map +1 -0
- package/release/dist/JsonHelper.d.ts +7 -0
- package/release/dist/JsonHelper.js +20 -0
- package/release/dist/JsonHelper.js.map +1 -0
- package/release/dist/JsonSchema.d.ts +31 -0
- package/release/dist/JsonSchema.js +58 -0
- package/release/dist/JsonSchema.js.map +1 -0
- package/release/dist/ProtoHelper.d.ts +7 -0
- package/release/dist/ProtoHelper.js +23 -0
- package/release/dist/ProtoHelper.js.map +1 -0
- package/release/dist/ProtoSchema.d.ts +14 -0
- package/release/dist/ProtoSchema.js +66 -0
- package/release/dist/ProtoSchema.js.map +1 -0
- package/release/dist/SchemaRegistry.d.ts +48 -0
- package/release/dist/SchemaRegistry.js +250 -0
- package/release/dist/SchemaRegistry.js.map +1 -0
- package/release/dist/api/index.d.ts +43 -0
- package/release/dist/api/index.js +90 -0
- package/release/dist/api/index.js.map +1 -0
- package/release/dist/api/middleware/confluentEncoderMiddleware.d.ts +3 -0
- package/release/dist/api/middleware/confluentEncoderMiddleware.js +31 -0
- package/release/dist/api/middleware/confluentEncoderMiddleware.js.map +1 -0
- package/release/dist/api/middleware/errorMiddleware.d.ts +3 -0
- package/release/dist/api/middleware/errorMiddleware.js +20 -0
- package/release/dist/api/middleware/errorMiddleware.js.map +1 -0
- package/release/dist/api/middleware/userAgent.d.ts +3 -0
- package/release/dist/api/middleware/userAgent.js +18 -0
- package/release/dist/api/middleware/userAgent.js.map +1 -0
- package/release/dist/cache.d.ts +20 -0
- package/release/dist/cache.js +24 -0
- package/release/dist/cache.js.map +1 -0
- package/release/dist/constants.d.ts +11 -0
- package/release/dist/constants.js +15 -0
- package/release/dist/constants.js.map +1 -0
- package/release/dist/errors.d.ts +14 -0
- package/release/dist/errors.js +26 -0
- package/release/dist/errors.js.map +1 -0
- package/release/dist/index.d.ts +4 -0
- package/release/dist/index.js +13 -0
- package/release/dist/index.js.map +1 -0
- package/release/dist/schemaTypeResolver.d.ts +4 -0
- package/release/dist/schemaTypeResolver.js +80 -0
- package/release/dist/schemaTypeResolver.js.map +1 -0
- package/release/dist/utils/avdlToAVSC.d.ts +2 -0
- package/release/dist/utils/avdlToAVSC.js +85 -0
- package/release/dist/utils/avdlToAVSC.js.map +1 -0
- package/release/dist/utils/index.d.ts +2 -0
- package/release/dist/utils/index.js +9 -0
- package/release/dist/utils/index.js.map +1 -0
- package/release/dist/utils/readAVSC.d.ts +3 -0
- package/release/dist/utils/readAVSC.js +33 -0
- package/release/dist/utils/readAVSC.js.map +1 -0
- package/release/dist/wireDecoder.d.ts +7 -0
- package/release/dist/wireDecoder.js +8 -0
- package/release/dist/wireDecoder.js.map +1 -0
- package/release/dist/wireEncoder.d.ts +3 -0
- package/release/dist/wireEncoder.js +10 -0
- package/release/dist/wireEncoder.js.map +1 -0
- package/release/package.json +56 -0
- package/src/@types.ts +105 -0
- package/src/AvroHelper.ts +91 -0
- package/src/JsonHelper.ts +35 -0
- package/src/JsonSchema.ts +80 -0
- package/src/ProtoHelper.ts +38 -0
- package/src/ProtoSchema.ts +80 -0
- package/src/SchemaRegistry.avro.spec.ts +558 -0
- package/src/SchemaRegistry.json.spec.ts +364 -0
- package/src/SchemaRegistry.newApi.spec.ts +622 -0
- package/src/SchemaRegistry.protobuf.spec.ts +372 -0
- package/src/SchemaRegistry.spec.ts +252 -0
- package/src/SchemaRegistry.ts +387 -0
- package/src/api/index.spec.ts +23 -0
- package/src/api/index.ts +121 -0
- package/src/api/middleware/confluentEncoderMiddleware.ts +36 -0
- package/src/api/middleware/errorMiddleware.spec.ts +67 -0
- package/src/api/middleware/errorMiddleware.ts +37 -0
- package/src/api/middleware/userAgent.spec.ts +53 -0
- package/src/api/middleware/userAgent.ts +19 -0
- package/src/cache.ts +34 -0
- package/src/constants.ts +13 -0
- package/src/errors.ts +26 -0
- package/src/index.ts +4 -0
- package/src/schemaTypeResolver.ts +101 -0
- package/src/utils/avdlToAVSC.spec.ts +79 -0
- package/src/utils/avdlToAVSC.ts +106 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/readAVSC.spec.ts +23 -0
- package/src/utils/readAVSC.ts +36 -0
- package/src/wireDecoder.ts +5 -0
- package/src/wireEncoder.ts +10 -0
- package/tsconfig.json +22 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
import { Request } from 'mappersmith'
|
2
|
+
|
3
|
+
import UserAgentMiddleware from './userAgent'
|
4
|
+
|
5
|
+
const middlewareParams = (clientId?: string) => ({
|
6
|
+
resourceName: 'resourceNameMock',
|
7
|
+
resourceMethod: 'resourceMethodMock',
|
8
|
+
context: { context: 'contextMock' },
|
9
|
+
clientId,
|
10
|
+
})
|
11
|
+
|
12
|
+
describe('UserAgentMiddleware', () => {
|
13
|
+
let next, request
|
14
|
+
|
15
|
+
beforeEach(() => {
|
16
|
+
request = ({
|
17
|
+
enhance: jest.fn(),
|
18
|
+
} as unknown) as jest.Mocked<Request>
|
19
|
+
next = jest.fn().mockResolvedValue(request)
|
20
|
+
})
|
21
|
+
|
22
|
+
describe('When the user has provided a clientId', () => {
|
23
|
+
const params = middlewareParams('some-client-id')
|
24
|
+
|
25
|
+
it('should add the client id as a user agent comment', async () => {
|
26
|
+
const middleware = UserAgentMiddleware(params)
|
27
|
+
|
28
|
+
await middleware.prepareRequest(next, jest.fn())
|
29
|
+
|
30
|
+
expect(request.enhance).toHaveBeenCalledWith({
|
31
|
+
headers: {
|
32
|
+
'User-Agent': `@kafkajs/confluent-schema-registry (${params.clientId})`,
|
33
|
+
},
|
34
|
+
})
|
35
|
+
})
|
36
|
+
})
|
37
|
+
|
38
|
+
describe('When the user has not provided a clientId', () => {
|
39
|
+
const params = middlewareParams()
|
40
|
+
|
41
|
+
it('should not include a comment in the user agent', async () => {
|
42
|
+
const middleware = UserAgentMiddleware(params)
|
43
|
+
|
44
|
+
await middleware.prepareRequest(next, jest.fn())
|
45
|
+
|
46
|
+
expect(request.enhance).toHaveBeenCalledWith({
|
47
|
+
headers: {
|
48
|
+
'User-Agent': `@kafkajs/confluent-schema-registry`,
|
49
|
+
},
|
50
|
+
})
|
51
|
+
})
|
52
|
+
})
|
53
|
+
})
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { Middleware } from 'mappersmith'
|
2
|
+
import { DEFAULT_API_CLIENT_ID } from '../../constants'
|
3
|
+
|
4
|
+
const product = '@kafkajs/confluent-schema-registry'
|
5
|
+
|
6
|
+
const userAgentMiddleware: Middleware = ({ clientId }) => {
|
7
|
+
const comment = clientId !== DEFAULT_API_CLIENT_ID ? clientId : undefined
|
8
|
+
const userAgent = comment ? `${product} (${comment})` : product
|
9
|
+
const headers = {
|
10
|
+
'User-Agent': userAgent,
|
11
|
+
}
|
12
|
+
return {
|
13
|
+
prepareRequest: next => {
|
14
|
+
return next().then(req => req.enhance({ headers }))
|
15
|
+
},
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
export default userAgentMiddleware
|
package/src/cache.ts
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import { AvroSchema, Schema, SchemaType } from './@types'
|
2
|
+
|
3
|
+
type CacheEntry = { type: SchemaType; schema: Schema | AvroSchema }
|
4
|
+
|
5
|
+
export default class Cache {
|
6
|
+
registryIdBySubject: { [key: string]: number }
|
7
|
+
schemasByRegistryId: { [key: string]: CacheEntry }
|
8
|
+
|
9
|
+
constructor() {
|
10
|
+
this.registryIdBySubject = {}
|
11
|
+
this.schemasByRegistryId = {}
|
12
|
+
}
|
13
|
+
|
14
|
+
getLatestRegistryId = (subject: string): number | undefined => this.registryIdBySubject[subject]
|
15
|
+
|
16
|
+
setLatestRegistryId = (subject: string, id: number): number => {
|
17
|
+
this.registryIdBySubject[subject] = id
|
18
|
+
|
19
|
+
return this.registryIdBySubject[subject]
|
20
|
+
}
|
21
|
+
|
22
|
+
getSchema = (registryId: number): CacheEntry | undefined => this.schemasByRegistryId[registryId]
|
23
|
+
|
24
|
+
setSchema = (registryId: number, type: SchemaType, schema: Schema): CacheEntry => {
|
25
|
+
this.schemasByRegistryId[registryId] = { type, schema }
|
26
|
+
|
27
|
+
return this.schemasByRegistryId[registryId]
|
28
|
+
}
|
29
|
+
|
30
|
+
clear = (): void => {
|
31
|
+
this.registryIdBySubject = {}
|
32
|
+
this.schemasByRegistryId = {}
|
33
|
+
}
|
34
|
+
}
|
package/src/constants.ts
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
export enum COMPATIBILITY {
|
2
|
+
NONE = 'NONE',
|
3
|
+
FULL = 'FULL',
|
4
|
+
BACKWARD = 'BACKWARD',
|
5
|
+
FORWARD = 'FORWARD',
|
6
|
+
BACKWARD_TRANSITIVE = 'BACKWARD_TRANSITIVE',
|
7
|
+
FORWARD_TRANSITIVE = 'FORWARD_TRANSITIVE',
|
8
|
+
FULL_TRANSITIVE = 'FULL_TRANSITIVE',
|
9
|
+
}
|
10
|
+
|
11
|
+
export const DEFAULT_SEPERATOR = '.'
|
12
|
+
|
13
|
+
export const DEFAULT_API_CLIENT_ID = 'Confluent_Schema_Registry'
|
package/src/errors.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class ConfluentSchemaRegistryError extends Error {
|
2
|
+
constructor(error: any) {
|
3
|
+
super(error.message || error)
|
4
|
+
this.name = this.constructor.name
|
5
|
+
}
|
6
|
+
}
|
7
|
+
|
8
|
+
class ConfluentSchemaRegistryArgumentError extends ConfluentSchemaRegistryError {}
|
9
|
+
class ConfluentSchemaRegistryCompatibilityError extends ConfluentSchemaRegistryError {}
|
10
|
+
class ConfluentSchemaRegistryInvalidSchemaError extends ConfluentSchemaRegistryError {}
|
11
|
+
class ConfluentSchemaRegistryValidationError extends ConfluentSchemaRegistryError {
|
12
|
+
public paths: string[][]
|
13
|
+
|
14
|
+
constructor(error: any, paths: string[][]) {
|
15
|
+
super(error)
|
16
|
+
this.paths = paths
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
export {
|
21
|
+
ConfluentSchemaRegistryError,
|
22
|
+
ConfluentSchemaRegistryArgumentError,
|
23
|
+
ConfluentSchemaRegistryCompatibilityError,
|
24
|
+
ConfluentSchemaRegistryInvalidSchemaError,
|
25
|
+
ConfluentSchemaRegistryValidationError,
|
26
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
import AvroHelper from './AvroHelper'
|
2
|
+
import JsonHelper from './JsonHelper'
|
3
|
+
import JsonSchema from './JsonSchema'
|
4
|
+
import ProtoHelper from './ProtoHelper'
|
5
|
+
import ProtoSchema from './ProtoSchema'
|
6
|
+
import {
|
7
|
+
SchemaType,
|
8
|
+
SchemaHelper,
|
9
|
+
ConfluentSchema,
|
10
|
+
SchemaRegistryAPIClientOptions,
|
11
|
+
LegacyOptions,
|
12
|
+
ProtocolOptions,
|
13
|
+
AvroOptions,
|
14
|
+
JsonOptions,
|
15
|
+
ProtoOptions,
|
16
|
+
Schema,
|
17
|
+
AvroSchema,
|
18
|
+
} from './@types'
|
19
|
+
import { ConfluentSchemaRegistryArgumentError } from './errors'
|
20
|
+
|
21
|
+
const helperTypeFromSchemaTypeMap: Record<string, SchemaHelper> = {}
|
22
|
+
|
23
|
+
export const schemaTypeFromString = (schemaTypeString: string) => {
|
24
|
+
switch (schemaTypeString) {
|
25
|
+
case 'AVRO':
|
26
|
+
case undefined:
|
27
|
+
return SchemaType.AVRO
|
28
|
+
case 'JSON':
|
29
|
+
return SchemaType.JSON
|
30
|
+
case 'PROTOBUF':
|
31
|
+
return SchemaType.PROTOBUF
|
32
|
+
default:
|
33
|
+
return SchemaType.UNKNOWN
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
export const helperTypeFromSchemaType = (
|
38
|
+
schemaType: SchemaType = SchemaType.AVRO,
|
39
|
+
): SchemaHelper => {
|
40
|
+
const schemaTypeStr = schemaType.toString()
|
41
|
+
|
42
|
+
if (!helperTypeFromSchemaTypeMap[schemaTypeStr]) {
|
43
|
+
let helper
|
44
|
+
switch (schemaType) {
|
45
|
+
case SchemaType.AVRO: {
|
46
|
+
helper = new AvroHelper()
|
47
|
+
break
|
48
|
+
}
|
49
|
+
case SchemaType.JSON: {
|
50
|
+
helper = new JsonHelper()
|
51
|
+
break
|
52
|
+
}
|
53
|
+
case SchemaType.PROTOBUF: {
|
54
|
+
helper = new ProtoHelper()
|
55
|
+
break
|
56
|
+
}
|
57
|
+
default:
|
58
|
+
throw new ConfluentSchemaRegistryArgumentError('invalid schemaType')
|
59
|
+
}
|
60
|
+
helperTypeFromSchemaTypeMap[schemaTypeStr] = helper
|
61
|
+
}
|
62
|
+
return helperTypeFromSchemaTypeMap[schemaTypeStr]
|
63
|
+
}
|
64
|
+
|
65
|
+
export const schemaFromConfluentSchema = (
|
66
|
+
confluentSchema: ConfluentSchema,
|
67
|
+
options?: SchemaRegistryAPIClientOptions,
|
68
|
+
): Schema | AvroSchema => {
|
69
|
+
try {
|
70
|
+
let schema: Schema
|
71
|
+
|
72
|
+
switch (confluentSchema.type) {
|
73
|
+
case SchemaType.AVRO: {
|
74
|
+
const opts: AvroOptions | undefined =
|
75
|
+
(options as LegacyOptions)?.forSchemaOptions ||
|
76
|
+
(options as ProtocolOptions)?.[SchemaType.AVRO]
|
77
|
+
schema = (helperTypeFromSchemaType(confluentSchema.type) as AvroHelper).getAvroSchema(
|
78
|
+
confluentSchema,
|
79
|
+
opts,
|
80
|
+
)
|
81
|
+
break
|
82
|
+
}
|
83
|
+
case SchemaType.JSON: {
|
84
|
+
const opts: JsonOptions | undefined = (options as ProtocolOptions)?.[SchemaType.JSON]
|
85
|
+
schema = new JsonSchema(confluentSchema, opts)
|
86
|
+
break
|
87
|
+
}
|
88
|
+
case SchemaType.PROTOBUF: {
|
89
|
+
const opts: ProtoOptions | undefined = (options as ProtocolOptions)?.[SchemaType.PROTOBUF]
|
90
|
+
schema = new ProtoSchema(confluentSchema, opts)
|
91
|
+
break
|
92
|
+
}
|
93
|
+
default:
|
94
|
+
throw new ConfluentSchemaRegistryArgumentError('invalid schemaType')
|
95
|
+
}
|
96
|
+
|
97
|
+
return schema
|
98
|
+
} catch (err) {
|
99
|
+
throw new ConfluentSchemaRegistryArgumentError(err.message)
|
100
|
+
}
|
101
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import path from 'path'
|
2
|
+
import fs from 'fs-extra'
|
3
|
+
import execa from 'execa'
|
4
|
+
import avro from 'avsc'
|
5
|
+
|
6
|
+
import SchemaRegistry from '../SchemaRegistry'
|
7
|
+
import { avdlToAVSCAsync } from './avdlToAVSC'
|
8
|
+
|
9
|
+
const registry = new SchemaRegistry({ host: 'http://localhost:8982' })
|
10
|
+
const absolutePath = (...paths: string[]) => path.join(__dirname, '../..', ...paths)
|
11
|
+
|
12
|
+
const compareWithJavaImplementation = (avdlPath: string, name: string) => async () => {
|
13
|
+
const absolutePathToAvdlToAVSC = absolutePath('./bin/avdlToAVSC.sh')
|
14
|
+
const execaArgs = [`./fixtures/avdl/${avdlPath}`, name]
|
15
|
+
|
16
|
+
let expectedAVSC
|
17
|
+
try {
|
18
|
+
const { stdout: result } = await execa(absolutePathToAvdlToAVSC, execaArgs)
|
19
|
+
expectedAVSC = JSON.parse(result)
|
20
|
+
} catch (error) {
|
21
|
+
console.error(`Error when running ${absolutePathToAvdlToAVSC}`, error) // eslint-disable-line no-console
|
22
|
+
throw error
|
23
|
+
}
|
24
|
+
|
25
|
+
const avsc = await avdlToAVSCAsync(absolutePath('./fixtures/avdl', avdlPath))
|
26
|
+
|
27
|
+
expect(avsc).toEqual(expectedAVSC)
|
28
|
+
expect(avro.Type.forSchema(avsc)).toBeTruthy()
|
29
|
+
expect(await registry.register(avsc)).toBeTruthy()
|
30
|
+
}
|
31
|
+
|
32
|
+
beforeAll(async () => {
|
33
|
+
jest.setTimeout(10000)
|
34
|
+
await fs.emptyDir(absolutePath('./tmp'))
|
35
|
+
})
|
36
|
+
|
37
|
+
test('simple protocol', compareWithJavaImplementation('simple.avdl', 'Simple'))
|
38
|
+
|
39
|
+
test('protocol with two levels', compareWithJavaImplementation('two.avdl', 'Two'))
|
40
|
+
|
41
|
+
test('protocol with multiple levels', compareWithJavaImplementation('multiple.avdl', 'Multiple'))
|
42
|
+
|
43
|
+
test('protocol with union', compareWithJavaImplementation('union.avdl', 'Union'))
|
44
|
+
|
45
|
+
test(
|
46
|
+
'protocol with multiple union levels',
|
47
|
+
compareWithJavaImplementation('multiple_union.avdl', 'MultipleUnion'),
|
48
|
+
)
|
49
|
+
|
50
|
+
test('protocol with enum', compareWithJavaImplementation('enum.avdl', 'Enum'))
|
51
|
+
|
52
|
+
test('protocol with enum & union', compareWithJavaImplementation('enum_union.avdl', 'EnumUnion'))
|
53
|
+
|
54
|
+
test('protocol with array', compareWithJavaImplementation('array.avdl', 'Array'))
|
55
|
+
|
56
|
+
test('protocol with array & union', compareWithJavaImplementation('array_union.avdl', 'ArrayUnion'))
|
57
|
+
|
58
|
+
test('protocol with really complex stuff', compareWithJavaImplementation('complex.avdl', 'Complex'))
|
59
|
+
|
60
|
+
test(
|
61
|
+
'protocol with multiple namespaces',
|
62
|
+
compareWithJavaImplementation('multiple_namespaces.avdl', 'MultipleNamespaces'),
|
63
|
+
)
|
64
|
+
|
65
|
+
/*
|
66
|
+
* AVSC includes the namespace of imported records even if they are being imported
|
67
|
+
* into the same namespace, causing a difference against the Java version.
|
68
|
+
*
|
69
|
+
* @issue: https://github.com/mtth/avsc/issues/281
|
70
|
+
*/
|
71
|
+
test.skip(
|
72
|
+
'protocol with import from same namespace',
|
73
|
+
compareWithJavaImplementation('import.avdl', 'Import'),
|
74
|
+
)
|
75
|
+
|
76
|
+
test(
|
77
|
+
'protocol with import from different namespace',
|
78
|
+
compareWithJavaImplementation('import_multiple_namespaces.avdl', 'ImportMultipleNamespaces'),
|
79
|
+
)
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import * as fs from 'fs'
|
2
|
+
import { assembleProtocol, readProtocol } from 'avsc'
|
3
|
+
|
4
|
+
import { ConfluentSchemaRegistryError } from '../errors'
|
5
|
+
|
6
|
+
interface AssembleProtocolError extends Error {
|
7
|
+
path: string
|
8
|
+
}
|
9
|
+
interface Obj {
|
10
|
+
[key: string]: any
|
11
|
+
}
|
12
|
+
interface Iterable extends Obj {
|
13
|
+
map: any
|
14
|
+
}
|
15
|
+
interface Field {
|
16
|
+
type: {
|
17
|
+
type: string
|
18
|
+
items: any
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let cache: any
|
23
|
+
const merge = Object.assign
|
24
|
+
const isObject = (obj: unknown): obj is Obj => obj && typeof obj === 'object'
|
25
|
+
const isIterable = (obj: unknown): obj is Iterable =>
|
26
|
+
isObject(obj) && typeof obj.map !== 'undefined'
|
27
|
+
const isFieldArray = (field: unknown): field is Field =>
|
28
|
+
isObject(field) && isObject(field.type) && field.type.type === 'array'
|
29
|
+
|
30
|
+
const combine = (rootType: any, types: any) => {
|
31
|
+
if (!rootType.fields) {
|
32
|
+
return rootType
|
33
|
+
}
|
34
|
+
|
35
|
+
const find = (name: any) => {
|
36
|
+
if (typeof name === 'string') {
|
37
|
+
name = name.toLowerCase()
|
38
|
+
}
|
39
|
+
|
40
|
+
const typeToCombine = types.find((t: any) => {
|
41
|
+
const names = []
|
42
|
+
if (t.namespace) {
|
43
|
+
names.push(`${t.namespace}.`)
|
44
|
+
}
|
45
|
+
names.push(t.name.toLowerCase())
|
46
|
+
|
47
|
+
return names.join('') === name
|
48
|
+
})
|
49
|
+
|
50
|
+
if (!typeToCombine || cache[typeToCombine.name]) {
|
51
|
+
return null
|
52
|
+
}
|
53
|
+
|
54
|
+
cache[typeToCombine.name] = 1
|
55
|
+
|
56
|
+
return combine(typeToCombine, types)
|
57
|
+
}
|
58
|
+
|
59
|
+
const combinedFields = rootType.fields.map((field: any) => {
|
60
|
+
if (isFieldArray(field)) {
|
61
|
+
const typeToCombine = find(field.type.items)
|
62
|
+
return typeToCombine
|
63
|
+
? merge(field, { type: merge(field.type, { items: typeToCombine }) })
|
64
|
+
: field
|
65
|
+
} else if (isIterable(field.type)) {
|
66
|
+
const type = field.type.map((unionType: any) => {
|
67
|
+
if (isObject(unionType)) {
|
68
|
+
const typeToCombine = find(unionType.items)
|
69
|
+
return typeToCombine ? merge(unionType, { items: typeToCombine }) : unionType
|
70
|
+
} else {
|
71
|
+
return find(unionType) || unionType
|
72
|
+
}
|
73
|
+
})
|
74
|
+
|
75
|
+
return merge(field, { type })
|
76
|
+
}
|
77
|
+
|
78
|
+
const typeToCombine = find(field.type)
|
79
|
+
return typeToCombine ? merge(field, { type: typeToCombine }) : field
|
80
|
+
})
|
81
|
+
|
82
|
+
return merge(rootType, { fields: combinedFields })
|
83
|
+
}
|
84
|
+
|
85
|
+
export function avdlToAVSC(path: any) {
|
86
|
+
cache = {}
|
87
|
+
const protocol = readProtocol(fs.readFileSync(path, 'utf8'))
|
88
|
+
|
89
|
+
return merge({ namespace: protocol.namespace }, combine(protocol.types.pop(), protocol.types))
|
90
|
+
}
|
91
|
+
|
92
|
+
export async function avdlToAVSCAsync(path: string) {
|
93
|
+
cache = {}
|
94
|
+
|
95
|
+
const protocol: { [key: string]: any } = await new Promise((resolve, reject) => {
|
96
|
+
assembleProtocol(path, (err: AssembleProtocolError, schema) => {
|
97
|
+
if (err) {
|
98
|
+
reject(new ConfluentSchemaRegistryError(`${err.message}. Caused by: ${err.path}`))
|
99
|
+
} else {
|
100
|
+
resolve(schema)
|
101
|
+
}
|
102
|
+
})
|
103
|
+
})
|
104
|
+
|
105
|
+
return merge({ namespace: protocol.namespace }, combine(protocol.types.pop(), protocol.types))
|
106
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import path from 'path'
|
2
|
+
|
3
|
+
import { readAVSC, readAVSCAsync } from './readAVSC'
|
4
|
+
import { ConfluentSchemaRegistryInvalidSchemaError } from '../errors'
|
5
|
+
|
6
|
+
describe('readAVSC', () => {
|
7
|
+
const invalidSchemaFiles = ['invalidType', 'missingFields', 'missingName', 'missingType']
|
8
|
+
invalidSchemaFiles.forEach(schemaName => {
|
9
|
+
it(`throws an exception for invalid schema definitions - ${schemaName}`, () => {
|
10
|
+
expect(() =>
|
11
|
+
readAVSC(path.join(__dirname, `../../fixtures/avsc/invalid/${schemaName}.avsc`)),
|
12
|
+
).toThrow(ConfluentSchemaRegistryInvalidSchemaError)
|
13
|
+
})
|
14
|
+
})
|
15
|
+
})
|
16
|
+
|
17
|
+
describe('readAVSCAsync', () => {
|
18
|
+
it('returns a validated schema asynchronously', async () => {
|
19
|
+
return expect(
|
20
|
+
readAVSCAsync(path.join(__dirname, `../../fixtures/avsc/person.avsc`)),
|
21
|
+
).resolves.toHaveProperty('name', 'Person')
|
22
|
+
})
|
23
|
+
})
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import fs from 'fs'
|
2
|
+
import { promisify } from 'util'
|
3
|
+
|
4
|
+
import { RawAvroSchema } from '../@types'
|
5
|
+
import { ConfluentSchemaRegistryInvalidSchemaError } from '../errors'
|
6
|
+
|
7
|
+
const readFileAsync = promisify(fs.readFile)
|
8
|
+
const ENCODING = 'utf-8'
|
9
|
+
|
10
|
+
function isValidSchema(rawSchema: any): rawSchema is RawAvroSchema {
|
11
|
+
return (
|
12
|
+
'name' in rawSchema &&
|
13
|
+
'type' in rawSchema &&
|
14
|
+
rawSchema.type === 'record' &&
|
15
|
+
'fields' in rawSchema
|
16
|
+
)
|
17
|
+
}
|
18
|
+
|
19
|
+
function validatedSchema(path: string, rawSchema: any): RawAvroSchema {
|
20
|
+
if (!isValidSchema(rawSchema)) {
|
21
|
+
throw new ConfluentSchemaRegistryInvalidSchemaError(
|
22
|
+
`${path} is not recognized as a valid AVSC file (expecting valid top-level name, type and fields attributes)`,
|
23
|
+
)
|
24
|
+
}
|
25
|
+
return rawSchema
|
26
|
+
}
|
27
|
+
|
28
|
+
export function readAVSC(path: string): RawAvroSchema {
|
29
|
+
const rawSchema = JSON.parse(fs.readFileSync(path, ENCODING))
|
30
|
+
return validatedSchema(path, rawSchema)
|
31
|
+
}
|
32
|
+
|
33
|
+
export async function readAVSCAsync(path: string): Promise<RawAvroSchema> {
|
34
|
+
const rawSchema = JSON.parse(await readFileAsync(path, ENCODING))
|
35
|
+
return validatedSchema(path, rawSchema)
|
36
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
const DEFAULT_OFFSET = 0
|
2
|
+
|
3
|
+
export const MAGIC_BYTE = Buffer.alloc(1)
|
4
|
+
|
5
|
+
export const encode = (registryId: number, payload: Buffer) => {
|
6
|
+
const registryIdBuffer = Buffer.alloc(4)
|
7
|
+
registryIdBuffer.writeInt32BE(registryId, DEFAULT_OFFSET)
|
8
|
+
|
9
|
+
return Buffer.concat([MAGIC_BYTE, registryIdBuffer, payload])
|
10
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"baseUrl": ".",
|
4
|
+
"paths": {
|
5
|
+
"*": ["src/*", "declarations/*"]
|
6
|
+
},
|
7
|
+
"outDir": "dist",
|
8
|
+
"lib": ["es2019", "dom"],
|
9
|
+
"module": "commonjs",
|
10
|
+
"moduleResolution": "node",
|
11
|
+
"sourceMap": true,
|
12
|
+
"strict": true,
|
13
|
+
"target": "es2019",
|
14
|
+
"noUnusedParameters": true,
|
15
|
+
"noUnusedLocals": true,
|
16
|
+
"esModuleInterop": true,
|
17
|
+
"declaration": true,
|
18
|
+
"resolveJsonModule": true
|
19
|
+
},
|
20
|
+
"include": ["./src/**/*.ts", "declarations/**/*.ts"],
|
21
|
+
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
|
22
|
+
}
|