@reldens/storage 0.116.0 → 0.118.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.
- package/.claude/test-architecture.md +27 -17
- package/CLAUDE.md +67 -11
- package/README.md +204 -22
- package/bin/reldens-storage.js +43 -10
- package/index.js +53 -12
- package/lib/drizzle/drizzle-data-server.js +123 -0
- package/lib/drizzle/drizzle-driver.js +228 -0
- package/lib/drizzle/drizzle-models-generation.js +49 -0
- package/lib/drizzle/drizzle-modules-loader.js +32 -0
- package/lib/drizzle/drizzle-modules-validator.js +74 -0
- package/lib/entities-generator.js +36 -7
- package/lib/entity-templates/drizzle-model.template +28 -0
- package/lib/entity-templates/query-builder-model.template +17 -0
- package/lib/generators/base-generator.js +13 -0
- package/lib/generators/entities-generation.js +0 -13
- package/lib/generators/models-generation.js +179 -693
- package/lib/generators/relations-detection.js +171 -0
- package/lib/knex/knex-data-server.js +128 -0
- package/lib/knex/knex-driver.js +157 -0
- package/lib/knex/knex-modules-loader.js +28 -0
- package/lib/knex/knex-modules-validator.js +45 -0
- package/lib/kysely/kysely-data-server.js +128 -0
- package/lib/kysely/kysely-driver.js +176 -0
- package/lib/kysely/kysely-modules-loader.js +28 -0
- package/lib/kysely/kysely-modules-validator.js +53 -0
- package/lib/mikro-orm/mikro-orm-data-server.js +48 -29
- package/lib/mikro-orm/mikro-orm-driver.js +385 -121
- package/lib/mikro-orm/mikro-orm-models-generation.js +178 -0
- package/lib/mikro-orm/mikro-orm-modules-loader.js +50 -0
- package/lib/mikro-orm/mikro-orm-modules-validator.js +46 -0
- package/lib/mysql2-connection-config.js +38 -0
- package/lib/objection-js/objection-js-data-server.js +71 -125
- package/lib/objection-js/objection-js-driver.js +4 -1
- package/lib/objection-js/objection-js-models-generation.js +98 -0
- package/lib/objection-js/objection-modules-loader.js +28 -0
- package/lib/objection-js/objection-modules-validator.js +31 -0
- package/lib/package-resolver.js +45 -0
- package/lib/prisma/prisma-client-loader.js +11 -1
- package/lib/prisma/prisma-driver.js +16 -2
- package/lib/prisma/prisma-filter-processor.js +5 -6
- package/lib/prisma/prisma-models-generation.js +236 -0
- package/lib/prisma/prisma-schema-generator.js +5 -1
- package/lib/prisma/prisma-type-caster.js +14 -0
- package/lib/query-builder-driver.js +210 -0
- package/lib/query-builder-models-generation.js +80 -0
- package/lib/relations-loader.js +192 -0
- package/lib/type-mapper.js +38 -0
- package/package.json +7 -7
- package/tests/.env.test.example +6 -0
- package/tests/fixtures/categories-fixtures.js +8 -0
- package/tests/fixtures/expected-entities/entities/test-product-details-entity.js +72 -0
- package/tests/fixtures/expected-entities/entities-config.js +6 -4
- package/tests/fixtures/expected-entities/entities-translations.js +25 -14
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +2 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +13 -8
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-product-details-model.js +80 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +20 -14
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +13 -12
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +2 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-product-details-model.js +33 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +9 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +2 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-product-details-model.js +43 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +2 -0
- package/tests/fixtures/product-details-fixtures.js +22 -0
- package/tests/fixtures/reviews-fixtures.js +2 -1
- package/tests/fixtures/sql/test-schema.sql +23 -1
- package/tests/fixtures/table-columns-fixtures.js +97 -0
- package/tests/integration/test-cross-driver-equivalence.js +286 -0
- package/tests/integration/test-nested-filters.js +265 -408
- package/tests/integration/test-relations.js +199 -0
- package/tests/integration/test-reldens-sample-data.js +255 -0
- package/tests/integration/test-reldens-shape-relations.js +292 -0
- package/tests/run-tests.js +112 -60
- package/tests/unit/test-drivers.js +7 -1
- package/tests/unit/test-models-generation.js +174 -0
- package/tests/utils/column-value-assert.js +139 -0
- package/tests/utils/driver-registry.js +1 -1
- package/tests/utils/entity-projection.js +111 -0
- package/tests/utils/observed-value.js +41 -0
- package/tests/utils/projection-difference.js +151 -0
- package/tests/utils/relations-shape-support.js +104 -0
- package/tests/utils/reldens-schema-loader.js +274 -0
- package/tests/utils/test-helpers.js +152 -19
- package/tests/utils/test-runner.js +26 -5
- package/tests/utils/whole-result-assert.js +243 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - ModelsGeneration Test
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { TestRunner, assert } = require('../utils/test-runner');
|
|
8
|
+
const { RelationsDetection } = require('../../lib/generators/relations-detection');
|
|
9
|
+
const { MikroOrmModelsGeneration } = require('../../lib/mikro-orm/mikro-orm-models-generation');
|
|
10
|
+
|
|
11
|
+
class ModelsGenerationTest
|
|
12
|
+
{
|
|
13
|
+
|
|
14
|
+
constructor()
|
|
15
|
+
{
|
|
16
|
+
this.runner = new TestRunner();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createColumn(props)
|
|
20
|
+
{
|
|
21
|
+
return Object.assign(
|
|
22
|
+
{
|
|
23
|
+
name: 'test_column',
|
|
24
|
+
type: 'int',
|
|
25
|
+
columnType: 'int(10) unsigned',
|
|
26
|
+
length: null,
|
|
27
|
+
nullable: false,
|
|
28
|
+
key: '',
|
|
29
|
+
extra: '',
|
|
30
|
+
default: null
|
|
31
|
+
},
|
|
32
|
+
props
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createForeignKeyColumn(name, key)
|
|
37
|
+
{
|
|
38
|
+
return this.createColumn({
|
|
39
|
+
name,
|
|
40
|
+
key,
|
|
41
|
+
referencedTable: 'test_products',
|
|
42
|
+
referencedColumn: 'id',
|
|
43
|
+
referencedDeleteRule: 'CASCADE'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
createTables()
|
|
48
|
+
{
|
|
49
|
+
return {
|
|
50
|
+
test_products: {
|
|
51
|
+
name: 'test_products',
|
|
52
|
+
columns: {
|
|
53
|
+
id: this.createColumn({name: 'id', key: 'PRI', extra: 'auto_increment'}),
|
|
54
|
+
name: this.createColumn({name: 'name', type: 'varchar', columnType: 'varchar(200)', length: '200'})
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
test_product_details: {
|
|
58
|
+
name: 'test_product_details',
|
|
59
|
+
columns: {
|
|
60
|
+
id: this.createColumn({name: 'id', key: 'PRI', extra: 'auto_increment'}),
|
|
61
|
+
product_id: this.createForeignKeyColumn('product_id', 'UNI')
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
test_reviews: {
|
|
65
|
+
name: 'test_reviews',
|
|
66
|
+
columns: {
|
|
67
|
+
id: this.createColumn({name: 'id', key: 'PRI', extra: 'auto_increment'}),
|
|
68
|
+
product_id: this.createForeignKeyColumn('product_id', 'MUL')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
createRelationsDetection()
|
|
75
|
+
{
|
|
76
|
+
let relationsDetection = new RelationsDetection({});
|
|
77
|
+
relationsDetection.allTablesData = this.createTables();
|
|
78
|
+
return relationsDetection;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createGeneration()
|
|
82
|
+
{
|
|
83
|
+
return new MikroOrmModelsGeneration(this.createRelationsDetection());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
mikroOrmDefinition(generation, tableName)
|
|
87
|
+
{
|
|
88
|
+
let tableData = generation.relationsDetection.allTablesData[tableName];
|
|
89
|
+
return generation.generateEntityProperties(tableData.columns, tableName, tableData);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async run()
|
|
93
|
+
{
|
|
94
|
+
this.runner.suite('ModelsGeneration');
|
|
95
|
+
await this.testForwardRelationKind();
|
|
96
|
+
await this.testMikroOrmOneToOneDefinitions();
|
|
97
|
+
await this.testObjectionJsRelationTypes();
|
|
98
|
+
return this.runner.getResults();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async testForwardRelationKind()
|
|
102
|
+
{
|
|
103
|
+
this.runner.group('forwardRelationKind');
|
|
104
|
+
let generation = this.createGeneration();
|
|
105
|
+
await this.runner.test('should return 1:1 for a unique foreign key column', async () => {
|
|
106
|
+
let kind = generation.forwardRelationKind(this.createColumn({key: 'UNI'}));
|
|
107
|
+
assert.strictEqual(kind, '1:1');
|
|
108
|
+
});
|
|
109
|
+
await this.runner.test('should return 1:1 for a primary foreign key column', async () => {
|
|
110
|
+
let kind = generation.forwardRelationKind(this.createColumn({key: 'PRI'}));
|
|
111
|
+
assert.strictEqual(kind, '1:1');
|
|
112
|
+
});
|
|
113
|
+
await this.runner.test('should return m:1 for an indexed foreign key column', async () => {
|
|
114
|
+
let kind = generation.forwardRelationKind(this.createColumn({key: 'MUL'}));
|
|
115
|
+
// @possible-hallucinated-undefined-method
|
|
116
|
+
assert.strictEqual(kind, 'm:1');
|
|
117
|
+
});
|
|
118
|
+
await this.runner.test('should return m:1 for a non indexed foreign key column', async () => {
|
|
119
|
+
let kind = generation.forwardRelationKind(this.createColumn({key: ''}));
|
|
120
|
+
assert.strictEqual(kind, 'm:1');
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async testMikroOrmOneToOneDefinitions()
|
|
125
|
+
{
|
|
126
|
+
this.runner.group('getEntityPropertiesDefinition - mikro-orm one to one');
|
|
127
|
+
let generation = this.createGeneration();
|
|
128
|
+
await this.runner.test('should emit a 1:1 owning side with joinColumns for a unique foreign key', async () => {
|
|
129
|
+
let definition = this.mikroOrmDefinition(generation, 'test_product_details');
|
|
130
|
+
assert.ok(definition.includes('related_test_products: {\n kind: \'1:1\','));
|
|
131
|
+
assert.ok(definition.includes('joinColumns: [\'product_id\']'));
|
|
132
|
+
assert.ok(!definition.includes('mappedBy'));
|
|
133
|
+
});
|
|
134
|
+
await this.runner.test('should emit a 1:1 inverse side with mappedBy for a unique foreign key', async () => {
|
|
135
|
+
let definition = this.mikroOrmDefinition(generation, 'test_products');
|
|
136
|
+
assert.ok(definition.includes('related_test_product_details: {\n kind: \'1:1\','));
|
|
137
|
+
assert.ok(definition.includes('mappedBy: \'related_test_products\''));
|
|
138
|
+
});
|
|
139
|
+
await this.runner.test('should emit a 1:m inverse side for a non unique foreign key', async () => {
|
|
140
|
+
let definition = this.mikroOrmDefinition(generation, 'test_products');
|
|
141
|
+
assert.ok(definition.includes('related_test_reviews: {\n kind: \'1:m\','));
|
|
142
|
+
});
|
|
143
|
+
await this.runner.test('should emit a m:1 owning side for a non unique foreign key', async () => {
|
|
144
|
+
let definition = this.mikroOrmDefinition(generation, 'test_reviews');
|
|
145
|
+
assert.ok(definition.includes('related_test_products: {\n kind: \'m:1\','));
|
|
146
|
+
assert.ok(definition.includes('joinColumns: [\'product_id\']'));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async testObjectionJsRelationTypes()
|
|
151
|
+
{
|
|
152
|
+
this.runner.group('ObjectionJS relation types for unique foreign keys');
|
|
153
|
+
let relationsDetection = this.createRelationsDetection();
|
|
154
|
+
let tables = relationsDetection.allTablesData;
|
|
155
|
+
await this.runner.test('should keep BelongsToOneRelation on the owning side', async () => {
|
|
156
|
+
let relations = relationsDetection.detectForwardRelations(
|
|
157
|
+
'test_product_details',
|
|
158
|
+
tables.test_product_details
|
|
159
|
+
);
|
|
160
|
+
assert.strictEqual(relations.related_test_products.relationType, 'BelongsToOneRelation');
|
|
161
|
+
});
|
|
162
|
+
await this.runner.test('should use HasOneRelation on the referenced side of a unique foreign key', async () => {
|
|
163
|
+
let reverseRelations = relationsDetection.detectReverseRelations('test_products');
|
|
164
|
+
assert.strictEqual(reverseRelations.related_test_product_details.relationType, 'HasOneRelation');
|
|
165
|
+
});
|
|
166
|
+
await this.runner.test('should use HasManyRelation on the referenced side of a non unique foreign key', async () => {
|
|
167
|
+
let reverseRelations = relationsDetection.detectReverseRelations('test_products');
|
|
168
|
+
assert.strictEqual(reverseRelations.related_test_reviews.relationType, 'HasManyRelation');
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = ModelsGenerationTest;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Column Value Assert
|
|
4
|
+
* Value and type assertion of a single database column against the value the fixture row was inserted with.
|
|
5
|
+
* Both sides are reduced to the same canonical form, identical for every driver, so a driver returning
|
|
6
|
+
* another representation of the stored value fails stating what it actually returned.
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const { ObservedValue } = require('./observed-value');
|
|
12
|
+
const { TableColumnsFixtures } = require('../fixtures/table-columns-fixtures');
|
|
13
|
+
const { sc } = require('@reldens/utils');
|
|
14
|
+
|
|
15
|
+
class ColumnValueAssert
|
|
16
|
+
{
|
|
17
|
+
|
|
18
|
+
static anyValue = {reldensAnyColumnValue: true};
|
|
19
|
+
|
|
20
|
+
static assertColumns(actualRecord, tableName, expectedRow, label)
|
|
21
|
+
{
|
|
22
|
+
let columns = TableColumnsFixtures[tableName].columns;
|
|
23
|
+
for(let columnName of Object.keys(columns)){
|
|
24
|
+
this.assertColumn(
|
|
25
|
+
actualRecord[columnName],
|
|
26
|
+
expectedRow[columnName],
|
|
27
|
+
columns[columnName],
|
|
28
|
+
label+'.'+columnName
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static assertColumn(actualValue, expectedValue, columnType, label)
|
|
35
|
+
{
|
|
36
|
+
if(ColumnValueAssert.anyValue === expectedValue){
|
|
37
|
+
this.assertColumnType(actualValue, columnType, label);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if(null === expectedValue){
|
|
41
|
+
assert.strictEqual(
|
|
42
|
+
actualValue,
|
|
43
|
+
null,
|
|
44
|
+
label+' expected null, observed: '+ObservedValue.describe(actualValue)
|
|
45
|
+
);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
assert.strictEqual(
|
|
49
|
+
this.canonicalValue(actualValue, columnType),
|
|
50
|
+
this.canonicalValue(expectedValue, columnType),
|
|
51
|
+
label+' expected '+ObservedValue.describe(expectedValue)
|
|
52
|
+
+' but observed: '+ObservedValue.describe(actualValue)
|
|
53
|
+
);
|
|
54
|
+
this.assertColumnType(actualValue, columnType, label);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static assertColumnType(actualValue, columnType, label)
|
|
59
|
+
{
|
|
60
|
+
assert.ok(
|
|
61
|
+
null !== actualValue,
|
|
62
|
+
label+' is null but the column holds a value, observed: '+ObservedValue.describe(actualValue)
|
|
63
|
+
);
|
|
64
|
+
if('date' === columnType){
|
|
65
|
+
assert.ok(
|
|
66
|
+
actualValue instanceof Date,
|
|
67
|
+
label+' is not a Date instance, observed: '+ObservedValue.describe(actualValue)
|
|
68
|
+
);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if('json' === columnType){
|
|
72
|
+
assert.strictEqual(
|
|
73
|
+
typeof actualValue,
|
|
74
|
+
'object',
|
|
75
|
+
label+' is not a parsed json value, observed: '+ObservedValue.describe(actualValue)
|
|
76
|
+
);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
assert.strictEqual(
|
|
80
|
+
typeof actualValue,
|
|
81
|
+
columnType,
|
|
82
|
+
label+' must be a '+columnType+', observed: '+ObservedValue.describe(actualValue)
|
|
83
|
+
);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static canonicalValue(value, columnType)
|
|
88
|
+
{
|
|
89
|
+
if('json' === columnType){
|
|
90
|
+
return this.canonicalJson(value);
|
|
91
|
+
}
|
|
92
|
+
if(value instanceof Date){
|
|
93
|
+
return value.toISOString();
|
|
94
|
+
}
|
|
95
|
+
if(sc.isObject(value) || sc.isArray(value)){
|
|
96
|
+
return sc.toJsonString(this.sortedJsonValue(value));
|
|
97
|
+
}
|
|
98
|
+
if('number' === columnType && null !== value){
|
|
99
|
+
return String(Number(value));
|
|
100
|
+
}
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static canonicalJson(value)
|
|
105
|
+
{
|
|
106
|
+
let parsedValue = value;
|
|
107
|
+
if(sc.isString(value)){
|
|
108
|
+
parsedValue = sc.parseJson(value, value);
|
|
109
|
+
}
|
|
110
|
+
return sc.toJsonString(this.sortedJsonValue(parsedValue));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static sortedJsonValue(value)
|
|
114
|
+
{
|
|
115
|
+
if(sc.isArray(value)){
|
|
116
|
+
return this.sortedJsonList(value);
|
|
117
|
+
}
|
|
118
|
+
if(!sc.isObject(value)){
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
let sortedObject = {};
|
|
122
|
+
for(let key of Object.keys(value).sort()){
|
|
123
|
+
sortedObject[key] = this.sortedJsonValue(value[key]);
|
|
124
|
+
}
|
|
125
|
+
return sortedObject;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static sortedJsonList(value)
|
|
129
|
+
{
|
|
130
|
+
let sortedList = [];
|
|
131
|
+
for(let item of value){
|
|
132
|
+
sortedList.push(this.sortedJsonValue(item));
|
|
133
|
+
}
|
|
134
|
+
return sortedList;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports.ColumnValueAssert = ColumnValueAssert;
|
|
@@ -22,7 +22,7 @@ class DriverRegistry
|
|
|
22
22
|
instanceId: Math.random()
|
|
23
23
|
};
|
|
24
24
|
this.schemaPath = FileHandler.joinPaths(__dirname, '..', 'fixtures', 'sql', 'test-schema.sql');
|
|
25
|
-
this.repoNames = ['testCategories', 'testProducts', 'testReviews'];
|
|
25
|
+
this.repoNames = ['testCategories', 'testProductDetails', 'testProducts', 'testReviews'];
|
|
26
26
|
this.driverNames = TestHelpers.activeDriverNames();
|
|
27
27
|
this.skipGeneration = false;
|
|
28
28
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Entity Projection
|
|
4
|
+
* Turns any driver result (plain row, ORM managed entity, ORM collection) into a normalised plain object
|
|
5
|
+
* projection: own enumerable keys sorted, dates as tagged strings, collections as arrays, nested entities
|
|
6
|
+
* projected recursively. Two projections built from the same database rows must be strictly equal on every
|
|
7
|
+
* driver, so any divergence found between them is a driver contract divergence.
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { sc } = require('@reldens/utils');
|
|
12
|
+
|
|
13
|
+
class EntityProjection
|
|
14
|
+
{
|
|
15
|
+
|
|
16
|
+
static maxDepth = 12;
|
|
17
|
+
static functionLabel = 'function';
|
|
18
|
+
static circularReferenceLabel = 'circular-reference';
|
|
19
|
+
static maxDepthLabel = 'max-depth-reached';
|
|
20
|
+
static uninitializedCollectionLabel = 'uninitialized-collection';
|
|
21
|
+
|
|
22
|
+
static project(value, parentVisited = null, depth = 0)
|
|
23
|
+
{
|
|
24
|
+
let visited = null === parentVisited ? new Set() : parentVisited;
|
|
25
|
+
if(null === value){
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if('undefined' === typeof value){
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if('function' === typeof value){
|
|
32
|
+
return EntityProjection.functionLabel;
|
|
33
|
+
}
|
|
34
|
+
if('bigint' === typeof value){
|
|
35
|
+
return 'bigint:'+String(value);
|
|
36
|
+
}
|
|
37
|
+
if('object' !== typeof value){
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if(value instanceof Date){
|
|
41
|
+
return 'date:'+value.toISOString();
|
|
42
|
+
}
|
|
43
|
+
if(Buffer.isBuffer(value)){
|
|
44
|
+
return 'buffer:'+value.length;
|
|
45
|
+
}
|
|
46
|
+
if(EntityProjection.maxDepth < depth){
|
|
47
|
+
return EntityProjection.maxDepthLabel;
|
|
48
|
+
}
|
|
49
|
+
if(visited.has(value)){
|
|
50
|
+
return EntityProjection.circularReferenceLabel;
|
|
51
|
+
}
|
|
52
|
+
return this.projectContainer(value, visited, depth);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static projectContainer(value, visited, depth)
|
|
56
|
+
{
|
|
57
|
+
visited.add(value);
|
|
58
|
+
let projected = this.projectByShape(value, visited, depth);
|
|
59
|
+
visited.delete(value);
|
|
60
|
+
return projected;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static projectByShape(value, visited, depth)
|
|
64
|
+
{
|
|
65
|
+
if(sc.isArray(value)){
|
|
66
|
+
return this.projectArray(value, visited, depth);
|
|
67
|
+
}
|
|
68
|
+
if(this.isCollection(value)){
|
|
69
|
+
return this.projectCollection(value, visited, depth);
|
|
70
|
+
}
|
|
71
|
+
return this.projectObject(value, visited, depth);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static isCollection(value)
|
|
75
|
+
{
|
|
76
|
+
if(!sc.isFunction(value.getItems)){
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return sc.isFunction(value.isInitialized);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static projectCollection(value, visited, depth)
|
|
83
|
+
{
|
|
84
|
+
if(!value.isInitialized()){
|
|
85
|
+
return EntityProjection.uninitializedCollectionLabel;
|
|
86
|
+
}
|
|
87
|
+
return this.projectArray(value.getItems(), visited, depth);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static projectArray(value, visited, depth)
|
|
91
|
+
{
|
|
92
|
+
let projected = [];
|
|
93
|
+
for(let item of value){
|
|
94
|
+
projected.push(this.project(item, visited, depth + 1));
|
|
95
|
+
}
|
|
96
|
+
return projected;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static projectObject(value, visited, depth)
|
|
100
|
+
{
|
|
101
|
+
let projected = {};
|
|
102
|
+
let keys = Object.keys(value).sort();
|
|
103
|
+
for(let key of keys){
|
|
104
|
+
projected[key] = this.project(value[key], visited, depth + 1);
|
|
105
|
+
}
|
|
106
|
+
return projected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports.EntityProjection = EntityProjection;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Observed Value
|
|
4
|
+
* Shared diagnostics helper: turns any runtime value into a readable shape description and logs it, so a
|
|
5
|
+
* failing assertion message states what was actually observed instead of only what was expected.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Logger } = require('@reldens/utils');
|
|
10
|
+
|
|
11
|
+
class ObservedValue
|
|
12
|
+
{
|
|
13
|
+
|
|
14
|
+
static describe(value)
|
|
15
|
+
{
|
|
16
|
+
if(null === value){
|
|
17
|
+
return 'null';
|
|
18
|
+
}
|
|
19
|
+
let valueType = typeof value;
|
|
20
|
+
if('object' !== valueType){
|
|
21
|
+
return valueType+'('+String(value)+')';
|
|
22
|
+
}
|
|
23
|
+
if(Array.isArray(value)){
|
|
24
|
+
return 'Array[length='+value.length+']('+value.join(',')+')';
|
|
25
|
+
}
|
|
26
|
+
if(!value.constructor){
|
|
27
|
+
return 'NoConstructor{'+Object.keys(value).join(',')+'}';
|
|
28
|
+
}
|
|
29
|
+
return value.constructor.name+'{'+Object.keys(value).join(',')+'}';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static log(label, value)
|
|
33
|
+
{
|
|
34
|
+
let described = this.describe(value);
|
|
35
|
+
Logger.debug(' OBSERVED '+label+' => '+described);
|
|
36
|
+
return described;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports.ObservedValue = ObservedValue;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Projection Difference
|
|
4
|
+
* Compares two normalised entity projections and returns every path where they diverge, so a cross driver
|
|
5
|
+
* assertion can name the exact key that broke the contract instead of only stating that the results differ.
|
|
6
|
+
* Keys listed as volatile (database generated timestamps) are compared by type only, since every driver
|
|
7
|
+
* writes them at a different moment.
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { sc } = require('@reldens/utils');
|
|
12
|
+
|
|
13
|
+
class ProjectionDifference
|
|
14
|
+
{
|
|
15
|
+
|
|
16
|
+
static missingKeyLabel = 'key-not-present';
|
|
17
|
+
|
|
18
|
+
static collect(expected, actual, volatileKeys)
|
|
19
|
+
{
|
|
20
|
+
let differences = [];
|
|
21
|
+
this.compareValues(expected, actual, 'result', differences, volatileKeys);
|
|
22
|
+
return differences;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static compareValues(expected, actual, path, differences, volatileKeys)
|
|
26
|
+
{
|
|
27
|
+
if(this.isVolatilePath(path, volatileKeys)){
|
|
28
|
+
this.compareTypes(expected, actual, path, differences);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if(this.typeName(expected) !== this.typeName(actual)){
|
|
32
|
+
differences.push({path, expected: this.render(expected), actual: this.render(actual)});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if(sc.isArray(expected)){
|
|
36
|
+
this.compareArrays(expected, actual, path, differences, volatileKeys);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if(sc.isObject(expected)){
|
|
40
|
+
this.compareObjects(expected, actual, path, differences, volatileKeys);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if(expected === actual){
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
differences.push({path, expected: this.render(expected), actual: this.render(actual)});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static compareArrays(expected, actual, path, differences, volatileKeys)
|
|
50
|
+
{
|
|
51
|
+
if(expected.length !== actual.length){
|
|
52
|
+
differences.push({
|
|
53
|
+
path: path+'.length',
|
|
54
|
+
expected: this.render(expected.length),
|
|
55
|
+
actual: this.render(actual.length)
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for(let index = 0; index < expected.length; index++){
|
|
60
|
+
this.compareValues(expected[index], actual[index], path+'['+index+']', differences, volatileKeys);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static compareObjects(expected, actual, path, differences, volatileKeys)
|
|
65
|
+
{
|
|
66
|
+
for(let key of Object.keys(expected)){
|
|
67
|
+
if(!sc.hasOwn(actual, key)){
|
|
68
|
+
differences.push({
|
|
69
|
+
path: path+'.'+key,
|
|
70
|
+
expected: this.render(expected[key]),
|
|
71
|
+
actual: ProjectionDifference.missingKeyLabel
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
this.compareValues(expected[key], actual[key], path+'.'+key, differences, volatileKeys);
|
|
76
|
+
}
|
|
77
|
+
for(let key of Object.keys(actual)){
|
|
78
|
+
if(sc.hasOwn(expected, key)){
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
differences.push({
|
|
82
|
+
path: path+'.'+key,
|
|
83
|
+
expected: ProjectionDifference.missingKeyLabel,
|
|
84
|
+
actual: this.render(actual[key])
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static compareTypes(expected, actual, path, differences)
|
|
90
|
+
{
|
|
91
|
+
if(this.typeName(expected) === this.typeName(actual)){
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
differences.push({path, expected: this.render(expected), actual: this.render(actual)});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static isVolatilePath(path, volatileKeys)
|
|
98
|
+
{
|
|
99
|
+
if(!sc.isArray(volatileKeys)){
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return -1 !== volatileKeys.indexOf(path.split('.').pop());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static typeName(value)
|
|
106
|
+
{
|
|
107
|
+
if(null === value){
|
|
108
|
+
return 'null';
|
|
109
|
+
}
|
|
110
|
+
if(sc.isArray(value)){
|
|
111
|
+
return 'array';
|
|
112
|
+
}
|
|
113
|
+
if(sc.isObject(value)){
|
|
114
|
+
return 'object';
|
|
115
|
+
}
|
|
116
|
+
return typeof value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static render(value)
|
|
120
|
+
{
|
|
121
|
+
if(null === value){
|
|
122
|
+
return 'null';
|
|
123
|
+
}
|
|
124
|
+
if(sc.isArray(value)){
|
|
125
|
+
return 'array(length='+value.length+')';
|
|
126
|
+
}
|
|
127
|
+
if(sc.isObject(value)){
|
|
128
|
+
return 'object(keys='+Object.keys(value).join('|')+')';
|
|
129
|
+
}
|
|
130
|
+
return typeof value+'('+String(value)+')';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static describe(driverName, referenceDriverName, entityName, methodLabel, differences)
|
|
134
|
+
{
|
|
135
|
+
let lines = [];
|
|
136
|
+
for(let difference of differences){
|
|
137
|
+
lines.push(
|
|
138
|
+
'driver: '+driverName
|
|
139
|
+
+' | method: '+methodLabel
|
|
140
|
+
+' | entity: '+entityName
|
|
141
|
+
+' | key: '+difference.path
|
|
142
|
+
+' | '+referenceDriverName+' returned: '+difference.expected
|
|
143
|
+
+' | '+driverName+' returned: '+difference.actual
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return lines;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports.ProjectionDifference = ProjectionDifference;
|