Browse Source

Initial commit

master
Sven Slootweg 7 years ago
parent
commit
1faf24a0ca
  1. 3
      .gitignore
  2. 1
      index.coffee
  3. 97
      lib/index.coffee
  4. 188
      lib/model.coffee
  5. 66
      lib/record.coffee
  6. 30
      package.json
  7. 38
      test.coffee

3
.gitignore

@ -2,4 +2,5 @@
# https://help.github.com/articles/ignoring-files
# Example .gitignore files: https://github.com/github/gitignore
/bower_components/
/node_modules/
/node_modules/
/pe

1
index.coffee

@ -0,0 +1 @@
module.exports = require "./lib"

97
lib/index.coffee

@ -0,0 +1,97 @@
Model = require "./model"
knex = require "knex"
class YAORM
constructor: (options) ->
@_models = {}
if options.knex?
@knex = options.knex
else if options.hostname?
@knex = knex({
client: switch options.driver
# Some aliases...
when "postgres", "postgresql" then "pg"
when "mysql" then "mysql2"
else options.driver
connection:
host: options.hostname
user: options.username
password: options.password
database: options.database
charset: options.charset ? "utf8"
debug: options.debug ? false
})
else if options.knexfile?
knexfile = require "knexfile"
if knexfile.connection?
# Basic configuration
@knex = knex(knexfile)
else
# Environment configuration
if not options.environment?
# FIXME: Error
null
@knex = knex(knexfile[options.environment])
else
# FIXME: Error
_registerModel: (model) ->
model._YAORM = this
@_models[model.name] = model
return this
_createRelation: (type, options) ->
options.type = type
return options
loadModel: (modelPath) ->
# We use the existing model as a prototype, so that we don't run into conflicts if two different YAORM instances were to use the same loaded model.
baseModel = require(modelPath)
model = Object.create(baseModel)
@_registerModel(model)
defineModel: (modelName, options) ->
model = new Model(modelName, options)
@_registerModel(model)
model: (modelName) ->
return @_models[modelName]
express: ->
return (req, res, next) ->
null
next()
hasOne: (modelName, options = {}) ->
if options.foreignKey?
options.remoteKey = options.foreignKey
delete options.foreignKey
options.modelName = modelName
@_createRelation "hasOne", options
hasMany: (modelName, options = {}) ->
if options.foreignKey?
options.remoteKey = options.foreignKey
delete options.foreignKey
options.modelName = modelName
@_createRelation "hasMany", options
belongsTo: (modelName, options = {}) ->
if options.foreignKey?
options.localKey = options.foreignKey
delete options.foreignKey
options.modelName = modelName
@_createRelation "belongsTo", options
exportMethod = (options) ->
return new YAORM(options)
exportMethod.defineModel = (modelName, options) ->
return new Model(modelName, options)
module.exports = exportMethod

188
lib/model.coffee

@ -0,0 +1,188 @@
Promise = require "bluebird"
util = require "util"
Record = require "./record"
module.exports = class Model
constructor: (@name, @options) ->
# FIXME: Validation! tableName
@_isInstance = false
@options.idAttribute ?= "id"
_getInstance: ->
if @_isInstance
return this
else
instance = Object.create(this)
instance._isInstance = true
instance._queryBuilder = @_YAORM.knex(@options.tableName)
return instance
_fromQuery: (qbFunc) ->
instance = @_getInstance()
qbFunc(instance._queryBuilder)
return instance
_where: (whereStatements) ->
@_fromQuery (queryBuilder) ->
queryBuilder.where whereStatements
_all: ->
# We don't need to add anything to the query here, since we want all the records.
@_getInstance()
_createResultHandler: (options = {}) ->
return (rows) =>
self = this
Promise.map rows, (row) =>
# FIXME: Can this be done with a JOIN, perhaps?
record = self._createRecord()
record.isNew = false
record._setData(row)
if not options.relations?
options.relations = []
else if not util.isArray(options.relations)
options.relations = [options.relations]
record._loadRelations(options.relations ? [], row)
.then (rows) ->
if (options.single ? false)
if rows.length > 0
return rows[0]
else
# FIXME: Error
else
if rows.length > 0 or (options.required ? true) == false
return rows
else
# FIXME: Error
_createResultHandlerSingle: (options = {}) ->
options.single = true
@_createResultHandler(options)
_createResultHandlerCount: (options = {}) ->
return (rows) =>
return rows[0].CNT
_createRecord: ->
record = new Record()
record._setModel(this)
return record
_populateRecord: (record, data) ->
record._setData(data)
_getRelations: (relations, data) ->
Promise.try =>
relationKeys = Object.keys(relations)
Promise.map relationKeys, (attribute) =>
# FIXME: Shallow-clone these options? Immutability etc.
options = relations[attribute]
switch options.type
when "hasOne" then @_getHasOne(options.modelName, options, data)
when "hasMany" then @_getHasMany(options.modelName, options, data)
when "belongsTo" then @_getBelongsTo(options.modelName, options, data)
.reduce ((obj, remoteRecord, i) ->
obj[relationKeys[i]] = remoteRecord
return obj
), {}
_getSimpleRelation: (modelName, options, data) ->
Promise.try =>
remoteModel = @_YAORM.model(modelName)
switch options.type
when "hasOne", "hasMany" then options.localKey ?= remoteModel.options.idAttribute
when "belongsTo" then options.remoteKey ?= remoteModel.options.idAttribute
whereStatements = {}
whereStatements[options.remoteKey] = data[options.localKey]
options.query ?= (->)
queryBuilder = remoteModel
._where whereStatements
._fromQuery(options.query)
.query()
if (options.single ? false)
queryBuilder
.limit 1
.then remoteModel._createResultHandlerSingle(options)
else
queryBuilder
.then remoteModel._createResultHandler(options)
# The logic regarding which is the localKey and which is the remoteKey, is handled in the YAORM instance.
_getHasOne: (modelName, options, data) ->
options.single = true
@_getSimpleRelation(modelName, options, data)
_getHasMany: (modelName, options, data) ->
@_getSimpleRelation(modelName, options, data)
_getBelongsTo: (modelName, options, data) ->
options.single = true
@_getSimpleRelation(modelName, options, data)
create: (data) ->
record = @_createRecord()
record.isNew = true
return record
query: ->
return @_queryBuilder
find: (id, options = {}) ->
whereStatements = {}
whereStatements[@options.idAttribute] = id
@getOneWhere(whereStatements, options)
getOneWhere: (whereStatements, options = {}) ->
@_where(whereStatements)
.query()
.limit(1)
.then(@_createResultHandlerSingle(options))
getAllWhere: (whereStatements, options = {}) ->
@_where(whereStatements)
.query()
.then(@_createResultHandler(options))
countWhere: (whereStatements, options = {}) ->
@_where(whereStatements)
.query()
.count("#{@options.idAttribute} as CNT")
.then(@_createResultHandlerCount(options))
getOneFromQuery: (qbFunc, options = {}) ->
@_fromQuery(qbFunc)
.query()
.limit(1)
.then(@_createResultHandlerSingle(options))
getAllFromQuery: (qbFunc, options = {}) ->
@_fromQuery(qbFunc)
.query()
.then(@_createResultHandler(options))
countFromQuery: (qbFunc, options = {}) ->
@_fromQuery(qbFunc)
.query()
.count("#{@options.idAttribute} as CNT")
.then(@_createResultHandlerCount(options))
getAll: (options = {}) ->
@_all()
.query()
.then(@_createResultHandler(options))
countAll: (options = {}) ->
@_all()
.query()
.count("#{@options.idAttribute} as CNT")
.then(@_createResultHandlerCount(options))

66
lib/record.coffee

@ -0,0 +1,66 @@
Promise = require "bluebird"
_shallowClone = (obj) ->
newObject = {}
for key, value of obj
newObject[key] = value
return newObject
module.exports = class Record
constructor: ->
@_data = {}
@_savedData = {}
@_changedData = {}
_setModel: (model) ->
self = this
@_model = model
if model.options.columns?
model.options.columns.forEach (column) =>
Object.defineProperty this, column,
get: -> self.get column
set: (value) -> self.set column, value
_setData: (data) ->
# We might need a deep clone here?
@_data = _shallowClone(data)
@_savedData = _shallowClone(data)
_loadRelations: (relations, data) ->
Promise.map relations, (relation) =>
{key: relation, value: @_model.options.relations[relation]}
.reduce ((obj, relationData) =>
obj[relationData.key] = relationData.value
return obj
), {}
.then (relations) =>
@_model._getRelations(relations, data)
.then (relations) =>
for attribute, record of relations
this[attribute] = record
return this
_saveAttributes: (attributes) ->
null # do stuff
# Upon success...
@_savedData = @_data
@_changedData = {}
get: (attribute) ->
return @_data[attribute]
set: (attribute, value) ->
@_data[attribute] = value
@_changedData[attribute] = value
save: ->
# This only saves the changed attributes - it is almost always what you want.
@_saveAttributes(@_changedData)
saveAll: ->
# This saves *all* the attributes as they are currently set in the object - even if something else has changed them in the database in the meantime. You probably don't need this.
@_saveAttributes(@_data)

30
package.json

@ -0,0 +1,30 @@
{
"name": "yaorm",
"version": "1.0.0",
"description": "Yet Another ORM based on Knex.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:projects/joepie91/node-yaorm"
},
"keywords": [
"orm",
"knex",
"database",
"mysql",
"postgresql",
"sqlite"
],
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"bluebird": "^2.9.22",
"knex": "^0.7.6"
},
"devDependencies": {
"pretty-error": "^1.1.1"
}
}

38
test.coffee

@ -0,0 +1,38 @@
require("pretty-error").start().skipPackage("bluebird", "coffee-script").skipNodeFiles()
Promise = require "bluebird"
#Promise.longStackTraces()
yaorm = require("./")({
driver: "pg"
hostname: "localhost"
username: "postgres"
database: "team"
})
yaorm.defineModel "User",
tableName: "users"
columns: ["id", "username", "display_name", "email_address", "hash", "external_identifier", "external_authentication", "created_at", "updated_at", "activated", "activation_key"]
relations:
"projects": yaorm.hasMany("Project", foreignKey: "owner_id")
yaorm.defineModel "Project",
tableName: "projects"
columns: ["id", "owner_id", "public", "name", "slug", "description", "created_at", "updated_at", "default_permissions"]
relations:
"owner": yaorm.belongsTo("User", foreignKey: "owner_id")
Promise.try ->
yaorm.model("Project").getAll(relations: "owner")
.map (project) -> [project.name, project.owner.display_name]
.then (projects) ->
console.log projects
.catch (err) ->
console.log err.stack
### TODO:
* Nested relations
* More complex relations (many-to-many?)
* Save (UPDATE + INSERT)
* Deep save (incl. nested relations)
###
Loading…
Cancel
Save