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.
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