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,622 @@
1
+ import { Type } from 'avsc'
2
+ import { v4 as uuid } from 'uuid'
3
+
4
+ import SchemaRegistry from './SchemaRegistry'
5
+ import { ConfluentSubject, ConfluentSchema, SchemaType } from './@types'
6
+ import API, { SchemaRegistryAPIClient } from './api'
7
+ import { COMPATIBILITY, DEFAULT_API_CLIENT_ID } from './constants'
8
+ import encodedAnotherPersonV2Avro from '../fixtures/avro/encodedAnotherPersonV2'
9
+ import encodedAnotherPersonV2Json from '../fixtures/json/encodedAnotherPersonV2'
10
+ import encodedAnotherPersonV2Proto from '../fixtures/proto/encodedAnotherPersonV2'
11
+ import encodedNestedV2Proto from '../fixtures/proto/encodedNestedV2'
12
+ import wrongMagicByte from '../fixtures/wrongMagicByte'
13
+ import Ajv2020 from 'ajv8/dist/2020'
14
+ import Ajv from 'ajv'
15
+ import { ConfluentSchemaRegistryValidationError } from './errors'
16
+
17
+ const REGISTRY_HOST = 'http://localhost:8982'
18
+ const schemaRegistryAPIClientArgs = { host: REGISTRY_HOST }
19
+ const schemaRegistryArgs = { host: REGISTRY_HOST }
20
+
21
+ const payload = { fullName: 'John Doe' }
22
+
23
+ type KnownSchemaTypes = Exclude<SchemaType, SchemaType.UNKNOWN>
24
+
25
+ describe('SchemaRegistry - new Api', () => {
26
+ let schemaRegistry: SchemaRegistry
27
+
28
+ const schemaStringsByType: Record<KnownSchemaTypes, any> = {
29
+ [SchemaType.AVRO]: {
30
+ random: (namespace: string) => `
31
+ {
32
+ "type": "record",
33
+ "name": "RandomTest",
34
+ "namespace": "${namespace}",
35
+ "fields": [{ "type": "string", "name": "fullName" }]
36
+ }
37
+ `,
38
+ otherRandom: (namespace: string) => `
39
+ {
40
+ "type": "record",
41
+ "name": "RandomTest",
42
+ "namespace": "${namespace}",
43
+ "fields": [{ "type": "string", "name": "notFullName" }]
44
+ }
45
+ `,
46
+ v1: `{
47
+ "type": "record",
48
+ "name": "AnotherPerson",
49
+ "namespace": "com.org.domain.fixtures",
50
+ "fields": [ { "type": "string", "name": "fullName" } ]
51
+ }`,
52
+ v2: `{
53
+ "type": "record",
54
+ "name": "AnotherPerson",
55
+ "namespace": "com.org.domain.fixtures",
56
+ "fields": [
57
+ { "type": "string", "name": "fullName" },
58
+ { "type": "string", "name": "city", "default": "Stockholm" }
59
+ ]
60
+ }`,
61
+ encodedAnotherPersonV2: encodedAnotherPersonV2Avro,
62
+ },
63
+ [SchemaType.JSON]: {
64
+ random: (namespace: string) => `
65
+ {
66
+ "definitions" : {
67
+ "record:${namespace}.RandomTest" : {
68
+ "type" : "object",
69
+ "required" : [ "fullName" ],
70
+ "additionalProperties" : false,
71
+ "properties" : {
72
+ "fullName" : {
73
+ "type" : "string"
74
+ }
75
+ }
76
+ }
77
+ },
78
+ "$ref" : "#/definitions/record:${namespace}.RandomTest"
79
+ }
80
+ `,
81
+ otherRandom: (namespace: string) => `
82
+ {
83
+ "definitions" : {
84
+ "record:${namespace}.RandomTest" : {
85
+ "type" : "object",
86
+ "required" : [ "notFullName" ],
87
+ "additionalProperties" : false,
88
+ "properties" : {
89
+ "notFullName" : {
90
+ "type" : "string"
91
+ }
92
+ }
93
+ }
94
+ },
95
+ "$ref" : "#/definitions/record:${namespace}.RandomTest"
96
+ }
97
+ `,
98
+ v1: `
99
+ {
100
+ "title": "AnotherPerson",
101
+ "type": "object",
102
+ "required": [
103
+ "fullName"
104
+ ],
105
+ "properties": {
106
+ "fullName": {
107
+ "type": "string",
108
+ "pattern": "^.*$"
109
+ }
110
+ }
111
+ }
112
+ `,
113
+ v2: `
114
+ {
115
+ "title": "AnotherPerson",
116
+ "type": "object",
117
+ "required": [
118
+ "fullName"
119
+ ],
120
+ "properties": {
121
+ "fullName": {
122
+ "type": "string",
123
+ "pattern": "^.*$"
124
+ },
125
+ "city": {
126
+ "type": "string",
127
+ "pattern": "^.*$"
128
+ }
129
+ }
130
+ }
131
+ `,
132
+ encodedAnotherPersonV2: encodedAnotherPersonV2Json,
133
+ },
134
+ [SchemaType.PROTOBUF]: {
135
+ random: (namespace: string) => `
136
+ package ${namespace};
137
+ message RandomTest {
138
+ required string fullName = 1;
139
+ }
140
+ `,
141
+ otherRandom: (namespace: string) => `
142
+ package ${namespace};
143
+ message RandomTest {
144
+ required string notFullName = 1;
145
+ }
146
+ `,
147
+ v1: `
148
+ syntax = "proto2";
149
+ package com.org.domain.fixtures;
150
+ message AnotherPerson {
151
+ required string fullName = 1;
152
+ }
153
+ `,
154
+ v2: `
155
+ syntax = "proto2";
156
+ package com.org.domain.fixtures;
157
+ message AnotherPerson {
158
+ required string fullName = 1;
159
+ optional string city = 2 [default = "Stockholm"];
160
+ }
161
+ `,
162
+ encodedAnotherPersonV2: encodedAnotherPersonV2Proto,
163
+ },
164
+ }
165
+ const types = Object.keys(schemaStringsByType).map(str => SchemaType[str]) as KnownSchemaTypes[]
166
+
167
+ types.forEach(type =>
168
+ describe(`${type}`, () => {
169
+ const subject: ConfluentSubject = {
170
+ name: [type, 'com.org.domain.fixtures', 'AnotherPerson'].join('.'),
171
+ }
172
+ const schema: ConfluentSchema = {
173
+ type,
174
+ schema: schemaStringsByType[type].v1,
175
+ }
176
+
177
+ beforeEach(async () => {
178
+ schemaRegistry = new SchemaRegistry(schemaRegistryArgs)
179
+ await schemaRegistry.register(schema, { subject: subject.name })
180
+ })
181
+
182
+ describe('#register', () => {
183
+ let namespace,
184
+ Schema,
185
+ subject: string,
186
+ api: SchemaRegistryAPIClient,
187
+ confluentSubject: ConfluentSubject,
188
+ confluentSchema: ConfluentSchema
189
+
190
+ beforeEach(() => {
191
+ api = API(schemaRegistryAPIClientArgs)
192
+ namespace = `N${uuid().replace(/-/g, '_')}`
193
+ subject = `${namespace}.RandomTest`
194
+ Schema = schemaStringsByType[type].random(namespace)
195
+ confluentSubject = { name: subject }
196
+ confluentSchema = { type, schema: Schema }
197
+ })
198
+
199
+ it('uploads the new schema', async () => {
200
+ await expect(api.Subject.latestVersion({ subject })).rejects.toHaveProperty(
201
+ 'message',
202
+ `${DEFAULT_API_CLIENT_ID} - Subject '${subject}' not found.`,
203
+ )
204
+
205
+ await expect(
206
+ schemaRegistry.register(confluentSchema, { subject: confluentSubject.name }),
207
+ ).resolves.toEqual({
208
+ id: expect.any(Number),
209
+ })
210
+ })
211
+
212
+ it('automatically cache the id and schema', async () => {
213
+ const { id } = await schemaRegistry.register(confluentSchema, {
214
+ subject: confluentSubject.name,
215
+ })
216
+
217
+ expect(schemaRegistry.cache.getSchema(id)).toBeTruthy()
218
+ })
219
+
220
+ it('fetch and validate the latest schema id after registering a new schema', async () => {
221
+ const { id } = await schemaRegistry.register(confluentSchema, {
222
+ subject: confluentSubject.name,
223
+ })
224
+ const latestSchemaId = await schemaRegistry.getLatestSchemaId(subject)
225
+
226
+ expect(id).toBe(latestSchemaId)
227
+ })
228
+
229
+ it('set the default compatibility to BACKWARD', async () => {
230
+ await schemaRegistry.register(confluentSchema, { subject: confluentSubject.name })
231
+ const response = await api.Subject.config({ subject })
232
+ expect(response.data()).toEqual({ compatibilityLevel: COMPATIBILITY.BACKWARD })
233
+ })
234
+
235
+ it('sets the compatibility according to param', async () => {
236
+ await schemaRegistry.register(confluentSchema, {
237
+ subject: confluentSubject.name,
238
+ compatibility: COMPATIBILITY.NONE,
239
+ })
240
+ const response = await api.Subject.config({ subject })
241
+ expect(response.data()).toEqual({ compatibilityLevel: COMPATIBILITY.NONE })
242
+ })
243
+
244
+ it('throws an error when the configured compatibility is different than defined in the client', async () => {
245
+ await schemaRegistry.register(confluentSchema, { subject: confluentSubject.name })
246
+ await api.Subject.updateConfig({ subject, body: { compatibility: COMPATIBILITY.FULL } })
247
+ await expect(
248
+ schemaRegistry.register(confluentSchema, { subject: confluentSubject.name }),
249
+ ).rejects.toHaveProperty(
250
+ 'message',
251
+ 'Compatibility does not match the configuration (BACKWARD != FULL)',
252
+ )
253
+ })
254
+
255
+ it('throws an error when the given schema string is invalid', async () => {
256
+ const invalidSchema = `asdf`
257
+ const invalidConfluentSchema: ConfluentSchema = {
258
+ type,
259
+ schema: invalidSchema,
260
+ }
261
+ await expect(
262
+ schemaRegistry.register(invalidConfluentSchema, { subject: confluentSubject.name }),
263
+ ).rejects.toHaveProperty('name', 'ConfluentSchemaRegistryArgumentError')
264
+ })
265
+ })
266
+
267
+ describe('#encode', () => {
268
+ beforeEach(async () => {
269
+ await schemaRegistry.register(schema, { subject: subject.name })
270
+ })
271
+
272
+ it('throws an error if registryId is empty', async () => {
273
+ await expect(schemaRegistry.encode(undefined, payload)).rejects.toHaveProperty(
274
+ 'message',
275
+ 'Invalid registryId: undefined',
276
+ )
277
+ })
278
+
279
+ it('encodes using a defined registryId', async () => {
280
+ const confluentSchemaV1: ConfluentSchema = {
281
+ type,
282
+ schema: schemaStringsByType[type].v1,
283
+ }
284
+ const confluentSchemaV2: ConfluentSchema = {
285
+ type,
286
+ schema: schemaStringsByType[type].v2,
287
+ }
288
+
289
+ const schema1 = await schemaRegistry.register(confluentSchemaV1, {
290
+ subject: `${type}_test1`,
291
+ })
292
+ const schema2 = await schemaRegistry.register(confluentSchemaV2, {
293
+ subject: `${type}_test2`,
294
+ })
295
+ expect(schema2.id).not.toEqual(schema1.id)
296
+
297
+ const data = await schemaRegistry.encode(schema2.id, payload)
298
+
299
+ expect(data).toMatchConfluentEncodedPayload({
300
+ registryId: schema2.id,
301
+ payload: Buffer.from(schemaStringsByType[type].encodedAnotherPersonV2),
302
+ })
303
+ })
304
+
305
+ it('throws an error if the payload does not match the schema', async () => {
306
+ const confluentSchema: ConfluentSchema = {
307
+ type,
308
+ schema: schemaStringsByType[type].v1,
309
+ }
310
+ const schema = await schemaRegistry.register(confluentSchema, {
311
+ subject: `${type}_test`,
312
+ })
313
+
314
+ const badPayload = { asdf: 123 }
315
+
316
+ await expect(schemaRegistry.encode(schema.id, badPayload)).rejects.toHaveProperty(
317
+ 'name',
318
+ 'ConfluentSchemaRegistryValidationError',
319
+ )
320
+ })
321
+ })
322
+
323
+ describe('#decode', () => {
324
+ let registryId: number
325
+
326
+ beforeEach(async () => {
327
+ registryId = (await schemaRegistry.register(schema, { subject: subject.name })).id
328
+ })
329
+
330
+ it('decodes data', async () => {
331
+ const buffer = Buffer.from(await schemaRegistry.encode(registryId, payload))
332
+ const data = await schemaRegistry.decode(buffer)
333
+
334
+ expect(data).toEqual(payload)
335
+ })
336
+
337
+ it('throws an error if the magic byte is not supported', async () => {
338
+ const buffer = Buffer.from(wrongMagicByte)
339
+ await expect(schemaRegistry.decode(buffer)).rejects.toHaveProperty(
340
+ 'message',
341
+ 'Message encoded with magic byte {"type":"Buffer","data":[48]}, expected {"type":"Buffer","data":[0]}',
342
+ )
343
+ })
344
+
345
+ it.skip('throws an error if the payload does not match the schema', async () => {
346
+ const badPayload = { asdf: 123 }
347
+ // TODO: find a way to encode the bad payload with the registryId
348
+ const buffer = Buffer.from(await schemaRegistry.encode(registryId, badPayload))
349
+
350
+ await expect(schemaRegistry.decode(buffer)).rejects.toHaveProperty(
351
+ 'name',
352
+ 'ConfluentSchemaRegistryValidationError',
353
+ )
354
+ })
355
+
356
+ it('caches the schema', async () => {
357
+ const buffer = Buffer.from(await schemaRegistry.encode(registryId, payload))
358
+
359
+ schemaRegistry.cache.clear()
360
+ await schemaRegistry.decode(buffer)
361
+
362
+ expect(schemaRegistry.cache.getSchema(registryId)).toBeTruthy()
363
+ })
364
+
365
+ it('creates a single origin request for a schema cache-miss', async () => {
366
+ const buffer = Buffer.from(await schemaRegistry.encode(registryId, payload))
367
+
368
+ schemaRegistry.cache.clear()
369
+
370
+ const spy = jest.spyOn((schemaRegistry as any).api.Schema, 'find')
371
+
372
+ await Promise.all([
373
+ schemaRegistry.decode(buffer),
374
+ schemaRegistry.decode(buffer),
375
+ schemaRegistry.decode(buffer),
376
+ ])
377
+
378
+ expect(spy).toHaveBeenCalledTimes(1)
379
+ })
380
+
381
+ describe('when the cache is populated', () => {
382
+ it('uses the cache data', async () => {
383
+ const buffer = Buffer.from(await schemaRegistry.encode(registryId, payload))
384
+ expect(schemaRegistry.cache.getSchema(registryId)).toBeTruthy()
385
+
386
+ jest.spyOn(schemaRegistry.cache, 'setSchema')
387
+ await schemaRegistry.decode(buffer)
388
+
389
+ expect(schemaRegistry.cache.setSchema).not.toHaveBeenCalled()
390
+ })
391
+ })
392
+ })
393
+
394
+ describe('#getRegistryIdBySchema', () => {
395
+ let namespace: string, confluentSubject: ConfluentSubject, confluentSchema: ConfluentSchema
396
+
397
+ beforeEach(() => {
398
+ namespace = `N${uuid().replace(/-/g, '_')}`
399
+ const subject = `${namespace}.RandomTest`
400
+ const schema = schemaStringsByType[type].random(namespace)
401
+ confluentSubject = { name: subject }
402
+ confluentSchema = { type, schema: schema }
403
+ })
404
+
405
+ it('returns the registry id if the schema has already been registered under that subject', async () => {
406
+ const { id } = await schemaRegistry.register(confluentSchema, {
407
+ subject: confluentSubject.name,
408
+ })
409
+
410
+ await expect(
411
+ schemaRegistry.getRegistryIdBySchema(confluentSubject.name, confluentSchema),
412
+ ).resolves.toEqual(id)
413
+ })
414
+
415
+ it('throws an error if the subject does not exist', async () => {
416
+ await expect(
417
+ schemaRegistry.getRegistryIdBySchema(confluentSubject.name, confluentSchema),
418
+ ).rejects.toHaveProperty(
419
+ 'message',
420
+ `Confluent_Schema_Registry - Subject '${confluentSubject.name}' not found.`,
421
+ )
422
+ })
423
+
424
+ it('throws an error if the schema has not been registered under that subject', async () => {
425
+ const otherSchema = schemaStringsByType[type].otherRandom(namespace)
426
+ const confluentOtherSchema: ConfluentSchema = {
427
+ type,
428
+ schema: otherSchema,
429
+ }
430
+
431
+ await schemaRegistry.register(confluentOtherSchema, { subject: confluentSubject.name })
432
+
433
+ await expect(
434
+ schemaRegistry.getRegistryIdBySchema(confluentSubject.name, confluentSchema),
435
+ ).rejects.toHaveProperty('message', 'Confluent_Schema_Registry - Schema not found')
436
+ })
437
+ })
438
+ }),
439
+ )
440
+
441
+ describe('PROTOBUF tests', () => {
442
+ const v3 = `
443
+ syntax = "proto2";
444
+ package com.org.domain.fixtures;
445
+ message SomeOtherMessage {
446
+ required string bla = 1;
447
+ required string foo = 2;
448
+ }
449
+ message AnotherPerson {
450
+ required string fullName = 1;
451
+ optional string city = 2 [default = "Stockholm"];
452
+ }
453
+ `,
454
+ v3Opts = { [SchemaType.PROTOBUF]: { messageName: 'AnotherPerson' } },
455
+ type = SchemaType.PROTOBUF
456
+
457
+ beforeAll(() => {
458
+ schemaRegistry = new SchemaRegistry(schemaRegistryArgs, v3Opts)
459
+ })
460
+
461
+ it('encodes using schemaOptions', async () => {
462
+ const confluentSchemaV3: ConfluentSchema = {
463
+ type,
464
+ schema: v3,
465
+ }
466
+
467
+ const schema3 = await schemaRegistry.register(confluentSchemaV3, {
468
+ subject: `${type}_test3`,
469
+ })
470
+
471
+ const data = await schemaRegistry.encode(schema3.id, payload)
472
+
473
+ expect(data).toMatchConfluentEncodedPayload({
474
+ registryId: schema3.id,
475
+ payload: Buffer.from(schemaStringsByType[type].encodedAnotherPersonV2),
476
+ })
477
+ })
478
+
479
+ it('decodes using schemaOptions', async () => {
480
+ const confluentSchemaV3: ConfluentSchema = {
481
+ type,
482
+ schema: v3,
483
+ }
484
+
485
+ const schema3 = await schemaRegistry.register(confluentSchemaV3, {
486
+ subject: `${type}_test3`,
487
+ })
488
+
489
+ const buffer = Buffer.from(await schemaRegistry.encode(schema3.id, payload))
490
+ const data = await schemaRegistry.decode(buffer)
491
+
492
+ expect(data).toEqual(payload)
493
+ })
494
+
495
+ describe('nested message types tests', () => {
496
+ const v4 = `
497
+ syntax = "proto2";
498
+ package com.org.domain.fixtures;
499
+ message OuterMessageType {
500
+ required string data = 1;
501
+ required InnerMessageType1 innerMessageType1 = 2;
502
+ required InnerMessageType2 innerMessageType2 = 3;
503
+
504
+ message InnerMessageType1 {
505
+ required string someField = 1;
506
+ }
507
+ message InnerMessageType2 {
508
+ required string someOtherField = 1;
509
+ }
510
+ }
511
+ `,
512
+ type = SchemaType.PROTOBUF,
513
+ nestedPayload = {
514
+ data: 'data-value',
515
+ innerMessageType1: {
516
+ someField: 'someField-value',
517
+ },
518
+ innerMessageType2: {
519
+ someOtherField: 'someOtherField-value',
520
+ },
521
+ }
522
+
523
+ beforeAll(() => {
524
+ schemaRegistry = new SchemaRegistry(schemaRegistryArgs)
525
+ })
526
+
527
+ it('encodes', async () => {
528
+ const confluentSchemaV4: ConfluentSchema = {
529
+ type,
530
+ schema: v4,
531
+ }
532
+
533
+ const schema4 = await schemaRegistry.register(confluentSchemaV4, {
534
+ subject: `${type}_test4`,
535
+ })
536
+
537
+ const data = await schemaRegistry.encode(schema4.id, nestedPayload)
538
+
539
+ expect(data).toMatchConfluentEncodedPayload({
540
+ registryId: schema4.id,
541
+ payload: Buffer.from(encodedNestedV2Proto),
542
+ })
543
+ })
544
+
545
+ it('decodes', async () => {
546
+ const confluentSchemaV4: ConfluentSchema = {
547
+ type,
548
+ schema: v4,
549
+ }
550
+
551
+ const schema4 = await schemaRegistry.register(confluentSchemaV4, {
552
+ subject: `${type}_test4`,
553
+ })
554
+
555
+ const buffer = Buffer.from(await schemaRegistry.encode(schema4.id, nestedPayload))
556
+ const data = await schemaRegistry.decode(buffer)
557
+
558
+ expect(data).toEqual(nestedPayload)
559
+ })
560
+ })
561
+ })
562
+
563
+ describe('JSON Schema tests', () => {
564
+ describe('passing an Ajv instance in the constructor', () => {
565
+ test.each([
566
+ ['Ajv 7', new Ajv()],
567
+ ['Ajv2020', new Ajv2020()],
568
+ ])(
569
+ 'Errors are thrown with their path in %s when the validation fails',
570
+ async (_, ajvInstance) => {
571
+ expect.assertions(3)
572
+ const registry = new SchemaRegistry(schemaRegistryArgs, {
573
+ [SchemaType.JSON]: { ajvInstance },
574
+ })
575
+ const subject: ConfluentSubject = {
576
+ name: [SchemaType.JSON, 'com.org.domain.fixtures', 'AnotherPerson'].join('.'),
577
+ }
578
+ const schema: ConfluentSchema = {
579
+ type: SchemaType.JSON,
580
+ schema: schemaStringsByType[SchemaType.JSON].v1,
581
+ }
582
+
583
+ const { id: schemaId } = await registry.register(schema, { subject: subject.name })
584
+
585
+ try {
586
+ await schemaRegistry.encode(schemaId, { fullName: true })
587
+ } catch (error) {
588
+ expect(error).toBeInstanceOf(ConfluentSchemaRegistryValidationError)
589
+ expect(error.message).toEqual('invalid payload')
590
+ expect(error.paths).toEqual([['/fullName']])
591
+ }
592
+ },
593
+ )
594
+ })
595
+ })
596
+
597
+ describe('Avro tests', () => {
598
+ it('uses reader schema if specified (avro-only)', async () => {
599
+ const subject: ConfluentSubject = {
600
+ name: [SchemaType.AVRO, 'com.org.domain.fixtures', 'AnotherPerson'].join('.'),
601
+ }
602
+ const schema: ConfluentSchema = {
603
+ type: SchemaType.AVRO,
604
+ schema: schemaStringsByType[SchemaType.AVRO].v1,
605
+ }
606
+ const registryId = (await schemaRegistry.register(schema, { subject: subject.name })).id
607
+ const writerBuffer = Buffer.from(await schemaRegistry.encode(registryId, payload))
608
+ const readerSchema = JSON.parse(schemaStringsByType[SchemaType.AVRO].v2)
609
+
610
+ await expect(
611
+ schemaRegistry.decode(writerBuffer, { [SchemaType.AVRO]: { readerSchema } }),
612
+ ).resolves.toHaveProperty('city', 'Stockholm')
613
+
614
+ const registeredReaderSchema = await schemaRegistry.getSchema(registryId)
615
+ await expect(
616
+ schemaRegistry.decode(writerBuffer, {
617
+ [SchemaType.AVRO]: { readerSchema: registeredReaderSchema },
618
+ }),
619
+ )
620
+ })
621
+ })
622
+ })