lemon-core
Version:
Lemon Serverless Micro-Service Platform
548 lines • 21.7 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DummyDynamoService = exports.DynamoService = void 0;
/**
* `dynamo-service.ts`
* - common service for dynamo
*
* @author Steve Jung <steve@lemoncloud.io>
* @date 2019-08-28 initial version
* @date 2019-10-16 cleanup and optimize log
* @date 2019-11-19 optimize 404 error case, and normalize key.
* @date 2019-12-10 support `DummyDynamoService.listItems()` for mocks
*
* @copyright (C) 2019 LemonCloud Co Ltd. - All Rights Reserved.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const engine_1 = __importStar(require("../../engine/"));
const tools_1 = require("../../tools/");
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
const client_dynamodb_2 = require("@aws-sdk/client-dynamodb");
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
const client_dynamodb_streams_1 = require("@aws-sdk/client-dynamodb-streams");
const NS = engine_1.$U.NS('DYNA', 'green'); // NAMESPACE TO BE PRINTED.
//* create(or get) instance.
const instance = () => {
const region = 'ap-northeast-2';
return DynamoService.instance(region);
};
//* normalize dynamo properties.
const normalize = (data) => {
if (data === '')
return null;
if (!data)
return data;
if (Array.isArray(data))
return data.map(normalize);
if (typeof data == 'object') {
return Object.keys(data).reduce((O, key) => {
const val = data[key];
O[key] = normalize(val);
return O;
}, {});
}
return data;
};
/**
* class: `DynamoService`
* - basic CRUD service for AWS DynamoDB.
*/
class DynamoService {
constructor(options) {
/**
* say hello of identity.
*/
this.hello = () => `dynamo-service:${this.options.tableName}`;
// eslint-disable-next-line prettier/prettier
(0, engine_1._inf)(NS, `DynamoService(${options.tableName}/${options.idName}${options.sortName ? '/' : ''}${options.sortName || ''})...`);
if (!options.tableName)
throw new Error('.tableName is required');
if (!options.idName)
throw new Error('.idName is required');
this.options = options;
}
/**
* simple instance maker.
* @param region (default as `ap-northeast-2`)
*/
static instance(region) {
region = `${region || 'ap-northeast-2'}`;
const cfg = (0, tools_1.awsConfig)(engine_1.default, region);
const dynamo = new client_dynamodb_2.DynamoDBClient(cfg); // Low-level DynamoDB client
const $client = () => __awaiter(this, void 0, void 0, function* () {
const credentials = yield cfg.credentials;
const dynamo = new client_dynamodb_2.DynamoDBClient(Object.assign(Object.assign({}, cfg), { credentials })); // Low-level DynamoDB client
return lib_dynamodb_1.DynamoDBDocumentClient.from(dynamo); // High-level Document client
});
const dynamostr = new client_dynamodb_streams_1.DynamoDBStreamsClient(cfg); // DynamoDB Streams client
return { dynamo, dynamostr, dynamodoc: $client };
}
/**
* prepare `CreateTable` payload.
*
* @param ReadCapacityUnits
* @param WriteCapacityUnits
* @param StreamEnabled
*/
prepareCreateTable(ReadCapacityUnits = 1, WriteCapacityUnits = 1, StreamEnabled = true) {
const { tableName, idName, sortName, idType, sortType } = this.options;
(0, engine_1._log)(NS, `prepareCreateTable(${tableName}, ${idName}, ${sortName || ''}, ${sortType || ''})...`);
const keyType = (type = '') => {
type = type || 'string';
switch (type) {
case 'number':
return 'N';
case 'string':
return 'S';
default:
break;
}
throw new Error(`invalid key-type:${type}`);
};
//* prepare payload.
const payload = {
TableName: tableName,
KeySchema: [
{
AttributeName: idName,
KeyType: 'HASH',
},
],
AttributeDefinitions: [
{
AttributeName: idName,
AttributeType: keyType(idType),
},
],
ProvisionedThroughput: { ReadCapacityUnits, WriteCapacityUnits },
StreamSpecification: { StreamEnabled, StreamViewType: client_dynamodb_1.StreamViewType.NEW_AND_OLD_IMAGES },
};
//* set sort-key.
if (sortName) {
payload.KeySchema.push({
AttributeName: sortName,
KeyType: 'RANGE',
});
payload.AttributeDefinitions.push({
AttributeName: sortName,
AttributeType: keyType(sortType),
});
}
//* returns.
return payload;
}
/**
* prepare `DeleteTable` payload.
*/
prepareDeleteTable() {
const { tableName } = this.options;
(0, engine_1._log)(NS, `prepareDeleteTable(${tableName})...`);
return {
TableName: tableName,
};
}
/**
* prepare `SaveItem` payload.
*
* @param id partition-key
* @param item
*/
prepareSaveItem(id, item) {
const { tableName, idName, sortName } = this.options;
// _log(NS, `prepareSaveItem(${tableName})...`);
// item && _log(NS, '> item =', item);
if (sortName && item[sortName] === undefined)
throw new Error(`.${sortName} is required. ${idName}:${id}`);
delete item[idName]; // clear the saved id.
const node = Object.assign({ [idName]: id }, item); // copy
const data = normalize(node);
//* prepare payload.
const payload = {
TableName: tableName,
Item: data,
};
return payload;
}
/**
* prepare `Key` by id + sort key.
*
* @param id partition-key
* @param sort sort-key
*/
prepareItemKey(id, sort) {
const { tableName, idName, sortName } = this.options;
if (!id)
throw new Error(`@id is required - prepareItemKey(${tableName}/${idName})`);
// _log(NS, `prepareItemKey(${tableName}/${id}/${sort || ''})...`);
//* prepare payload.
const payload = {
TableName: tableName,
Key: {
[idName]: id,
},
};
if (sortName) {
if (sort === undefined)
throw new Error(`@sort is required. ${idName}:${id}`);
payload.Key[sortName] = sort;
}
return payload;
}
/**
* prepare `UpdateItem` payload.
*
* @param id partition-key
* @param sort sort-key
* @param $update update set
* @param $increment increment set.
*/
prepareUpdateItem(id, sort, $update, $increment) {
const debug = 0 ? true : false;
const { tableName, idName, sortName } = this.options;
debug && (0, engine_1._log)(NS, `prepareUpdateItem(${tableName}/${id}/${sort || ''})...`);
debug && $update && (0, engine_1._log)(NS, `> $update =`, engine_1.$U.json($update));
debug && $increment && (0, engine_1._log)(NS, `> $increment =`, engine_1.$U.json($increment));
const Key = this.prepareItemKey(id, sort).Key;
const norm = (_) => `${_}`.replace(/[.\\:\/$]/g, '_');
//* prepare payload.
let payload = Object.entries($update).reduce((memo, [key, value]) => {
//* ignore if key
if (key === idName || key === sortName)
return memo;
const key2 = norm(key);
value = normalize(value);
if (value && Array.isArray(value.setIndex)) {
//* support set items in list
value.setIndex.forEach(([idx, value], seq) => {
if (idx !== undefined && value !== undefined) {
memo.ExpressionAttributeNames[`#${key2}`] = key;
memo.ExpressionAttributeValues[`:${key2}_${seq}_`] = value;
memo.UpdateExpression.SET.push(`#${key2}[${idx}] = :${key2}_${seq}_`);
}
});
}
else if (value && Array.isArray(value.removeIndex)) {
//* support removing items from list
value.removeIndex.forEach((idx) => {
if (idx !== undefined) {
memo.ExpressionAttributeNames[`#${key2}`] = key2;
memo.UpdateExpression.REMOVE.push(`#${key2}[${idx}]`);
}
});
}
else {
//* prepare update-expression.
memo.ExpressionAttributeNames[`#${key2}`] = key;
memo.ExpressionAttributeValues[`:${key2}`] = value === '' ? null : value;
memo.UpdateExpression.SET.push(`#${key2} = :${key2}`);
debug && (0, engine_1._log)(NS, '>> ' + `#${key} :=`, typeof value, engine_1.$U.json(value));
}
return memo;
}, {
TableName: tableName,
Key,
UpdateExpression: { SET: [], REMOVE: [], ADD: [], DELETE: [] },
ExpressionAttributeNames: {},
ExpressionAttributeValues: {},
ConditionExpression: null,
ReturnValues: 'UPDATED_NEW',
});
//* prepare increment update.
if ($increment) {
//* increment field.
payload = Object.entries($increment).reduce((memo, [key, value]) => {
const key2 = norm(key);
if (!Array.isArray(value)) {
memo.ExpressionAttributeNames[`#${key2}`] = key;
memo.ExpressionAttributeValues[`:${key2}`] = value;
memo.UpdateExpression.ADD.push(`#${key2} :${key2}`);
debug && (0, engine_1._log)(NS, '>> ' + `#${key2} = #${key2} + :${value}`);
}
else {
memo.ExpressionAttributeNames[`#${key2}`] = key; // target attribute name
memo.ExpressionAttributeValues[`:${key2}`] = value; // list to append like `[1,2,3]`
memo.ExpressionAttributeValues[`:${key2}_0`] = []; // empty array if not exists.
memo.UpdateExpression.SET.push(`#${key2} = list_append(if_not_exists(#${key2}, :${key2}_0), :${key2})`);
debug && (0, engine_1._log)(NS, '>> ' + `#${key2} = #${key2} + ${value}`);
}
return memo;
}, payload);
}
//* build final update expression.
payload.UpdateExpression = Object.keys(payload.UpdateExpression) // ['SET', 'REMOVE', 'ADD', 'DELETE']
.map(actionName => {
const actions = payload.UpdateExpression[actionName];
return actions.length > 0 ? `${actionName} ${actions.join(', ')}` : ''; // e.g 'SET #a = :a, #b = :b'
})
.filter(exp => exp.length > 0)
.join(' ');
(0, engine_1._log)(NS, `> UpdateExpression[${id}] =`, payload.UpdateExpression);
return payload;
}
/**
* create-table
*
* @param ReadCapacityUnits
* @param WriteCapacityUnits
*/
createTable(ReadCapacityUnits = 1, WriteCapacityUnits = 1) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `createTable(${ReadCapacityUnits}, ${WriteCapacityUnits})...`);
const payload = this.prepareCreateTable(ReadCapacityUnits, WriteCapacityUnits);
return instance()
.dynamo.send(new client_dynamodb_2.CreateTableCommand(payload))
.then(res => {
(0, engine_1._log)(NS, '> createTable.res =', res);
return res;
});
});
}
/**
* delete-table
*
*/
deleteTable() {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `deleteTable()...`);
const payload = this.prepareDeleteTable();
return instance()
.dynamo.send(new client_dynamodb_2.DeleteTableCommand(payload))
.then(res => {
(0, engine_1._log)(NS, '> deleteTable.res =', res);
return res;
});
});
}
/**
* read-item
* - read whole data of item.
*
* @param id
* @param sort
*/
readItem(id, sort) {
return __awaiter(this, void 0, void 0, function* () {
const { tableName, idName, sortName } = this.options;
// _log(NS, `readItem(${id})...`);
const itemKey = this.prepareItemKey(id, sort);
// _log(NS, `> pkey[${id}${sort ? '/' : ''}${sort || ''}] =`, $U.json(itemKey));
const dynamodoc = yield instance().dynamodoc();
return dynamodoc
.send(new lib_dynamodb_1.GetCommand(itemKey))
.then(res => {
// _log(NS, '> readItem.res =', $U.json(res));
if (!res.Item)
throw new Error(`404 NOT FOUND - ${idName}:${id}${sort ? '/' : ''}${sort || ''}`);
return res.Item;
})
.catch((e) => {
if (`${e.message}` == 'Requested resource not found')
throw new Error(`404 NOT FOUND - ${idName}:${id}`);
throw e;
});
});
}
/**
* save-item
* - save whole data with param (use update if partial save)
*
* **WARN** overwrited if exists.
*
* @param id
* @param item
*/
saveItem(id, item) {
return __awaiter(this, void 0, void 0, function* () {
const { tableName, idName, sortName } = this.options;
// _log(NS, `saveItem(${id})...`);
const payload = this.prepareSaveItem(id, item);
// _log(NS, '> payload :=', payload);
const dynamodoc = yield DynamoService.instance().dynamodoc();
return dynamodoc
.send(new lib_dynamodb_1.PutCommand(payload))
.then(res => {
(0, engine_1._log)(NS, '> saveItem.res =', engine_1.$U.json(res));
return payload.Item;
})
.catch((e) => {
if (`${e.message}` == 'Requested resource not found')
throw new Error(`404 NOT FOUND - ${idName}:${id}`);
throw e;
});
});
}
/**
* delete-item
* - destroy whole data of item.
*
* @param id
* @param sort
*/
deleteItem(id, sort) {
return __awaiter(this, void 0, void 0, function* () {
// _log(NS, `deleteItem(${id})...`);
const payload = this.prepareItemKey(id, sort);
const dynamodoc = yield DynamoService.instance().dynamodoc();
return dynamodoc
.send(new lib_dynamodb_1.DeleteCommand(payload))
.then(res => {
(0, engine_1._log)(NS, '> deleteItem.res =', engine_1.$U.json(res));
return null;
})
.catch((e) => {
if (`${e.message}` == 'Requested resource not found')
return {};
throw e;
});
});
}
/**
* update-item (or increment-item)
* - update or create if not exists.
*
* @param id
* @param sort
* @param updates
* @param increments
*/
updateItem(id, sort, updates, increments) {
return __awaiter(this, void 0, void 0, function* () {
const { idName } = this.options;
// _log(NS, `updateItem(${id})...`);
const payload = this.prepareUpdateItem(id, sort, updates, increments);
const dynamodoc = yield DynamoService.instance().dynamodoc();
return dynamodoc
.send(new lib_dynamodb_1.UpdateCommand(payload))
.then(res => {
(0, engine_1._log)(NS, `> updateItem[${id}].res =`, engine_1.$U.json(res));
const attr = res.Attributes;
const $key = Object.assign({}, payload.Key);
return Object.assign(attr, $key);
})
.catch((e) => {
if (`${e.message}` == 'Requested resource not found')
throw new Error(`404 NOT FOUND - ${idName}:${id}`);
throw e;
});
});
}
}
exports.DynamoService = DynamoService;
/**
* export to test..
*/
DynamoService.normalize = normalize;
/** ****************************************************************************************************************
* Dummy Dynamo Service
** ****************************************************************************************************************/
/**
* class: `DummyDynamoService`
* - service in-memory dummy data
*/
class DummyDynamoService extends DynamoService {
constructor(dataFile, options) {
super(options);
this.buffer = {};
/**
* say hello()
*/
this.hello = () => `dummy-dynamo-service:${this.options.tableName}`;
(0, engine_1._log)(NS, `DummyDynamoService(${dataFile || ''})...`);
if (!dataFile)
throw new Error('@dataFile(string) is required!');
const dummy = (0, tools_1.loadDataYml)(dataFile);
this.load(dummy.data);
}
load(data) {
const { idName } = this.options;
if (!data || !Array.isArray(data))
throw new Error('@data should be array!');
data.map(item => {
const id = `${item[idName] || ''}`;
this.buffer[id] = item;
});
}
/**
* ONLY FOR DUMMY
* - send list of data.
*
* @param page page number starts from 1
* @param limit limit of count.
*/
listItems(page, limit) {
return __awaiter(this, void 0, void 0, function* () {
page = engine_1.$U.N(page, 1);
limit = engine_1.$U.N(limit, 2);
const keys = Object.keys(this.buffer);
const total = keys.length;
const list = keys.slice((page - 1) * limit, page * limit).map(_ => this.buffer[_]);
return { page, limit, total, list };
});
}
readItem(id, sort) {
return __awaiter(this, void 0, void 0, function* () {
const { idName } = this.options;
const item = this.buffer[id];
if (item === undefined)
throw new Error(`404 NOT FOUND - ${idName}:${id}`);
return Object.assign({ [idName]: id }, item);
});
}
saveItem(id, item) {
return __awaiter(this, void 0, void 0, function* () {
const { idName } = this.options;
this.buffer[id] = normalize(item);
return Object.assign({ [idName]: id }, this.buffer[id]);
});
}
deleteItem(id, sort) {
return __awaiter(this, void 0, void 0, function* () {
delete this.buffer[id];
return null;
});
}
updateItem(id, sort, updates, increments) {
return __awaiter(this, void 0, void 0, function* () {
const { idName } = this.options;
const item = this.buffer[id];
if (item === undefined)
throw new Error(`404 NOT FOUND - ${idName}:${id}`);
this.buffer[id] = Object.assign(Object.assign({}, item), normalize(updates));
return Object.assign({ [idName]: id }, this.buffer[id]);
});
}
}
exports.DummyDynamoService = DummyDynamoService;
//# sourceMappingURL=dynamo-service.js.map
;