@grucloud/core
Version:
GruCloud core, generate infrastructure code
426 lines (421 loc) • 13.2 kB
JavaScript
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);
}),
])();