Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.

Commit bd82b58

Browse files
committed
1 parent c0cd384 commit bd82b58

6 files changed

Lines changed: 145 additions & 57 deletions

File tree

spec/helpers/sakuraapi.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ export interface ITestSapiOptions {
1515
plugins?: SakuraApiPlugin[];
1616
}
1717

18+
process.on('unhandledRejection', (r) => {
19+
20+
// tslint:disable:no-console
21+
console.log('Unhandled Rejection'.red.underline);
22+
console.log('-'.repeat(process.stdout.columns).red);
23+
console.log('↓'.repeat(process.stdout.columns).zebra.red);
24+
console.log('-'.repeat(process.stdout.columns).red);
25+
console.log(r);
26+
console.log('-'.repeat(process.stdout.columns).red);
27+
console.log('↑'.repeat(process.stdout.columns).zebra.red);
28+
console.log('-'.repeat(process.stdout.columns).red);
29+
// tslint:enable:no-console
30+
31+
});
32+
1833
export function testSapi(options: ITestSapiOptions): SakuraApi {
1934

2035
const sapi = new SakuraApi({
@@ -41,12 +56,14 @@ export function testSapi(options: ITestSapiOptions): SakuraApi {
4156
sapi.addLastErrorHandlers((err, req, res, next) => {
4257

4358
// tslint:disable:no-console
44-
console.log('------------------------------------------------'.red);
45-
console.log('↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'.zebra);
59+
console.log('-'.repeat(process.stdout.columns).red);
60+
console.log('↓'.repeat(process.stdout.columns).zebra.red);
61+
console.log('-'.repeat(process.stdout.columns).red);
4662
console.log('An error bubbled up in an unexpected way during testing');
4763
console.log(err);
48-
console.log('↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'.zebra);
49-
console.log('------------------------------------------------'.red);
64+
console.log('-'.repeat(process.stdout.columns).red);
65+
console.log('↑'.repeat(process.stdout.columns).zebra.red);
66+
console.log('-'.repeat(process.stdout.columns).red);
5067
// tslint:enable:no-console
5168

5269
next(err);

src/core/@model/db.spec.ts

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,18 @@ describe('@Db', () => {
9191
});
9292

9393
describe('maps db fields to deeply nested model properties', () => {
94+
@Model()
9495
class Address {
95-
@Db('st')
96+
@Db('st') @Json()
9697
street = '1600 Pennsylvania Ave NW';
9798

98-
@Db('c')
99+
@Db('c') @Json()
99100
city = 'Washington';
100101

101-
@Db('s')
102+
@Db('s') @Json()
102103
state = 'DC';
103104

104-
@Db('z')
105+
@Db('z') @Json()
105106
zip = '20500';
106107

107108
@Db({field: 'gc', private: true})
@@ -262,6 +263,85 @@ describe('@Db', () => {
262263
expect(result.lastName).toBeUndefined();
263264
});
264265

266+
it('unmarshalls _id', (done) => {
267+
268+
@Model()
269+
class Test extends SakuraApiModel {
270+
271+
@Db({field: 'ph'})
272+
phone: string;
273+
}
274+
275+
const data = {
276+
_id: new ObjectID(),
277+
ph: '1234567890'
278+
};
279+
280+
const test = Test.fromDb(data);
281+
282+
expect((test._id || 'missing _id').toString()).toBe(data._id.toString());
283+
expect((test.id || 'missing id').toString()).toBe(data._id.toString());
284+
done();
285+
});
286+
287+
it('model with default value will take default value if db returns field empty, issue #94', async (done) => {
288+
289+
@Model({
290+
dbConfig: {
291+
collection: 'users',
292+
db: 'userDb'
293+
}
294+
})
295+
class Test94 extends SakuraApiModel {
296+
@Db({field: 'ad', model: Address}) @Json()
297+
address = new Address();
298+
}
299+
300+
try {
301+
const sapi = testSapi({
302+
models: [Address, Test94]
303+
});
304+
await sapi.listen({bootMessage: ''});
305+
await Test94.removeAll({});
306+
307+
const createResult = await Test94.fromJson({
308+
address: {
309+
city: '2',
310+
gateCode: '5',
311+
state: '3',
312+
street: '1',
313+
zip: '4'
314+
}
315+
}).create();
316+
const fullDoc = await Test94.getById(createResult.insertedId);
317+
318+
expect(fullDoc.address.street).toBe('1');
319+
expect(fullDoc.address.city).toBe('2');
320+
expect(fullDoc.address.state).toBe('3');
321+
expect(fullDoc.address.zip).toBe('4');
322+
expect(fullDoc.address.gateCode).toBe('5');
323+
324+
delete fullDoc.address;
325+
await fullDoc.save({ad: undefined});
326+
327+
const updated = await Test94.getById(createResult.insertedId);
328+
updated.address = updated.address || {} as Address;
329+
330+
const defaultAddress = new Address();
331+
332+
expect(updated.address.street).toBe(defaultAddress.street);
333+
expect(updated.address.city).toBe(defaultAddress.city);
334+
expect(updated.address.state).toBe(defaultAddress.state);
335+
expect(updated.address.zip).toBe(defaultAddress.zip);
336+
expect(updated.address.gateCode).toBe(defaultAddress.gateCode);
337+
338+
await sapi.close();
339+
done();
340+
} catch (err) {
341+
done.fail(err);
342+
}
343+
});
344+
265345
describe('with dbOptions.promiscuous mode', () => {
266346

267347
@Model({
@@ -314,27 +394,6 @@ describe('@Db', () => {
314394
expect(result._id instanceof ObjectID).toBeTruthy('result._id should have been an instance of ObjectID');
315395
});
316396
});
317-
318-
it('unmarshalls _id', (done) => {
319-
320-
@Model()
321-
class Test extends SakuraApiModel {
322-
323-
@Db({field: 'ph'})
324-
phone: string;
325-
}
326-
327-
const data = {
328-
_id: new ObjectID(),
329-
ph: '1234567890'
330-
};
331-
332-
const test = Test.fromDb(data);
333-
334-
expect((test._id || 'missing _id').toString()).toBe(data._id.toString());
335-
expect((test.id || 'missing id').toString()).toBe(data._id.toString());
336-
done();
337-
});
338397
});
339398

340399
describe('prunes model fields missing from db document in strict mode', () => {

src/core/@model/db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export const dbSymbols = {
1111

1212
export interface IDbOptions {
1313
/**
14-
* An optional constructor function (ES6 Class) that is used to instantiate the property.
14+
* An optional `@`[[Model]] decorated class. If provided, the property will be instantiated as a sub document
15+
* with its default values or the values from the database. `@`[[Json]] will utilize this same model
16+
* if one is not set on that attribute.
1517
*/
1618
model?: any;
1719

src/core/@model/json.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export const jsonSymbols = {
1111
export interface IJsonOptions {
1212

1313
/**
14-
* An optional constructor function (ES6 Class) that is used to instantiate the property.
14+
* An optional `@`[[Model]] decorated class. If provided, the property will be instantiated as a sub document
15+
* with its default values or the values from the json object. `@`[[Json]] will utilize this same model
16+
* as the one set in `@`[[Db]] if `model` is not set on this attribute.
1517
*/
1618
model?: any;
1719

src/core/@model/model.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -408,25 +408,23 @@ function fromDb(json: any, options?: IFromDbOptions): object {
408408
const propertyNames = Object.getOwnPropertyNames(source);
409409
for (const key of propertyNames) {
410410

411-
if (shouldRecurse(source[key])) {
412-
413-
// convert the DB key name to the Model key name
414-
const mapper = map(key, source[key], dbOptionsByFieldName);
411+
// convert the DB key name to the Model key name
412+
const mapper = map(key, source[key], dbOptionsByFieldName);
413+
const model = mapper.model;
414+
415+
let nextTarget;
416+
try {
417+
nextTarget = (model)
418+
? Object.assign(new model(), target[mapper.newKey])
419+
: target[mapper.newKey];
420+
} catch (err) {
421+
throw new Error(`Model '${modelName}' has a property '${key}' that defines its model with a value that`
422+
+ ` cannot be constructed`);
423+
}
415424

425+
if (shouldRecurse(source[key])) {
416426
// if the key should be included, recurse into it
417427
if (mapper.newKey !== undefined) {
418-
const model = mapper.model;
419-
420-
// if recurrsing into a model, set that up, otherwise just pass the target in
421-
let nextTarget;
422-
try {
423-
nextTarget = (model)
424-
? Object.assign(new model(), target[mapper.newKey])
425-
: target[mapper.newKey];
426-
} catch (err) {
427-
throw new Error(`Model '${modelName}' has a property '${key}' that defines its model with a value that`
428-
+ ` cannot be constructed`);
429-
}
430428

431429
let value = mapDbToModel(source[key], nextTarget, map, ++depth);
432430

@@ -441,13 +439,13 @@ function fromDb(json: any, options?: IFromDbOptions): object {
441439
target[mapper.newKey] = value;
442440
}
443441

444-
continue;
445-
}
446-
447-
// otherwise, map a property that has a primitive value or an ObjectID value
448-
const mapper = map(key, source[key], dbOptionsByFieldName);
449-
if (mapper.newKey !== undefined) {
450-
target[mapper.newKey] = source[key];
442+
} else {
443+
// otherwise, map a property that has a primitive value or an ObjectID value
444+
if (mapper.newKey !== undefined) {
445+
target[mapper.newKey] = (source[key] !== undefined && source[key] !== null)
446+
? source[key]
447+
: nextTarget; // resolves issue #94
448+
}
451449
}
452450
}
453451

@@ -470,12 +468,13 @@ function fromDb(json: any, options?: IFromDbOptions): object {
470468
function pruneNonDbProperties(source, target) {
471469
const dbOptionsByProperty: Map<string, IDbOptions> = Reflect.getMetadata(dbSymbols.dbByPropertyName, target);
472470

473-
for (const key of Object.getOwnPropertyNames(target)) {
471+
const keys = Object.getOwnPropertyNames(target);
472+
for (const key of keys) {
474473

475474
const dbOptions = (dbOptionsByProperty) ? dbOptionsByProperty.get(key) || {} : null;
476475
const fieldName = (dbOptions) ? dbOptions.field || key : key;
477476

478-
if (!source.hasOwnProperty(fieldName)) {
477+
if (!!source && !source.hasOwnProperty(fieldName)) {
479478
if (key === 'id' && source.hasOwnProperty('_id')) {
480479
continue;
481480
}
@@ -836,6 +835,14 @@ function getCursorById(id, project?: any): Cursor<any> {
836835
function getDb(): Db {
837836
// can be called as instance or static method, so get the appropriate context
838837
const constructor = this[modelSymbols.constructor] || this;
838+
839+
if (!constructor.sapi) {
840+
const target = constructor.name || constructor.constructor.name;
841+
throw new Error(`getDb called on model ${target} without an instance of ` +
842+
`SakuraAPI. Make sure you pass ${target} into the Model injector when you're ` +
843+
`instantiating SakuraApi`);
844+
}
845+
839846
const db = constructor[modelSymbols.sapi].dbConnections.getDb(constructor[modelSymbols.dbName]);
840847

841848
debug.normal(`.getDb called, dbName: '${constructor[modelSymbols.dbName]}', found?: ${!!db}`);

tslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
],
2727
"interface-name": false,
2828
"prefer-object-spread": false,
29-
"variable-name": false
29+
"variable-name": false,
30+
"no-object-literal-type-assertion": false
3031
},
3132
"jsRules": {
3233
"curly": true

0 commit comments

Comments
 (0)