Browse Source

many changes to the crud generator.

Tomislav Cvetic 8 years ago
parent
commit
6e79297326

+ 1 - 0
server/project/route.js

@@ -61,6 +61,7 @@ const routing = {
       if (err) {
         res.status(422)
         res.send(err)
+        return
       }
       res.send({ success: 'Project added.' })
     })

+ 76 - 0
src/__mocks__/httpApi.js

@@ -0,0 +1,76 @@
+const data = {}
+let idx
+for (idx = 0; idx < 10; idx += 1) {
+  data[idx] = `data ${idx}`
+}
+
+export default function request (url, options) {
+  return new Promise((resolve, reject) => {
+    // Parse the url.
+    const parsed = url.match(/^http:\/\/localhost\/db(\/\w+)?(\/\w+)?\/?$/)
+    if (!parsed) {
+      reject(Error('url could not be parsed.'))
+    }
+
+    // extract the params.
+    let [ urlString, module, id ] = parsed
+    if (module) {
+      module = module.substring(1)
+    }
+    if (id) {
+      id = id.substring(1)
+    }
+
+    // check the method
+    if (typeof options.method === 'undefined' || !options.method.match(/GET|PUT|POST|DELETE/)) {
+      reject(Error('method not supported.'))
+    }
+    // http://www.restapitutorial.com/lessons/httpmethods.html
+    switch (options.method) {
+      case 'POST':
+        if (id) {
+          if (typeof data[id] !== 'undefined') {
+            reject(Error('409: conflict'))
+          } else {
+            reject(Error('404: not found'))
+          }
+        } else {
+          resolve({ message: `201: created. ${urlString}/2` })
+        }
+        break
+      case 'GET':
+        if (id) {
+          if (typeof data[id] !== 'undefined') {
+            resolve(data[id])
+          } else {
+            reject(Error('404: not found'))
+          }
+        } else {
+          resolve(data)
+        }
+        break
+      case 'PUT':
+        if (id) {
+          if (typeof data[id] !== 'undefined') {
+            resolve({ message: '200: ok' })
+          } else {
+            reject(Error('404: not found'))
+          }
+        } else {
+          reject(Error('404: not found'))
+        }
+        break
+      case 'DELETE':
+        if (id) {
+          if (typeof data[id] !== 'undefined') {
+            resolve({ message: '200: ok' })
+          } else {
+            reject(Error('404: not found'))
+          }
+        } else {
+          reject(Error('404: not found'))
+        }
+        break
+    }
+  })
+}

+ 45 - 0
src/redux-gen/basic.js

@@ -0,0 +1,45 @@
+/** genStuff
+  * you input the name of a module (e.g. project) and the API (e.g. http://localhost/db/project)
+  * it outputs a state, action types, action creators, reducer, workers and watchers
+  * to handle a restful API.
+  */
+export function reduxGen (name) {
+  const actionTypes = {}
+  const actions = {}
+
+  const actionList = ['create', 'update', 'delete']
+
+  /** Populate the actionTypes and actions */
+  actionList.forEach(action => {
+    const actionType = `${action.toUpperCase()}_${name.toUpperCase()}`
+    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
+    actionTypes[actionType] = `${name}/${actionType}`
+    actions[actionName] = (id, data) => { return { type: `${name}/${actionType}`, id, data } }
+  })
+
+  const state = []
+
+  function reducer (state = [], action) {
+    let nextState
+    switch (action.type) {
+      case actionTypes[`CREATE_${name.toUpperCase()}`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      default:
+        return state
+    }
+  }
+
+  return {
+    actionTypes,
+    actions,
+    state,
+    reducer
+  }
+}

+ 173 - 0
src/redux-gen/crud.js

@@ -0,0 +1,173 @@
+import { call, put, takeEvery } from 'redux-saga/effects'
+
+/** The basic CRUD API supports the following functionality:
+  * /api[/module][/id|/offset[/limit]]
+  *
+  * Other functions
+  */
+
+import request from 'request-promise'
+export function crudHttpApi (action, options) {
+  const { uri } = options
+  // determine the operation
+  const [ name, actionType ] = action.type.split('/')
+  const requestOptions = {
+    uri: `${uri}/${name}`,
+    method: null,
+    body: null
+  }
+  switch (actionType) {
+    case 'READ_REQUEST':
+      options.uri = `${uri}/${action.id}`
+      options.method = 'GET'
+      break
+    case 'CREATE_REQUEST':
+      options.method = 'POST'
+      options.body = action.data
+      break
+    case 'UPDATE_REQUEST':
+      options.uri = `${uri}/${action.id}`
+      options.method = 'PUT'
+      break
+    case 'DELETE_REQUEST':
+      options.uri = `${uri}/${action.id}`
+      options.method = 'DELETE'
+      break
+  }
+  // Request from the request-promise library already returns a promise.
+  return request(requestOptions)
+}
+
+export function crudLocalstorageApi (action, options) {
+  // determine the operation
+  const [ name, actionType ] = action.type.split('/')
+  switch (actionType) {
+    case 'READ_REQUEST':
+      return new Promise((resolve, reject) => {
+        const items = localStorage.getItem(name) || []
+        resolve(items[action.id])
+      })
+    case 'CREATE_REQUEST':
+      return new Promise((resolve, reject) => {
+        try {
+          const items = localStorage.getItem(name) || {}
+          items[action.id] = action.data
+          localStorage.setItem(name, items)
+          resolve(null)
+        } catch (error) {
+          reject(error)
+        }
+      })
+    case 'UPDATE_REQUEST':
+      return new Promise((resolve, reject) => {
+        try {
+          const items = localStorage.getItem(name) || {}
+          items[action.id] = action.data
+          localStorage.setItem(name, items)
+          resolve(null)
+        } catch (error) {
+          reject(error)
+        }
+      })
+    case 'DELETE_REQUEST':
+      return new Promise((resolve, reject) => {
+        try {
+          const items = localStorage.getItem(name) || {}
+          delete items[action.id]
+          localStorage.setItem(name, items)
+          resolve(null)
+        } catch (error) {
+          reject(error)
+        }
+      })
+  }
+}
+
+/** reduxCrudGen
+  *
+  * Generates CRUD action types, actions, reducers, workers and watchers.
+  */
+export function reduxCrudGen (name, prefix, api) {
+  const actionTypes = {}
+  const actions = {}
+
+  /** Supported actions */
+  const actionList = ['create', 'read', 'update', 'delete']
+
+  /** For each supported action create an action creator to request
+    * data from the API, a success action when data is processed, and
+    * a failure action, when the API call was not successful.
+    * You will get e.g. create_request, update_success... */
+  actionList.forEach(action => {
+    /** generate sth like 'createProject' which dispatches 'project/CREATE_REQUEST' */
+    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
+    if (action === 'list') { actions[actionName] = (data, id = null) => { return { type: `${name}/${action}_REQUEST`, id, data } } }
+    ['request', 'success', 'failure'].forEach(mode => {
+      /** generate sth like 'project/CREATE_REQUEST' */
+      const actionType = `${action}_${mode}`.toUpperCase()
+      actionTypes[actionType] = `${name}/${actionType}`
+    })
+  })
+  actions[`create${name[0].toUpperCase() + name.substring(1)}`] = (data) => { return { type: `${name}/CREATE_REQUEST`, data } }
+  actions[`read${name[0].toUpperCase() + name.substring(1)}`] = (id, data) => { return { type: `${name}/READ_REQUEST`, id, data } }
+  actions[`update${name[0].toUpperCase() + name.substring(1)}`] = (id, data) => { return { type: `${name}/UPDATE_REQUEST`, id, data } }
+  actions[`delete${name[0].toUpperCase() + name.substring(1)}`] = (id) => { return { type: `${name}/DELETE_REQUEST`, id } }
+
+  const state = []
+
+  function * worker (action) {
+    try {
+      const data = yield call(api, action)
+      yield put({ type: action.type.replace('REQUEST', 'SUCCESS'), data })
+    } catch (error) {
+      yield put({ type: action.type.replace('REQUEST', 'FAILURE'), error })
+    }
+  }
+
+  /** The watcher function hijacks the _REQUEST actions and calls
+    * asynchronous workers. Once they yield their result, a _SUCCESS
+    * or _FAILURE action is triggered. */
+  function * watcher () {
+    let action
+    for (action of actionList) {
+      yield takeEvery(`${name}/${action.toUpperCase()}_REQUEST`, worker)
+    }
+  }
+
+  function reducer (state = [], action) {
+    let nextState
+    if (!action) {
+      return state
+    }
+    switch (action.type) {
+      case actionTypes[`CREATE_SUCCESS`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_SUCCESS`]:
+        if (typeof action.id !== 'undefined' && action.id >= 0) {
+          nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+          return nextState
+        } else {
+          return state
+        }
+      case actionTypes[`DELETE_SUCCESS`]:
+        if (typeof action.id !== 'undefined' && action.id >= 0) {
+          nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+          return nextState
+        } else {
+          return state
+        }
+      default:
+        return state
+    }
+  }
+
+  return {
+    actionTypes,
+    actions,
+    state,
+    reducer,
+    worker,
+    watcher
+  }
+}

+ 117 - 0
src/redux-gen/crud.test.js

@@ -0,0 +1,117 @@
+/** crud.test.js
+  * Test file for crud.js.
+  */
+
+import { crudHttpApi, crudLocalstorageApi, reduxCrudGen } from './crud'
+import { put, call, takeEvery } from 'redux-saga/effects'
+
+describe('HTTP API', () => {
+  it('opens http connections', () => {
+    const action = {
+      type: 'project/LIST_REQUEST',
+      id: 1,
+      data: {name: 'Titlis'}
+    }
+    const options = { uri: 'http://localhost:4000/db' }
+    return crudHttpApi(action, options).then(data => {
+      expect(JSON.parse(data)).toHaveLength(1)
+    })
+  })
+})
+
+describe('LocalStorage API', () => {
+  it('reads from localstorage', () => {
+  })
+  it('writes to localstorage', () => {
+  })
+})
+
+describe('Crud Generator', () => {
+  const api = () => null
+  const project = reduxCrudGen('project', 'PROJ', api)
+  it('generates action types', () => {
+    expect(project.actionTypes).toHaveProperty('CREATE_REQUEST', 'project/CREATE_REQUEST')
+    expect(project.actionTypes).toHaveProperty('READ_REQUEST', 'project/READ_REQUEST')
+    expect(project.actionTypes).toHaveProperty('UPDATE_REQUEST', 'project/UPDATE_REQUEST')
+    expect(project.actionTypes).toHaveProperty('DELETE_REQUEST', 'project/DELETE_REQUEST')
+    expect(project.actionTypes).toHaveProperty('CREATE_SUCCESS', 'project/CREATE_SUCCESS')
+    expect(project.actionTypes).toHaveProperty('READ_SUCCESS', 'project/READ_SUCCESS')
+    expect(project.actionTypes).toHaveProperty('UPDATE_SUCCESS', 'project/UPDATE_SUCCESS')
+    expect(project.actionTypes).toHaveProperty('DELETE_SUCCESS', 'project/DELETE_SUCCESS')
+    expect(project.actionTypes).toHaveProperty('CREATE_FAILURE', 'project/CREATE_FAILURE')
+    expect(project.actionTypes).toHaveProperty('READ_FAILURE', 'project/READ_FAILURE')
+    expect(project.actionTypes).toHaveProperty('UPDATE_FAILURE', 'project/UPDATE_FAILURE')
+    expect(project.actionTypes).toHaveProperty('DELETE_FAILURE', 'project/DELETE_FAILURE')
+  })
+  it('generates actions', () => {
+    expect(project.actions).toHaveProperty('createProject')
+    expect(project.actions).toHaveProperty('readProject')
+    expect(project.actions).toHaveProperty('updateProject')
+    expect(project.actions).toHaveProperty('deleteProject')
+  })
+  it('actions return action objects', () => {
+    const id = 1
+    const data = { payload: 'Some thing' }
+    expect(project.actions.createProject(data)).toEqual({type: 'project/CREATE_REQUEST', data})
+    expect(project.actions.readProject(id, data)).toEqual({type: 'project/READ_REQUEST', id, data})
+    expect(project.actions.updateProject(id, data)).toEqual({type: 'project/UPDATE_REQUEST', id, data})
+    expect(project.actions.deleteProject(id)).toEqual({type: 'project/DELETE_REQUEST', id})
+  })
+  it('generates the empty state', () => {
+    expect(project.state).toEqual([])
+  })
+  it('generates the worker', () => {
+    expect(project.worker).not.toBeUndefined()
+  })
+  it('worker handles api calls', () => {
+    const triggerAction = { type: 'project/READ_REQUEST', id: 1, data: 'API in' }
+    const data = 'API out'
+    const expectedAction1 = { type: 'project/READ_SUCCESS', data }
+    const error = 'Oopsie...'
+    const expectedAction2 = { type: 'project/READ_FAILURE', error }
+    const iterator1 = project.worker(triggerAction)
+    expect(iterator1.next().value).toEqual(call(api, triggerAction))
+    expect(iterator1.next(data).value).toEqual(put(expectedAction1))
+    expect(iterator1.next().done).toBe(true)
+    const iterator2 = project.worker(triggerAction)
+    expect(iterator2.next().value).toEqual(call(api, triggerAction))
+    expect(iterator2.throw(error).value).toEqual(put(expectedAction2))
+    expect(iterator2.next().done).toBe(true)
+  })
+  it('generates the watcher', () => {
+    expect(project.watcher).not.toBeUndefined()
+  })
+  it('watcher intercepts the requests', () => {
+    const iterator1 = project.watcher()
+    expect(iterator1.next().value).toEqual(takeEvery('project/CREATE_REQUEST', project.worker))
+    expect(iterator1.next().value).toEqual(takeEvery('project/READ_REQUEST', project.worker))
+    expect(iterator1.next().value).toEqual(takeEvery('project/UPDATE_REQUEST', project.worker))
+    expect(iterator1.next().value).toEqual(takeEvery('project/DELETE_REQUEST', project.worker))
+    expect(iterator1.next().done).toBe(true)
+  })
+  it('generates the reducer', () => {
+    expect(project.reducer).not.toBeUndefined()
+  })
+  it('reducer returns the empty state', () => {
+    const state = []
+    const action = { type: 'project/CREATE_SUCCESS' }
+    expect(project.reducer()).toEqual([])
+  })
+  it('reducer returns the empty state', () => {
+    let state = []
+    state = project.reducer()
+    expect(state).toEqual([])
+    expect(state).toEqual([])
+  })
+  it('reducer handles the crud actions', () => {
+    let state = []
+    state = project.reducer()
+    expect(state).toEqual([])
+    state = project.reducer(state, { type: 'project/CREATE_SUCCESS', data: { name: 'MyNewObject', happy: true } })
+    expect(state).toEqual([{ name: 'MyNewObject', happy: true }])
+    state = project.reducer(state, { type: 'project/UPDATE_SUCCESS', id: 0, data: { name: 'MyUpdatedObject' } })
+    expect(state).toEqual([{ name: 'MyUpdatedObject', happy: true }])
+    state = project.reducer(state, { type: 'project/DELETE_SUCCESS', id: 0 })
+    expect(state).toEqual([])
+  })
+})

+ 6 - 3
src/redux-gen/rest.js → src/redux-gen/localstorage.js

@@ -5,12 +5,12 @@ import { call, put, takeEvery } from 'redux-sagas/effects'
   * it outputs a state, action types, action creators, reducer, workers and watchers
   * to handle a restful API.
   */
-export function genStuff (name, api = `http://localhost/db/${name}`) {
+export function reduxLocalstorageGen (name) {
   const actionTypes = {}
   const actions = {}
 
   /** Supported actions */
-  const actionList = ['list', 'create', 'read', 'update', 'delete']
+  const actionList = ['save', 'restore']
 
   /** For each supported action create an action creator to request
     * data from the API, a success action when data is processed, and
@@ -19,15 +19,17 @@ export function genStuff (name, api = `http://localhost/db/${name}`) {
   actionList.forEach(action => {
     /** generate sth like 'createProject' which dispatches 'project/CREATE_REQUEST' */
     const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
-    actions[actionName] = (id, data) => { return { type: `${name}/${actionType}_REQUEST`, id, data } }
+    actions[actionName] = (id, data) => { return { type: `${name}/${action}_REQUEST`, id, data } }
     ['request', 'success', 'failure'].forEach(mode => {
       /** generate sth like 'project/CREATE_REQUEST' */
       const actionType = `${action}_${mode}`.toUpperCase()
       actionTypes[actionType] = `${name}/${actionType}`
     })
   })
+  console.log('RESTGen: actionTypes, actions', actionTypes, actions)
 
   const state = []
+  console.log('RESTGen: state', state)
 
   function * worker (action) {
     try {
@@ -47,6 +49,7 @@ export function genStuff (name, api = `http://localhost/db/${name}`) {
   function reducer (state = [], action) {
     let nextState
     switch (action.type) {
+      // On success,
       case actionTypes[`CREATE_REQUEST_${name.toUpperCase()}`]:
         worker(action)
         return nextState

+ 0 - 97
src/redux-gen/simple.js

@@ -1,97 +0,0 @@
-import { call, put, takeEvery } from 'redux-sagas/effects'
-
-/** genStuff
-  * you input the name of a module (e.g. project) and the API (e.g. http://localhost/db/project)
-  * it outputs a state, action types, action creators, reducer, workers and watchers
-  * to handle a restful API.
-  */
-export function genStuff (name, api = `http://localhost/db/${name}`) {
-  const actionTypes = {}
-  const actions = {}
-
-  let actionList = ['list', 'create', 'read', 'update', 'delete']
-  if (!sync) {
-    let tmpActionList = []
-    actionList.forEach(action => {
-      tmpActionList.push(`${action}_request`)
-      tmpActionList.push(`${action}_success`)
-      tmpActionList.push(`${action}_failure`)
-    })
-    actionList = tmpActionList
-  }
-
-  /** Populate the actionTypes and actions */
-  actionList.forEach(action => {
-    const actionType = `${action.toUpperCase()}_${name.toUpperCase()}`
-    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
-    actionTypes[actionType] = `${name}/${actionType}`
-    actions[actionName] = (id, data) => { return { type: `${name}/${actionType}`, id, data } }
-  })
-
-  const state = []
-
-  function * worker (action) {
-    try {
-      const data = yield call(api, payload)
-      yield put({ type: '??_SUCCESS', data })
-    } catch (error) {
-      yield put({ type: '??_FAILURE', error })
-    }
-  }
-
-  function * watcher (action) {
-    for (action in actions) {
-      yield takeEvery(`${action.toUpperCase()}_REQUEST`, worker)
-    }
-  }
-
-  const reducer = (sync) ? (state = [], action) => {
-    let nextState
-    switch (action.type) {
-      case actionTypes[`CREATE_${name.toUpperCase()}`]:
-        nextState = [ ...state, action.data ]
-        return nextState
-      case actionTypes[`UPDATE_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`REMOVE_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
-        return nextState
-      default:
-        return state
-    }
-  } : (state = [], action) => {
-    let nextState
-    switch (action.type) {
-      case actionTypes[`CREATE_REQUEST_${name.toUpperCase()}`]:
-        worker(action)
-        return nextState
-      case actionTypes[`CREATE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state, action.data ]
-        return nextState
-      case actionTypes[`UPDATE_REQUEST_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`UPDATE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`REMOVE_REQUEST_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
-        return nextState
-      case actionTypes[`REMOVE_SUCCESS_${name.toUpperCase()}`]:
-        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
-        return nextState
-      default:
-        return state
-    }
-  }
-
-  return {
-    actionTypes,
-    actions,
-    state,
-    reducer,
-    worker: (sync) ? null : worker,
-    watcher: (sync) ? null : watcher
-  }
-}