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,387 @@
|
|
|
1
|
+
import { Type } from 'avsc'
|
|
2
|
+
import { Response } from 'mappersmith'
|
|
3
|
+
|
|
4
|
+
import { encode, MAGIC_BYTE } from './wireEncoder'
|
|
5
|
+
import decode from './wireDecoder'
|
|
6
|
+
import { COMPATIBILITY, DEFAULT_SEPERATOR } from './constants'
|
|
7
|
+
import API, { SchemaRegistryAPIClientArgs, SchemaRegistryAPIClient } from './api'
|
|
8
|
+
import Cache from './cache'
|
|
9
|
+
import {
|
|
10
|
+
ConfluentSchemaRegistryError,
|
|
11
|
+
ConfluentSchemaRegistryArgumentError,
|
|
12
|
+
ConfluentSchemaRegistryCompatibilityError,
|
|
13
|
+
ConfluentSchemaRegistryValidationError,
|
|
14
|
+
} from './errors'
|
|
15
|
+
import {
|
|
16
|
+
Schema,
|
|
17
|
+
RawAvroSchema,
|
|
18
|
+
AvroSchema,
|
|
19
|
+
SchemaType,
|
|
20
|
+
ConfluentSchema,
|
|
21
|
+
ConfluentSubject,
|
|
22
|
+
SchemaRegistryAPIClientOptions,
|
|
23
|
+
AvroConfluentSchema,
|
|
24
|
+
SchemaResponse,
|
|
25
|
+
ProtocolOptions,
|
|
26
|
+
SchemaHelper,
|
|
27
|
+
SchemaReference,
|
|
28
|
+
LegacyOptions,
|
|
29
|
+
} from './@types'
|
|
30
|
+
import {
|
|
31
|
+
helperTypeFromSchemaType,
|
|
32
|
+
schemaTypeFromString,
|
|
33
|
+
schemaFromConfluentSchema,
|
|
34
|
+
} from './schemaTypeResolver'
|
|
35
|
+
|
|
36
|
+
export interface RegisteredSchema {
|
|
37
|
+
id: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Opts {
|
|
41
|
+
compatibility?: COMPATIBILITY
|
|
42
|
+
separator?: string
|
|
43
|
+
subject: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AvroDecodeOptions {
|
|
47
|
+
readerSchema?: RawAvroSchema | AvroSchema | Schema
|
|
48
|
+
}
|
|
49
|
+
interface DecodeOptions {
|
|
50
|
+
[SchemaType.AVRO]?: AvroDecodeOptions
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_OPTS = {
|
|
54
|
+
compatibility: COMPATIBILITY.BACKWARD,
|
|
55
|
+
separator: DEFAULT_SEPERATOR,
|
|
56
|
+
}
|
|
57
|
+
export default class SchemaRegistry {
|
|
58
|
+
private api: SchemaRegistryAPIClient
|
|
59
|
+
private cacheMissRequests: { [key: number]: Promise<Response> } = {}
|
|
60
|
+
private options: SchemaRegistryAPIClientOptions | undefined
|
|
61
|
+
|
|
62
|
+
public cache: Cache
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
{ auth, clientId, host, retry, agent }: SchemaRegistryAPIClientArgs,
|
|
66
|
+
options?: SchemaRegistryAPIClientOptions,
|
|
67
|
+
) {
|
|
68
|
+
this.api = API({ auth, clientId, host, retry, agent })
|
|
69
|
+
this.cache = new Cache()
|
|
70
|
+
this.options = options
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private isConfluentSchema(
|
|
74
|
+
schema: RawAvroSchema | AvroSchema | ConfluentSchema,
|
|
75
|
+
): schema is ConfluentSchema {
|
|
76
|
+
return (schema as ConfluentSchema).schema != null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getConfluentSchema(
|
|
80
|
+
schema: RawAvroSchema | AvroSchema | ConfluentSchema,
|
|
81
|
+
): ConfluentSchema {
|
|
82
|
+
let confluentSchema: ConfluentSchema
|
|
83
|
+
// convert data from old api (for backwards compatibility)
|
|
84
|
+
if (!this.isConfluentSchema(schema)) {
|
|
85
|
+
// schema is instanceof RawAvroSchema or AvroSchema
|
|
86
|
+
confluentSchema = {
|
|
87
|
+
type: SchemaType.AVRO,
|
|
88
|
+
schema: JSON.stringify(schema),
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
confluentSchema = schema as ConfluentSchema
|
|
92
|
+
}
|
|
93
|
+
return confluentSchema
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async register(
|
|
97
|
+
schema: Exclude<ConfluentSchema, AvroConfluentSchema>,
|
|
98
|
+
userOpts: Opts,
|
|
99
|
+
): Promise<RegisteredSchema>
|
|
100
|
+
public async register(
|
|
101
|
+
schema: RawAvroSchema | AvroConfluentSchema,
|
|
102
|
+
userOpts?: Omit<Opts, 'subject'> & { subject?: string },
|
|
103
|
+
): Promise<RegisteredSchema>
|
|
104
|
+
public async register(
|
|
105
|
+
schema: RawAvroSchema | ConfluentSchema,
|
|
106
|
+
userOpts: Opts,
|
|
107
|
+
): Promise<RegisteredSchema>
|
|
108
|
+
public async register(
|
|
109
|
+
schema: RawAvroSchema | ConfluentSchema,
|
|
110
|
+
userOpts?: Opts,
|
|
111
|
+
): Promise<RegisteredSchema> {
|
|
112
|
+
const { compatibility, separator } = { ...DEFAULT_OPTS, ...userOpts }
|
|
113
|
+
|
|
114
|
+
const confluentSchema: ConfluentSchema = this.getConfluentSchema(schema)
|
|
115
|
+
|
|
116
|
+
const helper = helperTypeFromSchemaType(confluentSchema.type)
|
|
117
|
+
|
|
118
|
+
const options = await this.updateOptionsWithSchemaReferences(confluentSchema, this.options)
|
|
119
|
+
const schemaInstance = schemaFromConfluentSchema(confluentSchema, options)
|
|
120
|
+
helper.validate(schemaInstance)
|
|
121
|
+
let isFirstTimeRegistration = false
|
|
122
|
+
let subject: ConfluentSubject
|
|
123
|
+
if (userOpts?.subject) {
|
|
124
|
+
subject = {
|
|
125
|
+
name: userOpts.subject,
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
subject = helper.getSubject(confluentSchema, schemaInstance, separator)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await this.api.Subject.config({ subject: subject.name })
|
|
133
|
+
const { compatibilityLevel }: { compatibilityLevel: COMPATIBILITY } = response.data()
|
|
134
|
+
|
|
135
|
+
if (compatibilityLevel.toUpperCase() !== compatibility) {
|
|
136
|
+
throw new ConfluentSchemaRegistryCompatibilityError(
|
|
137
|
+
`Compatibility does not match the configuration (${compatibility} != ${compatibilityLevel.toUpperCase()})`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.status !== 404) {
|
|
142
|
+
throw error
|
|
143
|
+
} else {
|
|
144
|
+
isFirstTimeRegistration = true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const response = await this.api.Subject.register({
|
|
149
|
+
subject: subject.name,
|
|
150
|
+
body: {
|
|
151
|
+
schemaType: confluentSchema.type === SchemaType.AVRO ? undefined : confluentSchema.type,
|
|
152
|
+
schema: confluentSchema.schema,
|
|
153
|
+
references: confluentSchema.references,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (compatibility && isFirstTimeRegistration) {
|
|
158
|
+
await this.api.Subject.updateConfig({ subject: subject.name, body: { compatibility } })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const registeredSchema: RegisteredSchema = response.data()
|
|
162
|
+
this.cache.setLatestRegistryId(subject.name, registeredSchema.id)
|
|
163
|
+
this.cache.setSchema(registeredSchema.id, confluentSchema.type, schemaInstance)
|
|
164
|
+
|
|
165
|
+
return registeredSchema
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async updateOptionsWithSchemaReferences(
|
|
169
|
+
schema: ConfluentSchema,
|
|
170
|
+
options?: SchemaRegistryAPIClientOptions,
|
|
171
|
+
) {
|
|
172
|
+
const helper = helperTypeFromSchemaType(schema.type)
|
|
173
|
+
const referencedSchemas = await this.getreferencedSchemas(schema, helper)
|
|
174
|
+
|
|
175
|
+
const protocolOptions = this.asProtocolOptions(options)
|
|
176
|
+
return helper.updateOptionsFromSchemaReferences(referencedSchemas, protocolOptions)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private asProtocolOptions(options?: SchemaRegistryAPIClientOptions): ProtocolOptions | undefined {
|
|
180
|
+
if (!(options as LegacyOptions)?.forSchemaOptions) {
|
|
181
|
+
return options as ProtocolOptions | undefined
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
[SchemaType.AVRO]: (options as LegacyOptions)?.forSchemaOptions,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async getreferencedSchemas(
|
|
189
|
+
schema: ConfluentSchema,
|
|
190
|
+
helper: SchemaHelper,
|
|
191
|
+
): Promise<ConfluentSchema[]> {
|
|
192
|
+
const referencesSet = new Set<string>()
|
|
193
|
+
return this.getreferencedSchemasRecursive(schema, helper, referencesSet)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async getreferencedSchemasRecursive(
|
|
197
|
+
schema: ConfluentSchema,
|
|
198
|
+
helper: SchemaHelper,
|
|
199
|
+
referencesSet: Set<string>,
|
|
200
|
+
): Promise<ConfluentSchema[]> {
|
|
201
|
+
const references = schema.references || []
|
|
202
|
+
|
|
203
|
+
let referencedSchemas: ConfluentSchema[] = []
|
|
204
|
+
for (const reference of references) {
|
|
205
|
+
const schemas = await this.getreferencedSchemasFromReference(reference, helper, referencesSet)
|
|
206
|
+
referencedSchemas = referencedSchemas.concat(schemas)
|
|
207
|
+
}
|
|
208
|
+
return referencedSchemas
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getreferencedSchemasFromReference(
|
|
212
|
+
reference: SchemaReference,
|
|
213
|
+
helper: SchemaHelper,
|
|
214
|
+
referencesSet: Set<string>,
|
|
215
|
+
): Promise<ConfluentSchema[]> {
|
|
216
|
+
const { name, subject, version } = reference
|
|
217
|
+
const key = `${name}-${subject}-${version}`
|
|
218
|
+
|
|
219
|
+
// avoid duplicates
|
|
220
|
+
if (referencesSet.has(key)) {
|
|
221
|
+
return []
|
|
222
|
+
}
|
|
223
|
+
referencesSet.add(key)
|
|
224
|
+
|
|
225
|
+
const versionResponse = await this.api.Subject.version(reference)
|
|
226
|
+
const foundSchema = versionResponse.data() as SchemaResponse
|
|
227
|
+
|
|
228
|
+
const schema = helper.toConfluentSchema(foundSchema)
|
|
229
|
+
const referencedSchemas = await this.getreferencedSchemasRecursive(
|
|
230
|
+
schema,
|
|
231
|
+
helper,
|
|
232
|
+
referencesSet,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
referencedSchemas.push(schema)
|
|
236
|
+
return referencedSchemas
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async _getSchema(
|
|
240
|
+
registryId: number,
|
|
241
|
+
): Promise<{ type: SchemaType; schema: Schema | AvroSchema }> {
|
|
242
|
+
const cacheEntry = this.cache.getSchema(registryId)
|
|
243
|
+
|
|
244
|
+
if (cacheEntry) {
|
|
245
|
+
return cacheEntry
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const response = await this.getSchemaOriginRequest(registryId)
|
|
249
|
+
const foundSchema: SchemaResponse = response.data()
|
|
250
|
+
|
|
251
|
+
const schemaType = schemaTypeFromString(foundSchema.schemaType)
|
|
252
|
+
|
|
253
|
+
const helper = helperTypeFromSchemaType(schemaType)
|
|
254
|
+
const confluentSchema = helper.toConfluentSchema(foundSchema)
|
|
255
|
+
|
|
256
|
+
const options = await this.updateOptionsWithSchemaReferences(confluentSchema, this.options)
|
|
257
|
+
const schemaInstance = schemaFromConfluentSchema(confluentSchema, options)
|
|
258
|
+
return this.cache.setSchema(registryId, schemaType, schemaInstance)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async getSchema(registryId: number): Promise<Schema | AvroSchema> {
|
|
262
|
+
return await (await this._getSchema(registryId)).schema
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async encode(registryId: number, payload: any): Promise<Buffer> {
|
|
266
|
+
if (!registryId) {
|
|
267
|
+
throw new ConfluentSchemaRegistryArgumentError(
|
|
268
|
+
`Invalid registryId: ${JSON.stringify(registryId)}`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const { schema } = await this._getSchema(registryId)
|
|
273
|
+
try {
|
|
274
|
+
const serializedPayload = schema.toBuffer(payload)
|
|
275
|
+
return encode(registryId, serializedPayload)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof ConfluentSchemaRegistryValidationError) throw error
|
|
278
|
+
|
|
279
|
+
const paths = this.collectInvalidPaths(schema, payload)
|
|
280
|
+
throw new ConfluentSchemaRegistryValidationError(error, paths)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private collectInvalidPaths(schema: Schema, jsonPayload: object) {
|
|
285
|
+
const paths: string[][] = []
|
|
286
|
+
schema.isValid(jsonPayload, {
|
|
287
|
+
errorHook: path => paths.push(path),
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return paths
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public async decode(buffer: Buffer, options?: DecodeOptions): Promise<any> {
|
|
294
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
295
|
+
throw new ConfluentSchemaRegistryArgumentError('Invalid buffer')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { magicByte, registryId, payload } = decode(buffer)
|
|
299
|
+
if (Buffer.compare(MAGIC_BYTE, magicByte) !== 0) {
|
|
300
|
+
throw new ConfluentSchemaRegistryArgumentError(
|
|
301
|
+
`Message encoded with magic byte ${JSON.stringify(magicByte)}, expected ${JSON.stringify(
|
|
302
|
+
MAGIC_BYTE,
|
|
303
|
+
)}`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { type, schema: writerSchema } = await this._getSchema(registryId)
|
|
308
|
+
|
|
309
|
+
let rawReaderSchema
|
|
310
|
+
switch (type) {
|
|
311
|
+
case SchemaType.AVRO:
|
|
312
|
+
rawReaderSchema = options?.[SchemaType.AVRO]?.readerSchema as RawAvroSchema | AvroSchema
|
|
313
|
+
}
|
|
314
|
+
if (rawReaderSchema) {
|
|
315
|
+
const readerSchema = schemaFromConfluentSchema(
|
|
316
|
+
{ type: SchemaType.AVRO, schema: rawReaderSchema },
|
|
317
|
+
this.options,
|
|
318
|
+
) as AvroSchema
|
|
319
|
+
if (readerSchema.equals(writerSchema as Type)) {
|
|
320
|
+
/* Even when schemas are considered equal by `avsc`,
|
|
321
|
+
* they still aren't interchangeable:
|
|
322
|
+
* provided `readerSchema` may have different `opts` (e.g. logicalTypes / unionWrap flags)
|
|
323
|
+
* see https://github.com/mtth/avsc/issues/362 */
|
|
324
|
+
return readerSchema.fromBuffer(payload)
|
|
325
|
+
} else {
|
|
326
|
+
// decode using a resolver from writer type into reader type
|
|
327
|
+
return readerSchema.fromBuffer(payload, readerSchema.createResolver(writerSchema as Type))
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return writerSchema.fromBuffer(payload)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public async getRegistryId(subject: string, version: number | string): Promise<number> {
|
|
335
|
+
const response = await this.api.Subject.version({ subject, version })
|
|
336
|
+
const { id }: { id: number } = response.data()
|
|
337
|
+
|
|
338
|
+
return id
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public async getRegistryIdBySchema(
|
|
342
|
+
subject: string,
|
|
343
|
+
schema: AvroSchema | RawAvroSchema | ConfluentSchema,
|
|
344
|
+
): Promise<number> {
|
|
345
|
+
try {
|
|
346
|
+
const confluentSchema: ConfluentSchema = this.getConfluentSchema(schema)
|
|
347
|
+
const response = await this.api.Subject.registered({
|
|
348
|
+
subject,
|
|
349
|
+
body: {
|
|
350
|
+
schemaType: confluentSchema.type === SchemaType.AVRO ? undefined : confluentSchema.type,
|
|
351
|
+
schema: confluentSchema.schema,
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
const { id }: { id: number } = response.data()
|
|
355
|
+
|
|
356
|
+
return id
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error.status && error.status === 404) {
|
|
359
|
+
throw new ConfluentSchemaRegistryError(error)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
throw error
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public async getLatestSchemaId(subject: string): Promise<number> {
|
|
367
|
+
const response = await this.api.Subject.latestVersion({ subject })
|
|
368
|
+
const { id }: { id: number } = response.data()
|
|
369
|
+
|
|
370
|
+
return id
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private getSchemaOriginRequest(registryId: number) {
|
|
374
|
+
// ensure that cache-misses result in a single origin request
|
|
375
|
+
if (this.cacheMissRequests[registryId]) {
|
|
376
|
+
return this.cacheMissRequests[registryId]
|
|
377
|
+
} else {
|
|
378
|
+
const request = this.api.Schema.find({ id: registryId }).finally(() => {
|
|
379
|
+
delete this.cacheMissRequests[registryId]
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
this.cacheMissRequests[registryId] = request
|
|
383
|
+
|
|
384
|
+
return request
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import API from '.'
|
|
2
|
+
import { mockClient, install, uninstall } from 'mappersmith/test'
|
|
3
|
+
|
|
4
|
+
const client = API({ clientId: 'test-client', host: 'http://example.com' })
|
|
5
|
+
const mock = mockClient<typeof client>(client)
|
|
6
|
+
.resource('Schema')
|
|
7
|
+
.method('find')
|
|
8
|
+
.with({ id: 'abc' })
|
|
9
|
+
.response({})
|
|
10
|
+
.assertObject()
|
|
11
|
+
|
|
12
|
+
describe('API Client', () => {
|
|
13
|
+
beforeEach(() => install())
|
|
14
|
+
|
|
15
|
+
afterEach(() => uninstall())
|
|
16
|
+
|
|
17
|
+
it('should include a user agent header', async () => {
|
|
18
|
+
const response = await client.Schema.find({ id: 'abc' })
|
|
19
|
+
|
|
20
|
+
expect(mock.callsCount()).toBe(1)
|
|
21
|
+
expect(response.request().header('User-Agent')).not.toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
})
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Agent } from 'http'
|
|
2
|
+
import forge, { Authorization, Client, Options, GatewayConfiguration } from 'mappersmith'
|
|
3
|
+
import RetryMiddleware, { RetryMiddlewareOptions } from 'mappersmith/middleware/retry/v2'
|
|
4
|
+
import BasicAuthMiddleware from 'mappersmith/middleware/basic-auth'
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_API_CLIENT_ID } from '../constants'
|
|
7
|
+
import errorMiddleware from './middleware/errorMiddleware'
|
|
8
|
+
import confluentEncoder from './middleware/confluentEncoderMiddleware'
|
|
9
|
+
import userAgentMiddleware from './middleware/userAgent'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_RETRY = {
|
|
12
|
+
maxRetryTimeInSecs: 5,
|
|
13
|
+
initialRetryTimeInSecs: 0.1,
|
|
14
|
+
factor: 0.2, // randomization factor
|
|
15
|
+
multiplier: 2, // exponential factor
|
|
16
|
+
retries: 3, // max retries
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SchemaRegistryAPIClientArgs {
|
|
20
|
+
host: string
|
|
21
|
+
auth?: Authorization
|
|
22
|
+
clientId?: string
|
|
23
|
+
retry?: Partial<RetryMiddlewareOptions>
|
|
24
|
+
/** HTTP Agent that will be passed to underlying API calls */
|
|
25
|
+
agent?: Agent
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// TODO: Improve typings
|
|
29
|
+
export type SchemaRegistryAPIClient = Client<{
|
|
30
|
+
Schema: {
|
|
31
|
+
find: (_: any) => any
|
|
32
|
+
}
|
|
33
|
+
Subject: {
|
|
34
|
+
all: (_: any) => any
|
|
35
|
+
latestVersion: (_: any) => any
|
|
36
|
+
version: (_: any) => any
|
|
37
|
+
config: (_: any) => any
|
|
38
|
+
updateConfig: (_: any) => any
|
|
39
|
+
register: (_: any) => any
|
|
40
|
+
registered: (_: any) => any
|
|
41
|
+
compatible: (_: any) => any
|
|
42
|
+
}
|
|
43
|
+
}>
|
|
44
|
+
|
|
45
|
+
export default ({
|
|
46
|
+
auth,
|
|
47
|
+
clientId: userClientId,
|
|
48
|
+
host,
|
|
49
|
+
retry = {},
|
|
50
|
+
agent,
|
|
51
|
+
}: SchemaRegistryAPIClientArgs): SchemaRegistryAPIClient => {
|
|
52
|
+
const clientId = userClientId || DEFAULT_API_CLIENT_ID
|
|
53
|
+
// FIXME: ResourcesType typings is not exposed by mappersmith
|
|
54
|
+
const manifest: Options<any> = {
|
|
55
|
+
clientId,
|
|
56
|
+
ignoreGlobalMiddleware: true,
|
|
57
|
+
host,
|
|
58
|
+
middleware: [
|
|
59
|
+
userAgentMiddleware,
|
|
60
|
+
confluentEncoder,
|
|
61
|
+
RetryMiddleware(Object.assign(DEFAULT_RETRY, retry)),
|
|
62
|
+
errorMiddleware,
|
|
63
|
+
...(auth ? [BasicAuthMiddleware(auth)] : []),
|
|
64
|
+
],
|
|
65
|
+
resources: {
|
|
66
|
+
Schema: {
|
|
67
|
+
find: {
|
|
68
|
+
method: 'get',
|
|
69
|
+
path: '/schemas/ids/{id}',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
Subject: {
|
|
73
|
+
all: {
|
|
74
|
+
method: 'get',
|
|
75
|
+
path: '/subjects',
|
|
76
|
+
},
|
|
77
|
+
latestVersion: {
|
|
78
|
+
method: 'get',
|
|
79
|
+
path: '/subjects/{subject}/versions/latest',
|
|
80
|
+
},
|
|
81
|
+
version: {
|
|
82
|
+
method: 'get',
|
|
83
|
+
path: '/subjects/{subject}/versions/{version}',
|
|
84
|
+
},
|
|
85
|
+
registered: {
|
|
86
|
+
method: 'post',
|
|
87
|
+
path: '/subjects/{subject}',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
config: {
|
|
91
|
+
method: 'get',
|
|
92
|
+
path: '/config/{subject}',
|
|
93
|
+
},
|
|
94
|
+
updateConfig: {
|
|
95
|
+
method: 'put',
|
|
96
|
+
path: '/config/{subject}',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
register: {
|
|
100
|
+
method: 'post',
|
|
101
|
+
path: '/subjects/{subject}/versions',
|
|
102
|
+
},
|
|
103
|
+
compatible: {
|
|
104
|
+
method: 'post',
|
|
105
|
+
path: '/compatibility/subjects/{subject}/versions/{version}',
|
|
106
|
+
params: { version: 'latest' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
// if an agent was provided, bind the agent to the mappersmith configs
|
|
112
|
+
if (agent) {
|
|
113
|
+
// gatewayConfigs is not listed as a type on manifest object in mappersmith
|
|
114
|
+
;((manifest as unknown) as { gatewayConfigs: Partial<GatewayConfiguration> }).gatewayConfigs = {
|
|
115
|
+
HTTP: {
|
|
116
|
+
configure: () => ({ agent }),
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return forge(manifest)
|
|
121
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Middleware, Response } from 'mappersmith'
|
|
2
|
+
|
|
3
|
+
const REQUEST_HEADERS = {
|
|
4
|
+
'Content-Type': 'application/vnd.schemaregistry.v1+json',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const updateContentType = (response: Response) =>
|
|
8
|
+
response.enhance({
|
|
9
|
+
headers: {
|
|
10
|
+
'content-type': 'application/json',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const confluentEncoderMiddleware: Middleware = () => ({
|
|
15
|
+
request: req => {
|
|
16
|
+
try {
|
|
17
|
+
if (req.body()) {
|
|
18
|
+
return req.enhance({
|
|
19
|
+
headers: REQUEST_HEADERS,
|
|
20
|
+
body: JSON.stringify(req.body()),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
} catch (_) {}
|
|
24
|
+
|
|
25
|
+
return req.enhance({ headers: REQUEST_HEADERS })
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
response: next =>
|
|
29
|
+
next()
|
|
30
|
+
.then(updateContentType)
|
|
31
|
+
.catch((response: Response) => {
|
|
32
|
+
throw updateContentType(response)
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export default confluentEncoderMiddleware
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { MiddlewareDescriptor } from 'mappersmith'
|
|
2
|
+
|
|
3
|
+
import ErrorMiddleware from './errorMiddleware'
|
|
4
|
+
|
|
5
|
+
const middlewareParams = {
|
|
6
|
+
resourceName: 'resourceNameMock',
|
|
7
|
+
resourceMethod: 'resourceMethodMock',
|
|
8
|
+
context: { context: 'contextMock' },
|
|
9
|
+
clientId: 'clientIdMock',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('ErrorMiddleware', () => {
|
|
13
|
+
let executedMiddleware: MiddlewareDescriptor
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
executedMiddleware = ErrorMiddleware(middlewareParams)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('when the request succeeds', () => {
|
|
20
|
+
it('does not interfere with the promise', async () => {
|
|
21
|
+
await expect(
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
executedMiddleware.response(() => Promise.resolve('arbitrary value'), undefined),
|
|
24
|
+
).resolves.toBe('arbitrary value')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('when the request fails', () => {
|
|
29
|
+
const createResponse = data => ({
|
|
30
|
+
data: jest.fn(() => data),
|
|
31
|
+
status: jest.fn(() => 500),
|
|
32
|
+
request: jest.fn(() => ({
|
|
33
|
+
method: jest.fn(() => 'get'),
|
|
34
|
+
url: jest.fn(() => 'url'),
|
|
35
|
+
})),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('raise an error with message', async () => {
|
|
39
|
+
const message = 'error message'
|
|
40
|
+
const response = createResponse({ message })
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
executedMiddleware.response(() => Promise.reject(response), undefined),
|
|
44
|
+
).rejects.toHaveProperty('message', `${middlewareParams.clientId} - ${message}`)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('raises an error with a message in case of client-side errors', async () => {
|
|
48
|
+
const message = 'error message'
|
|
49
|
+
const response = createResponse(message)
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
executedMiddleware.response(() => Promise.reject(response), undefined),
|
|
53
|
+
).rejects.toHaveProperty(
|
|
54
|
+
'message',
|
|
55
|
+
`${middlewareParams.clientId} - Error, status 500: ${message}`,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('raise an error with a default message if the error payload is empty', async () => {
|
|
60
|
+
const response = createResponse('')
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
executedMiddleware.response(() => Promise.reject(response), undefined),
|
|
64
|
+
).rejects.toHaveProperty('message', `${middlewareParams.clientId} - Error, status 500`)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Middleware, Response } from 'mappersmith'
|
|
2
|
+
|
|
3
|
+
interface ConfluenceResponse extends Omit<Response, 'data'> {
|
|
4
|
+
data: () => {
|
|
5
|
+
message: string
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ResponseError extends Error {
|
|
10
|
+
status: number
|
|
11
|
+
unauthorized: boolean
|
|
12
|
+
url: string
|
|
13
|
+
|
|
14
|
+
constructor(clientName: string, response: ConfluenceResponse) {
|
|
15
|
+
super(
|
|
16
|
+
`${clientName} - ${response.data().message ||
|
|
17
|
+
`Error, status ${response.status()}${response.data() ? `: ${response.data()}` : ''}`}`,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const request = response.request()
|
|
21
|
+
this.name = this.constructor.name
|
|
22
|
+
this.status = response.status()
|
|
23
|
+
this.unauthorized = this.status === 401
|
|
24
|
+
this.url = `${request.method()} ${request.url()}`
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const errorMiddleware: Middleware = ({ clientId }) => ({
|
|
29
|
+
response: next =>
|
|
30
|
+
new Promise((resolve, reject) =>
|
|
31
|
+
next()
|
|
32
|
+
.then(resolve)
|
|
33
|
+
.catch((response: Response) => reject(new ResponseError(clientId, response))),
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export default errorMiddleware
|