Comparing version 0.5.0-alpha.2 to 0.5.0-alpha.3
@@ -24,2 +24,3 @@ 'use strict'; | ||
// https://sequelize.org/master/class/lib/model.js~Model.html | ||
// https://sequelize.org/master/manual/model-querying-finders.html | ||
module.exports = Bone => { | ||
@@ -77,9 +78,7 @@ return class Spine extends Bone { | ||
// static async count() { | ||
// return await super.count(); | ||
// } | ||
// EXISTS | ||
// static async count() {} | ||
// static async create(props) { | ||
// return await super.create(props); | ||
// } | ||
// EXISTS | ||
// static async create(props) {} | ||
@@ -99,5 +98,4 @@ static decrement() { | ||
// static drop() { | ||
// throw new Error('unimplemented'); | ||
// } | ||
// EXISTS | ||
// static drop() {} | ||
@@ -121,4 +119,15 @@ static async findAll(options = {}) { | ||
static findCreateFind() { | ||
throw new Error('unimplemented'); | ||
static async findCreateFind(options = {}) { | ||
const { where, defaults } = options; | ||
let instance = await this.findOne({ where }); | ||
if (!instance) { | ||
try { | ||
instance = await this.create({ ...defaults, ...where }); | ||
} catch (err) { | ||
instance = await this.findOne({ where }); | ||
} | ||
} | ||
return instance; | ||
} | ||
@@ -148,9 +157,7 @@ | ||
// static hasMany() { | ||
// throw new Error('unimplemented'); | ||
// } | ||
// BREAKING | ||
// static hasMany() {} | ||
// static hasOne() { | ||
// throw new Error('unimplemented'); | ||
// } | ||
// BREAKING | ||
// static hasOne() {} | ||
@@ -161,9 +168,7 @@ static increment() { | ||
// static async max() { | ||
// return await super.max(); | ||
// } | ||
// EXISTS | ||
// static async max() {} | ||
// static async min() { | ||
// return await super.min(); | ||
// } | ||
// EXISTS | ||
// static async min() {} | ||
@@ -178,10 +183,10 @@ static removeAttribute(name) { | ||
static restore() { | ||
static async restore(options = {}) { | ||
await super.update(options.where || {}, { deltedAt: null }); | ||
} | ||
static schema() { | ||
throw new Error('unimplemented'); | ||
} | ||
// static schema() { | ||
// throw new Error('unimplemented'); | ||
// } | ||
static scope() { | ||
@@ -191,9 +196,7 @@ throw new Error('unimplemented'); | ||
// static async sum() { | ||
// return await super.sum(); | ||
// } | ||
// EXISTS | ||
// static async sum() {} | ||
// static async sync() { | ||
// return await super.sync(); | ||
// } | ||
// EXISTS | ||
// static async sync() {} | ||
@@ -225,4 +228,10 @@ static truncate() { | ||
changed() { | ||
throw new Error('unimplemented'); | ||
changed(key) { | ||
if (key != null) return this.attributeChanged(key); | ||
const result = []; | ||
for (const key of Object.keys(this.constructor.attributes)) { | ||
if (this.attributeChanged(key)) result.push(key); | ||
} | ||
return result.length > 0 ? result : false; | ||
} | ||
@@ -247,7 +256,7 @@ | ||
get(key) { | ||
return this.attribute(key); | ||
return this[key]; | ||
} | ||
getDataValue(key) { | ||
return this.raw[key]; | ||
return this.attribute(key); | ||
} | ||
@@ -264,29 +273,33 @@ | ||
previous() { | ||
throw new Error('unimplemented'); | ||
} | ||
previous(key) { | ||
if (key != null) return this.attributeWas(key); | ||
reload() { | ||
throw new Error('unimplemented'); | ||
const result = {}; | ||
for (const key of Object.keys(this.attributes)) { | ||
result[key] = this.attributeWas(key); | ||
} | ||
return result; | ||
} | ||
save() { | ||
throw new Error('unimplemented'); | ||
} | ||
// EXISTS | ||
// async reload() {} | ||
restore() {} | ||
// EXISTS | ||
// async save() {} | ||
set(key, value) { | ||
this.attribute(key, value); | ||
this[key] = value; | ||
} | ||
setDataValue(key, value) { | ||
this.raw[key] = value; | ||
this.attribute(key, value); | ||
} | ||
toJSON() { | ||
return super.toJSON(); | ||
} | ||
// EXISTS | ||
// toJSON() {} | ||
async update() { | ||
return await super.update(); | ||
} | ||
// EXISTS | ||
// async update() {} | ||
@@ -298,5 +311,6 @@ validate() { | ||
where() { | ||
throw new Error('unimplemented'); | ||
const { primaryKey } = this.constructor; | ||
return { [primaryKey]: this[primaryKey] }; | ||
} | ||
}; | ||
}; |
161
lib/bone.js
@@ -93,2 +93,26 @@ 'use strict'; | ||
/** | ||
* Find the corresponding JavaScript type of the type in database. | ||
* @param {string} dataType | ||
*/ | ||
function findJsType(dataType) { | ||
switch (dataType.toLowerCase().split('(')[0]) { | ||
case 'bigint': | ||
case 'smallint': | ||
case 'tinyint': | ||
case 'int': | ||
case 'integer': | ||
case 'decimal': | ||
return Number; | ||
case 'datetime': | ||
return Date; | ||
case 'longtext': | ||
case 'mediumtext': | ||
case 'text': | ||
case 'varchar': | ||
default: | ||
return String; | ||
} | ||
} | ||
/** | ||
* The base class that provides Object-relational mapping. This class is never intended to be used directly. We need to create models that extends from Bone. Most of the query features of Bone is implemented by {@link Spell} such as {@link Spell#$group} and {@link Spell#$join}. With Bone, you can create models like this: | ||
@@ -112,3 +136,3 @@ * | ||
* @property {Object} raw | ||
* @property {Object} rawOriginal | ||
* @property {Object} rawInitial | ||
* @property {Set} rawUnset | ||
@@ -128,3 +152,3 @@ * @example | ||
raw: {}, | ||
rawOriginal: {}, | ||
rawInitial: {}, | ||
rawUnset: new Set(), | ||
@@ -161,8 +185,11 @@ })); | ||
*/ | ||
attribute(name, value) { | ||
attribute(...args) { | ||
const [ name, value ] = args; | ||
const { attributes } = this.constructor; | ||
if (!(name in attributes)) { | ||
if (!attributes.hasOwnProperty(name)) { | ||
throw new Error(`${this.constructor.name} has no attribute "${name}"`); | ||
} | ||
if (arguments.length > 1) { | ||
if (args.length > 1) { | ||
this.raw[name] = value; | ||
@@ -189,3 +216,3 @@ this.rawUnset.delete(name); | ||
if (this.rawUnset.has(name)) throw new Error(`Unset attribute "${name}"`); | ||
const value = this.rawOriginal[name]; | ||
const value = this.rawInitial[name]; | ||
return value == null ? null : value; | ||
@@ -297,3 +324,3 @@ } | ||
/** | ||
* Sync changes made in {@link Bone.raw} back to {@link Bone.rawOriginal}. Mostly used after the changes are persisted to database, to make {@link Bone.attributeChanged} function properly. | ||
* Sync changes made in {@link Bone.raw} back to {@link Bone.rawInitial}. Mostly used after the changes are persisted to database, to make {@link Bone.attributeChanged} function properly. | ||
* @private | ||
@@ -305,6 +332,6 @@ */ | ||
for (const name in attributes) { | ||
const { type } = attributes[name]; | ||
const { jsType } = attributes[name]; | ||
// Take advantage of uncast/cast to create new copy of value | ||
const value = this.constructor.uncast(this.raw[name], type); | ||
this.rawOriginal[name] = this.constructor.cast(value, type); | ||
const value = this.constructor.uncast(this.raw[name], jsType); | ||
this.rawInitial[name] = this.constructor.cast(value, jsType); | ||
} | ||
@@ -423,2 +450,11 @@ } | ||
async reload() { | ||
const { primaryKey } = this.constructor; | ||
const instance = await this.constructor.findOne(this[primaryKey]); | ||
if (instance) { | ||
Object.assign(this.raw, instance.raw); | ||
Object.assign(this.rawInitial, instance.rawInitial); | ||
} | ||
} | ||
/** | ||
@@ -446,2 +482,16 @@ * Delete current instance. If `deletedAt` attribute exists, calling {@link Bone#remove} does not actually delete the record from the database. Instead, it updates the value of `deletedAt` attribute to current date. This is called [soft delete](../querying#scopes). To force a regular `DELETE`, use `.remove(true)`. | ||
async restore() { | ||
const Model = this.constructor; | ||
const { primaryKey, shardingKey } = Model; | ||
if (this[primaryKey] == null) { | ||
throw new Error('The instance is not persisted yet.'); | ||
} | ||
const conditions = { [primaryKey]: this[primaryKey] }; | ||
if (shardingKey) conditions[shardingKey] = this[shardingKey]; | ||
return await this.constructor.update(conditions, { deltedAt: null }); | ||
} | ||
/** | ||
@@ -458,7 +508,8 @@ * Override attribute metadata. Currently only `type` is needed to be overriden with this method. | ||
*/ | ||
static attribute(name, meta) { | ||
static attribute(name, meta = {}) { | ||
if (!this.attributes[name]) { | ||
throw new Error(`${this.name} has no attribute called ${name}`); | ||
} | ||
Object.assign(this.attributes[name], meta); | ||
const { type: jsType } = meta; | ||
Object.assign(this.attributes[name], { jsType }); | ||
} | ||
@@ -589,39 +640,2 @@ | ||
/** | ||
* Find model by class name. Models are stored at {@link Bone.model}. | ||
* @param {string} className | ||
* @returns {Bone} | ||
*/ | ||
static reflectClass(className) { | ||
const model = this.models[className]; | ||
if (!(model && model.prototype instanceof Bone)) { | ||
throw new Error(`Unable to find model "${className}"`); | ||
} | ||
return model; | ||
} | ||
/** | ||
* Find the corresponding JavaScript type of the type in database. | ||
* @param {string} dataType | ||
*/ | ||
static reflectType(dataType) { | ||
switch (dataType.toLowerCase().split('(')[0]) { | ||
case 'bigint': | ||
case 'smallint': | ||
case 'tinyint': | ||
case 'int': | ||
case 'integer': | ||
case 'decimal': | ||
return Number; | ||
case 'datetime': | ||
return Date; | ||
case 'longtext': | ||
case 'mediumtext': | ||
case 'text': | ||
case 'varchar': | ||
default: | ||
return String; | ||
} | ||
} | ||
/** | ||
* Cast raw values from database to JavaScript types. When the raw packet is fetched from database, `Date`s and special numbers are transformed by drivers already. This method is used to cast said values to custom types set by {@link Bone.attribute}, such as `JSON`. | ||
@@ -663,3 +677,3 @@ * @private | ||
/** | ||
* Set a `hasOne` association to another model. The model is inferred by {@link Bone.reflectClass} from `opts.className` or the association `name` by default. | ||
* Set a `hasOne` association to another model. The model is inferred by `opts.className` or the association `name` by default. | ||
* @param {string} name | ||
@@ -682,3 +696,3 @@ * @param {Object} [opts] | ||
/** | ||
* Set a `hasMany` association to another model. The model is inferred by {@link Bone.reflectClass} from `opts.className` or the association `name` by default. | ||
* Set a `hasMany` association to another model. The model is inferred by `opts.className` or the association `name` by default. | ||
* @param {string} name | ||
@@ -702,3 +716,3 @@ * @param {Object} [opts] | ||
/** | ||
* Set a `belongsTo` association to another model. The model is inferred by {@link Bone.reflectClass} from `opts.className` or the association `name` by default. | ||
* Set a `belongsTo` association to another model. The model is inferred by `opts.className` or the association `name` by default. | ||
* @param {string} name | ||
@@ -730,7 +744,10 @@ * @param {Object} [opts] | ||
*/ | ||
static associate(name, opts) { | ||
static associate(name, opts = {}) { | ||
if (name in this.associations) { | ||
throw new Error(this.name + ' has duplicated association at: ' + name); | ||
} | ||
const Model = this.reflectClass(opts.className); | ||
const { className } = opts; | ||
const Model = this.models[className]; | ||
if (!Model) throw new Error(`Unable to find model "${className}"`); | ||
const { deletedAt } = this.timestamps; | ||
@@ -857,10 +874,10 @@ if (Model.attributes[deletedAt] && !opts.where) { | ||
const instance = new this(); | ||
const { raw, rawOriginal, rawUnset } = instance; | ||
const { raw, rawInitial, rawUnset } = instance; | ||
for (const name in this.attributes) { | ||
const { columnName, type } = this.attributes[name]; | ||
const { columnName, jsType } = this.attributes[name]; | ||
if (columnName in row) { | ||
// to make sure raw and rawOriginal hold two different objects | ||
raw[name] = this.cast(row[columnName], type); | ||
rawOriginal[name] = this.cast(row[columnName], type); | ||
// to make sure raw and rawInitial hold two different objects | ||
raw[name] = this.cast(row[columnName], jsType); | ||
rawInitial[name] = this.cast(row[columnName], jsType); | ||
} else { | ||
@@ -1062,7 +1079,12 @@ rawUnset.add(name); | ||
static init(attributes, opts = {}) { | ||
opts = { | ||
underscored: true, | ||
table: this.table || opts.tableName, | ||
...(this.options && this.options.define), | ||
...opts, | ||
}; | ||
const result = {}; | ||
const table = this.table || opts.table || opts.tableName || | ||
snakeCase(pluralize(this.name)); | ||
const table = opts.table || snakeCase(pluralize(this.name)); | ||
const aliasName = camelCase(pluralize(this.name || table)); | ||
const { underscored } = this.options && this.options.define || {}; | ||
const { underscored } = opts; | ||
@@ -1072,16 +1094,12 @@ for (const name in attributes) { | ||
const definition = typeof opts === 'function' ? { type: opts } : opts; | ||
const { | ||
type: dataType, | ||
allowNull, | ||
defaultValue, | ||
...others | ||
} = { | ||
const { allowNull, defaultValue, ...others } = { | ||
columnName: underscored === false ? name : snakeCase(name), | ||
...definition, | ||
}; | ||
const dataType = definition.dataType || definition.type.type; | ||
result[name] = { | ||
dataType, | ||
...others, | ||
type: this.reflectType(dataType.type || dataType), | ||
dataType, | ||
jsType: findJsType(dataType), | ||
allowNull: allowNull == null ? true : allowNull, | ||
@@ -1113,2 +1131,3 @@ defaultValue: defaultValue == null ? null : defaultValue, | ||
// a model that is not connected before | ||
if (this.synchronized == null) { | ||
@@ -1122,3 +1141,3 @@ const schemaInfo = await driver.querySchemaInfo(database, [ table ]); | ||
if (this.physicTables) { | ||
throw new Error('Unable to migrate multiple tables simutaneously'); | ||
throw new Error('Unable to sync model with custom physic tables'); | ||
} | ||
@@ -1125,0 +1144,0 @@ |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const { findDriver } = require('./drivers'); | ||
const { STRING, INTEGER, BIGINT, DATE, TEXT, BOOLEAN } = require('./data_types'); | ||
@@ -35,2 +36,27 @@ const fs = require('fs').promises; | ||
function findType(dataType) { | ||
switch (dataType) { | ||
case 'varchar': | ||
return STRING; | ||
case 'text': | ||
return TEXT; | ||
case 'datetime': | ||
case 'timestamp': | ||
return DATE; | ||
case 'decimal': | ||
case 'int': | ||
case 'integer': | ||
case 'numeric': | ||
case 'smallint': | ||
case 'tinyint': | ||
return INTEGER; | ||
case 'bigint': | ||
return BIGINT; | ||
case 'boolean': | ||
return BOOLEAN; | ||
default: | ||
throw new Error(`Unexpected data type ${dataType}`); | ||
} | ||
} | ||
function initAttributes(model, columns) { | ||
@@ -43,4 +69,4 @@ const attributes = {}; | ||
attributes[ LEGACY_TIMESTAMP_MAP[name] || name ] = { | ||
type: dataType, | ||
...columnInfo, | ||
type: findType(dataType), | ||
}; | ||
@@ -47,0 +73,0 @@ } |
@@ -8,4 +8,4 @@ 'use strict'; | ||
function formatColumnDefinition(definition) { | ||
const { dataType, allowNull, defaultValue, primaryKey } = definition; | ||
const chunks = [ dataType.toSqlString() ]; | ||
const { type, allowNull, defaultValue, primaryKey } = definition; | ||
const chunks = [ type.toSqlString() ]; | ||
@@ -36,3 +36,9 @@ if (primaryKey) chunks.push('PRIMARY KEY'); | ||
const { logger } = opts; | ||
this.logger = logger && logger.info ? logger : { info: debug }; | ||
if (logger != null && typeof logger === 'object') { | ||
this.logger = logger; | ||
} else if (typeof logger === 'function') { | ||
this.logger = { info: logger, debug }; | ||
} else { | ||
this.logger = { info: debug, debug }; | ||
} | ||
} | ||
@@ -39,0 +45,0 @@ |
@@ -90,2 +90,3 @@ 'use strict'; | ||
WHERE table_schema = ? AND table_name in (?) | ||
ORDER BY table_name, column_name | ||
`); | ||
@@ -92,0 +93,0 @@ |
@@ -5,3 +5,2 @@ 'use strict'; | ||
const SqlString = require('sqlstring'); | ||
const debug = require('debug')('leoric'); | ||
const spellbook = require('../abstract/spellbook'); | ||
@@ -11,4 +10,4 @@ const AbstractDriver = require('../abstract'); | ||
function formatColumnDefinition({ dataType, allowNull, defaultValue }) { | ||
const chunks = [ dataType.toSqlString() ]; | ||
function formatColumnDefinition({ type, allowNull, defaultValue }) { | ||
const chunks = [ type.toSqlString() ]; | ||
@@ -131,5 +130,5 @@ if (allowNull != null) { | ||
function formatAlterColumns(columnName, definition) { | ||
const { allowNull, dataType, defaultValue } = definition; | ||
const { allowNull, type, defaultValue } = definition; | ||
const sets = [ | ||
`TYPE ${dataType.toSqlString()}`, | ||
`TYPE ${type.toSqlString()}`, | ||
allowNull ? 'DROP NOT NULL' : 'SET NOT NULL', | ||
@@ -221,3 +220,6 @@ defaultValue == null ? 'DROP DEFAULT' : `SET DEFAULT ${SqlString.format(defaultValue)}`, | ||
let { data_type: dataType, character_octet_length: maxLength } = row; | ||
if (dataType === 'character varying') dataType = 'varchar'; | ||
if (dataType === 'timestamp without time zone') dataType = 'timestamp'; | ||
const columnType = maxLength > 0 ? `${dataType}(${maxLength})` : dataType; | ||
@@ -224,0 +226,0 @@ |
'use strict'; | ||
const path = require('path'); | ||
const SqlString = require('sqlstring'); | ||
@@ -10,10 +9,4 @@ const debug = require('debug')('leoric'); | ||
const { heresql, camelCase, snakeCase } = require('../../utils/string'); | ||
const sqlite = require('sqlite3'); | ||
let sqlite; | ||
try { | ||
sqlite = require(path.join(process.cwd(), 'node_modules/sqlite3')); | ||
} catch (e) { | ||
sqlite = require('sqlite3'); | ||
} | ||
// SELECT users.id AS "users:id", ... | ||
@@ -44,9 +37,6 @@ // => [ { users: { id, ... } } ] | ||
function formatColumnDefinition(definition) { | ||
const { allowNull, defaultValue, primaryKey } = definition; | ||
const { type, allowNull, defaultValue, primaryKey } = definition; | ||
// int => integer | ||
// - https://www.sqlite.org/datatype3.html | ||
const dataType = typeof definition.dataType === 'string' | ||
? definition.dataType | ||
: definition.dataType.toSqlString().replace(/^INT\b/, 'INTEGER'); | ||
const chunks = [ dataType ]; | ||
const chunks = [ type.toSqlString().replace(/^INT\b/, 'INTEGER') ]; | ||
@@ -53,0 +43,0 @@ if (primaryKey) chunks.push('PRIMARY KEY'); |
@@ -239,3 +239,3 @@ 'use strict'; | ||
if (name in Model.attributes) { | ||
sets[name] = Model.uncast(obj[name], Model.attributes[name].type); | ||
sets[name] = Model.uncast(obj[name], Model.attributes[name].jsType); | ||
} else { | ||
@@ -242,0 +242,0 @@ throw new Error(`Undefined attribute "${name}"`); |
@@ -8,4 +8,2 @@ 'use strict'; | ||
function invokable(DataType) { | ||
let dataType; | ||
return new Proxy(DataType, { | ||
@@ -24,4 +22,3 @@ // STRING(255) | ||
get(target, p) { | ||
if (!dataType) dataType = new target(); | ||
return dataType[p]; | ||
return new target()[p]; | ||
} | ||
@@ -28,0 +25,0 @@ }); |
{ | ||
"name": "leoric", | ||
"version": "0.5.0-alpha.2", | ||
"version": "0.5.0-alpha.3", | ||
"description": "JavaScript Object-relational mapping alchemy", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
159441
4577
5