qdadm 0.28.0 → 0.30.0

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.
@@ -0,0 +1,499 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { ManualConnector } from './ManualConnector.js'
3
+
4
+ describe('ManualConnector', () => {
5
+ describe('parse()', () => {
6
+ describe('input normalization', () => {
7
+ it('parses a single entity object', () => {
8
+ const connector = new ManualConnector()
9
+ const input = {
10
+ name: 'users',
11
+ endpoint: '/api/users',
12
+ fields: {
13
+ id: { name: 'id', type: 'number' },
14
+ email: { name: 'email', type: 'email' }
15
+ }
16
+ }
17
+
18
+ const result = connector.parse(input)
19
+
20
+ expect(result).toHaveLength(1)
21
+ expect(result[0].name).toBe('users')
22
+ expect(result[0].endpoint).toBe('/api/users')
23
+ })
24
+
25
+ it('parses an array of entities', () => {
26
+ const connector = new ManualConnector()
27
+ const input = [
28
+ { name: 'users', endpoint: '/api/users' },
29
+ { name: 'posts', endpoint: '/api/posts' }
30
+ ]
31
+
32
+ const result = connector.parse(input)
33
+
34
+ expect(result).toHaveLength(2)
35
+ expect(result[0].name).toBe('users')
36
+ expect(result[1].name).toBe('posts')
37
+ })
38
+
39
+ it('parses an object with entities array', () => {
40
+ const connector = new ManualConnector()
41
+ const input = {
42
+ entities: [
43
+ { name: 'users', endpoint: '/api/users' },
44
+ { name: 'posts', endpoint: '/api/posts' }
45
+ ]
46
+ }
47
+
48
+ const result = connector.parse(input)
49
+
50
+ expect(result).toHaveLength(2)
51
+ expect(result[0].name).toBe('users')
52
+ expect(result[1].name).toBe('posts')
53
+ })
54
+
55
+ it('returns empty array for null source', () => {
56
+ const connector = new ManualConnector()
57
+ const result = connector.parse(null)
58
+ expect(result).toEqual([])
59
+ })
60
+
61
+ it('returns empty array for undefined source', () => {
62
+ const connector = new ManualConnector()
63
+ const result = connector.parse(undefined)
64
+ expect(result).toEqual([])
65
+ })
66
+
67
+ it('returns empty array for empty array source', () => {
68
+ const connector = new ManualConnector()
69
+ const result = connector.parse([])
70
+ expect(result).toEqual([])
71
+ })
72
+ })
73
+
74
+ describe('entity transformation', () => {
75
+ it('transforms entity with all optional properties', () => {
76
+ const connector = new ManualConnector()
77
+ const input = {
78
+ name: 'users',
79
+ endpoint: '/api/users',
80
+ label: 'User',
81
+ labelPlural: 'Users',
82
+ labelField: 'name',
83
+ routePrefix: 'user',
84
+ idField: 'user_id',
85
+ readOnly: true,
86
+ fields: {}
87
+ }
88
+
89
+ const result = connector.parse(input)
90
+
91
+ expect(result[0].label).toBe('User')
92
+ expect(result[0].labelPlural).toBe('Users')
93
+ expect(result[0].labelField).toBe('name')
94
+ expect(result[0].routePrefix).toBe('user')
95
+ expect(result[0].idField).toBe('user_id')
96
+ expect(result[0].readOnly).toBe(true)
97
+ })
98
+
99
+ it('preserves entity extensions', () => {
100
+ const connector = new ManualConnector()
101
+ const input = {
102
+ name: 'users',
103
+ endpoint: '/api/users',
104
+ extensions: { customProp: 'value', nested: { a: 1 } }
105
+ }
106
+
107
+ const result = connector.parse(input)
108
+
109
+ expect(result[0].extensions).toEqual({
110
+ customProp: 'value',
111
+ nested: { a: 1 }
112
+ })
113
+ })
114
+
115
+ it('merges connector extensions with entity extensions', () => {
116
+ const connector = new ManualConnector({
117
+ extensions: { fromConnector: true }
118
+ })
119
+ const input = {
120
+ name: 'users',
121
+ endpoint: '/api/users',
122
+ extensions: { fromEntity: true }
123
+ }
124
+
125
+ const result = connector.parse(input)
126
+
127
+ expect(result[0].extensions).toEqual({
128
+ fromConnector: true,
129
+ fromEntity: true
130
+ })
131
+ })
132
+
133
+ it('entity extensions override connector extensions', () => {
134
+ const connector = new ManualConnector({
135
+ extensions: { shared: 'connector' }
136
+ })
137
+ const input = {
138
+ name: 'users',
139
+ endpoint: '/api/users',
140
+ extensions: { shared: 'entity' }
141
+ }
142
+
143
+ const result = connector.parse(input)
144
+
145
+ expect(result[0].extensions.shared).toBe('entity')
146
+ })
147
+ })
148
+
149
+ describe('field transformation', () => {
150
+ it('transforms all field types correctly', () => {
151
+ const connector = new ManualConnector()
152
+ const input = {
153
+ name: 'users',
154
+ endpoint: '/api/users',
155
+ fields: {
156
+ id: { name: 'id', type: 'number' },
157
+ name: { name: 'name', type: 'text' },
158
+ email: { name: 'email', type: 'email' },
159
+ website: { name: 'website', type: 'url' },
160
+ uuid: { name: 'uuid', type: 'uuid' },
161
+ active: { name: 'active', type: 'boolean' },
162
+ birthdate: { name: 'birthdate', type: 'date' },
163
+ created: { name: 'created', type: 'datetime' },
164
+ tags: { name: 'tags', type: 'array' },
165
+ meta: { name: 'meta', type: 'object' }
166
+ }
167
+ }
168
+
169
+ const result = connector.parse(input)
170
+ const fields = result[0].fields
171
+
172
+ expect(fields.id.type).toBe('number')
173
+ expect(fields.name.type).toBe('text')
174
+ expect(fields.email.type).toBe('email')
175
+ expect(fields.website.type).toBe('url')
176
+ expect(fields.uuid.type).toBe('uuid')
177
+ expect(fields.active.type).toBe('boolean')
178
+ expect(fields.birthdate.type).toBe('date')
179
+ expect(fields.created.type).toBe('datetime')
180
+ expect(fields.tags.type).toBe('array')
181
+ expect(fields.meta.type).toBe('object')
182
+ })
183
+
184
+ it('preserves all field optional properties', () => {
185
+ const connector = new ManualConnector()
186
+ const input = {
187
+ name: 'users',
188
+ endpoint: '/api/users',
189
+ fields: {
190
+ status: {
191
+ name: 'status',
192
+ type: 'text',
193
+ label: 'Status',
194
+ required: true,
195
+ readOnly: false,
196
+ hidden: true,
197
+ format: 'custom-format',
198
+ enum: ['active', 'inactive'],
199
+ default: 'active',
200
+ reference: { entity: 'statuses', labelField: 'name' },
201
+ extensions: { width: 100 }
202
+ }
203
+ }
204
+ }
205
+
206
+ const result = connector.parse(input)
207
+ const field = result[0].fields.status
208
+
209
+ expect(field.label).toBe('Status')
210
+ expect(field.required).toBe(true)
211
+ expect(field.readOnly).toBe(false)
212
+ expect(field.hidden).toBe(true)
213
+ expect(field.format).toBe('custom-format')
214
+ expect(field.enum).toEqual(['active', 'inactive'])
215
+ expect(field.default).toBe('active')
216
+ expect(field.reference).toEqual({ entity: 'statuses', labelField: 'name' })
217
+ expect(field.extensions).toEqual({ width: 100 })
218
+ })
219
+ })
220
+ })
221
+
222
+ describe('validation in non-strict mode', () => {
223
+ it('skips entity with missing name', () => {
224
+ const connector = new ManualConnector({ strict: false })
225
+ const input = [
226
+ { endpoint: '/api/users' },
227
+ { name: 'posts', endpoint: '/api/posts' }
228
+ ]
229
+
230
+ const result = connector.parse(input)
231
+
232
+ expect(result).toHaveLength(1)
233
+ expect(result[0].name).toBe('posts')
234
+ })
235
+
236
+ it('skips entity with empty name', () => {
237
+ const connector = new ManualConnector({ strict: false })
238
+ const input = [
239
+ { name: '', endpoint: '/api/users' },
240
+ { name: 'posts', endpoint: '/api/posts' }
241
+ ]
242
+
243
+ const result = connector.parse(input)
244
+
245
+ expect(result).toHaveLength(1)
246
+ expect(result[0].name).toBe('posts')
247
+ })
248
+
249
+ it('skips entity with missing endpoint', () => {
250
+ const connector = new ManualConnector({ strict: false })
251
+ const input = [
252
+ { name: 'users' },
253
+ { name: 'posts', endpoint: '/api/posts' }
254
+ ]
255
+
256
+ const result = connector.parse(input)
257
+
258
+ expect(result).toHaveLength(1)
259
+ expect(result[0].name).toBe('posts')
260
+ })
261
+
262
+ it('skips entity with empty endpoint', () => {
263
+ const connector = new ManualConnector({ strict: false })
264
+ const input = [
265
+ { name: 'users', endpoint: '' },
266
+ { name: 'posts', endpoint: '/api/posts' }
267
+ ]
268
+
269
+ const result = connector.parse(input)
270
+
271
+ expect(result).toHaveLength(1)
272
+ expect(result[0].name).toBe('posts')
273
+ })
274
+
275
+ it('skips invalid fields silently', () => {
276
+ const connector = new ManualConnector({ strict: false })
277
+ const input = {
278
+ name: 'users',
279
+ endpoint: '/api/users',
280
+ fields: {
281
+ valid: { name: 'valid', type: 'text' },
282
+ noName: { type: 'text' },
283
+ noType: { name: 'noType' },
284
+ invalidType: { name: 'invalidType', type: 'invalid' }
285
+ }
286
+ }
287
+
288
+ const result = connector.parse(input)
289
+
290
+ expect(Object.keys(result[0].fields)).toEqual(['valid'])
291
+ })
292
+ })
293
+
294
+ describe('validation in strict mode', () => {
295
+ it('throws for entity with missing name', () => {
296
+ const connector = new ManualConnector({ strict: true })
297
+ const input = { endpoint: '/api/users' }
298
+
299
+ expect(() => connector.parse(input)).toThrow("missing required field 'name'")
300
+ })
301
+
302
+ it('throws for entity with empty name', () => {
303
+ const connector = new ManualConnector({ strict: true })
304
+ const input = { name: ' ', endpoint: '/api/users' }
305
+
306
+ expect(() => connector.parse(input)).toThrow("missing required field 'name'")
307
+ })
308
+
309
+ it('throws for entity with missing endpoint', () => {
310
+ const connector = new ManualConnector({ strict: true })
311
+ const input = { name: 'users' }
312
+
313
+ expect(() => connector.parse(input)).toThrow("entity 'users' missing required field 'endpoint'")
314
+ })
315
+
316
+ it('throws for entity with empty endpoint', () => {
317
+ const connector = new ManualConnector({ strict: true })
318
+ const input = { name: 'users', endpoint: ' ' }
319
+
320
+ expect(() => connector.parse(input)).toThrow("entity 'users' missing required field 'endpoint'")
321
+ })
322
+
323
+ it('throws for field with missing name', () => {
324
+ const connector = new ManualConnector({ strict: true })
325
+ const input = {
326
+ name: 'users',
327
+ endpoint: '/api/users',
328
+ fields: {
329
+ bad: { type: 'text' }
330
+ }
331
+ }
332
+
333
+ expect(() => connector.parse(input)).toThrow("field 'bad' missing required property 'name'")
334
+ })
335
+
336
+ it('throws for field with missing type', () => {
337
+ const connector = new ManualConnector({ strict: true })
338
+ const input = {
339
+ name: 'users',
340
+ endpoint: '/api/users',
341
+ fields: {
342
+ email: { name: 'email' }
343
+ }
344
+ }
345
+
346
+ expect(() => connector.parse(input)).toThrow("field 'email' missing required property 'type'")
347
+ })
348
+
349
+ it('throws for field with invalid type', () => {
350
+ const connector = new ManualConnector({ strict: true })
351
+ const input = {
352
+ name: 'users',
353
+ endpoint: '/api/users',
354
+ fields: {
355
+ email: { name: 'email', type: 'string' }
356
+ }
357
+ }
358
+
359
+ expect(() => connector.parse(input)).toThrow("has invalid type 'string'")
360
+ expect(() => connector.parse(input)).toThrow('Valid types:')
361
+ })
362
+ })
363
+
364
+ describe('parseWithWarnings()', () => {
365
+ it('returns schemas and empty warnings for valid input', () => {
366
+ const connector = new ManualConnector()
367
+ const input = {
368
+ name: 'users',
369
+ endpoint: '/api/users',
370
+ fields: {
371
+ id: { name: 'id', type: 'number' }
372
+ }
373
+ }
374
+
375
+ const result = connector.parseWithWarnings(input)
376
+
377
+ expect(result.schemas).toHaveLength(1)
378
+ expect(result.warnings).toEqual([])
379
+ })
380
+
381
+ it('collects warnings for missing entity name', () => {
382
+ const connector = new ManualConnector()
383
+ const input = { endpoint: '/api/users' }
384
+
385
+ const result = connector.parseWithWarnings(input)
386
+
387
+ expect(result.schemas).toHaveLength(0)
388
+ expect(result.warnings).toHaveLength(1)
389
+ expect(result.warnings[0].code).toBe('MISSING_ENTITY_NAME')
390
+ })
391
+
392
+ it('collects warnings for missing endpoint', () => {
393
+ const connector = new ManualConnector()
394
+ const input = { name: 'users' }
395
+
396
+ const result = connector.parseWithWarnings(input)
397
+
398
+ expect(result.schemas).toHaveLength(0)
399
+ expect(result.warnings).toHaveLength(1)
400
+ expect(result.warnings[0].code).toBe('MISSING_ENTITY_ENDPOINT')
401
+ })
402
+
403
+ it('collects warnings for missing field name', () => {
404
+ const connector = new ManualConnector()
405
+ const input = {
406
+ name: 'users',
407
+ endpoint: '/api/users',
408
+ fields: {
409
+ bad: { type: 'text' }
410
+ }
411
+ }
412
+
413
+ const result = connector.parseWithWarnings(input)
414
+
415
+ expect(result.warnings.some(w => w.code === 'MISSING_FIELD_NAME')).toBe(true)
416
+ })
417
+
418
+ it('collects warnings for missing field type', () => {
419
+ const connector = new ManualConnector()
420
+ const input = {
421
+ name: 'users',
422
+ endpoint: '/api/users',
423
+ fields: {
424
+ email: { name: 'email' }
425
+ }
426
+ }
427
+
428
+ const result = connector.parseWithWarnings(input)
429
+
430
+ expect(result.warnings.some(w => w.code === 'MISSING_FIELD_TYPE')).toBe(true)
431
+ })
432
+
433
+ it('collects warnings for invalid field type', () => {
434
+ const connector = new ManualConnector()
435
+ const input = {
436
+ name: 'users',
437
+ endpoint: '/api/users',
438
+ fields: {
439
+ email: { name: 'email', type: 'string' }
440
+ }
441
+ }
442
+
443
+ const result = connector.parseWithWarnings(input)
444
+
445
+ expect(result.warnings.some(w => w.code === 'INVALID_FIELD_TYPE')).toBe(true)
446
+ })
447
+
448
+ it('collects multiple warnings', () => {
449
+ const connector = new ManualConnector()
450
+ const input = [
451
+ { name: 'users' }, // missing endpoint
452
+ {
453
+ name: 'posts',
454
+ endpoint: '/api/posts',
455
+ fields: {
456
+ bad1: { type: 'text' }, // missing name
457
+ bad2: { name: 'bad2' } // missing type
458
+ }
459
+ }
460
+ ]
461
+
462
+ const result = connector.parseWithWarnings(input)
463
+
464
+ expect(result.warnings.length).toBeGreaterThanOrEqual(3)
465
+ })
466
+ })
467
+
468
+ describe('constructor options', () => {
469
+ it('uses constructor name by default', () => {
470
+ const connector = new ManualConnector()
471
+ expect(connector.name).toBe('ManualConnector')
472
+ })
473
+
474
+ it('allows custom name', () => {
475
+ const connector = new ManualConnector({ name: 'MyConnector' })
476
+ expect(connector.name).toBe('MyConnector')
477
+ })
478
+
479
+ it('defaults to non-strict mode', () => {
480
+ const connector = new ManualConnector()
481
+ expect(connector.strict).toBe(false)
482
+ })
483
+
484
+ it('allows strict mode', () => {
485
+ const connector = new ManualConnector({ strict: true })
486
+ expect(connector.strict).toBe(true)
487
+ })
488
+
489
+ it('defaults to empty extensions', () => {
490
+ const connector = new ManualConnector()
491
+ expect(connector.extensions).toEqual({})
492
+ })
493
+
494
+ it('accepts extensions option', () => {
495
+ const connector = new ManualConnector({ extensions: { custom: true } })
496
+ expect(connector.extensions).toEqual({ custom: true })
497
+ })
498
+ })
499
+ })