Home Reference Source

packages/skygear-core/lib/cloud/index.js

/**
 * Copyright 2015 Oursky Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import registry from './registry';
import _skyconfig from './skyconfig';
import { pool as _pool } from './pg';
import { settings as _settings } from './settings';
import crypto from 'crypto';
import _CloudCodeContainer from './container';
import { createLogger as _createLogger } from './logging';
import {
  ErrorCodes as _ErrorCodes,
  SkygearError as _SkygearError
} from '../error';

const nameCntMap = {};

function incSuffix(name) {
  if (nameCntMap[name] !== undefined) {
    nameCntMap[name] += 1;
    return name + '-' + nameCntMap[name];
  }
  nameCntMap[name] = 0;
  return name;
}

function funcName(func) {
  let name = func.name;
  if (!name) {
    const hash = crypto.createHash('sha');
    hash.update(func.toString());
    name = hash.digest('hex');
  }
  return incSuffix(name);
}

/**
 * You can register the cloud code as lambda, with JSON input and output.
 *
 * This is a convenient way to inject custom logic to Skygear server and can
 * be called easily with `skygear.lambda` in the client side.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');
 * skygearCloud.op('greeting', function(param) {
 *     return {
 *       'content': 'Hello, ' + param.name,
 *     };
 * }, {
 *     userRequired: false
 * });
 *
 * @param {String} name - lambda name
 * @param {function(param:Object, options:*)} func - function to be registered
 * @param {Object} [options]
 * @param {Boolean} [options.keyRequired] - require api key to call the lambda
 * @param {Boolean} [options.userRequired] - require user to call the lambda
 */
export function op(name, func, options = {}) {
  // move authRequired to keyRequired, if keyRequired not specified
  if (options.keyRequired === undefined &&
      options.authRequired !== undefined) {
    const logger = _createLogger('plugin').child({tag: 'plugin'});
    logger.warn('authRequired is deprecated, use keyRequired instead');
    options.keyRequired = options.authRequired;
  }

  registry.registerOp(name, func, options);
}

/**
 * You can register the cloud code to get run at specific time intervals like
 * a cron job.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');
 * skygearCloud.every('@daily', function() {
 *   console.log('Meow');
 * });
 *
 * @param  {String} cron - time interval in cron job syntax
 * @param  {function(param: *)} func - function to be registered
 * @param  {Object} [options]
 */
export function every(cron, func, options = {}) {
  // TODO: check cron format
  options.spec = cron;
  let name = funcName(func);
  registry.registerTimer(name, func, options);
}

/**
 * You can register the cloud code to run at skygear life cycle event.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');
 * skygearCloud.event('after-plugins-ready', function() {
 *   console.log('Meow');
 * });
 *
 * @param {String} name - skygear life cycle event name
 * @param {function(param: *)} func - function to be registered
 * @param {Object} [options]
 */
export function event(name, func, options = {}) {
  registry.registerEvent(name, func, options);
}

/**
 * You can configure the cloud code as an HTTP handler, which can respond to
 * requests coming from outside the SDK. A custom HTTP endpoint can be
 * created using the `handler` function.
 *
 * A custom HTTP endpoint can be useful for the followings:
 *
 * - receiving requests from outside the Skygear SDK
 * - allowing a third party webhook to call upon (e.g. payment
 *   service)
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.handler('handler1', function(req) {
 *     // cloud code handling the request
 *     return 'String';
 * }, {
 *     method: ['GET', 'POST'],
 *     userRequired: false
 * });
 *
 * @example
 * skygearCloud.handler('private', function(req, options) {
 *     // cloud code handling the request
 *     const {
 *       context,
 *       container  // a cloud code container for the current request context
 *     } = options;
 *     return {
 *       status: 'ok',
 *       user_id: context.user_id // only available if userRequired=true
 *     };
 * }, {
 *     method: ['GET', 'POST'],
 *     userRequired: true
 * });
 *
 * @param {string} path - The path of the handler to be mount.
 * @param {function(request:*, options:*): object} func - function to be
 * registered.
 * @param {Object} [options]
 * @param {String[]|String} [options.method] - handler methods, e.g. GET, POST
 * @param {Boolean} [options.keyRequired] - require api key to call the lambda
 * @param {Boolean} [options.userRequired] - require user to call the lambda
 */
export function handler(path, func, options = {}) {
  const logger = _createLogger('plugin').child({tag: 'plugin'});
  if (typeof options.method === 'string') {
    options.method = [options.method];
  }

  // move authRequired to keyRequired, if keyRequired not specified
  if (options.keyRequired === undefined &&
      options.authRequired !== undefined) {
    logger.warn('authRequired is deprecated, use keyRequired instead');
    options.keyRequired = options.authRequired;
  }

  registry.registerHandler(path, func, options);
}

/**
 * Authentication Provider allows a plugin to authenticate user based on
 * credentials from a third-party. The Authentication Provider responds to
 * Skygear Server whether the credentials are accepted.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 *
 * skygearCloud.provides('auth', 'com.facebook', Provider);
 *
 * @param {string} providerType - Type of the provider, only auth is supported
 * now.
 * @param {string} providerID - unique identifier to the provider
 * @param {object} ProviderCls - a provider class
 * @param {object} [options] - options for setting
 */
export function provides(providerType, providerID, ProviderCls, options = {}) {
  const provider = new ProviderCls();
  registry.registerProvider(providerType, providerID, provider, options);
}

/**
 * BaseAuthProvider provides example interface that an AuthProvider should
 * provide.
 *
 * To create an actual AuthProvider, you can extend the BaseAuthProvider and
 * provide `login`, `logout` and `info` functions.
 *
 * @example
 *  class Provider extends skygearCloud.BaseAuthProvider {
 *    login(authData) {
 *      console.log(authData);
 *      // third-party API call
 *      return {
 *        principal_id: 'identifier',
 *        auth_data: {...}
 *      }
 *    }
 *
 *    logout(authData) {
 *      console.log(authData);
 *    }
 *
 *    info(authData) {
 *      console.log(authData);
 *    }
 *  }
 *
 */
export class BaseAuthProvider {
  async handleAction(action, param) {
    if (!this[action]) {
      throw new Error(`Provider not support action: ${param.action}`);
    }
    return this[action](param);
  }
 /**
  * this method must be overridden by subclass.
  * @abstract
  */
  async login(authData) { // eslint-disable-line
    throw new Error(
      'Subclass of BaseAuthProvider should implement login method.');
  }
 /**
  * this method must be overridden by subclass.
  * @abstract
  */
  async logout(authData) { // eslint-disable-line
    throw new Error(
      'Subclass of BaseAuthProvider should implement logout method.');
  }
 /**
  * this method must be overridden by subclass.
  * @abstract
  */
  async info(authData) { // eslint-disable-line
    throw new Error(
      'Subclass of BaseAuthProvider should implement info method.');
  }
}

/**
 * You can register the cloud code to run at database event.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');
 * skygearCloud.hook('before-save', function(newRecord, oldRecord, pool) {
 *   console.log('Meow');
 *   return newRecord;
 * });
 *
 * @param {String} name - function name
 * @param {function(newRecord:*, oldRecord:*, pool:*, options: *)} func -
 * function to be registered
 * @param {Object} [options]
 * @param {String} [options.type] - record type
 * @param {String} [options.trigger] - type of database event that trigger the
 * hook
 * @param {Boolean} [options.async] - true if the function triggered
 * asynchronously
 */
export function hook(name, func, options = {}) {
  registry.registerHook(name, func, options);
}

/**
 * beforeSave — executes decorated function before a record save operation
 * occurs
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.beforeSave('note', function(record, original, pool, options) {
 *     // cloud code handling the request
 *     return;
 * }, {
 *     async: true
 * });
 *
 * @param {string} recordType - The type of the record.
 * @param {function(record: lib/record.js~Record, originalRecord: lib/record.js~Record, pool: pool, options: *): *} func - function to be registered.
 * @param {Object} [options]
 * @param {Boolean} [options.async] - true if the function triggered
 * asynchronously
 */
export function beforeSave(recordType, func, options = {}) {
  let name = funcName(func);
  options.type = recordType;
  options.trigger = 'beforeSave';
  registry.registerHook(name, func, options);
}

/**
 * afterSave — executes decorated function after a record save operation
 * occurs
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.afterSave('note', function(record, original, pool, options) {
 *     // cloud code handling the request
 *     return;
 * }, {
 *     async: true
 * });
 *
 * @param {string} recordType - The type of the record.
 * @param {function(record: lib/record.js~Record, originalRecord: lib/record.js~Record, pool: pool, options: *): *} func - function to be registered.
 * @param {Object} [options]
 * @param {Boolean} [options.async] - true if the function triggered
 */
export function afterSave(recordType, func, options = {}) {
  let name = funcName(func);
  options.type = recordType;
  options.trigger = 'afterSave';
  registry.registerHook(name, func, options);
}

/**
 * beforeDelete — executes decorated function before a record delete operation
 * occurs
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.beforeDelete('note', function(record, original, pool, options) {
 *     // cloud code handling the request
 *     return;
 * }, {
 *     async: true
 * });
 *
 * @param {string} recordType - The type of the record.
 * @param {function(record: lib/record.js~Record, originalRecord: lib/record.js~Record, pool: pool, options: *): *} func - function to be registered.
 * @param {Object} [options]
 * @param {Boolean} [options.async] - true if the function triggered
 */
export function beforeDelete(recordType, func, options = {}) {
  let name = funcName(func);
  options.type = recordType;
  options.trigger = 'beforeDelete';
  registry.registerHook(name, func, options);
}

/**
 * afterDelete — executes decorated function after a record delete operation
 * occurs
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.afterDelete('note', function(record, original, pool, options) {
 *     // cloud code handling the request
 *     return;
 * }, {
 *     async: true
 * });
 *
 * @param {string} recordType - The type of the record.
 * @param {function(record: lib/record.js~Record, originalRecord: lib/record.js~Record, pool: pool, options: *): *} func - function to be registered.
 * @param {Object} [options]
 * @param {Boolean} [options.async] - true if the function triggered
 */
export function afterDelete(recordType, func, options = {}) {
  let name = funcName(func);
  options.type = recordType;
  options.trigger = 'afterDelete';
  registry.registerHook(name, func, options);
}

/**
 * staticAsset — declare certain path to be serve in static asset.
 *
 * All asset will be serve with the prefix `/static`. i.e. if the mount ping
 * is declare as `/css`. The final URL for requesting assets will be
 * `/static/css`.
 *
 * @example
 * const skygearCloud = require('skygear/cloud');¬
 * skygearCloud.staticAsset('/styles', function() {
 *     // Return the absolute path of the static assets directory
 *     // http://<yourapp>.skygeario.com/static/styles will be serving files
 *     // located at '<project_path>/css`
 *     return __dirname + '/css/';
 * });
 *
 * @param {string} mountPoint - the target mount point
 * @param {function(): string} func - function to return the absolute path of
 * the static assets.
 */
export function staticAsset(mountPoint, func) {
  registry.registerAsset(mountPoint, func);
}

/**
 * Import and config the cloud code plugin module.
 *
 * This function will load the specified module by name using the `require`
 * function. The specified module must exists or an error is thrown.
 *
 * It is expected that the module will exports a function called `includeme`.
 * This function will also call the `includeme` function to config the module.
 */
export async function configModule(moduleName, options) {
  const logger = _createLogger('plugin').child({tag: 'plugin'});
  const { ignoreWarning } = options || {};
  const { includeme } = require(moduleName);
  if (includeme !== undefined) {
    const settings = {};
    await includeme(module.exports, settings);
  } else if (ignoreWarning !== true) {
    logger.warn(`The ${moduleName} module does not export the includeme` +
    ' function. This function is required to config the module.');
  }
}

export const skyconfig = _skyconfig;
export const pool = _pool;
export { poolConnect } from './pg';
export { SkygearRequest, SkygearResponse } from './transport/common';
export const settings = _settings;
export const CloudCodeContainer = _CloudCodeContainer;
export { getContainer, publishEventsToChannels } from './container';
export const ErrorCodes = _ErrorCodes;
export const SkygearError = _SkygearError;
export const log = _createLogger;
export { getSigner } from './asset';