confluent-schema-registry 3.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. package/.dockerignore +2 -0
  2. package/.prettierrc.js +8 -0
  3. package/CHANGELOG.md +166 -0
  4. package/Dockerfile +10 -0
  5. package/LICENSE +21 -0
  6. package/README.md +44 -0
  7. package/bin/avdlToAVSC.sh +9 -0
  8. package/dist/@types.d.ts +93 -0
  9. package/dist/@types.js +10 -0
  10. package/dist/@types.js.map +1 -0
  11. package/dist/AvroHelper.d.ts +12 -0
  12. package/dist/AvroHelper.js +67 -0
  13. package/dist/AvroHelper.js.map +1 -0
  14. package/dist/JsonHelper.d.ts +7 -0
  15. package/dist/JsonHelper.js +20 -0
  16. package/dist/JsonHelper.js.map +1 -0
  17. package/dist/JsonSchema.d.ts +31 -0
  18. package/dist/JsonSchema.js +58 -0
  19. package/dist/JsonSchema.js.map +1 -0
  20. package/dist/ProtoHelper.d.ts +7 -0
  21. package/dist/ProtoHelper.js +23 -0
  22. package/dist/ProtoHelper.js.map +1 -0
  23. package/dist/ProtoSchema.d.ts +14 -0
  24. package/dist/ProtoSchema.js +66 -0
  25. package/dist/ProtoSchema.js.map +1 -0
  26. package/dist/SchemaRegistry.d.ts +48 -0
  27. package/dist/SchemaRegistry.js +250 -0
  28. package/dist/SchemaRegistry.js.map +1 -0
  29. package/dist/api/index.d.ts +43 -0
  30. package/dist/api/index.js +90 -0
  31. package/dist/api/index.js.map +1 -0
  32. package/dist/api/middleware/confluentEncoderMiddleware.d.ts +3 -0
  33. package/dist/api/middleware/confluentEncoderMiddleware.js +31 -0
  34. package/dist/api/middleware/confluentEncoderMiddleware.js.map +1 -0
  35. package/dist/api/middleware/errorMiddleware.d.ts +3 -0
  36. package/dist/api/middleware/errorMiddleware.js +20 -0
  37. package/dist/api/middleware/errorMiddleware.js.map +1 -0
  38. package/dist/api/middleware/userAgent.d.ts +3 -0
  39. package/dist/api/middleware/userAgent.js +18 -0
  40. package/dist/api/middleware/userAgent.js.map +1 -0
  41. package/dist/cache.d.ts +20 -0
  42. package/dist/cache.js +24 -0
  43. package/dist/cache.js.map +1 -0
  44. package/dist/constants.d.ts +11 -0
  45. package/dist/constants.js +15 -0
  46. package/dist/constants.js.map +1 -0
  47. package/dist/errors.d.ts +14 -0
  48. package/dist/errors.js +26 -0
  49. package/dist/errors.js.map +1 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +13 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/schemaTypeResolver.d.ts +4 -0
  54. package/dist/schemaTypeResolver.js +80 -0
  55. package/dist/schemaTypeResolver.js.map +1 -0
  56. package/dist/utils/avdlToAVSC.d.ts +2 -0
  57. package/dist/utils/avdlToAVSC.js +85 -0
  58. package/dist/utils/avdlToAVSC.js.map +1 -0
  59. package/dist/utils/index.d.ts +2 -0
  60. package/dist/utils/index.js +9 -0
  61. package/dist/utils/index.js.map +1 -0
  62. package/dist/utils/readAVSC.d.ts +3 -0
  63. package/dist/utils/readAVSC.js +33 -0
  64. package/dist/utils/readAVSC.js.map +1 -0
  65. package/dist/wireDecoder.d.ts +7 -0
  66. package/dist/wireDecoder.js +8 -0
  67. package/dist/wireDecoder.js.map +1 -0
  68. package/dist/wireEncoder.d.ts +3 -0
  69. package/dist/wireEncoder.js +10 -0
  70. package/dist/wireEncoder.js.map +1 -0
  71. package/dockest-error.json +11 -0
  72. package/dockest.ts +30 -0
  73. package/jest.setup.ts +60 -0
  74. package/package.json +56 -0
  75. package/release/CHANGELOG.md +166 -0
  76. package/release/LICENSE +21 -0
  77. package/release/README.md +44 -0
  78. package/release/dist/@types.d.ts +93 -0
  79. package/release/dist/@types.js +10 -0
  80. package/release/dist/@types.js.map +1 -0
  81. package/release/dist/AvroHelper.d.ts +12 -0
  82. package/release/dist/AvroHelper.js +67 -0
  83. package/release/dist/AvroHelper.js.map +1 -0
  84. package/release/dist/JsonHelper.d.ts +7 -0
  85. package/release/dist/JsonHelper.js +20 -0
  86. package/release/dist/JsonHelper.js.map +1 -0
  87. package/release/dist/JsonSchema.d.ts +31 -0
  88. package/release/dist/JsonSchema.js +58 -0
  89. package/release/dist/JsonSchema.js.map +1 -0
  90. package/release/dist/ProtoHelper.d.ts +7 -0
  91. package/release/dist/ProtoHelper.js +23 -0
  92. package/release/dist/ProtoHelper.js.map +1 -0
  93. package/release/dist/ProtoSchema.d.ts +14 -0
  94. package/release/dist/ProtoSchema.js +66 -0
  95. package/release/dist/ProtoSchema.js.map +1 -0
  96. package/release/dist/SchemaRegistry.d.ts +48 -0
  97. package/release/dist/SchemaRegistry.js +250 -0
  98. package/release/dist/SchemaRegistry.js.map +1 -0
  99. package/release/dist/api/index.d.ts +43 -0
  100. package/release/dist/api/index.js +90 -0
  101. package/release/dist/api/index.js.map +1 -0
  102. package/release/dist/api/middleware/confluentEncoderMiddleware.d.ts +3 -0
  103. package/release/dist/api/middleware/confluentEncoderMiddleware.js +31 -0
  104. package/release/dist/api/middleware/confluentEncoderMiddleware.js.map +1 -0
  105. package/release/dist/api/middleware/errorMiddleware.d.ts +3 -0
  106. package/release/dist/api/middleware/errorMiddleware.js +20 -0
  107. package/release/dist/api/middleware/errorMiddleware.js.map +1 -0
  108. package/release/dist/api/middleware/userAgent.d.ts +3 -0
  109. package/release/dist/api/middleware/userAgent.js +18 -0
  110. package/release/dist/api/middleware/userAgent.js.map +1 -0
  111. package/release/dist/cache.d.ts +20 -0
  112. package/release/dist/cache.js +24 -0
  113. package/release/dist/cache.js.map +1 -0
  114. package/release/dist/constants.d.ts +11 -0
  115. package/release/dist/constants.js +15 -0
  116. package/release/dist/constants.js.map +1 -0
  117. package/release/dist/errors.d.ts +14 -0
  118. package/release/dist/errors.js +26 -0
  119. package/release/dist/errors.js.map +1 -0
  120. package/release/dist/index.d.ts +4 -0
  121. package/release/dist/index.js +13 -0
  122. package/release/dist/index.js.map +1 -0
  123. package/release/dist/schemaTypeResolver.d.ts +4 -0
  124. package/release/dist/schemaTypeResolver.js +80 -0
  125. package/release/dist/schemaTypeResolver.js.map +1 -0
  126. package/release/dist/utils/avdlToAVSC.d.ts +2 -0
  127. package/release/dist/utils/avdlToAVSC.js +85 -0
  128. package/release/dist/utils/avdlToAVSC.js.map +1 -0
  129. package/release/dist/utils/index.d.ts +2 -0
  130. package/release/dist/utils/index.js +9 -0
  131. package/release/dist/utils/index.js.map +1 -0
  132. package/release/dist/utils/readAVSC.d.ts +3 -0
  133. package/release/dist/utils/readAVSC.js +33 -0
  134. package/release/dist/utils/readAVSC.js.map +1 -0
  135. package/release/dist/wireDecoder.d.ts +7 -0
  136. package/release/dist/wireDecoder.js +8 -0
  137. package/release/dist/wireDecoder.js.map +1 -0
  138. package/release/dist/wireEncoder.d.ts +3 -0
  139. package/release/dist/wireEncoder.js +10 -0
  140. package/release/dist/wireEncoder.js.map +1 -0
  141. package/release/package.json +56 -0
  142. package/src/@types.ts +105 -0
  143. package/src/AvroHelper.ts +91 -0
  144. package/src/JsonHelper.ts +35 -0
  145. package/src/JsonSchema.ts +80 -0
  146. package/src/ProtoHelper.ts +38 -0
  147. package/src/ProtoSchema.ts +80 -0
  148. package/src/SchemaRegistry.avro.spec.ts +558 -0
  149. package/src/SchemaRegistry.json.spec.ts +364 -0
  150. package/src/SchemaRegistry.newApi.spec.ts +622 -0
  151. package/src/SchemaRegistry.protobuf.spec.ts +372 -0
  152. package/src/SchemaRegistry.spec.ts +252 -0
  153. package/src/SchemaRegistry.ts +387 -0
  154. package/src/api/index.spec.ts +23 -0
  155. package/src/api/index.ts +121 -0
  156. package/src/api/middleware/confluentEncoderMiddleware.ts +36 -0
  157. package/src/api/middleware/errorMiddleware.spec.ts +67 -0
  158. package/src/api/middleware/errorMiddleware.ts +37 -0
  159. package/src/api/middleware/userAgent.spec.ts +53 -0
  160. package/src/api/middleware/userAgent.ts +19 -0
  161. package/src/cache.ts +34 -0
  162. package/src/constants.ts +13 -0
  163. package/src/errors.ts +26 -0
  164. package/src/index.ts +4 -0
  165. package/src/schemaTypeResolver.ts +101 -0
  166. package/src/utils/avdlToAVSC.spec.ts +79 -0
  167. package/src/utils/avdlToAVSC.ts +106 -0
  168. package/src/utils/index.ts +2 -0
  169. package/src/utils/readAVSC.spec.ts +23 -0
  170. package/src/utils/readAVSC.ts +36 -0
  171. package/src/wireDecoder.ts +5 -0
  172. package/src/wireEncoder.ts +10 -0
  173. 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
+ })
@@ -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