diff --git a/examples/photo-album/memserver/models/photo.js b/examples/photo-album/memserver/models/photo.js index 3027ed6..ea04727 100644 --- a/examples/photo-album/memserver/models/photo.js +++ b/examples/photo-album/memserver/models/photo.js @@ -1,5 +1,5 @@ import Model from '../../../../lib/mem-server/model'; -export default Object.assign(Model, { - +export default Model({ + }); diff --git a/lib/mem-server.js b/lib/mem-server.js index 688d939..9fd07fb 100644 --- a/lib/mem-server.js +++ b/lib/mem-server.js @@ -14,9 +14,8 @@ if (!fs.existsSync('memserver')) { throw new Error(chalk.red('/memserver/server.js doesn\'t exist for this directory!')); } -console.log(process.cwd()); const Server = require(`${process.cwd()}/memserver/server`).default; // NOTE: make this ES6 import -const modelFileNames = fs.readdirSync('memserver/models'); +const modelFileNames = fs.readdirSync(`${process.cwd()}/memserver/models`); const targetNamespace = ENVIRONMENT_IS_NODE ? global : window; targetNamespace.MemServer = { @@ -67,6 +66,7 @@ function colorStatusCode(statusCode) { function registerModels(modelFileNames) { return modelFileNames.reduce((result, modelFileName) => { const ModelName = stringUtils.classify(modelFileName.slice(0, -3)); + result[ModelName] = require(`${process.cwd()}/memserver/models/${modelFileName}`).default; // NOTE: make this ES6 import result[ModelName].modelName = ModelName; @@ -79,15 +79,65 @@ function resetDatabase(models) { const fileName = stringUtils.dasherize(inflect.pluralize(modelName)); const path = `${process.cwd()}/memserver/fixtures/${fileName}.js`; - if (fs.existsSync(path)) { - result[modelName] = require(path).default; // NOTE: make this ES6 import - // TODO: maybe check ids must exist and all of them are integers - } else { + if (!fs.existsSync(path)) { result[modelName] = [] + + return result; + } + + const fixtureModels = require(path).default; // NOTE: make this ES6 import + + if (fixtureModels.length === 0) { + result[modelName] = [] + + return result; } + const modelPrimaryKey = fixtureModels.reduce((existingPrimaryKey, model) => { + const primaryKey = getModelPrimaryKey(model, existingPrimaryKey, modelName); + + if (!primaryKey) { + throw new Error(chalk.red(`MemServer DATABASE ERROR: At least one of your ${modelName} fixtures missing a primary key. Please make sure all your ${modelName} fixtures have either id or uuid primaryKey`)); + } + + return primaryKey; + }, null); + + targetNamespace.MemServer.Models[modelName].primaryKey = modelPrimaryKey; + result[modelName] = fixtureModels; + return result; }, {}); } + +function getModelPrimaryKey(model, existingPrimaryKeyType, modelName) { + if (!existingPrimaryKeyType) { + const primaryKey = model.id || model.uuid; + + if (!primaryKey) { + return; + } + + existingPrimaryKeyType = model.id ? 'id' : 'uuid'; + + return primaryKeyTypeCheck(existingPrimaryKeyType, primaryKey, modelName); + } + + return primaryKeyTypeCheck(existingPrimaryKeyType, model[existingPrimaryKeyType], modelName); +} + +// NOTE: move this to utils +function primaryKeyTypeCheck(targetPrimaryKeyType, primaryKey, modelName) { + const primaryKeyType = typeof primaryKey; + + if (targetPrimaryKeyType === 'id' && (primaryKeyType !== 'number')) { + throw new Error(chalk.red(`MemServer ${modelName} model primaryKeyType is 'id'. Instead you've tried to enter ${primaryKey} with ${primaryKeyType}`)); + } else if (targetPrimaryKeyType === 'uuid' && (primaryKeyType !== 'string')) { + throw new Error(chalk.red(`MemServer ${modelName} model primaryKeyType is 'uuid'. Instead you've tried to enter ${primaryKey} with ${primaryKeyType}`)); + } + + return targetPrimaryKeyType; +} + // TODO: BUILD A CLI diff --git a/lib/mem-server/model.js b/lib/mem-server/model.js index 9b8bf9d..e389ef2 100644 --- a/lib/mem-server/model.js +++ b/lib/mem-server/model.js @@ -1,86 +1,89 @@ const ENVIRONMENT_IS_NODE = typeof global === 'object'; const targetNamespace = ENVIRONMENT_IS_NODE ? global : window; -export default { - modelName: '', - attributes: [], - find(id) { - const models = targetNamespace.MemServer.DB[this.modelName] || []; - - return models.find((model) => model.id === id); - }, - findAll(options={}) { - const keys = Object.keys(options); - const models = targetNamespace.MemServer.DB[this.modelName] || []; - - if (keys.length === 0) { - return models; - } - - return models.filter((model) => comparison(model, options, keys, 0)); - }, - findBy(options) { - const keys = Object.keys(options); - const models = targetNamespace.MemServer.DB[this.modelName] || []; - console.log('models are:', models); - - return models.find((model) => comparison(model, options, keys, 0)); - }, - insert(options) { // NOTE: what if there is same id? - const models = targetNamespace.MemServer.DB[this.modelName] || []; - - // TODO: auto-increment ids - const defaultAttributes = this.attributes.reduce((result, attribute) => { - // TODO: enable functions - result[attribute] = this[attribute]; - }, {}); - - const targetAttributes = Object.assign(defaultAttributes, options); - - models.push(targetAttributes); - }, - bulkInsert(count, options) { - return Array.from({ length: count }).map(() => this.insert(options)); - }, - update(record) { - const targetRecord = record.id ? this.find(record.id) : this.findBy({ uuid: record.uuid }); - - if (!targetRecord) { - throw new Error('[MemServer] $Model.update(record) requires id or uuid primary key to update a record'); - } - - const targetIndex = models.indexOf(targetRecord); +export default function(options) { + return Object.assign({}, { + modelName: '', + primaryKey: '', + attributes: [], + find(id) { + const models = targetNamespace.MemServer.DB[this.modelName] || []; + + return models.find((model) => model.id === id); + }, + findAll(options={}) { + const keys = Object.keys(options); + const models = targetNamespace.MemServer.DB[this.modelName] || []; + + if (keys.length === 0) { + return models; + } + + return models.filter((model) => comparison(model, options, keys, 0)); + }, + findBy(options) { + const keys = Object.keys(options); + const models = targetNamespace.MemServer.DB[this.modelName] || []; + console.log('models are:', models); + + return models.find((model) => comparison(model, options, keys, 0)); + }, + insert(options) { // NOTE: what if there is same id? + const models = targetNamespace.MemServer.DB[this.modelName] || []; + + // TODO: auto-increment ids + const defaultAttributes = this.attributes.reduce((result, attribute) => { + // TODO: enable functions + result[attribute] = this[attribute]; + }, {}); + + const targetAttributes = Object.assign(defaultAttributes, options); + + models.push(targetAttributes); + }, + bulkInsert(count, options) { + return Array.from({ length: count }).map(() => this.insert(options)); + }, + update(record) { + const targetRecord = record.id ? this.find(record.id) : this.findBy({ uuid: record.uuid }); + + if (!targetRecord) { + throw new Error('[MemServer] $Model.update(record) requires id or uuid primary key to update a record'); + } + + const targetIndex = models.indexOf(targetRecord); + + targetNamespace.MemServer.DB[this.modelName][targetIndex] = Object.assign(targetRecord, record); + + return targetNamespace.MemServer.DB[this.modelName][targetIndex]; + }, + bulkUpdate() { + + }, + destroy(record) { + const models = targetNamespace.MemServer.DB[this.modelName]; + + if (models.length === 0) { + throw new Error(`[MemServer] ${this.modelName} has no records in the database to remove`); + } + + const targetRecord = record.id ? this.find(record.id) : this.findBy({ uuid: record.uuid }); + + if (!targetRecord) { + throw new Error('[MemServer] $Model.destroy(record) requires id or uuid primary key to destroy a record'); + } + + const targetIndex = models.indexOf(targetRecord); + + targetNamespace.MemServer.DB[this.modelName] = models.splice(targetIndex, 1); + }, + bulkDestroy() { + + }, + serialize(objectOrArray) { - targetNamespace.MemServer.DB[this.modelName][targetIndex] = Object.assign(targetRecord, record); - - return targetNamespace.MemServer.DB[this.modelName][targetIndex]; - }, - bulkUpdate() { - - }, - destroy(record) { - const models = targetNamespace.MemServer.DB[this.modelName]; - - if (models.length === 0) { - throw new Error(`[MemServer] ${this.modelName} has no records in the database to remove`); } - - const targetRecord = record.id ? this.find(record.id) : this.findBy({ uuid: record.uuid }); - - if (!targetRecord) { - throw new Error('[MemServer] $Model.destroy(record) requires id or uuid primary key to destroy a record'); - } - - const targetIndex = models.indexOf(targetRecord); - - targetNamespace.MemServer.DB[this.modelName] = models.splice(targetIndex, 1); - }, - bulkDestroy() { - - }, - serialize(objectOrArray) { - - } + }, options); } // NOTE: if records were ordered by ID, then there could be performance benefit diff --git a/lib/mem-server/utils.js b/lib/mem-server/utils.js new file mode 100644 index 0000000..cdd5d71 --- /dev/null +++ b/lib/mem-server/utils.js @@ -0,0 +1,17 @@ +export function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +} + +// export function getModelPrimaryKey(models) { +// if (!models.length) { +// return false; +// } +// +// +// // take random 3 elements from the array +// // check if all of them has id, otherwise getModelPrimaryKey is uuid, check they have uuid +// // models. +// } diff --git a/test/mem-server.js b/test/mem-server.js index f6d262d..71d8b25 100644 --- a/test/mem-server.js +++ b/test/mem-server.js @@ -1,4 +1,3 @@ -// const assert = require('chai').assert; const assert = require('assert'); const fs = require('fs'); const rimraf = require('rimraf'); @@ -37,7 +36,6 @@ describe('MemServer', function() { fs.mkdirSync(`./memserver`); fs.mkdirSync(`./memserver/models`); - // fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); assert.throws(() => require('../index.js'), (err) => { return (err instanceof Error) && @@ -45,34 +43,53 @@ describe('MemServer', function() { }); }); - // it('exports not yet started MemServer with right functions, registered Models and empty DB', () => { - // this.timeout(5000); - // - // fs.mkdirSync(`./memserver`); - // fs.mkdirSync(`./memserver/models`); - // // fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); - // fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); - // - // const MemServer = require('../index.js'); - // - // assert.equal(MemServer.DB, {}); - // assert.equal(Object.keys(MemS)) - // }); - }); + it('exports a MemServer with right functions and empty DB when there is no model', function() { + this.timeout(5000); + + fs.mkdirSync(`./memserver`); + fs.mkdirSync(`./memserver/models`); + fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); + + const MemServer = require('../index.js'); + + assert.deepEqual(MemServer.DB, {}); + assert.deepEqual(MemServer.Pretender, {}); + assert.deepEqual(Object.keys(MemServer), ['DB', 'Pretender', 'Models', 'start', 'shutdown']); + assert.deepEqual(MemServer.Models, {}); + }); + + it('exports a MemServer with right functions and empty DB and models', function() { + this.timeout(5000); + + const modelFileContent = `import Model from '${process.cwd()}/lib/mem-server/model'; - // it('can be started with default options', () => { - // - // }); - // - // it('can be started with different options', () => { - // - // }); - // - // it('can be shut down', () => { - // - // }); - // - // it('can be shut down and started again with correct state', () => { - // - // }); + export default Model({});`; + + fs.mkdirSync(`./memserver`); + fs.mkdirSync(`./memserver/models`); + fs.writeFileSync(`${process.cwd()}/memserver/models/photo.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/models/user.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/models/photo-comment.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); + + Object.keys(require.cache).forEach((key) => delete require.cache[key]); + + const MemServer = require('../index.js'); + const models = Object.keys(MemServer.Models); + + assert.deepEqual(MemServer.DB, {}); + assert.deepEqual(MemServer.Pretender, {}); + assert.deepEqual(Object.keys(MemServer), ['DB', 'Pretender', 'Models', 'start', 'shutdown']); + assert.deepEqual(models, ['PhotoComment', 'Photo', 'User']); + models.forEach((modelName) => { + const model = MemServer.Models[modelName]; + + assert.equal(model.modelName, modelName); + assert.deepEqual(Object.keys(MemServer.Models[modelName]), [ + 'modelName', 'primaryKey', 'attributes', 'find', 'findAll', 'findBy', 'insert', 'bulkInsert', 'update', + 'bulkUpdate', 'destroy', 'bulkDestroy', 'serialize' + ]); + }); + }); + }); }); diff --git a/test/mem-server.start.js b/test/mem-server.start.js new file mode 100644 index 0000000..bd86adf --- /dev/null +++ b/test/mem-server.start.js @@ -0,0 +1,208 @@ +const assert = require('assert'); +const fs = require('fs'); +const rimraf = require('rimraf'); + +describe('MemServer start/stop functionality', function() { + beforeEach(function() { + this.timeout(5000); + + const modelFileContent = `import Model from '${process.cwd()}/lib/mem-server/model'; + + export default Model({});`; + + fs.mkdirSync(`./memserver`); + fs.mkdirSync(`./memserver/models`); + fs.writeFileSync(`${process.cwd()}/memserver/models/photo.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/models/user.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/models/photo-comment.js`, modelFileContent); + fs.writeFileSync(`${process.cwd()}/memserver/server.js`, 'export default function(Models) {}'); + + Object.keys(require.cache).forEach((key) => delete require.cache[key]); + }); + + afterEach(function(done) { + if (fs.existsSync(`${process.cwd()}/memserver`)) { + rimraf.sync(`${process.cwd()}/memserver`); + } + done(); + }); + + it('can be started without fixtures', function() { + const MemServer = require('../index.js'); + + assert.deepEqual(MemServer.Pretender, {}); + + MemServer.start(); + + assert.deepEqual(Object.keys(MemServer.Pretender), [ + 'hosts', 'handlers', 'handledRequests', 'passthroughRequests', 'unhandledRequests', + 'requestReferences', 'forcePassthrough', 'disableUnhandled', '_nativeXMLHttpRequest', + 'running', 'handledRequest', 'passthroughRequest', 'unhandledRequest' + ]); + }); + + it('can be started with fixtures', function() { + fs.mkdirSync(`./memserver/fixtures`); + fs.writeFileSync(`${process.cwd()}/memserver/fixtures/photos.js`, `export default [ + { + id: 1, + name: 'Ski trip', + href: 'ski-trip.jpeg', + is_public: false + }, + { + id: 2, + name: 'Family photo', + href: 'family-photo.jpeg', + is_public: true + }, + { + id: 3, + name: 'Selfie', + href: 'selfie.jpeg', + is_public: false + } + ];`); + + fs.writeFileSync(`${process.cwd()}/memserver/fixtures/photo-comments.js`, `export default [ + { + uuid: '499ec646-493f-4eea-b92e-e383d94182f4', + content: 'What a nice photo!', + photo_id: 1, + user_id: 1 + }, + { + uuid: '77653ad3-47e4-4ec2-b49f-57ea36a627e7', + content: 'I agree', + photo_id: 1, + user_id: 2 + }, + { + uuid: 'd351963d-e725-4092-a37c-1ca1823b57d3', + content: 'I was kidding', + photo_id: 1, + user_id: 1 + }, + { + uuid: '374c7f4a-85d6-429a-bf2a-0719525f5f29', + content: 'Interesting indeed', + photo_id: 2, + user_id: 1 + } + ];`); + + const MemServer = require('../index.js'); + const { Photo, PhotoComment } = MemServer.Models; + + assert.deepEqual(MemServer.Pretender, {}); + assert.deepEqual(MemServer.DB, {}); + + assert.deepEqual(Photo.findAll(), []); + assert.deepEqual(PhotoComment.findAll(), []); + + MemServer.start(); + + assert.deepEqual(Object.keys(MemServer.Pretender), [ + 'hosts', 'handlers', 'handledRequests', 'passthroughRequests', 'unhandledRequests', + 'requestReferences', 'forcePassthrough', 'disableUnhandled', '_nativeXMLHttpRequest', + 'running', 'handledRequest', 'passthroughRequest', 'unhandledRequest' + ]); + assert.deepEqual(MemServer.DB, { + Photo: Photo.findAll(), PhotoComment: PhotoComment.findAll(), User: [] + }); + assert.equal(Photo.primaryKey, 'id'); + assert.deepEqual(Photo.findAll().length, 3); + assert.deepEqual(Photo.find(1), { + id: 1, name: 'Ski trip', href: 'ski-trip.jpeg', is_public: false + }); + + assert.equal(PhotoComment.primaryKey, 'uuid'); + assert.deepEqual(PhotoComment.findAll().length, 4); + assert.deepEqual(PhotoComment.findBy({ uuid: '374c7f4a-85d6-429a-bf2a-0719525f5f29' }), { + uuid: '374c7f4a-85d6-429a-bf2a-0719525f5f29', content: 'Interesting indeed', + photo_id: 2, user_id: 1 + }); + }); + + it('should throw error if any of the fixtures missing id or uuid', function() { + fs.mkdirSync(`./memserver/fixtures`); + fs.writeFileSync(`${process.cwd()}/memserver/fixtures/photos.js`, `export default [ + { + id: 1, + name: 'Ski trip', + href: 'ski-trip.jpeg', + is_public: false + }, + { + id: 2, + name: 'Family photo', + href: 'family-photo.jpeg', + is_public: true + }, + { + id: 3, + name: 'Selfie', + href: 'selfie.jpeg', + is_public: false + } + ];`); + + fs.writeFileSync(`${process.cwd()}/memserver/fixtures/photo-comments.js`, `export default [ + { + content: 'What a nice photo!', + photo_id: 1, + user_id: 1 + }, + { + content: 'I agree', + photo_id: 1, + user_id: 2 + }, + { + content: 'I was kidding', + photo_id: 1, + user_id: 1 + }, + { + content: 'Interesting indeed', + photo_id: 2, + user_id: 1 + } + ];`); + + const MemServer = require('../index.js'); + const { Photo, PhotoComment } = MemServer.Models; + + assert.deepEqual(MemServer.Pretender, {}); + assert.deepEqual(MemServer.DB, {}); + + assert.deepEqual(Photo.findAll(), []); + assert.deepEqual(PhotoComment.findAll(), []); + + assert.throws(() => MemServer.start(), (err) => { + return (err instanceof Error) && + /MemServer DATABASE ERROR\: At least one of your PhotoComment fixtures missing a primary key\. Please make sure all your PhotoComment fixtures have either id or uuid primaryKey/.test(err); + }); + + assert.deepEqual(MemServer.Pretender, {}); + assert.deepEqual(MemServer.DB, {}); + assert.deepEqual(Photo.findAll(), []); + assert.deepEqual(PhotoComment.findAll(), []); + }); + + it('should throw error if any of the id fixtures have an incorrect type', function() { + + }); + + it('should throw error if any of the uuid fixtures have an incorrect type', function() { + + }); + + // it('can be shut down', () => { + // + // }); + + // it('can be shut down and started again with restarted state', () => { + // + // }); +});