8000 Moves out relation responsibilities from Mapper class by stalniy · Pull Request #405 · js-data/js-data · GitHub
[go: up one dir, main page]

Skip to content

Moves out relation responsibilities from Mapper class #405

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor(mapper): moves relation responsibility to appropriate relati…
…on type
  • Loading branch information
stalniy authored and Sergii Stotskyi committed Mar 16, 2017
commit 02fd90a21e8a0b7c974600032110b02dfad95596
263 changes: 136 additions & 127 deletions src/Mapper.js
10000
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,11 @@ const MAPPER_DEFAULTS = {
* to prevent confusion.)_
*
* [orm]: https://en.wikipedia.org/wiki/Object-relational_mapping
*
* @example
* [pattern]: https://en.wikipedia.org/wiki/Data_mapper_pattern
* [book]: http://martinfowler.com/books/eaa.html
* [record]: Record.html
*
* @example
* // Import and instantiate
* import {Mapper} from 'js-data'
* const UserMapper = new Mapper({ name: 'user' })
Expand Down Expand Up @@ -969,108 +969,52 @@ export default Component.extend({
* @since 3.0.0
*/
create (props, opts) {
let op, adapter
// Default values for arguments
props || (props = {})
opts || (opts = {})
const originalRecord = props
let parentRelationMap = {}
let adapterResponse = {}

// Fill in "opts" with the Mapper's configuration
utils._(opts, this)
adapter = opts.adapter = this.getAdapterName(opts)

// beforeCreate lifecycle hook
op = opts.op = 'beforeCreate'
return utils.resolve(this[op](props, opts)).then((_props) => {
// Allow for re-assignment from lifecycle hook
props = _props === undefined ? props : _props
opts.adapter = this.getAdapterName(opts)

// Deep pre-create belongsTo relations
const belongsToRelationData = {}
opts.op = 'beforeCreate'
return this._runHook(opts.op, props, opts).then((props) => {
opts.with || (opts.with = [])
let tasks = []
utils.forEachRelation(this, opts, (def, optsCopy) => {
const relationData = def.getLocalField(props)
const relatedMapper = def.getRelation()
const relatedIdAttribute = relatedMapper.idAttribute
optsCopy.raw = false
if (!relationData) {
return
}
if (def.type === belongsToType) {
// Create belongsTo relation first because we need a generated id to
// attach to the child
tasks.push(relatedMapper.create(relationData, optsCopy).then((data) => {
def.setLocalField(belongsToRelationData, data)
def.setForeignKey(props, data)
}))
} else if (def.type === hasManyType && def.localKeys) {
// Create his hasMany relation first because it uses localKeys
tasks.push(relatedMapper.createMany(relationData, optsCopy).then((data) => {
def.setLocalField(belongsToRelationData, data)
utils.set(props, def.localKeys, data.map((record) => utils.get(record, relatedIdAttribute)))
}))
}
})
return utils.Promise.all(tasks).then(() => {
// Now delegate to the adapter for the main create
op = opts.op = 'create'
this.dbg(op, props, opts)
return utils.resolve(this.getAdapter(adapter)[op](this, this.toJSON(props, { with: opts.pass || [] }), opts))
}).then((result) => {
const createdRecordData = opts.raw ? result.data : result
return this._createParentRecordIfRequired(props, opts)
}).then(relationMap => {
parentRelationMap = relationMap
}).then(() => {
const object = this.toJSON(props, { with: opts.pass || [] })
opts.op = 'create'
this.dbg(opts.op, props, opts)
return this._invokeAdapterMethod(opts.op, object, opts)
}).then((result) => {
const createdProps = opts.raw ? result.data : result
adapterResponse = result

// Deep post-create hasMany and hasOne relations
tasks = []
utils.forEachRelation(this, opts, (def, optsCopy) => {
const relationData = def.getLocalField(props)
if (!relationData) {
return
}
optsCopy.raw = false
let task
// Create hasMany and hasOne after the main create because we needed
// a generated id to attach to these items
if (def.type === hasManyType && def.foreignKey) {
def.setForeignKey(createdRecordData, relationData)
task = def.getRelation().createMany(relationData, optsCopy).then((result) => {
def.setLocalField(createdRecordData, result)
})
} else if (def.type === hasOneType) {
def.setForeignKey(createdRecordData, relationData)
task = def.getRelation().create(relationData, optsCopy).then((result) => {
def.setLocalField(createdRecordData, result)
})
} else if (def.type === belongsToType && def.getLocalField(belongsToRelationData)) {
def.setLocalField(createdRecordData, def.getLocalField(belongsToRelationData))
} else if (def.type === hasManyType && def.localKeys && def.getLocalField(belongsToRelationData)) {
def.setLocalField(createdRecordData, def.getLocalField(belongsToRelationData))
}
if (task) {
tasks.push(task)
}
})
return utils.Promise.all(tasks).then(() => {
utils.set(originalRecord, createdRecordData, { silent: true })
if (utils.isFunction(originalRecord.commit)) {
originalRecord.commit()
}
if (opts.raw) {
result.data = originalRecord
} else {
result = originalRecord
}
return result
})
return this._createOrAssignChildRecordIfRequired(createdProps, {
opts,
parentRelationMap,
originalProps: props
})
}).then((createdProps) => {
utils.set(originalRecord, createdProps, { silent: true })
if (utils.isFunction(originalRecord.commit)) {
originalRecord.commit()
}
if (opts.raw) {
adapterResponse.data = originalRecord
} else {
adapterResponse = originalRecord
}
return adapterResponse
}).then((result) => {
result = this._end(result, opts)
// afterCreate lifecycle hook
op = opts.op = 'afterCreate'
return utils.resolve(this[op](props, opts, result)).then((_result) => {
// Allow for re-assignment from lifecycle hook
return _result === undefined ? result : _result
})
opts.op = 'afterCreate'
return this._runHook(opts.op, props, opts, result)
})
},

Expand All @@ -1088,6 +1032,77 @@ export default Component.extend({
return this.createRecord(props, opts)
},

/**
* Creates parent record for relation types like BelongsTo or HasMany with localKeys
* in order to satisfy foreignKey dependency (so called child records).
* @param {Object} props See {@link Mapper#create}.
* @param {Object} opts See {@link Mapper#create}.
* @returns {Object} cached parent records map
* @see Mapper#create
* @since 3.0.0
*/
_createParentRecordIfRequired (props, opts) {
const tasks = []
const relations = []

utils.forEachRelation(this, opts, (def, optsCopy) => {
if (!def.isRequiresParentId() || !def.getLocalField(props)) {
return
}

optsCopy.raw = false
relations.push(def)
tasks.push(def.createParentRecord(props, optsCopy))
})

return utils.Promise.all(tasks).then(records => {
return relations.reduce((map, relation, index) => {
relation.setLocalField(map, records[index])
return map
}, {})
})
},

/**
* Creates child record for relation types like HasOne or HasMany with foreignKey
* in order to satisfy foreignKey dependency (so called parent records).
* @param {Object} props See {@link Mapper#create}.
* @param {Object} context contains collected information.
* @param {Object} context.opts See {@link Mapper#create}.
* @param {Object} context.parentRelationMap contains parent records map
* @param {Object} context.originalProps contains data passed into {@link Mapper#create} method
* @return {Promise} updated props
* @see Mapper#create
* @since 3.0.0
*/
_createOrAssignChildRecordIfRequired (props, context) {
const tasks = []

utils.forEachRelation(this, context.opts, (def, optsCopy) => {
const relationData = def.getLocalField(context.originalProps)

if (!relationData) {
return
}

optsCopy.raw = false
// Create hasMany and hasOne after the main create because we needed
// a generated id to attach to these items
if (def.isRequiresChildId()) {
tasks.push(def.createChildRecord(props, relationData, optsCopy))
} else if (def.isRequiresParentId()) {
const parent = def.getLocalField(context.parentRelationMap)

if (parent) {
def.setLocalField(props, parent)
}
}
})

return utils.Promise.all(tasks)
.then(() => props)
},

/**
* Fired during {@link Mapper#createMany}. See
* {@link Mapper~beforeCreateManyListener} for how to listen for this event.
Expand Down Expand Up @@ -1178,7 +1193,7 @@ export default Component.extend({
* @tutorial ["http://www.js-data.io/v3.0/docs/saving-data","Saving data"]
*/
createMany (records, opts) {
let op, adapter
let adapter
// Default values for arguments
records || (records = [])
opts || (opts = {})
Expand All @@ -1189,11 +1204,8 @@ export default Component.extend({
adapter = opts.adapter = this.getAdapterName(opts)

// beforeCreateMany lifecycle hook
op = opts.op = 'beforeCreateMany'
return utils.resolve(this[op](records, opts)).then((_records) => {
// Allow for re-assignment from lifecycle hook
records = _records === undefined ? records : _records

opts.op = 'beforeCreateMany'
return this._runHook(opts.op, records, opts).then((records) => {
// Deep pre-create belongsTo relations
const belongsToRelationData = {}
opts.with || (opts.with = [])
Expand All @@ -1216,10 +1228,10 @@ export default Component.extend({
})
return utils.Promise.all(tasks).then(() => {
// Now delegate to the adapter
op = opts.op = 'createMany'
opts.op = 'createMany'
const json = records.map((record) => this.toJSON(record, { with: opts.pass || [] }))
this.dbg(op, records, opts)
return utils.resolve(this.getAdapter(adapter)[op](this, json, opts))
this.dbg(opts.op, records, opts)
return this.getAdapter(adapter)[opts.op](this, json, opts)
}).then((result) => {
const createdRecordsData = opts.raw ? result.data : result

Expand All @@ -1228,7 +1240,7 @@ export default Component.extend({
utils.forEachRelation(this, opts, (def, optsCopy) => {
const relationData = records
.map((record) => def.getLocalField(record))
.filter((relatedRecord) => relatedRecord)
.filter(Boolean)
if (relationData.length !== records.length) {
return
}
Expand Down Expand Up @@ -1277,11 +1289,8 @@ export default Component.extend({
}).then((result) => {
result = this._end(result, opts)
// afterCreateMany lifecycle hook
op = opts.op = 'afterCreateMany'
return utils.resolve(this[op](records, opts, result)).then((_result) => {
// Allow for re-assignment from lifecycle hook
return _result === undefined ? result : _result
})
opts.op = 'afterCreateMany'
return this._runHook(opts.op, records, opts, result)
})
},

Expand Down Expand Up @@ -1368,23 +1377,15 @@ export default Component.extend({
if (!utils.isObject(props)) {
throw utils.err(`${DOMAIN}#createRecord`, 'props')(400, 'array or object', props)
}
const RecordCtor = this.recordClass
const relationList = this.relationList || []
relationList.forEach((def) => {
const relatedMapper = def.getRelation()
const relationData = def.getLocalField(props)
if (relationData && !relatedMapper.is(relationData)) {
if (utils.isArray(relationData) && (!relationData.length || relatedMapper.is(relationData[0]))) {
return
}
utils.set(props, def.localField, relatedMapper.createRecord(relationData, opts))
}
})
// Check to make sure "props" is not already an instance of this Mapper.
if (RecordCtor && (!(props instanceof RecordCtor))) {
return new RecordCtor(props, opts)

if (this.relationList) {
this.relationList.forEach(function (def) {
def.ensureLinkedDataHasProperType(props, opts)
})
}
return props
const RecordCtor = this.recordClass

return (!RecordCtor || props instanceof RecordCtor) ? props : new RecordCtor(props, opts)
},

/**
Expand Down Expand Up @@ -1958,6 +1959,17 @@ export default Component.extend({
}
},

_runHook (hookName, ...hookArgs) {
const defaultValueIndex = hookName.indexOf('after') === 0 ? hookArgs.length - 1 : 0

return utils.resolve(this[hookName](...hookArgs))
.then((overridenResult) => overridenResult === undefined ? hookArgs[defaultValueIndex] : overridenResult)
},

_invokeAdapterMethod (method, object, opts) {
return this.getAdapter(opts.adapter)[method](this, object, opts)
},

/**
* Select records according to the `query` argument, and aggregate the sum
* value of the property specified by `field`.
Expand Down Expand Up @@ -2384,11 +2396,8 @@ export default Component.extend({
const _opts = utils.pick(opts, ['existingOnly'])
if (utils.isArray(record)) {
const errors = record.map((_record) => schema.validate(_record, utils.pick(_opts, ['existingOnly'])))
const foundErrors = errors.filter((err) => err)
if (foundErrors.length) {
return errors
}
return undefined

return errors.some(Boolean) ? errors : undefined
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Babel transpile uses of Array.prototype.some into ES5-compatible code? If not then we can't use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's pity, the same story as in tests doesn't work here =) So, probably I need to change. But again this is only about IE8, does it make sense to care about it? https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/some
http://caniuse.com/usage-table

Let me know if you want to revert this back

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to change as JSData v3 declares than it requires no higher than ES5 support in order to work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for being a pain, but every/some methods are part of ES5.

http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.17

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But anyway this is up to you. If you say don't use some/every because of IE8 then I will change this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you are correct, I don't know why I was thinking every and some were introduced in the ES2015 spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, I've just found out that Array.isArray is also a part of ES5 spec :) http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.3.2

}
return schema.validate(record, _opts)
},
Expand Down
33 changes: 33 additions & 0 deletions src/Relation.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,38 @@ utils.addHiddenPropsToTarget(Relation.prototype, {
return this.relatedCollection.filter({
[this.foreignKey]: id
})
},

ensureLinkedDataHasProperType (props, opts) {
const relatedMapper = this.getRelation()
const relationData = this.getLocalField(props)

if (utils.isArray(relationData) && (!relationData.length || relatedMapper.is(relationData[0]))) {
return
}

if (relationData && !relatedMapper.is(relationData)) {
utils.set(props, this.localField, relatedMapper.createRecord(relationData, opts))
}
},

isRequiresParentId () {
return false
},

isRequiresChildId () {
return false
},

createChildRecord (props, relationData, opts) {
this.setForeignKey(props, relationData)

return this.createLinkedRecord(relationData, opts).then((result) => {
this.setLocalField(props, result)
})
},

createLinkedRecord (props, opts) {
return this.getRelation().create(props, opts)
}
})
Loading
0