UNPKG

@grucloud/core

Version:

GruCloud core, generate infrastructure code

426 lines (421 loc) 13.2 kB
const assert = require("assert"); const { assign, tryCatch, pipe, tap, switchCase, eq, get, map, not, } = require("rubico"); const { isEmpty, defaultsDeep, flatten, unless, includes, } = require("rubico/x"); const logger = require("./logger")({ prefix: "CoreClient" }); const { tos } = require("./tos"); const identity = (x) => x; const { retryCall, retryCallOnError } = require("./Retry"); const { getByNameCore, logError, axiosErrorToJSON } = require("./Common"); const shouldRetryOnExceptionCreateDefault = pipe([ get("error.response.status"), (status) => pipe([() => [409, 429], includes(status)])(), ]); const shouldRetryOnExceptionDeleteDefault = pipe([ get("error.response.status"), (status) => pipe([() => [409, 429], includes(status)])(), ]); module.exports = CoreClient = ({ spec, type, config, lives, axios, pathGet = ({ id }) => `/${id}`, pathCreate = () => `/`, pathDelete = ({ id }) => `/${id}`, pathUpdate = ({ id }) => `/${id}`, pathList = () => `/`, verbGet = "GET", verbList = "GET", verbCreate = "POST", verbUpdate = "PATCH", isInstanceUp = not(isEmpty), isInstanceDown = isEmpty, listIsExpectedException = () => false, configDefault = ({ name, properties }) => ({ name, ...properties, }), findName = ({}) => (live) => pipe([ () => live, get("name"), tap((name) => { assert(name, `missing name in live ${JSON.stringify(live)}`); }), ])(), findId = () => get("id"), findTargetId = () => get("id"), decorate = () => identity, //TODO curry onResponseGet = get("data"), onResponseList = () => identity, onResponseCreate = () => identity, onResponseDelete = identity, onResponseUpdate = identity, isDefault, managedByOther, cannotBeDeleted, shouldRetryOnExceptionGetById = shouldRetryOnExceptionCreateDefault, shouldRetryOnExceptionList = shouldRetryOnExceptionCreateDefault, shouldRetryOnExceptionCreate = shouldRetryOnExceptionCreateDefault, shouldRetryOnExceptionDelete = shouldRetryOnExceptionDeleteDefault, onCreateFilterPayload = identity, onCreateExpectedException = pipe([() => false]), findDependencies, isUpById, isDownById, getList, getById, getByName, create, destroy, }) => pipe([ tap((params) => { //assert(lives); assert(spec); assert(type); assert(config, "config"); }), () => ({ spec, type, config, findId, findDependencies, isInstanceUp, isInstanceDown, findName, isDefault, managedByOther, cannotBeDeleted: cannotBeDeleted || managedByOther, isUpById, isDownById, getList, getById, getByName, create, destroy, configDefault, axios, }), tap((params) => { assert(true); }), defaultsDeep({ getById: ({ name, id }) => tryCatch( pipe([ tap(() => { logger.info( `getById ${JSON.stringify({ type: spec.type, name, id })}` ); assert(!isEmpty(id), `getById ${type}: invalid id`); assert(!spec.listOnly); }), () => pathGet({ id }), (path) => retryCallOnError({ name: `getById type ${spec.type}, name: ${name}, path: ${path}`, fn: () => axios.request(path, { method: verbGet, }), shouldRetryOnException: shouldRetryOnExceptionGetById, config, }), get("data"), (data) => onResponseGet({ id, data }), tap((params) => { assert(true); }), decorate({ axios, lives }), tap((data) => { //logger.debug(`getById result: ${tos(data)}`); }), ]), switchCase([ eq(get("response.status"), 404), () => {}, (error) => { logError("getById", error); throw axiosErrorToJSON(error); }, () => {}, ]) )(), getList: tryCatch( pipe([ tap((params) => { //logger.debug(`getList ${spec.groupType}`); }), pathList, tap((params) => { assert(true); }), unless(Array.isArray, (path) => [path]), map.pool(5, (path) => pipe([ () => retryCallOnError({ name: `getList type: ${spec.groupType}, path ${path}`, fn: () => axios.request(path, { method: verbList, }), isExpectedException: listIsExpectedException, shouldRetryOnException: shouldRetryOnExceptionList, config, }), tap((params) => { assert(true); }), get("data"), tap((data) => { // logger.debug(`getList ${spec.groupType}, ${tos(data)}`); }), onResponseList({ axios, lives, path }), switchCase([ Array.isArray, map(decorate({ axios, lives })), pipe([ tap((params) => { assert(true); }), () => [], ]), ]), ])() ), flatten, tap((params) => { assert(true); }), ]), (error) => { logError(`getList ${spec.type}`, error); throw axiosErrorToJSON(error); } ), }), (client) => pipe([ () => client, defaultsDeep({ getByName: getByNameCore(client), isUpById: pipe([client.getById, isInstanceUp]), isDownById: pipe([ client.getById, tap((params) => { assert(true); }), isInstanceDown, ]), }), ])(), assign({ create: ({ isUpById }) => ({ name, payload, dependencies = () => ({}) }) => tryCatch( pipe([ tap(() => { // logger.debug( // `create ${type}/${name}, payload: ${tos(payload)}` // ); assert(name); assert(payload); assert(!spec.singleton); assert(!spec.listOnly); }), () => ({ dependencies: dependencies(), name, payload }), pathCreate, tap((path) => { logger.info(`create ${spec.groupType}/${name}, path: ${path}`); }), (path) => pipe([ () => retryCallOnError({ name: `create ${spec.type}/${name}`, isExpectedException: onCreateExpectedException, shouldRetryOnException: shouldRetryOnExceptionCreate, fn: () => axios.request(path, { method: verbCreate, data: onCreateFilterPayload(payload), }), config: { ...config, repeatCount: 0 }, }), tap((result) => { // logger.info( // `created ${spec.type}/${name}, status: ${ // result.status // }, data: ${tos(result.data)}` // ); }), switchCase([ eq(get("response.status"), 409), () => { logger.error( `create: already created ${type}/${name}, 409` ); //TODO get by id ? }, pipe([ tap((result) => { assert(result); }), get("data"), onResponseCreate({ name, payload, axios }), (data) => pipe([ () => data, findTargetId({ path }), tap((id) => { logger.debug( `create: ${spec.type}/${name} findTargetId ${id}` ); if (!id) { assert( id, `no target id from result: ${tos(data)}` ); } }), (id) => pipe([ () => retryCall({ name: `create isUpById ${spec.type}/${name}, id: ${id}`, fn: () => isUpById({ type: spec.type, name, id }), config, }), () => data, spec.create.postCreate({ name, id, dependencies: dependencies(), // TODO //config, }), ])(), ])(), ]), ]), ])(), ]), (error) => { logError(`create ${type}/${name}`, error); throw axiosErrorToJSON(error); } )(), update: ({ isUpById }) => ({ id, name, payload, dependencies = () => ({}) }) => tryCatch( pipe([ tap(() => { logger.info(`update ${tos({ type, name, id })}`); }), () => ({ id, name, payload, dependencies: dependencies() }), tap((params) => { assert(true); }), pathUpdate, (path) => retryCallOnError({ name: `update type ${spec.type}, path: ${path}`, fn: () => axios.request(path, { method: verbUpdate, data: payload, }), isExpectedResult: () => true, config: { ...config, repeatCount: 0 }, }), get("data"), onResponseUpdate, tap(() => retryCall({ name: `update type: ${spec.type}, name: ${name}, isDownById`, fn: () => isUpById({ id, name }), config, }) ), spec.destroy.postDestroy({ name, id }), tap((data) => { logger.info(`update ${tos({ name, type, id, data })} updated`); }), ]), (error) => { logError(`update ${type}/${name}`, error); throw axiosErrorToJSON(error); } )(), destroy: ({ isDownById }) => ({ id, name, dependencies = () => ({}) }) => tryCatch( pipe([ tap(() => { //logger.info(`destroy ${tos({ type, name, id })}`); assert(!spec.singleton); assert(!spec.listOnly); assert(!isEmpty(id), `destroy ${type}: invalid id`); }), () => ({ id, name, dependencies: dependencies() }), pathDelete, (path) => retryCallOnError({ name: `destroy type ${spec.groupType}, path: ${path}`, fn: () => axios.delete(path), isExpectedResult: () => true, config: { ...config, repeatCount: 0 }, isExpectedException: eq(get("response.status"), 404), shouldRetryOnException: shouldRetryOnExceptionDelete, }), get("data"), onResponseDelete, tap(() => retryCall({ name: `destroy type: ${spec.type}, name: ${name}, isDownById`, fn: () => isDownById({ id, name }), config, }) ), tap((data) => { // logger.info( // `destroy ${tos({ name, type, id, data })} destroyed` // ); }), ]), (error) => { logError(`delete ${type}/${name}`, error); throw axiosErrorToJSON(error); } )(), }), tap((params) => { assert(true); }), ])();