Last active
November 4, 2017 20:53
-
-
Save hetsch/23b36e1ad01b46dcf2226e327667f3a6 to your computer and use it in GitHub Desktop.
M2M update with objectionjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Knex from 'knex'; | |
import objection from 'objection'; | |
import Promise from 'bluebird'; | |
import util from 'util'; | |
function initDatabase() { | |
const knex = Knex({ | |
dialect: 'sqlite3', | |
connection: { | |
filename: './test.db' // ':memory:' | |
}, | |
useNullAsDefault: true, | |
debug: false | |
}); | |
objection.Model.knex(knex); | |
return knex; | |
} | |
class BaseModel extends objection.Model { | |
static get relations() { | |
return []; | |
} | |
static all() { | |
let query = this.query(); | |
if (this.relations.length > 0) { | |
query = query.eager(this.relations); | |
} | |
return query; | |
} | |
static find(id) { | |
let query = this.query().findById(parseInt(id, 10)); | |
if (this.relations.length > 0) { | |
query = query.eager(this.relations); | |
} | |
return query; | |
} | |
static create(data, upsertGraphOptions) { | |
return objection.transaction(this.knex(), trx => { | |
return this.query(trx).insertGraph( | |
data, | |
upsertGraphOptions || { | |
relate: true, | |
unrelate: true | |
} | |
); | |
}); | |
} | |
static update(data, upsertGraphOptions) { | |
return objection.transaction(this.knex(), trx => { | |
return this.query(trx) | |
.where('id', data.id) | |
.upsertGraph( | |
data, | |
upsertGraphOptions || { | |
relate: true, | |
unrelate: true | |
} | |
); | |
}); | |
} | |
static delete(id) { | |
return objection.transaction(this.knex(), trx => { | |
return this.query(trx) | |
.where('id', parseInt(id, 10)) | |
.delete(); | |
}); | |
} | |
castBooleanFields(json) { | |
const jsonSchema = this.constructor.jsonSchema.properties; | |
// const booleanFields = Object.keys(json).filter(key => schema.hasOwnProperty(key) && schema[key].type === "boolean"); | |
Object.entries(json).forEach(([key, value]) => { | |
if ( | |
Object.prototype.hasOwnProperty.call(jsonSchema, key) && | |
jsonSchema[key].type === 'boolean' | |
) { | |
json[key] = Boolean(value); | |
} | |
}); | |
return json; | |
} | |
// SQLITE stores booleans as integers (0 or 1) | |
// Cast them to Booleans, otherwise tcomb-form | |
// throws validation errors that integers are no | |
// booleans. | |
// See: https://github.com/Vincit/objection.js/issues/204 | |
$parseDatabaseJson(json) { | |
let newJson = super.$parseDatabaseJson(json); | |
newJson = this.castBooleanFields(newJson); | |
return newJson; | |
} | |
} | |
class ProductAttribute extends BaseModel { | |
static get tableName() { | |
return 'ProductAttribute'; | |
} | |
static get jsonSchema() { | |
return { | |
type: 'object', | |
properties: { | |
id: { type: 'integer' }, | |
name: { type: 'string' }, | |
slug: { type: 'string' }, | |
dataType: { type: 'string' }, | |
/* extra field on the m2m table */ | |
isOptional: { type: 'boolean' } | |
}, | |
required: ['name', 'slug'] | |
}; | |
} | |
} | |
class ProductClass extends BaseModel { | |
static get relations() { | |
return '[productAttributes, variantAttributes]'; | |
} | |
static get tableName() { | |
return 'ProductClass'; | |
} | |
static get jsonSchema() { | |
return { | |
type: 'object', | |
properties: { | |
id: { type: 'integer' }, | |
name: { type: 'string' }, | |
slug: { type: 'string' }, | |
hasVariants: { type: 'boolean' }, | |
productAttributes: { | |
type: 'array', | |
items: ProductAttribute.jsonSchema | |
}, | |
variantAttributes: { | |
type: 'array', | |
items: ProductAttribute.jsonSchema | |
} | |
}, | |
required: ['name'] | |
}; | |
} | |
static get relationMappings() { | |
return { | |
productAttributes: { | |
relation: objection.Model.ManyToManyRelation, | |
modelClass: ProductAttribute, | |
join: { | |
from: 'ProductClass.id', | |
through: { | |
from: 'ProductClass_ProductAttribute.productClassId', | |
to: 'ProductClass_ProductAttribute.productAttributeId', | |
extra: ['isOptional', 'order'] | |
}, | |
to: 'ProductAttribute.id' | |
} | |
}, | |
variantAttributes: { | |
relation: objection.Model.ManyToManyRelation, | |
modelClass: ProductAttribute, | |
join: { | |
from: 'ProductClass.id', | |
through: { | |
from: 'ProductClass_VariantAttribute.productClassId', | |
to: 'ProductClass_VariantAttribute.productAttributeId', | |
extra: ['isOptional', 'order'] | |
}, | |
to: 'ProductAttribute.id' | |
} | |
} | |
}; | |
} | |
} | |
function* schema(knex) { | |
yield knex.schema.dropTableIfExists('ProductAttribute'); | |
yield knex.schema.dropTableIfExists('ProductClass'); | |
yield knex.schema.dropTableIfExists('ProductClass_ProductAttribute'); | |
yield knex.schema.dropTableIfExists('ProductClass_VariantAttribute'); | |
yield knex.schema.createTable('ProductAttribute', table => { | |
table.bigincrements('id').primary(); | |
table.string('name'); | |
table.string('slug'); | |
table.boolean('isOptional'); | |
table.string('dataType'); | |
}); | |
yield knex.schema.createTable('ProductClass', table => { | |
table.bigincrements('id').primary(); | |
table.string('name'); | |
table.string('slug'); | |
table.boolean('hasVariants'); | |
}); | |
yield knex.schema.createTable('ProductClass_ProductAttribute', table => { | |
table.increments('id').primary(); | |
table | |
.biginteger('productClassId') | |
.unsigned() | |
.references('id') | |
.inTable('ProductClass'); | |
// .onDelete("CASCADE"); | |
// .index(); | |
table | |
.integer('productAttributeId') | |
.unsigned() | |
.references('id') | |
.inTable('ProductAttribute'); | |
// .onDelete("CASCADE"); | |
// .index(); | |
table.boolean('isOptional'); | |
table.boolean('order'); | |
}); | |
yield knex.schema.createTable('ProductClass_VariantAttribute', table => { | |
table.increments('id').primary(); | |
table | |
.biginteger('productClassId') | |
.unsigned() | |
.references('id') | |
.inTable('ProductClass'); | |
// .onDelete('CASCADE'); | |
// .index(); | |
table | |
.integer('productAttributeId') | |
.unsigned() | |
.references('id') | |
.inTable('ProductAttribute'); | |
// .onDelete('CASCADE'); | |
// .index(); | |
table.boolean('isOptional'); | |
table.boolean('order'); | |
}); | |
} | |
function* fixtures(knex) { | |
yield knex | |
.insert({ | |
name: 'attribute_name_1', | |
slug: 'attribute_slug_1', | |
dataType: 'string' | |
}) | |
.into('ProductAttribute'); | |
yield knex | |
.insert({ | |
name: 'attribute_name_2', | |
slug: 'attribute_slug_2', | |
dataType: 'number' | |
}) | |
.into('ProductAttribute'); | |
yield knex | |
.insert({ | |
name: 'productclass_name_1', | |
slug: 'productclass_slug_1', | |
hasVariants: true | |
}) | |
.into('ProductClass'); | |
yield knex | |
.insert({ | |
productClassId: 1, | |
productAttributeId: 1, | |
isOptional: true, | |
order: 1 | |
}) | |
.into('ProductClass_ProductAttribute'); | |
yield knex | |
.insert({ | |
productClassId: 1, | |
productAttributeId: 2, | |
isOptional: false, | |
order: 2 | |
}) | |
.into('ProductClass_VariantAttribute'); | |
} | |
(async () => { | |
const knex = initDatabase(); | |
// DB setup | |
await Promise.each( | |
[Promise.coroutine(schema), Promise.coroutine(fixtures)], | |
func => func(knex) | |
); | |
const dumpData = Promise.coroutine(function* dump() { | |
const line = '\n\n*****************\n%s:\n\n%s\n*****************\n\n'; | |
console.info( | |
line, | |
'ProductClasses', | |
util.inspect(yield ProductClass.all()) | |
); | |
console.info( | |
line, | |
'ProductAttributes', | |
util.inspect(yield ProductAttribute.all()) | |
); | |
console.info( | |
line, | |
'ProductClass_ProductAttribute', | |
util.inspect(yield knex.select().from('ProductClass_ProductAttribute')) | |
); | |
console.info( | |
line, | |
'ProductClass_VariantAttribute', | |
util.inspect(yield knex.select().from('ProductClass_VariantAttribute')) | |
); | |
// yield Promise.resolve(); | |
}); | |
console.log('\nINITIAL DATABASE STATE\n'); | |
await dumpData(); | |
const productClass = await ProductClass.update({ | |
id: 1, | |
name: 'productclass_name_1', | |
slug: 'productclass_slug_1', | |
hasVariants: true, | |
productAttributes: [ | |
// See: https://github.com/Vincit/objection.js/issues/480 | |
/* OLD RELATION: This is the ProductAttribute that was created in the fixtures */ | |
{ | |
isOptional: true, | |
id: 1 // this is the pk of the related ProductAttribute? | |
}, | |
/* NEW RELATION: This should relate the current ProductClass with an EXISTING ProductAttribute | |
with ProductAttribute::id = 2 */ | |
{ | |
isOptional: false, | |
id: 2 // this is the pk of the related ProductAttribute? | |
}, | |
/* NEW RELATION: Should create a NEW ProductAtribute and realte it to the current ProductClass */ | |
{ | |
name: 'attribute_name_3', | |
slug: 'slug_name_3', | |
dataType: 'float', | |
isOptional: true | |
} | |
], | |
variantAttributes: [ | |
/* OLD RELATION: This is the ProductAttribute that was created in the fixtures */ | |
{ | |
isOptional: false, | |
id: 2 // this is the pk of the related ProductAttribute? | |
} | |
] | |
}); | |
console.log('\nUPDATED PRODUCT CLASS\n'); | |
// const productClass = await ProductClass.find(1); | |
console.log('\n\n%s\n\n', util.inspect(productClass)); | |
console.log(productClass.productAttributes[0]); | |
console.log('\nAFTER UPDATE\n'); | |
await dumpData(); | |
})(); | |
// Run it with: node --experimental-modules test.mjs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment