confluent-schema-registry 3.3.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/.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
|
+
}
|