-
Notifications
You must be signed in to change notification settings - Fork 1
/
data-mapper.ts
134 lines (114 loc) · 4.96 KB
/
data-mapper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { Schema } from './';
type LookupType = {[key: string]: any};
type QueryRowType = LookupType;
/**
* Class that serializes a [[Schema]] instance in to a normalized document (a
* series of [[Table]]-decorated Entities).
*/
export class DataMapper {
/**
* Serialize the query into the an array of objects, as defined by schema.
* @param query - A set of query results, which is an array of objects
* containing keys as properties and values from a database query.
* @param schema - The [[Schema]] instance describing how to serialize the
* query.
* @return A normalized document (an array of T instances).
*/
serialize<T>(query: object[], schema: Schema): T[] {
const collection: T[] = [];
// An object is used instead of a map because it's sufficiently faster, and
// speed is important when normalizing a large query of tabular, relational
// data.
const lookup: LookupType = {};
// Helper function to recursively serialize a row of query data.
function serializeRow<R>(
queryRow: QueryRowType,
schema: Schema,
lookup: LookupType,
collection?: R[]): R {
// The keyCol is null, meaning this was an outer join and there is no
// related data.
if (schema.getKeyColumns().some(keyCol => queryRow[keyCol.name] === null))
return null;
// The key may be composed of multiple columns. Convert key values to
// a string so that an entity lookup can be kept.
const keyVal = schema.getKeyColumns()
.map(keyCol => queryRow[keyCol.name])
.toString();
let doc;
// First time encountering this key. Create a document for it.
if (lookup[keyVal] === undefined) {
// The schema holds the table (see the Table decorator), and the table
// stores the decorated Entity. Produce one (i.e. new() on the Entity).
doc = schema.table.produceEntity();
// Add the new document to the collection if provided (the collection
// is provided when this document is being mapped to an array (e.g.
// the parent relates to this Entity with a one-to-many cardinality).
if (collection)
collection.push(doc);
// Add each property->column value to the document.
for (let i = 0; i < schema.columns.length; ++i) {
const column = schema.columns[i];
const colMeta = column.meta;
const colName = column.name;
// If there's an onRetrieve converter then call it with the column
// value. Otherwise just map the column on to the document. (mapTo
// is the Column-decorated property name.)
if (colMeta.converter && colMeta.converter.onRetrieve)
doc[colMeta.mapTo] = colMeta.converter.onRetrieve(queryRow[colName]);
else {
doc[colMeta.mapTo] = queryRow[colName];
if (queryRow[colName] !== null && queryRow[colName] !== undefined) {
// Primitive types Boolean and Number get type-casted.
if (colMeta.dataType === 'Boolean') {
doc[colMeta.mapTo] = Boolean(queryRow[colName]);
}
else if (colMeta.dataType === 'Number') {
doc[colMeta.mapTo] = Number(queryRow[colName]);
}
}
}
}
// This lookup is used to ensure uniqueness.
lookup[keyVal] = {
document: doc,
lookup: {}
};
}
else
doc = lookup[keyVal].document;
// Now recursivly serialize each sub schema.
for (let i = 0; i < schema.schemata.length; ++i) {
const relationship = schema.schemata[i].relationship;
const relProp = relationship.mapTo;
const subSchema = schema.schemata[i].schema;
let subLookup, subCollection;
// Note that the lookup for each document needs to be unique because
// there could be two schemata at the same level that have key columns
// with the same value (e.g. a person with product and phone numbers,
// and phoneNumberID = 1 and productID = 1).
if (lookup[keyVal].lookup[relProp] === undefined)
lookup[keyVal].lookup[relProp] = {};
subLookup = lookup[keyVal].lookup[relProp];
// For OneToMany relationships the subdocument needs to be added to
// a subcollection.
if (relationship.cardinality === 'OneToMany') {
if (doc[relProp] === undefined)
doc[relProp] = [];
subCollection = doc[relProp];
serializeRow(queryRow, subSchema, subLookup, subCollection);
}
// Otherwise this is a ManyToOne or OneToOne relationship, so add
// the document under the relationship's property.
else {
doc[relProp] = serializeRow(queryRow, subSchema, subLookup);
}
}
return doc;
}
// Serialize each row recursively.
for (let i = 0; i < query.length; ++i)
serializeRow(query[i], schema, lookup, collection);
return collection;
}
}