<commit_before>import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
import { combineReducers as combineImmutableReducers } from 'redux-immutable';
import createSagaMiddleware from 'redux-saga';
import invariant from 'invariant';
import warning from 'warning';
import clone from 'clone';
import { call, fork, take, cancel, takeEvery, takeLatest, throttle } from 'redux-saga/effects';
import { reduxSagaMiddleware } from 'redux-saga-middleware';
import { createAction, handleActions } from './action';

const ActionTypes = {
    INIT: '@@redux/INIT',
    CANCEL_EFFECTS: '@@CANCEL_EFFECTS',
};

const noop = () => { };

const payloadEncode = (...args) => {
    const payload = {
        __resa__payload__: true,
    };
    args.forEach((item, i) => {
        payload[i] = item;
    });
    return payload;
};

const payloadDecode = (payload) => {
    if (payload.__resa__payload__) { // eslint-disable-line
        delete payload.__resa__payload__; // eslint-disable-line
        return Object.keys(payload).map(k => payload[k]);
    }
    return payload;
};

export default function createResa(options = {}) {
    const {
        reducers = {},
        /**
         * facebook immutable object, if null, we use plain redux state
         */
        immutable = null,
        errorHandle = noop,
    } = options;

    function getEmptyObject() {
        if (immutable) {
            return immutable.Map();
        }
        return {};
    }

    function resaReducer(state = {}, _action) {
        return state;
    }

    const initReducer = {
        ...reducers,
        resaReducer,
    };

    function makeRootReducer(asyncReducers) {
        if (!immutable) {
            return combineReducers({
                ...initReducer,
                ...asyncReducers,
            });
        }
        return combineImmutableReducers({
            ...initReducer,
            ...asyncReducers,
        });
    }

    function mergeImmutablePayload(state, payload = {}) {
        let newState = state;
        for (const name in payload) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(payload, name)) {
                newState = newState.set(name, payload[name]);
            }
        }
        return newState;
    }

    function commonReducerHandle(state, payload) {
        if (payload == null) {
            return state;
        }
        if (immutable) {
            return mergeImmutablePayload(state, payload);
        }
        if (Object.prototype.toString.call(state) === '[object Object]') {
            invariant(Object.prototype.toString.call(payload) === '[object Object]',
            'The payload must be an object if the shape of state is object');
            return Object.assign({}, state, payload);
        }
        return payload;
    }

    /**
     * merge reduer in same state node, state must hava been initialized in here
     * @param reducers
     */
    function composeHandleActions(reducerList = [], defaultState) {
        const reducer = (state, action) => {
            /**
             * handle ActionTypes.INIT action
             */
            if (action.type === ActionTypes.INIT) {
                if (state === undefined) {
                    return defaultState;
                }
                // last model default state should not cover current state;
                if (Object.prototype.toString.call(state) === '[object Object]') {
                    if (immutable) {
                        return mergeImmutablePayload(defaultState, state);
                    }
                    invariant(Object.prototype.toString.call(defaultState) === '[object Object]',
                        'The payload must be an object if the shape of state is object');
                    return Object.assign({}, defaultState, state);
                }
                return state;
            }
            const getState = (previous, current) => current(previous, action);
            return reducerList.reduce(getState, state);
        };
        return reducer;
    }

    function mergeReducer(reducerMap, state) {
        const reducerList = [];
        for (const key in reducerMap) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(reducerMap, key)) {
                reducerList.push(reducerMap[key]);
            }
        }
        if (reducerList.length === 0) {
            return null;
        }
        if (reducerList.length === 1) {
            return reducerList[0];
        }
        return composeHandleActions(reducerList, state);
    }

    function getEffectSaga(models, saga, name, dispatch) {
        return function* (action) { // eslint-disable-line
            const { resolve, reject, ...rest } = action;
            try {
                const that = Object.assign({}, models[name], dispatch, { models });
                const result = yield call([that, saga], ...payloadDecode(rest.payload));
                resolve(result);
            } catch (error) {
                errorHandle(error);
                reject(error);
            }
        };
    }

    function getSaga(app, action, effect, model, dispatch) {
        let type = 'takeEvery';
        let actualEffect = effect;
        if (Array.isArray(effect)) {
            type = effect[1];
            actualEffect = effect[0];

            invariant(
                ['takeEvery', 'takeLatest', 'throttle'].indexOf(type) > -1,
                'effect type should be takeEvery, takeLatest or throttle.'
            );

            if (type === 'throttle') {
                invariant(
                    Number.isInteger(effect[2]) === true && effect[2] > 0,
                    'ms of throttle should be positive integer.'
                );
            }
        }

        switch (type) {
            case 'takeLatest': // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield takeLatest(
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
            case 'throttle': // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield throttle(
                        effect[2],
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
            default: // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield takeEvery(
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
        }
    }

    function registerModel(model, reducerName) {
        const app = this;
        const store = this.store;
        const actions = {};

        if (this.models[model.name] != null) {
            warning(false, 'You want to register model twice or there have two model share same name.');
            return;
        }

        // use model.name for default reducer name
        if (reducerName == null) {
            reducerName = model.reducerName || model.name; // eslint-disable-line
        }

        const newEffects = {};
        const oldEffects = model.effects || {};
        for (const key in oldEffects) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldEffects, key)) {
                const action = createAction(`${model.name}/${key}`);
                const innerReducer = (state, action) => { // eslint-disable-line
                    try {
                        return commonReducerHandle(state, action.payload);
                    } catch (error) {
                        errorHandle(error);
                    }
                };
                actions[action.fulfilled] = innerReducer;
                actions[action.reject] = innerReducer;

                const dispatch = {
                    fulfilled(obj) {
                        store.dispatch(action.fulfilled(obj));
                    },
                    reject(obj) {
                        store.dispatch(action.reject(obj));
                    },
                };

                /**
                 * action creaters
                 */
                newEffects[key] = (...args) => store.dispatch(action.pending(payloadEncode(...args)));

                /**
                 * run watcher
                 */
                this.runSaga(function* () { // eslint-disable-line
                    const saga = getSaga(app, action, oldEffects[key], model, dispatch);
                    const task = yield fork(saga);
                    yield fork(function* () { // eslint-disable-line
                        yield take(`${model.name}/${ActionTypes.CANCEL_EFFECTS}`);
                        yield cancel(task);
                    });
                });
            }
        }

        const newReducers = {};
        const oldReducers = model.reducers || {};
        for (const key in oldReducers) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldReducers, key)) {
                actions[`${model.name}/${key}`] = (oldReducers[key]);
                actions[`${model.name}/${key}`] = (_state, { payload }) => {
                    const that = Object.assign({}, { state: app.models[model.name].state });
                    return oldReducers[key].call(that, ...payloadDecode(payload));
                };
                newReducers[key] = (...args) => {
                    store.dispatch({
                        type: `${model.name}/${key}`,
                        payload: payloadEncode(...args),
                    });
                };
            }
        }

        const oldPureReducers = model.pureReducers || {};
        for (const key in oldPureReducers) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldPureReducers, key)) {
                actions[key] = oldPureReducers[key];
            }
        }

        // find reducer and merge reducer
        invariant(model.state != null, 'State in model should not be null or undefined.');

        // avoid bug when registerModel after unRegisterModel
        const state = clone(model.state);

        if (store.reducerList[reducerName] == null) {
            store.reducerList[reducerName] = {};
            store.reducerList[reducerName][model.name] =
                handleActions(actions, state);
        } else {
            store.reducerList[reducerName][model.name] =
                handleActions(actions, state);
        }
        store.asyncReducers[reducerName] = mergeReducer(store.reducerList[reducerName], state);
        store.replaceReducer(makeRootReducer(store.asyncReducers));

        this.models[model.name] = {
            ...newEffects,
            ...newReducers,
            reducerName,
            name: model.name,
        };

        Object.defineProperty(this.models[model.name], 'state', {
            enumerable: true,
            configurable: false,
            get: () => {
                if (!immutable) {
                    return app.store.getState()[reducerName];
                }
                return app.store.getState().get(reducerName);
            },
        });

        if (model.setup) {
            this.runSaga(function* () { // eslint-disable-line
                const task = yield fork([app.models[model.name], model.setup]);
                yield fork(function* () { // eslint-disable-line
                    yield take(`${model.name}/${ActionTypes.CANCEL_EFFECTS}`);
                    yield cancel(task);
                });
            });
        }
    }

    /**
     * unregister model, including delete reducer, delete asyncReducers ,cancle saga, delete model
     * @param {*} model
     */
    function unRegisterModel(name) {
        const store = this.store;

        const model = this.models[name];
        if (model == null) {
            return;
        }

        store.dispatch({ type: `${model.name}/${ActionTypes.CANCEL_EFFECTS}` });
        if (store.reducerList[model.reducerName] && store.reducerList[model.reducerName][model.name]) {
            delete store.reducerList[model.reducerName][model.name];
            const mergedReducer = mergeReducer(store.reducerList[model.reducerName]);
            if (mergedReducer) {
                store.asyncReducers[model.reducerName] = mergedReducer;
                store.replaceReducer(makeRootReducer(store.asyncReducers));
            } else {
                delete store.asyncReducers[model.reducerName];
                store.replaceReducer(makeRootReducer(store.asyncReducers));
            }
        }

        delete this.models[name];
    }

    const app = {
        /**
         * redux store
         */
        store: null,
        /**
         * redux-saga middleware.run
         */
        runSaga: null,
        /**
         * model list
         */
        models: {},
        /**
         * register a model
         */
        registerModel,
        /**
         * unRegister a model
         */
        unRegisterModel,
    };

    /**
     * init state
     */
    const initialState = options.initialState || getEmptyObject();

    const sagaMiddleware = createSagaMiddleware();

    /**
     * init middlewares, compose redux-dev-tools
     */
    let middlewares = (options.middlewares || []).concat(sagaMiddleware, reduxSagaMiddleware);

    if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) { // eslint-disable-line
        middlewares = compose(
            applyMiddleware(...middlewares),
            window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ // eslint-disable-line
                // if set to false, will not recompute the states on hot reloading (or on replacing the reducers).
                shouldHotReload: false,
            }) // redux dev tool extension for chrome
        );
    } else {
        middlewares = applyMiddleware(...middlewares);
    }

    app.store = createStore(
        makeRootReducer(),
        initialState,
        middlewares
    );
    app.store.asyncReducers = {};
    app.store.reducerList = {};

    app.registerModel = registerModel.bind(app);
    app.unRegisterModel = unRegisterModel.bind(app);
    app.runSaga = sagaMiddleware.run;

    return app;
}
<commit_msg>remove warning when register twice<commit_after>import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
import { combineReducers as combineImmutableReducers } from 'redux-immutable';
import createSagaMiddleware from 'redux-saga';
import invariant from 'invariant';
import warning from 'warning';
import clone from 'clone';
import { call, fork, take, cancel, takeEvery, takeLatest, throttle } from 'redux-saga/effects';
import { reduxSagaMiddleware } from 'redux-saga-middleware';
import { createAction, handleActions } from './action';

const ActionTypes = {
    INIT: '@@redux/INIT',
    CANCEL_EFFECTS: '@@CANCEL_EFFECTS',
};

const noop = () => { };

const payloadEncode = (...args) => {
    const payload = {
        __resa__payload__: true,
    };
    args.forEach((item, i) => {
        payload[i] = item;
    });
    return payload;
};

const payloadDecode = (payload) => {
    if (payload.__resa__payload__) { // eslint-disable-line
        delete payload.__resa__payload__; // eslint-disable-line
        return Object.keys(payload).map(k => payload[k]);
    }
    return payload;
};

export default function createResa(options = {}) {
    const {
        reducers = {},
        /**
         * facebook immutable object, if null, we use plain redux state
         */
        immutable = null,
        errorHandle = noop,
    } = options;

    function getEmptyObject() {
        if (immutable) {
            return immutable.Map();
        }
        return {};
    }

    function resaReducer(state = {}, _action) {
        return state;
    }

    const initReducer = {
        ...reducers,
        resaReducer,
    };

    function makeRootReducer(asyncReducers) {
        if (!immutable) {
            return combineReducers({
                ...initReducer,
                ...asyncReducers,
            });
        }
        return combineImmutableReducers({
            ...initReducer,
            ...asyncReducers,
        });
    }

    function mergeImmutablePayload(state, payload = {}) {
        let newState = state;
        for (const name in payload) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(payload, name)) {
                newState = newState.set(name, payload[name]);
            }
        }
        return newState;
    }

    function commonReducerHandle(state, payload) {
        if (payload == null) {
            return state;
        }
        if (immutable) {
            return mergeImmutablePayload(state, payload);
        }
        if (Object.prototype.toString.call(state) === '[object Object]') {
            invariant(Object.prototype.toString.call(payload) === '[object Object]',
            'The payload must be an object if the shape of state is object');
            return Object.assign({}, state, payload);
        }
        return payload;
    }

    /**
     * merge reduer in same state node, state must hava been initialized in here
     * @param reducers
     */
    function composeHandleActions(reducerList = [], defaultState) {
        const reducer = (state, action) => {
            /**
             * handle ActionTypes.INIT action
             */
            if (action.type === ActionTypes.INIT) {
                if (state === undefined) {
                    return defaultState;
                }
                // last model default state should not cover current state;
                if (Object.prototype.toString.call(state) === '[object Object]') {
                    if (immutable) {
                        return mergeImmutablePayload(defaultState, state);
                    }
                    invariant(Object.prototype.toString.call(defaultState) === '[object Object]',
                        'The payload must be an object if the shape of state is object');
                    return Object.assign({}, defaultState, state);
                }
                return state;
            }
            const getState = (previous, current) => current(previous, action);
            return reducerList.reduce(getState, state);
        };
        return reducer;
    }

    function mergeReducer(reducerMap, state) {
        const reducerList = [];
        for (const key in reducerMap) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(reducerMap, key)) {
                reducerList.push(reducerMap[key]);
            }
        }
        if (reducerList.length === 0) {
            return null;
        }
        if (reducerList.length === 1) {
            return reducerList[0];
        }
        return composeHandleActions(reducerList, state);
    }

    function getEffectSaga(models, saga, name, dispatch) {
        return function* (action) { // eslint-disable-line
            const { resolve, reject, ...rest } = action;
            try {
                const that = Object.assign({}, models[name], dispatch, { models });
                const result = yield call([that, saga], ...payloadDecode(rest.payload));
                resolve(result);
            } catch (error) {
                errorHandle(error);
                reject(error);
            }
        };
    }

    function getSaga(app, action, effect, model, dispatch) {
        let type = 'takeEvery';
        let actualEffect = effect;
        if (Array.isArray(effect)) {
            type = effect[1];
            actualEffect = effect[0];

            invariant(
                ['takeEvery', 'takeLatest', 'throttle'].indexOf(type) > -1,
                'effect type should be takeEvery, takeLatest or throttle.'
            );

            if (type === 'throttle') {
                invariant(
                    Number.isInteger(effect[2]) === true && effect[2] > 0,
                    'ms of throttle should be positive integer.'
                );
            }
        }

        switch (type) {
            case 'takeLatest': // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield takeLatest(
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
            case 'throttle': // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield throttle(
                        effect[2],
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
            default: // eslint-disable-line
                return function* () { // eslint-disable-line
                    yield takeEvery(
                        action.pending,
                        getEffectSaga(app.models, actualEffect, model.name, dispatch)
                    );
                };
        }
    }

    function registerModel(model, reducerName) {
        const app = this;
        const store = this.store;
        const actions = {};

        if (this.models[model.name] != null) {
            return;
        }

        // use model.name for default reducer name
        if (reducerName == null) {
            reducerName = model.reducerName || model.name; // eslint-disable-line
        }

        const newEffects = {};
        const oldEffects = model.effects || {};
        for (const key in oldEffects) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldEffects, key)) {
                const action = createAction(`${model.name}/${key}`);
                const innerReducer = (state, action) => { // eslint-disable-line
                    try {
                        return commonReducerHandle(state, action.payload);
                    } catch (error) {
                        errorHandle(error);
                    }
                };
                actions[action.fulfilled] = innerReducer;
                actions[action.reject] = innerReducer;

                const dispatch = {
                    fulfilled(obj) {
                        store.dispatch(action.fulfilled(obj));
                    },
                    reject(obj) {
                        store.dispatch(action.reject(obj));
                    },
                };

                /**
                 * action creaters
                 */
                newEffects[key] = (...args) => store.dispatch(action.pending(payloadEncode(...args)));

                /**
                 * run watcher
                 */
                this.runSaga(function* () { // eslint-disable-line
                    const saga = getSaga(app, action, oldEffects[key], model, dispatch);
                    const task = yield fork(saga);
                    yield fork(function* () { // eslint-disable-line
                        yield take(`${model.name}/${ActionTypes.CANCEL_EFFECTS}`);
                        yield cancel(task);
                    });
                });
            }
        }

        const newReducers = {};
        const oldReducers = model.reducers || {};
        for (const key in oldReducers) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldReducers, key)) {
                actions[`${model.name}/${key}`] = (oldReducers[key]);
                actions[`${model.name}/${key}`] = (_state, { payload }) => {
                    const that = Object.assign({}, { state: app.models[model.name].state });
                    return oldReducers[key].call(that, ...payloadDecode(payload));
                };
                newReducers[key] = (...args) => {
                    store.dispatch({
                        type: `${model.name}/${key}`,
                        payload: payloadEncode(...args),
                    });
                };
            }
        }

        const oldPureReducers = model.pureReducers || {};
        for (const key in oldPureReducers) { // eslint-disable-line
            if (Object.prototype.hasOwnProperty.call(oldPureReducers, key)) {
                actions[key] = oldPureReducers[key];
            }
        }

        // find reducer and merge reducer
        invariant(model.state != null, 'State in model should not be null or undefined.');

        // avoid bug when registerModel after unRegisterModel
        const state = clone(model.state);

        if (store.reducerList[reducerName] == null) {
            store.reducerList[reducerName] = {};
            store.reducerList[reducerName][model.name] =
                handleActions(actions, state);
        } else {
            store.reducerList[reducerName][model.name] =
                handleActions(actions, state);
        }
        store.asyncReducers[reducerName] = mergeReducer(store.reducerList[reducerName], state);
        store.replaceReducer(makeRootReducer(store.asyncReducers));

        this.models[model.name] = {
            ...newEffects,
            ...newReducers,
            reducerName,
            name: model.name,
        };

        Object.defineProperty(this.models[model.name], 'state', {
            enumerable: true,
            configurable: false,
            get: () => {
                if (!immutable) {
                    return app.store.getState()[reducerName];
                }
                return app.store.getState().get(reducerName);
            },
        });

        if (model.setup) {
            this.runSaga(function* () { // eslint-disable-line
                const task = yield fork([app.models[model.name], model.setup]);
                yield fork(function* () { // eslint-disable-line
                    yield take(`${model.name}/${ActionTypes.CANCEL_EFFECTS}`);
                    yield cancel(task);
                });
            });
        }
    }

    /**
     * unregister model, including delete reducer, delete asyncReducers ,cancle saga, delete model
     * @param {*} model
     */
    function unRegisterModel(name) {
        const store = this.store;

        const model = this.models[name];
        if (model == null) {
            return;
        }

        store.dispatch({ type: `${model.name}/${ActionTypes.CANCEL_EFFECTS}` });
        if (store.reducerList[model.reducerName] && store.reducerList[model.reducerName][model.name]) {
            delete store.reducerList[model.reducerName][model.name];
            const mergedReducer = mergeReducer(store.reducerList[model.reducerName]);
            if (mergedReducer) {
                store.asyncReducers[model.reducerName] = mergedReducer;
                store.replaceReducer(makeRootReducer(store.asyncReducers));
            } else {
                delete store.asyncReducers[model.reducerName];
                store.replaceReducer(makeRootReducer(store.asyncReducers));
            }
        }

        delete this.models[name];
    }

    const app = {
        /**
         * redux store
         */
        store: null,
        /**
         * redux-saga middleware.run
         */
        runSaga: null,
        /**
         * model list
         */
        models: {},
        /**
         * register a model
         */
        registerModel,
        /**
         * unRegister a model
         */
        unRegisterModel,
    };

    /**
     * init state
     */
    const initialState = options.initialState || getEmptyObject();

    const sagaMiddleware = createSagaMiddleware();

    /**
     * init middlewares, compose redux-dev-tools
     */
    let middlewares = (options.middlewares || []).concat(sagaMiddleware, reduxSagaMiddleware);

    if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) { // eslint-disable-line
        middlewares = compose(
            applyMiddleware(...middlewares),
            window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ // eslint-disable-line
                // if set to false, will not recompute the states on hot reloading (or on replacing the reducers).
                shouldHotReload: false,
            }) // redux dev tool extension for chrome
        );
    } else {
        middlewares = applyMiddleware(...middlewares);
    }

    app.store = createStore(
        makeRootReducer(),
        initialState,
        middlewares
    );
    app.store.asyncReducers = {};
    app.store.reducerList = {};

    app.registerModel = registerModel.bind(app);
    app.unRegisterModel = unRegisterModel.bind(app);
    app.runSaga = sagaMiddleware.run;

    return app;
}
<|endoftext|>


<commit_before>import {buildQueryString, join} from 'aurelia-path';
import {HttpClient} from 'aurelia-fetch-client';
import extend from 'extend';

/**
 * Rest class. A simple rest client to fetch resources
 */
export class Rest {
  interceptor;

  /**
   * The defaults to apply to any request
   *
   * @param {{}} defaults The default fetch request options
   */
  defaults: {} = {
    headers: {
      'Accept'      : 'application/json',
      'Content-Type': 'application/json'
    }
  };

  /**
   * The client for the rest adapter
   *
   * @param {HttpClient} client The fetch client
   *
   */
  client: HttpClient;

  /**
   * The name of the endpoint it was registered under
   *
   * @param {string} endpoint The endpoint name
   */
  endpoint: string;

  /**
   * true to use the traditional URI template standard (RFC6570) when building
   * query strings from criteria objects, false otherwise. Default is false.
   *
   * @param {boolean} useTraditionalUriTemplates The flag that enables RFC6570 URI templates.
   */
  useTraditionalUriTemplates: boolean;

  /**
   * Inject the httpClient to use for requests.
   *
   * @param {HttpClient} httpClient                    The httpClient to use
   * @param {string}     endpoint                      The endpoint name
   * @param {boolean}    [useTraditionalUriTemplates]  true to use the traditional URI
   *                                                   template standard (RFC6570) when building
   *                                                   query strings from criteria objects, false
   *                                                   otherwise. Default is false.
   */
  constructor(httpClient: HttpClient, endpoint: string, useTraditionalUriTemplates?: boolean) {
    this.client   = httpClient;
    this.endpoint = endpoint;
    this.useTraditionalUriTemplates = !!useTraditionalUriTemplates;
  }

  /**
   * Make a request to the server.
   *
   * @param {string}          method     The fetch method
   * @param {string}          path       Path to the resource
   * @param {{}}              [body]     The body to send if applicable
   * @param {{}}              [options]  Fetch request options overwrites
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  request(method: string, path: string, body?: {}, options?: {}): Promise<any|Error> {
    let requestOptions = extend(true, {headers: {}}, this.defaults, options || {}, {method, body});
    let contentType    = requestOptions.headers['Content-Type'] || requestOptions.headers['content-type'];
    let interceptor    = this.interceptor;

    if (interceptor && typeof interceptor.request === 'function') {
      requestOptions = interceptor.request(requestOptions);
    }

    if (typeof body === 'object' && body !== null && contentType) {
      requestOptions.body = (/^application\/json/).test(contentType.toLowerCase())
                          ? JSON.stringify(body)
                          : buildQueryString(body);
    }

    return this.client.fetch(path, requestOptions).then((response: Response) => {
      if (response.status >= 200 && response.status < 400) {
        let result = response.json().catch(error => null);

        if (interceptor && typeof interceptor.response === 'function') {
          return result.then(res => {
            return interceptor.response(res);
          });
        }
      }

      throw response;
    });
  }

  /**
   * Find a resource.
   *
   * @param {string}                    resource  Resource to find in
   * @param {string|number|{}}          idOrCriteria  Object for where clause, string / number for id.
   * @param {{}}                        [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  find(resource: string, idOrCriteria?: string|number|{}, options?: {}): Promise<any|Error> {
    return this.request('GET', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), undefined, options);
  }

  /**
   * Find a resource.
   *
   * @param {string}           resource    Resource to find in
   * @param {string|number}    id          String / number for id to be added to the path.
   * @param {{}}               [criteria]  Object for where clause
   * @param {{}}               [options]   Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  findOne(resource: string, id: string|number, criteria?: {}, options?: {}): Promise<any|Error> {
    return this.request('GET', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), undefined, options);
  }

  /**
   * Create a new instance for resource.
   *
   * @param {string}           resource  Resource to create
   * @param {{}}               [body]    The data to post (as Object)
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  post(resource: string, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('POST', resource, body, options);
  }

  /**
   * Update a resource.
   *
   * @param {string}           resource  Resource to update
   * @param {string|number|{}} idOrCriteria  Object for where clause, string / number for id.
   * @param {{}}               [body]    New data for provided idOrCriteria.
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  update(resource: string, idOrCriteria?: string|number|{}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PUT', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), body, options);
  }

  /**
   * Update a resource.
   *
   * @param {string}           resource   Resource to update
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [body]     New data for provided criteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  updateOne(resource: string, id: string|number, criteria?: {}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PUT', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), body, options);
  }

  /**
   * Patch a resource.
  *
   * @param {string}           resource   Resource to patch
   * @param {string|number|{}} [idOrCriteria] Object for where clause, string / number for id.
   * @param {{}}               [body]     Data to patch for provided idOrCriteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  patch(resource: string, idOrCriteria?: string|number|{}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PATCH', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), body, options);
  }

  /**
   * Patch a resource.
   *
   * @param {string}           resource   Resource to patch
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [body]     Data to patch for provided criteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  patchOne(resource: string, id: string|number, criteria?: {}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PATCH', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), body, options);
  }

  /**
   * Delete a resource.
   *
   * @param {string}           resource   The resource to delete
   * @param {string|number|{}} [idOrCriteria] Object for where clause, string / number for id.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  destroy(resource: string, idOrCriteria?: string|number|{}, options?: {}): Promise<any|Error> {
    return this.request('DELETE', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), undefined, options);
  }

  /**
   * Delete a resource.
   *
   * @param {string}           resource   The resource to delete
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  destroyOne(resource: string, id: string|number, criteria?: {}, options?: {}): Promise<any|Error> {
    return this.request('DELETE', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), undefined, options);
  }

  /**
   * Create a new instance for resource.
   *
   * @param {string}           resource  The resource to create
   * @param {{}}               [body]    The data to post (as Object)
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>} Server response as Object
   */
  create(resource: string, body?: {}, options?: {}): Promise<any|Error> {
    return this.post(resource, body, options);
  }
}

/**
 * getRequestPath
 *
 * @param {string} resource
 * @param {boolean} traditional
 * @param {(string|number|{})} [idOrCriteria]
 * @param {{}} [criteria]
 * @returns {string}
 */
function getRequestPath(resource: string, traditional: boolean, idOrCriteria?: string|number|{}, criteria?: {}) {
  let hasSlash = resource.slice(-1) === '/';

  if (typeof idOrCriteria === 'string' || typeof idOrCriteria === 'number') {
    resource = `${join(resource, String(idOrCriteria))}${hasSlash ? '/' : ''}`;
  } else {
    criteria = idOrCriteria;
  }

  if (typeof criteria === 'object' && criteria !== null) {
    resource += `?${buildQueryString(criteria, traditional)}`;
  } else if (criteria) {
    resource += `${hasSlash ? '' : '/'}${criteria}${hasSlash ? '/' : ''}`;
  }

  return resource;
}
<commit_msg>fix(rest): fix request body and fix id in request path<commit_after>import {buildQueryString, join} from 'aurelia-path';
import {HttpClient} from 'aurelia-fetch-client';
import extend from 'extend';

/**
 * Rest class. A simple rest client to fetch resources
 */
export class Rest {
  interceptor;

  /**
   * The defaults to apply to any request
   *
   * @param {{}} defaults The default fetch request options
   */
  defaults: {} = {
    headers: {
      'Accept'      : 'application/json',
      'Content-Type': 'application/json'
    }
  };

  /**
   * The client for the rest adapter
   *
   * @param {HttpClient} client The fetch client
   *
   */
  client: HttpClient;

  /**
   * The name of the endpoint it was registered under
   *
   * @param {string} endpoint The endpoint name
   */
  endpoint: string;

  /**
   * true to use the traditional URI template standard (RFC6570) when building
   * query strings from criteria objects, false otherwise. Default is false.
   *
   * @param {boolean} useTraditionalUriTemplates The flag that enables RFC6570 URI templates.
   */
  useTraditionalUriTemplates: boolean;

  /**
   * Inject the httpClient to use for requests.
   *
   * @param {HttpClient} httpClient                    The httpClient to use
   * @param {string}     endpoint                      The endpoint name
   * @param {boolean}    [useTraditionalUriTemplates]  true to use the traditional URI
   *                                                   template standard (RFC6570) when building
   *                                                   query strings from criteria objects, false
   *                                                   otherwise. Default is false.
   */
  constructor(httpClient: HttpClient, endpoint: string, useTraditionalUriTemplates?: boolean) {
    this.client   = httpClient;
    this.endpoint = endpoint;
    this.useTraditionalUriTemplates = !!useTraditionalUriTemplates;
  }

  /**
   * Make a request to the server.
   *
   * @param {string}          method     The fetch method
   * @param {string}          path       Path to the resource
   * @param {{}}              [body]     The body to send if applicable
   * @param {{}}              [options]  Fetch request options overwrites
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  request(method: string, path: string, body?: {}, options?: {}): Promise<any|Error> {
    let requestOptions = extend(true, {headers: {}}, this.defaults, options || {}, {method, body});
    let contentType    = requestOptions.headers['Content-Type'] || requestOptions.headers['content-type'];
    let interceptor    = this.interceptor;

    if (interceptor && typeof interceptor.request === 'function') {
      requestOptions = interceptor.request(requestOptions);
    }

    if (typeof body === 'object' && body !== null && contentType) {
      requestOptions.body = (/^application\/json/).test(contentType.toLowerCase())
                          ? JSON.stringify(requestOptions.body)
                          : buildQueryString(requestOptions.body);
    }

    return this.client.fetch(path, requestOptions).then((response: Response) => {
      if (response.status >= 200 && response.status < 400) {
        let result = response.json().catch(error => null);

        if (interceptor && typeof interceptor.response === 'function') {
          return result.then(res => {
            return interceptor.response(res);
          });
        }
      }

      throw response;
    });
  }

  /**
   * Find a resource.
   *
   * @param {string}                    resource  Resource to find in
   * @param {string|number|{}}          idOrCriteria  Object for where clause, string / number for id.
   * @param {{}}                        [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  find(resource: string, idOrCriteria?: string|number|{}, options?: {}): Promise<any|Error> {
    return this.request('GET', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), undefined, options);
  }

  /**
   * Find a resource.
   *
   * @param {string}           resource    Resource to find in
   * @param {string|number}    id          String / number for id to be added to the path.
   * @param {{}}               [criteria]  Object for where clause
   * @param {{}}               [options]   Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  findOne(resource: string, id: string|number, criteria?: {}, options?: {}): Promise<any|Error> {
    return this.request('GET', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), undefined, options);
  }

  /**
   * Create a new instance for resource.
   *
   * @param {string}           resource  Resource to create
   * @param {{}}               [body]    The data to post (as Object)
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  post(resource: string, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('POST', resource, body, options);
  }

  /**
   * Update a resource.
   *
   * @param {string}           resource  Resource to update
   * @param {string|number|{}} idOrCriteria  Object for where clause, string / number for id.
   * @param {{}}               [body]    New data for provided idOrCriteria.
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  update(resource: string, idOrCriteria?: string|number|{}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PUT', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), body, options);
  }

  /**
   * Update a resource.
   *
   * @param {string}           resource   Resource to update
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [body]     New data for provided criteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  updateOne(resource: string, id: string|number, criteria?: {}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PUT', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), body, options);
  }

  /**
   * Patch a resource.
  *
   * @param {string}           resource   Resource to patch
   * @param {string|number|{}} [idOrCriteria] Object for where clause, string / number for id.
   * @param {{}}               [body]     Data to patch for provided idOrCriteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  patch(resource: string, idOrCriteria?: string|number|{}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PATCH', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), body, options);
  }

  /**
   * Patch a resource.
   *
   * @param {string}           resource   Resource to patch
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [body]     Data to patch for provided criteria.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  patchOne(resource: string, id: string|number, criteria?: {}, body?: {}, options?: {}): Promise<any|Error> {
    return this.request('PATCH', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), body, options);
  }

  /**
   * Delete a resource.
   *
   * @param {string}           resource   The resource to delete
   * @param {string|number|{}} [idOrCriteria] Object for where clause, string / number for id.
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  destroy(resource: string, idOrCriteria?: string|number|{}, options?: {}): Promise<any|Error> {
    return this.request('DELETE', getRequestPath(resource, this.useTraditionalUriTemplates, idOrCriteria), undefined, options);
  }

  /**
   * Delete a resource.
   *
   * @param {string}           resource   The resource to delete
   * @param {string|number}    id         String / number for id to be added to the path.
   * @param {{}}               [criteria] Object for where clause
   * @param {{}}               [options]  Extra request options.
   *
   * @return {Promise<*>|Promise<Error>} Server response as Object
   */
  destroyOne(resource: string, id: string|number, criteria?: {}, options?: {}): Promise<any|Error> {
    return this.request('DELETE', getRequestPath(resource, this.useTraditionalUriTemplates, id, criteria), undefined, options);
  }

  /**
   * Create a new instance for resource.
   *
   * @param {string}           resource  The resource to create
   * @param {{}}               [body]    The data to post (as Object)
   * @param {{}}               [options] Extra request options.
   *
   * @return {Promise<*>} Server response as Object
   */
  create(resource: string, body?: {}, options?: {}): Promise<any|Error> {
    return this.post(resource, body, options);
  }
}

/**
 * getRequestPath
 *
 * @param {string} resource
 * @param {boolean} traditional
 * @param {(string|number|{})} [idOrCriteria]
 * @param {{}} [criteria]
 * @returns {string}
 */
function getRequestPath(resource: string, traditional: boolean, idOrCriteria?: string|number|{}, criteria?: {}) {
  let hasSlash = resource.slice(-1) === '/';

  if (typeof idOrCriteria === 'string' || typeof idOrCriteria === 'number') {
    resource = `${join(resource, String(idOrCriteria))}${hasSlash ? '/' : ''}`;
  } else {
    criteria = idOrCriteria;
  }

  if (typeof criteria === 'object' && criteria !== null) {
    if (criteria.id) {
      resource += `${hasSlash ? '' : '/'}${criteria.id}`;
      delete criteria.id;
    }

    resource += `?${buildQueryString(criteria, traditional)}`;
  } else if (criteria) {
    resource += `${hasSlash ? '' : '/'}${criteria}${hasSlash ? '/' : ''}`;
  }

  return resource;
}
<|endoftext|>


<commit_before>import path from 'path';
import lang from './lang';
import { each, merge } from 'lodash';
import querystring from './querystring';
import { NotImplemented, InvalidArgument, UnexpectedValue } from './exceptions';
import { parse } from './xml';
import models from './models';
import { coerce } from './lang';
import sort from './sort';
import lru from './lru';
import string from './string';
import fetch from 'node-fetch';

const { integer } = coerce;
const P = Promise;
const noop = () => {};
const dummylogger = {log: noop, info: noop};

////////////////////////////////////////////////////////////////////////////////

/**
 * HTTP client
 */
export const Client = class {

  /**
   * Return instance configuration defaults
   * @param void
   * @return {Object}
   */
  defaults () {
    let location = global.location || {
      protocol: 'https:',
      host: 'localhost',
    };

    return {
      language: 'en',

      // these must match your PrestaShop backend languages
      languages: {
        // ISO code => PrestaShop language id
        'en': 1,
      },

      // PrestaShop web service parameters
      webservice: {
        key: 'your-prestashop-key',
        scheme: location.protocol.slice(0, -1),
        host: location.host,
        root: '/shop/api',
      },

      // LRUCache instance; see https://www.npmjs.com/package/lru-cache
      cache: lru.instance(),

      // logger
      logger: dummylogger,

      // Fetch-related options
      fetch: {

        // the actual fetch function; facilitates testing
        algo: fetch,
      },
    };
  }

  /**
   * @param {Object} options
   */
  constructor (options={}) {
    this.options = merge(this.defaults(), options);
    this.fetch = this.options.fetch.algo;
    this.cache = this.options.cache;
    this.logger = this.options.logger;
    this.funnel = {};
  }

  /**
   * Return a Prestashop API language id
   * @property
   * @type {Number}
   */
  get language () {
    let {languages, language: isocode} = this.options;
    return languages[isocode];
  }

  /**
   * Send a GET request
   * @async Promise
   * @param {String} url
   * @param {Object} query
   * @return {Response}
   */
  get (uri, options={}) {
    let url = this.url(uri, options.query);
    let key = `${this.options.language}:GET:${url}`;
    let response = this.cache.get(key);
    let funnel = this.funnel;

    if (response) {
      return P.resolve(response);
    }

    if (funnel[key]) {
      return funnel[key];
    }

    let fopts = this.createFetchOptions({...options.fetch, method: 'GET'});

    funnel[key] = this.fetch(url, fopts).then((response) => {
      this.validateResponse(response);
      this.cache.set(key, response);
      delete funnel[key];
      return response;
    })

    .catch((e) => {
      delete funnel[key];
      throw e;
    })

    return funnel[key];
  }

  /**
   * Return a fully qualified API url
   * @param {String} uri
   * @param {String}
   */
  url (uri, query={}) {
    let {webservice} = this.options;
    let fullpath = path.join(webservice.root, uri);

    if (!lang.empty(query)) {
      query = lang.tuples(query).sort(sort.ascending(tuple => tuple[0]));
      fullpath += '?' + querystring.stringify(query);
    }

    return `${webservice.scheme}://${webservice.host}${fullpath}`;
  }

  /**
   * Return node-fetch request options
   * @param {Object} augments
   * @return {Object}
   */
  createFetchOptions (augments={}) {
    return {
      ...this.options.fetch.defaults,
      headers: this.createHeaders(),
      ...augments,
    };
  }

  /**
   * Return a dictionary containing request headers
   * @param {Object} augments
   * @return {Object}
   */
  createHeaders (augments={}) {
    let {key} = {...this.options.webservice};
    let Authorization = this.createAuthorizationHeader(key);

    return {...augments, Authorization};
  }

  /**
   * @param {Response} response
   * @return void
   * @throws Error
   */
  validateResponse (response) {
    if (!response.ok) {
      throw new UnexpectedValue('got non-2XX HTTP response');
    }
  }

  /**
   * Return an Authorization header value given a web service key
   * @param {String} key
   * @return {String}
   */
  createAuthorizationHeader (key) {
    let username = key;
    let password = '';
    let precursor = `${username}:${password}`;
    let credentials = Buffer.from(precursor).toString('base64');

    return `Basic ${credentials}`;
  }

  /**
   * @param {String} api - snake case form of a resource class name
   * @param {Object} options
   * @return {rest.Resource}
   */
  resource (api, options={}) {
    let classname = string.studly(api);
    let constructor = resources[classname];

    if (!constructor) {
      throw new InvalidArgument(`invalid root resource: "${api}"`);
    }

    return new constructor({
      ...options,
      client: this,
      logger: this.logger,
    });
  }

}

////////////////////////////////////////////////////////////////////////////////
export const resources = {};

/**
 * Resource is an HTTP-aware context that uses a Client to fetch XML payloads
 * from the PrestaShop API. It converts XML payloads into Model instances.
 */
export const Resource = resources.Resource = class {

  /**
   * Return the canonical constructor name. This is a workaround to a side
   * effect of Babel transpilation, which is that Babel mangles class names.
   * @property
   * @type {String}
   */
  static get name () {
    return 'Resource';
  }

  /**
   * Return instance configuration defaults
   * @param void
   * @return {Object}
   */
  defaults () {
    let classname = this.constructor.name;
    let api = string.snake(classname);
    let nodetype = api.slice(0, -1);
    let root = `/${api}`;
    let modelname = classname.slice(0, -1);

    //console.log({api, nodetype, root, modelname});

    return {
      client: null,
      logger: dummylogger,
      model: models[modelname],
      root: root,
      api: api,
      nodetype: nodetype,

      // model list filter function
      filter: null,

      // model list sort function
      sort: null,
    };
  }

  /**
   * @param {Object} options
   */
  constructor (options={}) {
    this.options = {...this.defaults(), ...options};
    this.client = this.options.client;
    this.logger = this.options.logger;
  }

  /**
   * Return a PrestaShop language id
   * @property
   * @type {Number}
   */
  get language () {
    return this.client.language;
  }

  /**
   * Resolve an array of Model instances
   * @async Promise
   * @param void
   * @return {Array}
   */
  list () {
    return this.client.get(this.options.root)

    .then((response) => response.clone().text())
    .then((xml) => this.parseModelIds(xml))
    .then((ids) => this.createModels(ids))
    .then((models) => {
      let {sort, filter} = this.options;

      if (filter) {
        models = models.filter(filter);
      }
      if (sort) {
        models = models.sort(sort);
      }

      return models;
    })
  }

  /**
   * Return the first member of a list() result
   * @param void
   * @return {models.Model}
   */
  first () {
    return this.list().then(models => models.shift() || null);
  }

  /**
   * Resolve a single Model instance, or null if there was an error of any kind
   * @async Promise
   * @param {mixed} id
   * @return {Model|null}
   */
  get (id) {
    let uri = `${this.options.root}/${id}`;
    let promise;

    try {
      promise = this.client.get(uri)
    }
    catch (e) {
      this.logger.log(`failed to acquire model properties on request path ${uri}`);
      this.logger.log(e.message);
      this.logger.log(e.stack);
      return P.resolve(this.createModel());
    }

    return promise.then((response) => response.clone().text())
    .then((xml) => this.parseModelAttributes(xml))
    .then((attrs) => this.createModel(attrs))
  }

  /**
   * Map a list of model ids to model instances.
   * @async Promise
   * @param {Array} ids
   * @return {Array}
   */
  createModels (ids) {
    return P.all(ids.map((id) => this.get(id)));
  }

  /**
   * Given an object containing model properties, return a Model instance.
   * @param {Object} attrs
   * @return {Model}
   */
  createModel (attrs={}) {
    let {model: constructor} = this.options;

    return new constructor({
      client: this.client,
      resource: this,
      attrs: attrs,
    });
  }

  /**
   * Given the API response payload for a list of objects, return a list of
   * model ids.
   * @async Promise
   * @param {String} xml
   * @return {Array}
   */
  parseModelIds (xml) {
    let {api, nodetype} = this.options;
    return parse.model.ids(xml, api, nodetype);
  }

  /**
   * Given the API response payload for a single domain object, return a plain
   * object that contains model properties.
   * @param {String} xml
   * @return {Object}
   */
  parseModelAttributes (xml) {
    let nodetype = this.options.nodetype;
    let ns = parse[nodetype];

    if (!ns) {
      throw new UnexpectedValue(`parser namespace not found on node type ${nodetype}`);
    }
    if (!ns.properties) {
      throw new UnexpectedValue(`model properties parser not found on node type ${nodetype}`);
    }

    return parse[nodetype].properties(xml, this.language);
  }

}

resources.Products = class extends Resource {

  /**
   * inheritdoc
   */
  static get name () {
    return 'Products';
  }
}

resources.Images = class extends Resource {

  static get name () {
    return 'Images';
  }

  /**
   * @inheritdoc
   */
  list () {
    return this.client.get(this.options.root)
    .then((response) => response.text())
    .then((xml) => this.parseImageAttributes(xml))
    .then((attrsets) => attrsets.map((attrs) => this.createModel(attrs)));
  }

  /**
   * Return a list of urls given an XML payload
   * @async Promise
   * @param {String} xml
   * @return {Array}
   */
  parseImageAttributes (xml) {
    return parse.image.properties(xml);
  }
}

resources.Manufacturers = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'Manufacturers';
  }
}

resources.Combinations = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'Combinations';
  }
}

resources.StockAvailables = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'StockAvailables';
  }
}

resources.ProductOptionValues = class extends Resource {

  /**
   * inheritdoc
   */
  static get name () {
    return 'ProductOptionValues';
  }

  /**
   * @inheritdoc
   */
  defaults () {
    return {
      ...super.defaults(),
      sort: sort.ascending(model => model.position),
    };
  }
}

export default { Client, resources };
<commit_msg>bugfix in Images#list<commit_after>import path from 'path';
import lang from './lang';
import { each, merge } from 'lodash';
import querystring from './querystring';
import { NotImplemented, InvalidArgument, UnexpectedValue } from './exceptions';
import { parse } from './xml';
import models from './models';
import { coerce } from './lang';
import sort from './sort';
import lru from './lru';
import string from './string';
import fetch from 'node-fetch';

const { integer } = coerce;
const P = Promise;
const noop = () => {};
const dummylogger = {log: noop, info: noop};

////////////////////////////////////////////////////////////////////////////////

/**
 * HTTP client
 */
export const Client = class {

  /**
   * Return instance configuration defaults
   * @param void
   * @return {Object}
   */
  defaults () {
    let location = global.location || {
      protocol: 'https:',
      host: 'localhost',
    };

    return {
      language: 'en',

      // these must match your PrestaShop backend languages
      languages: {
        // ISO code => PrestaShop language id
        'en': 1,
      },

      // PrestaShop web service parameters
      webservice: {
        key: 'your-prestashop-key',
        scheme: location.protocol.slice(0, -1),
        host: location.host,
        root: '/shop/api',
      },

      // LRUCache instance; see https://www.npmjs.com/package/lru-cache
      cache: lru.instance(),

      // logger
      logger: dummylogger,

      // Fetch-related options
      fetch: {

        // the actual fetch function; facilitates testing
        algo: fetch,
      },
    };
  }

  /**
   * @param {Object} options
   */
  constructor (options={}) {
    this.options = merge(this.defaults(), options);
    this.fetch = this.options.fetch.algo;
    this.cache = this.options.cache;
    this.logger = this.options.logger;
    this.funnel = {};
  }

  /**
   * Return a Prestashop API language id
   * @property
   * @type {Number}
   */
  get language () {
    let {languages, language: isocode} = this.options;
    return languages[isocode];
  }

  /**
   * Send a GET request
   * @async Promise
   * @param {String} url
   * @param {Object} query
   * @return {Response}
   */
  get (uri, options={}) {
    let url = this.url(uri, options.query);
    let key = `${this.options.language}:GET:${url}`;
    let response = this.cache.get(key);
    let funnel = this.funnel;

    if (response) {
      return P.resolve(response);
    }

    if (funnel[key]) {
      return funnel[key];
    }

    let fopts = this.createFetchOptions({...options.fetch, method: 'GET'});

    funnel[key] = this.fetch(url, fopts).then((response) => {
      this.validateResponse(response);
      this.cache.set(key, response);
      delete funnel[key];
      return response;
    })

    .catch((e) => {
      delete funnel[key];
      throw e;
    })

    return funnel[key];
  }

  /**
   * Return a fully qualified API url
   * @param {String} uri
   * @param {String}
   */
  url (uri, query={}) {
    let {webservice} = this.options;
    let fullpath = path.join(webservice.root, uri);

    if (!lang.empty(query)) {
      query = lang.tuples(query).sort(sort.ascending(tuple => tuple[0]));
      fullpath += '?' + querystring.stringify(query);
    }

    return `${webservice.scheme}://${webservice.host}${fullpath}`;
  }

  /**
   * Return node-fetch request options
   * @param {Object} augments
   * @return {Object}
   */
  createFetchOptions (augments={}) {
    return {
      ...this.options.fetch.defaults,
      headers: this.createHeaders(),
      ...augments,
    };
  }

  /**
   * Return a dictionary containing request headers
   * @param {Object} augments
   * @return {Object}
   */
  createHeaders (augments={}) {
    let {key} = {...this.options.webservice};
    let Authorization = this.createAuthorizationHeader(key);

    return {...augments, Authorization};
  }

  /**
   * @param {Response} response
   * @return void
   * @throws Error
   */
  validateResponse (response) {
    if (!response.ok) {
      throw new UnexpectedValue('got non-2XX HTTP response');
    }
  }

  /**
   * Return an Authorization header value given a web service key
   * @param {String} key
   * @return {String}
   */
  createAuthorizationHeader (key) {
    let username = key;
    let password = '';
    let precursor = `${username}:${password}`;
    let credentials = Buffer.from(precursor).toString('base64');

    return `Basic ${credentials}`;
  }

  /**
   * @param {String} api - snake case form of a resource class name
   * @param {Object} options
   * @return {rest.Resource}
   */
  resource (api, options={}) {
    let classname = string.studly(api);
    let constructor = resources[classname];

    if (!constructor) {
      throw new InvalidArgument(`invalid root resource: "${api}"`);
    }

    return new constructor({
      ...options,
      client: this,
      logger: this.logger,
    });
  }

}

////////////////////////////////////////////////////////////////////////////////
export const resources = {};

/**
 * Resource is an HTTP-aware context that uses a Client to fetch XML payloads
 * from the PrestaShop API. It converts XML payloads into Model instances.
 */
export const Resource = resources.Resource = class {

  /**
   * Return the canonical constructor name. This is a workaround to a side
   * effect of Babel transpilation, which is that Babel mangles class names.
   * @property
   * @type {String}
   */
  static get name () {
    return 'Resource';
  }

  /**
   * Return instance configuration defaults
   * @param void
   * @return {Object}
   */
  defaults () {
    let classname = this.constructor.name;
    let api = string.snake(classname);
    let nodetype = api.slice(0, -1);
    let root = `/${api}`;
    let modelname = classname.slice(0, -1);

    //console.log({api, nodetype, root, modelname});

    return {
      client: null,
      logger: dummylogger,
      model: models[modelname],
      root: root,
      api: api,
      nodetype: nodetype,

      // model list filter function
      filter: null,

      // model list sort function
      sort: null,
    };
  }

  /**
   * @param {Object} options
   */
  constructor (options={}) {
    this.options = {...this.defaults(), ...options};
    this.client = this.options.client;
    this.logger = this.options.logger;
  }

  /**
   * Return a PrestaShop language id
   * @property
   * @type {Number}
   */
  get language () {
    return this.client.language;
  }

  /**
   * Resolve an array of Model instances
   * @async Promise
   * @param void
   * @return {Array}
   */
  list () {
    return this.client.get(this.options.root)

    .then((response) => response.clone().text())
    .then((xml) => this.parseModelIds(xml))
    .then((ids) => this.createModels(ids))
    .then((models) => {
      let {sort, filter} = this.options;

      if (filter) {
        models = models.filter(filter);
      }
      if (sort) {
        models = models.sort(sort);
      }

      return models;
    })
  }

  /**
   * Return the first member of a list() result
   * @param void
   * @return {models.Model}
   */
  first () {
    return this.list().then(models => models.shift() || null);
  }

  /**
   * Resolve a single Model instance, or null if there was an error of any kind
   * @async Promise
   * @param {mixed} id
   * @return {Model|null}
   */
  get (id) {
    let uri = `${this.options.root}/${id}`;
    let promise;

    try {
      promise = this.client.get(uri)
    }
    catch (e) {
      this.logger.log(`failed to acquire model properties on request path ${uri}`);
      this.logger.log(e.message);
      this.logger.log(e.stack);
      return P.resolve(this.createModel());
    }

    return promise.then((response) => response.clone().text())
    .then((xml) => this.parseModelAttributes(xml))
    .then((attrs) => this.createModel(attrs))
  }

  /**
   * Map a list of model ids to model instances.
   * @async Promise
   * @param {Array} ids
   * @return {Array}
   */
  createModels (ids) {
    return P.all(ids.map((id) => this.get(id)));
  }

  /**
   * Given an object containing model properties, return a Model instance.
   * @param {Object} attrs
   * @return {Model}
   */
  createModel (attrs={}) {
    let {model: constructor} = this.options;

    return new constructor({
      client: this.client,
      resource: this,
      attrs: attrs,
    });
  }

  /**
   * Given the API response payload for a list of objects, return a list of
   * model ids.
   * @async Promise
   * @param {String} xml
   * @return {Array}
   */
  parseModelIds (xml) {
    let {api, nodetype} = this.options;
    return parse.model.ids(xml, api, nodetype);
  }

  /**
   * Given the API response payload for a single domain object, return a plain
   * object that contains model properties.
   * @param {String} xml
   * @return {Object}
   */
  parseModelAttributes (xml) {
    let nodetype = this.options.nodetype;
    let ns = parse[nodetype];

    if (!ns) {
      throw new UnexpectedValue(`parser namespace not found on node type ${nodetype}`);
    }
    if (!ns.properties) {
      throw new UnexpectedValue(`model properties parser not found on node type ${nodetype}`);
    }

    return parse[nodetype].properties(xml, this.language);
  }

}

resources.Products = class extends Resource {

  /**
   * inheritdoc
   */
  static get name () {
    return 'Products';
  }
}

resources.Images = class extends Resource {

  static get name () {
    return 'Images';
  }

  /**
   * @inheritdoc
   */
  list () {
    return this.client.get(this.options.root)
    .then((response) => response.clone().text())
    .then((xml) => this.parseImageAttributes(xml))
    .then((attrsets) => attrsets.map((attrs) => this.createModel(attrs)));
  }

  /**
   * Return a list of urls given an XML payload
   * @async Promise
   * @param {String} xml
   * @return {Array}
   */
  parseImageAttributes (xml) {
    return parse.image.properties(xml);
  }
}

resources.Manufacturers = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'Manufacturers';
  }
}

resources.Combinations = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'Combinations';
  }
}

resources.StockAvailables = class extends Resource {

  /**
   * @inheritdoc
   */
  static get name () {
    return 'StockAvailables';
  }
}

resources.ProductOptionValues = class extends Resource {

  /**
   * inheritdoc
   */
  static get name () {
    return 'ProductOptionValues';
  }

  /**
   * @inheritdoc
   */
  defaults () {
    return {
      ...super.defaults(),
      sort: sort.ascending(model => model.position),
    };
  }
}

export default { Client, resources };
<|endoftext|>


<commit_before>/*jslint node: true */
/* global -Promise */
"use strict";

var _ = require('./lodash');

module.exports = function (Domain) {
    Domain.prototype.send = function (to, from, body, options) {
        var _this = this;
        if (!to || to.length < 1) {
            return;
        }
        return _this.emit(to, body, new _this.Message({
            domain: _this
            , to: to
            , from: from
            , body: body
            , options: options
        }));
    };

    Domain.prototype.Message.prototype.send = function () {
        var domain = this.domain;
        return domain.send.apply(domain, arguments);
    };
};
<commit_msg>Allow sending objects as messages.<commit_after>/*jslint node: true */
/* global -Promise */
"use strict";

var _ = require('./lodash');

module.exports = function (Domain) {
    Domain.prototype.send = function (to, from, body, options) {
        var _this = this;
        var msg;
        if (_.isArray(to) || _.isString(to)) {
            msg = new _this.Message({
                domain: _this
                , to: to
                , from: from
                , body: body
                , options: options
            });
        } else {
            msg = new _this.Message(_.extend({}, to, {
                domain: _this
            }));
        }
        if (!msg.to || msg.to.length < 1) {
            return void 0;
        }
        return _this.emit(msg.to, msg.body, msg);
    };

    Domain.prototype.Message.prototype.send = function () {
        var domain = this.domain;
        return domain.send.apply(domain, arguments);
    };
};
<|endoftext|>


<commit_before>import passport from 'passport';
import passportLocal from 'passport-local';
import { OAuth2Strategy } from 'passport-google-oauth';
import { User } from '../data/models';
import config from '../config';
import {
  ACTIVE,
  PENDING,
} from '../constants/userStatus';

/* eslint-disable */
const LocalStrategy = passportLocal.Strategy;
passport.use(
  'google',
  new OAuth2Strategy(
    {
      clientID: config.google.clientID,
      clientSecret: config.google.clientSecret,
      callbackURL: config.google.callbackURL,
      passReqToCallback: true,
    },
    (req, accessToken, refreshToken, profile, done) => {
      const { displayName, id, emails } = profile;
      const email = (emails[0].value || '').toLowerCase();
      const google = {
        id: profile.id,
        token: accessToken,
        refreshToken,
      }
      const userObj = {
        google,
        name: displayName,
        email: email,
        status: ACTIVE,
      }
      const message = 'Successfully Logged In';
      process.nextTick(async () => {
        try {
          if (!req.user) {
            const userWithEmail = await User.findOne({ email });
            if (userWithEmail && userWithEmail.status === PENDING) {
              const u = Object.assign({}, userObj, userWithEmail);
              await u.save();
              return done(null, {
                success: true,
                message,
                user: u,
              });
            }
            const user = await User.findOne({ 'google.id': profile.id });
            if (user) {
              user.google = google;
              await user.save();
              return done(null, {
                success: true,
                message,
                user,
              });
            }
          }
          const user = Object.assign({}, userObj, req.user);
          await user.save();
          return done(null, {
            success: true,
            message,
            user,
          });
        } catch (error) {
           return done(null, false, {
            success: false,
            message: error,
          });
        }
      });
    },
  ),
);

passport.serializeUser((req, done) => {
  done(null, req.user._id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, req) => {
    done(err, req);
  });
});
<commit_msg>try to fix new user<commit_after>import passport from 'passport';
import passportLocal from 'passport-local';
import { OAuth2Strategy } from 'passport-google-oauth';
import { User } from '../data/models';
import config from '../config';
import {
  ACTIVE,
  PENDING,
} from '../constants/userStatus';

/* eslint-disable */
const LocalStrategy = passportLocal.Strategy;
passport.use(
  'google',
  new OAuth2Strategy(
    {
      clientID: config.google.clientID,
      clientSecret: config.google.clientSecret,
      callbackURL: config.google.callbackURL,
      passReqToCallback: true,
    },
    (req, accessToken, refreshToken, profile, done) => {
      const { displayName, id, emails } = profile;
      const email = (emails[0].value || '').toLowerCase();
      const google = {
        id: profile.id,
        token: accessToken,
        refreshToken,
      }
      const userObj = {
        google,
        name: displayName,
        email: email,
        status: ACTIVE,
      }
      const message = 'Successfully Logged In';
      process.nextTick(async () => {
        try {
          if (!req.user) {
            const userWithEmail = await User.findOne({ email });
            if (userWithEmail && userWithEmail.status === PENDING) {
              const u = Object.assign({}, userObj, userWithEmail);
              await u.save();
              return done(null, {
                success: true,
                message,
                user: u,
              });
            }
            const user = await User.findOne({ 'google.id': profile.id });
            if (user) {
              user.google = google;
              await user.save();
              return done(null, {
                success: true,
                message,
                user,
              });
            }
          }
          const user = Object.assign({}, userObj);
          await user.save();
          return done(null, {
            success: true,
            message,
            user,
          });
        } catch (error) {
           return done(null, false, {
            success: false,
            message: error,
          });
        }
      });
    },
  ),
);

passport.serializeUser((req, done) => {
  done(null, req.user._id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, req) => {
    done(err, req);
  });
});
<|endoftext|>


<commit_before>import { capitalize, isArray, isString } from 'lodash'
import { supportedAuthProviders } from '../constants'
/**
 * @description Get correct login method and params order based on provided credentials
 * @param {Object} firebase - Internal firebase object
 * @param {String} providerName - Name of Auth Provider (i.e. google, github, facebook, twitter)
 * @param {Array|String} scopes - List of scopes to add to auth provider
 */
export const createAuthProvider = (firebase, providerName, scopes) => {
  // TODO: Verify scopes are valid before adding
  // Verify providerName is valid
  if (supportedAuthProviders.indexOf(providerName) === -1) {
    throw new Error(`${providerName} is not a valid Auth Provider`)
  }
  const provider = new firebase.auth[`${capitalize(providerName)}AuthProvider`]()
  provider.addScope('email')
  if (scopes) {
    if (isArray(scopes)) {
      scopes.forEach(scope => {
        provider.addScope(scope)
      })
    }
    if (isString(scopes)) {
      provider.addScope(scopes)
    }
  }
  return provider
}

/**
 * @description Get correct login method and params order based on provided credentials
 * @param {Object} firebase - Internal firebase object
 * @param {Object} credentials - Login credentials
 * @param {String} credentials.email - Email to login with (only needed for email login)
 * @param {String} credentials.password - Password to login with (only needed for email login)
 * @param {String} credentials.provider - Provider name such as google, twitter (only needed for 3rd party provider login)
 * @param {String} credentials.type - Popup or redirect (only needed for 3rd party provider login)
 * @param {String} credentials.token - Custom or provider token
 * @param {String} credentials.scopes - Scopes to add to provider (i.e. email)
 */
export const getLoginMethodAndParams = (firebase, {email, password, provider, type, token, scopes}) => {
  if (provider) {
    if (token) {
      return {
        method: 'signInWithCredential',
        params: [ provider, token ]
      }
    }
    const authProvider = createAuthProvider(firebase, provider, scopes)
    if (type === 'popup') {
      return {
        method: 'signInWithPopup',
        params: [ authProvider ]
      }
    }
    return {
      method: 'signInWithRedirect',
      params: [ authProvider ]
    }
  }
  if (token) {
    return {
      method: 'signInWithCustomToken',
      params: [ token ]
    }
  }
  return {
    method: 'signInWithEmailAndPassword',
    params: [ email, password ]
  }
}

export default { getLoginMethodAndParams }
<commit_msg>Lowercase provider name before comparing.<commit_after>import { capitalize, isArray, isString } from 'lodash'
import { supportedAuthProviders } from '../constants'
/**
 * @description Get correct login method and params order based on provided credentials
 * @param {Object} firebase - Internal firebase object
 * @param {String} providerName - Name of Auth Provider (i.e. google, github, facebook, twitter)
 * @param {Array|String} scopes - List of scopes to add to auth provider
 */
export const createAuthProvider = (firebase, providerName, scopes) => {
  // TODO: Verify scopes are valid before adding
  // Verify providerName is valid
  if (supportedAuthProviders.indexOf(providerName.toLowerCase()) === -1) {
    throw new Error(`${providerName} is not a valid Auth Provider`)
  }
  const provider = new firebase.auth[`${capitalize(providerName)}AuthProvider`]()
  provider.addScope('email')
  if (scopes) {
    if (isArray(scopes)) {
      scopes.forEach(scope => {
        provider.addScope(scope)
      })
    }
    if (isString(scopes)) {
      provider.addScope(scopes)
    }
  }
  return provider
}

/**
 * @description Get correct login method and params order based on provided credentials
 * @param {Object} firebase - Internal firebase object
 * @param {Object} credentials - Login credentials
 * @param {String} credentials.email - Email to login with (only needed for email login)
 * @param {String} credentials.password - Password to login with (only needed for email login)
 * @param {String} credentials.provider - Provider name such as google, twitter (only needed for 3rd party provider login)
 * @param {String} credentials.type - Popup or redirect (only needed for 3rd party provider login)
 * @param {String} credentials.token - Custom or provider token
 * @param {String} credentials.scopes - Scopes to add to provider (i.e. email)
 */
export const getLoginMethodAndParams = (firebase, {email, password, provider, type, token, scopes}) => {
  if (provider) {
    if (token) {
      return {
        method: 'signInWithCredential',
        params: [ provider, token ]
      }
    }
    const authProvider = createAuthProvider(firebase, provider, scopes)
    if (type === 'popup') {
      return {
        method: 'signInWithPopup',
        params: [ authProvider ]
      }
    }
    return {
      method: 'signInWithRedirect',
      params: [ authProvider ]
    }
  }
  if (token) {
    return {
      method: 'signInWithCustomToken',
      params: [ token ]
    }
  }
  return {
    method: 'signInWithEmailAndPassword',
    params: [ email, password ]
  }
}

export default { getLoginMethodAndParams }
<|endoftext|>