meadow-connection-postgresql 1.0.0 → 1.0.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.
@@ -11,6 +11,7 @@ const Expect = Chai.expect;
11
11
 
12
12
  const libFable = require('fable');
13
13
  const libMeadowConnectionPostgreSQL = require('../source/Meadow-Connection-PostgreSQL.js');
14
+ const libMeadowSchemaPostgreSQL = require('../source/Meadow-Schema-PostgreSQL.js');
14
15
 
15
16
  const _FableConfig = (
16
17
  {
@@ -32,7 +33,7 @@ const _FableConfig = (
32
33
  "PostgreSQL":
33
34
  {
34
35
  "Server": "127.0.0.1",
35
- "Port": 5432,
36
+ "Port": 25432,
36
37
  "User": "postgres",
37
38
  "Password": "testpassword",
38
39
  "Database": "testdb",
@@ -40,6 +41,92 @@ const _FableConfig = (
40
41
  }
41
42
  });
42
43
 
44
+ const _AnimalTableSchema =
45
+ {
46
+ TableName: 'Animal',
47
+ Columns:
48
+ [
49
+ { Column: 'IDAnimal', DataType: 'ID' },
50
+ { Column: 'GUIDAnimal', DataType: 'GUID', Size: 36 },
51
+ { Column: 'Name', DataType: 'String', Size: 128 },
52
+ { Column: 'Age', DataType: 'Numeric' },
53
+ { Column: 'IDFarm', DataType: 'ForeignKey' }
54
+ ]
55
+ };
56
+
57
+ const _AnimalTableSchemaWithColumnIndexed =
58
+ {
59
+ TableName: 'Animal',
60
+ Columns:
61
+ [
62
+ { Column: 'IDAnimal', DataType: 'ID' },
63
+ { Column: 'GUIDAnimal', DataType: 'GUID', Size: 36 },
64
+ { Column: 'Name', DataType: 'String', Size: 128, Indexed: true },
65
+ { Column: 'TagNumber', DataType: 'String', Size: 64, Indexed: 'unique' },
66
+ { Column: 'IDFarm', DataType: 'ForeignKey' }
67
+ ]
68
+ };
69
+
70
+ const _AnimalTableSchemaWithIndexName =
71
+ {
72
+ TableName: 'AnimalCustomIdx',
73
+ Columns:
74
+ [
75
+ { Column: 'IDAnimalCustomIdx', DataType: 'ID' },
76
+ { Column: 'GUIDAnimalCustomIdx', DataType: 'GUID', Size: 36 },
77
+ { Column: 'Name', DataType: 'String', Size: 128, Indexed: true, IndexName: 'IX_Custom_Name' },
78
+ { Column: 'TagNumber', DataType: 'String', Size: 64, Indexed: 'unique', IndexName: 'UQ_Animal_Tag' },
79
+ { Column: 'Weight', DataType: 'Decimal', Size: '10,2', Indexed: true },
80
+ { Column: 'IDFarm', DataType: 'ForeignKey' }
81
+ ]
82
+ };
83
+
84
+ // Schemas specifically for introspection testing (unique table names to avoid conflicts)
85
+ const _IntrospectAnimalSchema =
86
+ {
87
+ TableName: 'IntrospAnimal',
88
+ Columns:
89
+ [
90
+ { Column: 'IDIntrospAnimal', DataType: 'ID' },
91
+ { Column: 'GUIDIntrospAnimal', DataType: 'GUID', Size: 36 },
92
+ { Column: 'Name', DataType: 'String', Size: 128 },
93
+ { Column: 'Description', DataType: 'Text' },
94
+ { Column: 'Cost', DataType: 'Decimal', Size: '10,2' },
95
+ { Column: 'Age', DataType: 'Numeric' },
96
+ { Column: 'Birthday', DataType: 'DateTime' },
97
+ { Column: 'Active', DataType: 'Boolean' },
98
+ { Column: 'IDFarm', DataType: 'ForeignKey' }
99
+ ]
100
+ };
101
+
102
+ const _IntrospectAnimalIndexedSchema =
103
+ {
104
+ TableName: 'IntrospAnimalIdx',
105
+ Columns:
106
+ [
107
+ { Column: 'IDIntrospAnimalIdx', DataType: 'ID' },
108
+ { Column: 'GUIDIntrospAnimalIdx', DataType: 'GUID', Size: 36 },
109
+ { Column: 'Name', DataType: 'String', Size: 128, Indexed: true },
110
+ { Column: 'Description', DataType: 'Text' },
111
+ { Column: 'TagNumber', DataType: 'String', Size: 64, Indexed: 'unique' },
112
+ { Column: 'IDOwner', DataType: 'ForeignKey' }
113
+ ]
114
+ };
115
+
116
+ const _IntrospectAnimalCustomIdxSchema =
117
+ {
118
+ TableName: 'IntrospAnimalCustIdx',
119
+ Columns:
120
+ [
121
+ { Column: 'IDIntrospAnimalCustIdx', DataType: 'ID' },
122
+ { Column: 'GUIDIntrospAnimalCustIdx', DataType: 'GUID', Size: 36 },
123
+ { Column: 'Name', DataType: 'String', Size: 128, Indexed: true, IndexName: 'IX_Custom_Name' },
124
+ { Column: 'TagNumber', DataType: 'String', Size: 64, Indexed: 'unique', IndexName: 'UQ_IntrospAnimalCustIdx_Tag' },
125
+ { Column: 'Weight', DataType: 'Decimal', Size: '10,2', Indexed: true },
126
+ { Column: 'IDTrainer', DataType: 'ForeignKey' }
127
+ ]
128
+ };
129
+
43
130
  suite
44
131
  (
45
132
  'Meadow-Connection-PostgreSQL',
@@ -144,5 +231,782 @@ suite
144
231
  );
145
232
  }
146
233
  );
234
+
235
+ suite
236
+ (
237
+ 'Index Generation',
238
+ () =>
239
+ {
240
+ let libSchemaPostgreSQL = null;
241
+
242
+ setup(
243
+ () =>
244
+ {
245
+ let _Fable = new libFable(_FableConfig);
246
+ libSchemaPostgreSQL = _Fable.serviceManager.addServiceType('MeadowSchemaPostgreSQL', libMeadowSchemaPostgreSQL);
247
+ libSchemaPostgreSQL = _Fable.serviceManager.instantiateServiceProvider('MeadowSchemaPostgreSQL');
248
+ });
249
+
250
+ test
251
+ (
252
+ 'auto-detect GUID and ForeignKey indices',
253
+ () =>
254
+ {
255
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(_AnimalTableSchema);
256
+ Expect(tmpIndices).to.be.an('array');
257
+ Expect(tmpIndices.length).to.equal(2);
258
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDAnimal');
259
+ Expect(tmpIndices[0].Unique).to.equal(true);
260
+ Expect(tmpIndices[1].Name).to.equal('IX_M_IDFarm');
261
+ Expect(tmpIndices[1].Unique).to.equal(false);
262
+ }
263
+ );
264
+
265
+ test
266
+ (
267
+ 'include explicit indices alongside auto-detected ones',
268
+ () =>
269
+ {
270
+ let tmpSchemaWithExplicit = JSON.parse(JSON.stringify(_AnimalTableSchema));
271
+ tmpSchemaWithExplicit.Indices = [
272
+ { Name: 'IX_Animal_NameAge', Columns: ['Name', 'Age'], Unique: false }
273
+ ];
274
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(tmpSchemaWithExplicit);
275
+ Expect(tmpIndices.length).to.equal(3);
276
+ Expect(tmpIndices[2].Name).to.equal('IX_Animal_NameAge');
277
+ Expect(tmpIndices[2].Columns).to.deep.equal(['Name', 'Age']);
278
+ }
279
+ );
280
+
281
+ test
282
+ (
283
+ 'column-level Indexed property generates consistently named indices',
284
+ () =>
285
+ {
286
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(_AnimalTableSchemaWithColumnIndexed);
287
+ Expect(tmpIndices).to.be.an('array');
288
+ Expect(tmpIndices.length).to.equal(4);
289
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDAnimal');
290
+ Expect(tmpIndices[1].Name).to.equal('IX_M_T_Animal_C_Name');
291
+ Expect(tmpIndices[1].Unique).to.equal(false);
292
+ Expect(tmpIndices[2].Name).to.equal('AK_M_T_Animal_C_TagNumber');
293
+ Expect(tmpIndices[2].Unique).to.equal(true);
294
+ Expect(tmpIndices[3].Name).to.equal('IX_M_IDFarm');
295
+ }
296
+ );
297
+
298
+ test
299
+ (
300
+ 'generate script with column-level Indexed property',
301
+ () =>
302
+ {
303
+ let tmpScript = libSchemaPostgreSQL.generateCreateIndexScript(_AnimalTableSchemaWithColumnIndexed);
304
+ Expect(tmpScript).to.contain('IX_M_T_Animal_C_Name');
305
+ Expect(tmpScript).to.contain('AK_M_T_Animal_C_TagNumber');
306
+ Expect(tmpScript).to.contain('IF NOT EXISTS');
307
+ }
308
+ );
309
+
310
+ test
311
+ (
312
+ 'generate idempotent index script with IF NOT EXISTS',
313
+ () =>
314
+ {
315
+ let tmpScript = libSchemaPostgreSQL.generateCreateIndexScript(_AnimalTableSchema);
316
+ Expect(tmpScript).to.contain('CREATE UNIQUE INDEX IF NOT EXISTS');
317
+ Expect(tmpScript).to.contain('CREATE INDEX IF NOT EXISTS');
318
+ Expect(tmpScript).to.contain('"AK_M_GUIDAnimal"');
319
+ Expect(tmpScript).to.contain('"IX_M_IDFarm"');
320
+ }
321
+ );
322
+
323
+ test
324
+ (
325
+ 'generate script with strategy clause',
326
+ () =>
327
+ {
328
+ let tmpSchemaWithStrategy = JSON.parse(JSON.stringify(_AnimalTableSchema));
329
+ tmpSchemaWithStrategy.Indices = [
330
+ { Name: 'IX_Animal_Name_Hash', Columns: ['Name'], Unique: false, Strategy: 'hash' }
331
+ ];
332
+ let tmpScript = libSchemaPostgreSQL.generateCreateIndexScript(tmpSchemaWithStrategy);
333
+ Expect(tmpScript).to.contain('USING hash');
334
+ }
335
+ );
336
+
337
+ test
338
+ (
339
+ 'generate individual index statements with pg_indexes check',
340
+ () =>
341
+ {
342
+ let tmpStatements = libSchemaPostgreSQL.generateCreateIndexStatements(_AnimalTableSchema);
343
+ Expect(tmpStatements).to.be.an('array');
344
+ Expect(tmpStatements.length).to.equal(2);
345
+ Expect(tmpStatements[0].Name).to.equal('AK_M_GUIDAnimal');
346
+ Expect(tmpStatements[0].Statement).to.contain('CREATE UNIQUE INDEX');
347
+ Expect(tmpStatements[0].CheckStatement).to.contain('pg_indexes');
348
+ }
349
+ );
350
+
351
+ test
352
+ (
353
+ 'IndexName property overrides auto-generated index name',
354
+ () =>
355
+ {
356
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(_AnimalTableSchemaWithIndexName);
357
+ Expect(tmpIndices).to.be.an('array');
358
+ Expect(tmpIndices.length).to.equal(5);
359
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDAnimalCustomIdx');
360
+ Expect(tmpIndices[1].Name).to.equal('IX_Custom_Name');
361
+ Expect(tmpIndices[1].Unique).to.equal(false);
362
+ Expect(tmpIndices[2].Name).to.equal('UQ_Animal_Tag');
363
+ Expect(tmpIndices[2].Unique).to.equal(true);
364
+ Expect(tmpIndices[3].Name).to.equal('IX_M_T_AnimalCustomIdx_C_Weight');
365
+ Expect(tmpIndices[3].Unique).to.equal(false);
366
+ Expect(tmpIndices[4].Name).to.equal('IX_M_IDFarm');
367
+ }
368
+ );
369
+
370
+ test
371
+ (
372
+ 'generate script with IndexName uses custom names in SQL',
373
+ () =>
374
+ {
375
+ let tmpScript = libSchemaPostgreSQL.generateCreateIndexScript(_AnimalTableSchemaWithIndexName);
376
+ Expect(tmpScript).to.contain('IX_Custom_Name');
377
+ Expect(tmpScript).to.contain('UQ_Animal_Tag');
378
+ Expect(tmpScript).to.contain('IX_M_T_AnimalCustomIdx_C_Weight');
379
+ Expect(tmpScript).to.not.contain('IX_M_T_AnimalCustomIdx_C_Name');
380
+ Expect(tmpScript).to.not.contain('AK_M_T_AnimalCustomIdx_C_TagNumber');
381
+ }
382
+ );
383
+
384
+ test
385
+ (
386
+ 'schema provider is accessible from connection provider',
387
+ () =>
388
+ {
389
+ let _Fable = new libFable(_FableConfig);
390
+ _Fable.serviceManager.addServiceType('MeadowPostgreSQLProvider', libMeadowConnectionPostgreSQL);
391
+ _Fable.serviceManager.instantiateServiceProvider('MeadowPostgreSQLProvider');
392
+ Expect(_Fable.MeadowPostgreSQLProvider.schemaProvider).to.be.an('object');
393
+ }
394
+ );
395
+ }
396
+ );
397
+
398
+ suite
399
+ (
400
+ 'Database Introspection',
401
+ ()=>
402
+ {
403
+ let _Fable = null;
404
+ let libSchemaPostgreSQL = null;
405
+
406
+ setup(
407
+ (fDone) =>
408
+ {
409
+ _Fable = new libFable(_FableConfig);
410
+ _Fable.serviceManager.addServiceType('MeadowSchemaPostgreSQL', libMeadowSchemaPostgreSQL);
411
+ libSchemaPostgreSQL = _Fable.serviceManager.instantiateServiceProvider('MeadowSchemaPostgreSQL');
412
+ _Fable.serviceManager.addServiceType('MeadowPostgreSQLProvider', libMeadowConnectionPostgreSQL);
413
+ _Fable.serviceManager.instantiateServiceProvider('MeadowPostgreSQLProvider');
414
+
415
+ _Fable.MeadowPostgreSQLProvider.connectAsync(
416
+ (pError) =>
417
+ {
418
+ if (pError) return fDone(pError);
419
+ libSchemaPostgreSQL.setConnectionPool(_Fable.MeadowPostgreSQLProvider.pool);
420
+
421
+ // Drop test tables first (clean slate)
422
+ _Fable.MeadowPostgreSQLProvider.pool.query('DROP TABLE IF EXISTS "IntrospAnimal", "IntrospAnimalIdx", "IntrospAnimalCustIdx"',
423
+ (pDropError) =>
424
+ {
425
+ if (pDropError) return fDone(pDropError);
426
+
427
+ let tmpSchema = { Tables: [_IntrospectAnimalSchema, _IntrospectAnimalIndexedSchema, _IntrospectAnimalCustomIdxSchema] };
428
+ libSchemaPostgreSQL.createTables(tmpSchema,
429
+ (pCreateError) =>
430
+ {
431
+ if (pCreateError) return fDone(pCreateError);
432
+ libSchemaPostgreSQL.createAllIndices(tmpSchema,
433
+ (pIdxError) =>
434
+ {
435
+ return fDone(pIdxError);
436
+ });
437
+ });
438
+ });
439
+ });
440
+ });
441
+
442
+ test
443
+ (
444
+ 'listTables returns tables including introspection test tables',
445
+ (fDone) =>
446
+ {
447
+ libSchemaPostgreSQL.listTables(
448
+ (pError, pTables) =>
449
+ {
450
+ Expect(pError).to.not.exist;
451
+ Expect(pTables).to.be.an('array');
452
+ Expect(pTables).to.include('IntrospAnimal');
453
+ Expect(pTables).to.include('IntrospAnimalIdx');
454
+ Expect(pTables).to.include('IntrospAnimalCustIdx');
455
+ return fDone();
456
+ });
457
+ }
458
+ );
459
+
460
+ test
461
+ (
462
+ 'introspectTableColumns returns column definitions for IntrospAnimal',
463
+ (fDone) =>
464
+ {
465
+ libSchemaPostgreSQL.introspectTableColumns('IntrospAnimal',
466
+ (pError, pColumns) =>
467
+ {
468
+ Expect(pError).to.not.exist;
469
+ Expect(pColumns).to.be.an('array');
470
+ Expect(pColumns.length).to.equal(9);
471
+
472
+ // ID column (SERIAL)
473
+ Expect(pColumns[0].Column).to.equal('IDIntrospAnimal');
474
+ Expect(pColumns[0].DataType).to.equal('ID');
475
+
476
+ // GUID column (VARCHAR with GUID in name)
477
+ Expect(pColumns[1].Column).to.equal('GUIDIntrospAnimal');
478
+ Expect(pColumns[1].DataType).to.equal('GUID');
479
+
480
+ // String column (VARCHAR)
481
+ Expect(pColumns[2].Column).to.equal('Name');
482
+ Expect(pColumns[2].DataType).to.equal('String');
483
+
484
+ // Text column
485
+ Expect(pColumns[3].Column).to.equal('Description');
486
+ Expect(pColumns[3].DataType).to.equal('Text');
487
+
488
+ // Decimal column
489
+ Expect(pColumns[4].Column).to.equal('Cost');
490
+ Expect(pColumns[4].DataType).to.equal('Decimal');
491
+
492
+ // Numeric column (INTEGER)
493
+ Expect(pColumns[5].Column).to.equal('Age');
494
+ Expect(pColumns[5].DataType).to.equal('Numeric');
495
+
496
+ // DateTime column (TIMESTAMP)
497
+ Expect(pColumns[6].Column).to.equal('Birthday');
498
+ Expect(pColumns[6].DataType).to.equal('DateTime');
499
+
500
+ // Boolean column (native BOOLEAN)
501
+ Expect(pColumns[7].Column).to.equal('Active');
502
+ Expect(pColumns[7].DataType).to.equal('Boolean');
503
+
504
+ // ForeignKey column (no actual FK constraint, detected as Numeric)
505
+ Expect(pColumns[8].Column).to.equal('IDFarm');
506
+ Expect(pColumns[8].DataType).to.equal('Numeric');
507
+
508
+ return fDone();
509
+ });
510
+ }
511
+ );
512
+
513
+ test
514
+ (
515
+ 'introspectTableIndices returns index definitions for IntrospAnimal',
516
+ (fDone) =>
517
+ {
518
+ libSchemaPostgreSQL.introspectTableIndices('IntrospAnimal',
519
+ (pError, pIndices) =>
520
+ {
521
+ Expect(pError).to.not.exist;
522
+ Expect(pIndices).to.be.an('array');
523
+ Expect(pIndices.length).to.equal(2);
524
+
525
+ let tmpNames = pIndices.map((pIdx) => { return pIdx.Name; });
526
+ Expect(tmpNames).to.include('AK_M_GUIDIntrospAnimal');
527
+ Expect(tmpNames).to.include('IX_M_IDFarm');
528
+
529
+ let tmpGUIDIndex = pIndices.find((pIdx) => { return pIdx.Name === 'AK_M_GUIDIntrospAnimal'; });
530
+ Expect(tmpGUIDIndex.Unique).to.equal(true);
531
+ Expect(tmpGUIDIndex.Columns).to.deep.equal(['GUIDIntrospAnimal']);
532
+
533
+ return fDone();
534
+ });
535
+ }
536
+ );
537
+
538
+ test
539
+ (
540
+ 'introspectTableForeignKeys returns empty for table without FK constraints',
541
+ (fDone) =>
542
+ {
543
+ libSchemaPostgreSQL.introspectTableForeignKeys('IntrospAnimal',
544
+ (pError, pFKs) =>
545
+ {
546
+ Expect(pError).to.not.exist;
547
+ Expect(pFKs).to.be.an('array');
548
+ Expect(pFKs.length).to.equal(0);
549
+ return fDone();
550
+ });
551
+ }
552
+ );
553
+
554
+ test
555
+ (
556
+ 'introspectTableSchema combines columns and indices for IntrospAnimalIdx',
557
+ (fDone) =>
558
+ {
559
+ libSchemaPostgreSQL.introspectTableSchema('IntrospAnimalIdx',
560
+ (pError, pSchema) =>
561
+ {
562
+ Expect(pError).to.not.exist;
563
+ Expect(pSchema).to.be.an('object');
564
+ Expect(pSchema.TableName).to.equal('IntrospAnimalIdx');
565
+ Expect(pSchema.Columns).to.be.an('array');
566
+
567
+ // Check that column-level Indexed properties are folded in
568
+ let tmpNameCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'Name'; });
569
+ Expect(tmpNameCol.Indexed).to.equal(true);
570
+ Expect(tmpNameCol).to.not.have.property('IndexName');
571
+
572
+ let tmpTagCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'TagNumber'; });
573
+ Expect(tmpTagCol.Indexed).to.equal('unique');
574
+ Expect(tmpTagCol).to.not.have.property('IndexName');
575
+
576
+ return fDone();
577
+ });
578
+ }
579
+ );
580
+
581
+ test
582
+ (
583
+ 'introspectTableSchema preserves IndexName for custom-named indices',
584
+ (fDone) =>
585
+ {
586
+ libSchemaPostgreSQL.introspectTableSchema('IntrospAnimalCustIdx',
587
+ (pError, pSchema) =>
588
+ {
589
+ Expect(pError).to.not.exist;
590
+ Expect(pSchema.TableName).to.equal('IntrospAnimalCustIdx');
591
+
592
+ // Name has custom IndexName IX_Custom_Name
593
+ let tmpNameCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'Name'; });
594
+ Expect(tmpNameCol.Indexed).to.equal(true);
595
+ Expect(tmpNameCol.IndexName).to.equal('IX_Custom_Name');
596
+
597
+ // TagNumber has custom IndexName UQ_IntrospAnimalCustIdx_Tag
598
+ let tmpTagCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'TagNumber'; });
599
+ Expect(tmpTagCol.Indexed).to.equal('unique');
600
+ Expect(tmpTagCol.IndexName).to.equal('UQ_IntrospAnimalCustIdx_Tag');
601
+
602
+ // Weight has auto-generated name - no IndexName
603
+ let tmpWeightCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'Weight'; });
604
+ Expect(tmpWeightCol.Indexed).to.equal(true);
605
+ Expect(tmpWeightCol).to.not.have.property('IndexName');
606
+
607
+ return fDone();
608
+ });
609
+ }
610
+ );
611
+
612
+ test
613
+ (
614
+ 'introspectDatabaseSchema returns schemas for all tables',
615
+ (fDone) =>
616
+ {
617
+ libSchemaPostgreSQL.introspectDatabaseSchema(
618
+ (pError, pSchema) =>
619
+ {
620
+ Expect(pError).to.not.exist;
621
+ Expect(pSchema).to.be.an('object');
622
+ Expect(pSchema.Tables).to.be.an('array');
623
+ Expect(pSchema.Tables.length).to.be.greaterThan(0);
624
+
625
+ let tmpTableNames = pSchema.Tables.map((pT) => { return pT.TableName; });
626
+ Expect(tmpTableNames).to.include('IntrospAnimal');
627
+ Expect(tmpTableNames).to.include('IntrospAnimalIdx');
628
+ Expect(tmpTableNames).to.include('IntrospAnimalCustIdx');
629
+
630
+ return fDone();
631
+ });
632
+ }
633
+ );
634
+
635
+ test
636
+ (
637
+ 'generateMeadowPackageFromTable produces Meadow package JSON',
638
+ (fDone) =>
639
+ {
640
+ libSchemaPostgreSQL.generateMeadowPackageFromTable('IntrospAnimal',
641
+ (pError, pPackage) =>
642
+ {
643
+ Expect(pError).to.not.exist;
644
+ Expect(pPackage).to.be.an('object');
645
+ Expect(pPackage.Scope).to.equal('IntrospAnimal');
646
+ Expect(pPackage.DefaultIdentifier).to.equal('IDIntrospAnimal');
647
+ Expect(pPackage.Schema).to.be.an('array');
648
+ Expect(pPackage.DefaultObject).to.be.an('object');
649
+
650
+ // Verify schema entries
651
+ let tmpIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'IDIntrospAnimal'; });
652
+ Expect(tmpIDEntry.Type).to.equal('AutoIdentity');
653
+
654
+ let tmpGUIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'GUIDIntrospAnimal'; });
655
+ Expect(tmpGUIDEntry.Type).to.equal('AutoGUID');
656
+
657
+ let tmpNameEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'Name'; });
658
+ Expect(tmpNameEntry.Type).to.equal('String');
659
+
660
+ // Verify default object
661
+ Expect(pPackage.DefaultObject.IDIntrospAnimal).to.equal(0);
662
+ Expect(pPackage.DefaultObject.GUIDIntrospAnimal).to.equal('');
663
+ Expect(pPackage.DefaultObject.Name).to.equal('');
664
+
665
+ return fDone();
666
+ });
667
+ }
668
+ );
669
+
670
+ test
671
+ (
672
+ 'round-trip: introspect IntrospAnimalIdx and regenerate matching indices',
673
+ (fDone) =>
674
+ {
675
+ libSchemaPostgreSQL.introspectTableSchema('IntrospAnimalIdx',
676
+ (pError, pSchema) =>
677
+ {
678
+ Expect(pError).to.not.exist;
679
+
680
+ // Use the introspected schema to generate index definitions
681
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(pSchema);
682
+
683
+ // The original IntrospAnimalIdx had:
684
+ // AK_M_GUIDIntrospAnimalIdx (GUID auto)
685
+ // IX_M_T_IntrospAnimalIdx_C_Name (Indexed: true)
686
+ // AK_M_T_IntrospAnimalIdx_C_TagNumber (Indexed: 'unique')
687
+ // IX_M_IDOwner (FK auto)
688
+ let tmpNames = tmpIndices.map((pIdx) => { return pIdx.Name; });
689
+ Expect(tmpNames).to.include('AK_M_GUIDIntrospAnimalIdx');
690
+ Expect(tmpNames).to.include('IX_M_T_IntrospAnimalIdx_C_Name');
691
+ Expect(tmpNames).to.include('AK_M_T_IntrospAnimalIdx_C_TagNumber');
692
+ Expect(tmpNames).to.include('IX_M_IDOwner');
693
+
694
+ return fDone();
695
+ });
696
+ }
697
+ );
698
+
699
+ test
700
+ (
701
+ 'round-trip: introspect IntrospAnimalCustIdx and regenerate matching index names',
702
+ (fDone) =>
703
+ {
704
+ libSchemaPostgreSQL.introspectTableSchema('IntrospAnimalCustIdx',
705
+ (pError, pSchema) =>
706
+ {
707
+ Expect(pError).to.not.exist;
708
+
709
+ // Use the introspected schema to generate index definitions
710
+ let tmpIndices = libSchemaPostgreSQL.getIndexDefinitionsFromSchema(pSchema);
711
+
712
+ // The original IntrospAnimalCustIdx had:
713
+ // AK_M_GUIDIntrospAnimalCustIdx (GUID auto)
714
+ // IX_Custom_Name (IndexName override)
715
+ // UQ_IntrospAnimalCustIdx_Tag (IndexName override, unique)
716
+ // IX_M_T_IntrospAnimalCustIdx_C_Weight (auto)
717
+ // IX_M_IDTrainer (FK auto)
718
+ let tmpNames = tmpIndices.map((pIdx) => { return pIdx.Name; });
719
+ Expect(tmpNames).to.include('AK_M_GUIDIntrospAnimalCustIdx');
720
+ Expect(tmpNames).to.include('IX_Custom_Name');
721
+ Expect(tmpNames).to.include('UQ_IntrospAnimalCustIdx_Tag');
722
+ Expect(tmpNames).to.include('IX_M_T_IntrospAnimalCustIdx_C_Weight');
723
+ Expect(tmpNames).to.include('IX_M_IDTrainer');
724
+
725
+ return fDone();
726
+ });
727
+ }
728
+ );
729
+ }
730
+ );
731
+
732
+ suite
733
+ (
734
+ 'Chinook Database Introspection',
735
+ ()=>
736
+ {
737
+ let _Fable = null;
738
+ let libSchemaPostgreSQL = null;
739
+
740
+ setup(
741
+ (fDone) =>
742
+ {
743
+ _Fable = new libFable(_FableConfig);
744
+ _Fable.serviceManager.addServiceType('MeadowSchemaPostgreSQL', libMeadowSchemaPostgreSQL);
745
+ libSchemaPostgreSQL = _Fable.serviceManager.instantiateServiceProvider('MeadowSchemaPostgreSQL');
746
+ _Fable.serviceManager.addServiceType('MeadowPostgreSQLProvider', libMeadowConnectionPostgreSQL);
747
+ _Fable.serviceManager.instantiateServiceProvider('MeadowPostgreSQLProvider');
748
+ _Fable.MeadowPostgreSQLProvider.connectAsync(
749
+ (pError) =>
750
+ {
751
+ if (pError) return fDone(pError);
752
+ libSchemaPostgreSQL.setConnectionPool(_Fable.MeadowPostgreSQLProvider.pool);
753
+ return fDone();
754
+ });
755
+ });
756
+
757
+ test
758
+ (
759
+ 'listTables includes all 11 Chinook tables',
760
+ (fDone) =>
761
+ {
762
+ libSchemaPostgreSQL.listTables(
763
+ (pError, pTables) =>
764
+ {
765
+ Expect(pError).to.not.exist;
766
+ Expect(pTables).to.be.an('array');
767
+
768
+ let tmpChinookTables = ['album', 'artist', 'customer', 'employee',
769
+ 'genre', 'invoice', 'invoice_line', 'media_type',
770
+ 'playlist', 'playlist_track', 'track'];
771
+
772
+ tmpChinookTables.forEach(
773
+ (pTableName) =>
774
+ {
775
+ Expect(pTables).to.include(pTableName);
776
+ });
777
+
778
+ return fDone();
779
+ });
780
+ }
781
+ );
782
+
783
+ test
784
+ (
785
+ 'introspectTableColumns on track detects all 9 columns with correct types',
786
+ (fDone) =>
787
+ {
788
+ libSchemaPostgreSQL.introspectTableColumns('track',
789
+ (pError, pColumns) =>
790
+ {
791
+ Expect(pError).to.not.exist;
792
+ Expect(pColumns).to.be.an('array');
793
+ Expect(pColumns.length).to.equal(9);
794
+
795
+ let tmpTrackId = pColumns.find((pCol) => { return pCol.Column === 'track_id'; });
796
+ Expect(tmpTrackId.DataType).to.equal('ID');
797
+
798
+ let tmpName = pColumns.find((pCol) => { return pCol.Column === 'name'; });
799
+ Expect(tmpName.DataType).to.equal('String');
800
+
801
+ let tmpUnitPrice = pColumns.find((pCol) => { return pCol.Column === 'unit_price'; });
802
+ Expect(tmpUnitPrice.DataType).to.equal('Decimal');
803
+
804
+ let tmpMilliseconds = pColumns.find((pCol) => { return pCol.Column === 'milliseconds'; });
805
+ Expect(tmpMilliseconds.DataType).to.equal('Numeric');
806
+
807
+ return fDone();
808
+ });
809
+ }
810
+ );
811
+
812
+ test
813
+ (
814
+ 'introspectTableColumns on employee detects 15 columns',
815
+ (fDone) =>
816
+ {
817
+ libSchemaPostgreSQL.introspectTableColumns('employee',
818
+ (pError, pColumns) =>
819
+ {
820
+ Expect(pError).to.not.exist;
821
+ Expect(pColumns.length).to.equal(15);
822
+
823
+ let tmpEmployeeId = pColumns.find((pCol) => { return pCol.Column === 'employee_id'; });
824
+ Expect(tmpEmployeeId.DataType).to.equal('ID');
825
+
826
+ let tmpBirthDate = pColumns.find((pCol) => { return pCol.Column === 'birth_date'; });
827
+ Expect(tmpBirthDate.DataType).to.equal('DateTime');
828
+
829
+ return fDone();
830
+ });
831
+ }
832
+ );
833
+
834
+ test
835
+ (
836
+ 'introspectTableForeignKeys on track detects 3 FK relationships',
837
+ (fDone) =>
838
+ {
839
+ libSchemaPostgreSQL.introspectTableForeignKeys('track',
840
+ (pError, pFKs) =>
841
+ {
842
+ Expect(pError).to.not.exist;
843
+ Expect(pFKs).to.be.an('array');
844
+ Expect(pFKs.length).to.equal(3);
845
+
846
+ let tmpAlbumFK = pFKs.find((pFK) => { return pFK.Column === 'album_id'; });
847
+ Expect(tmpAlbumFK).to.exist;
848
+ Expect(tmpAlbumFK.ReferencedTable).to.equal('album');
849
+ Expect(tmpAlbumFK.ReferencedColumn).to.equal('album_id');
850
+
851
+ let tmpMediaTypeFK = pFKs.find((pFK) => { return pFK.Column === 'media_type_id'; });
852
+ Expect(tmpMediaTypeFK).to.exist;
853
+ Expect(tmpMediaTypeFK.ReferencedTable).to.equal('media_type');
854
+
855
+ let tmpGenreFK = pFKs.find((pFK) => { return pFK.Column === 'genre_id'; });
856
+ Expect(tmpGenreFK).to.exist;
857
+ Expect(tmpGenreFK.ReferencedTable).to.equal('genre');
858
+
859
+ return fDone();
860
+ });
861
+ }
862
+ );
863
+
864
+ test
865
+ (
866
+ 'introspectTableForeignKeys on employee detects self-referential FK',
867
+ (fDone) =>
868
+ {
869
+ libSchemaPostgreSQL.introspectTableForeignKeys('employee',
870
+ (pError, pFKs) =>
871
+ {
872
+ Expect(pError).to.not.exist;
873
+ Expect(pFKs).to.be.an('array');
874
+ Expect(pFKs.length).to.equal(1);
875
+
876
+ Expect(pFKs[0].Column).to.equal('reports_to');
877
+ Expect(pFKs[0].ReferencedTable).to.equal('employee');
878
+ Expect(pFKs[0].ReferencedColumn).to.equal('employee_id');
879
+
880
+ return fDone();
881
+ });
882
+ }
883
+ );
884
+
885
+ test
886
+ (
887
+ 'introspectTableForeignKeys on playlist_track detects 2 FKs',
888
+ (fDone) =>
889
+ {
890
+ libSchemaPostgreSQL.introspectTableForeignKeys('playlist_track',
891
+ (pError, pFKs) =>
892
+ {
893
+ Expect(pError).to.not.exist;
894
+ Expect(pFKs).to.be.an('array');
895
+ Expect(pFKs.length).to.equal(2);
896
+
897
+ let tmpPlaylistFK = pFKs.find((pFK) => { return pFK.Column === 'playlist_id'; });
898
+ Expect(tmpPlaylistFK).to.exist;
899
+ Expect(tmpPlaylistFK.ReferencedTable).to.equal('playlist');
900
+
901
+ let tmpTrackFK = pFKs.find((pFK) => { return pFK.Column === 'track_id'; });
902
+ Expect(tmpTrackFK).to.exist;
903
+ Expect(tmpTrackFK.ReferencedTable).to.equal('track');
904
+
905
+ return fDone();
906
+ });
907
+ }
908
+ );
909
+
910
+ test
911
+ (
912
+ 'introspectTableSchema on track combines columns with FK detection',
913
+ (fDone) =>
914
+ {
915
+ libSchemaPostgreSQL.introspectTableSchema('track',
916
+ (pError, pSchema) =>
917
+ {
918
+ Expect(pError).to.not.exist;
919
+ Expect(pSchema.TableName).to.equal('track');
920
+ Expect(pSchema.ForeignKeys.length).to.equal(3);
921
+
922
+ let tmpAlbumIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'album_id'; });
923
+ Expect(tmpAlbumIdCol.DataType).to.equal('ForeignKey');
924
+
925
+ let tmpMediaTypeIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'media_type_id'; });
926
+ Expect(tmpMediaTypeIdCol.DataType).to.equal('ForeignKey');
927
+
928
+ let tmpGenreIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'genre_id'; });
929
+ Expect(tmpGenreIdCol.DataType).to.equal('ForeignKey');
930
+
931
+ return fDone();
932
+ });
933
+ }
934
+ );
935
+
936
+ test
937
+ (
938
+ 'introspectDatabaseSchema includes all Chinook tables',
939
+ (fDone) =>
940
+ {
941
+ libSchemaPostgreSQL.introspectDatabaseSchema(
942
+ (pError, pSchema) =>
943
+ {
944
+ Expect(pError).to.not.exist;
945
+ Expect(pSchema.Tables).to.be.an('array');
946
+
947
+ let tmpTableNames = pSchema.Tables.map((pT) => { return pT.TableName; });
948
+ Expect(tmpTableNames).to.include('track');
949
+ Expect(tmpTableNames).to.include('album');
950
+ Expect(tmpTableNames).to.include('artist');
951
+ Expect(tmpTableNames).to.include('employee');
952
+ Expect(tmpTableNames).to.include('customer');
953
+ Expect(tmpTableNames).to.include('invoice');
954
+ Expect(tmpTableNames).to.include('invoice_line');
955
+ Expect(tmpTableNames).to.include('playlist_track');
956
+
957
+ let tmpTrack = pSchema.Tables.find((pT) => { return pT.TableName === 'track'; });
958
+ Expect(tmpTrack.ForeignKeys.length).to.equal(3);
959
+
960
+ return fDone();
961
+ });
962
+ }
963
+ );
964
+
965
+ test
966
+ (
967
+ 'generateMeadowPackageFromTable on album produces valid package',
968
+ (fDone) =>
969
+ {
970
+ libSchemaPostgreSQL.generateMeadowPackageFromTable('album',
971
+ (pError, pPackage) =>
972
+ {
973
+ Expect(pError).to.not.exist;
974
+ Expect(pPackage.Scope).to.equal('album');
975
+ Expect(pPackage.DefaultIdentifier).to.equal('album_id');
976
+ Expect(pPackage.Schema).to.be.an('array');
977
+ Expect(pPackage.DefaultObject).to.be.an('object');
978
+
979
+ let tmpIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'album_id'; });
980
+ Expect(tmpIDEntry.Type).to.equal('AutoIdentity');
981
+
982
+ let tmpTitleEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'title'; });
983
+ Expect(tmpTitleEntry.Type).to.equal('String');
984
+
985
+ return fDone();
986
+ });
987
+ }
988
+ );
989
+
990
+ test
991
+ (
992
+ 'generateMeadowPackageFromTable on track handles FKs and Decimal',
993
+ (fDone) =>
994
+ {
995
+ libSchemaPostgreSQL.generateMeadowPackageFromTable('track',
996
+ (pError, pPackage) =>
997
+ {
998
+ Expect(pError).to.not.exist;
999
+ Expect(pPackage.Scope).to.equal('track');
1000
+ Expect(pPackage.DefaultIdentifier).to.equal('track_id');
1001
+
1002
+ let tmpUnitPriceEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'unit_price'; });
1003
+ Expect(tmpUnitPriceEntry).to.exist;
1004
+
1005
+ return fDone();
1006
+ });
1007
+ }
1008
+ );
1009
+ }
1010
+ );
147
1011
  }
148
1012
  );