Skip to content

Commit

Permalink
refactor: remove unnecessary props (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kikobeats authored Apr 15, 2024
1 parent be2c6bb commit ec2181a
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 337 deletions.
7 changes: 4 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ const JSONB = require('json-buffer')
const createKeys = require('./keys')
const createPlans = require('./plans')

module.exports = ({ serialize = JSONB.stringify, deserialize = JSONB.parse, redis = new Map() } = {}) => {
const plans = createPlans({ serialize, deserialize, redis })
const keys = createKeys({ serialize, deserialize, redis, plans })
module.exports = ({ serialize = JSONB.stringify, deserialize = JSONB.parse, redis = new Map(), prefix = '' } = {}) => {
let _keys
const plans = createPlans({ serialize, deserialize, redis, prefix, keys: () => _keys })
const keys = (_keys = createKeys({ serialize, deserialize, redis, plans, prefix }))
return { keys, plans }
}
89 changes: 35 additions & 54 deletions src/keys.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,80 @@
'use strict'

const { pick, uid, validateKey, assert, assertMetadata } = require('./util')
const { pick, uid, assert, assertMetadata } = require('./util')

const KEY_PREFIX = 'key_'
const KEY_FIELDS = ['name', 'description', 'enabled', 'value', 'plan']
const KEY_FIELDS_OBJECT = ['metadata']

module.exports = ({ serialize, deserialize, plans, redis } = {}) => {
module.exports = ({ serialize, deserialize, plans, redis, prefix } = {}) => {
/**
* Create a key.
*
* @param {Object} options - The options for creating a plan.
* @param {string} options.name - The name of the key.
* @param {string} [options.value] - The value of the key.
* @param {string} [options.plan] - The id of the plan associated.
* @param {string} [options.description] - The description of the key.
* @param {string} [options.enabled] - Whether the key is enabled or not.
* @param {Object} [options.metadata] - Any extra information can be attached here.
*
* @returns {Object} The created key.
*/
const create = async (opts = {}) => {
assert(typeof opts.name === 'string' && opts.name.length > 0, 'The argument `name` is required.')
opts.metadata = assertMetadata(opts.metadata)
const key = pick(opts, KEY_FIELDS.concat(KEY_FIELDS_OBJECT))
key.id = await uid({ redis, prefix: KEY_PREFIX, size: 5 })
const key = { enabled: opts.enabled ?? true }
const metadata = assertMetadata(opts.metadata)
if (metadata) key.metadata = metadata
key.createdAt = key.updatedAt = Date.now()
key.value = await uid({ redis, size: 16 })
if (key.enabled === undefined) key.enabled = true
if (opts.plan) await plans.retrieve(opts.plan, { throwError: true })
return (await redis.setnx(key.id, serialize(key))) && key
const value = opts.value ?? (await uid({ redis, size: 16 }))
if (opts.plan) {
await plans.retrieve(opts.plan, { throwError: true })
key.plan = opts.plan
}
await redis.set(prefixKey(value), serialize(key), 'NX')
return Object.assign({ value }, key)
}

/**
* Retrieve a key by id.
* Retrieve a key by value.
*
* @param {string} keyId - The id of the key.
* @param {string} value - The value of the key.
* @param {Object} [options] - The options for retrieving a key.
* @param {boolean} [options.validate=true] - Validate if the plan id is valid.
* @param {boolean} [options.throwError=false] - Throw an error if the plan does not exist.
*
* @returns {Object|null} The key object, null if it doesn't exist.
*/
const retrieve = async (keyId, { throwError = false, validate = true } = {}) => {
const key = await redis.get(getKey(keyId, { validate }))
if (throwError) {
assert(key !== null, `The key \`${keyId}\` does not exist.`)
}
return deserialize(key)
const retrieve = async (value, { throwError = false } = {}) => {
const key = await redis.get(prefixKey(value))
if (throwError) assert(key !== null, () => `The key \`${value}\` does not exist.`)
else if (key === null) return null
return Object.assign({ value }, deserialize(key))
}

/**
* Delete a key by id.
* Delete a key by value.
*
* @param {string} keyId - The id of the key.
* @param {string} value - The value of the key.
*
* @returns {boolean} Whether the key was deleted or not.
*/
const del = async keyId => {
const key = await retrieve(keyId, { verify: true })
if (key !== null && typeof key.plan === 'string') {
const plan = await plans.retrieve(key.plan, {
throwError: true,
validate: false
})
assert(plan === null, `The key \`${keyId}\` is associated with the plan \`${getKey.plan}\``)
}
const isDeleted = (await redis.del(getKey(keyId, { verify: true }))) === 1
assert(isDeleted, `The key \`${keyId}\` does not exist.`)
const del = async value => {
const isDeleted = (await redis.del(prefixKey(value))) === 1
assert(isDeleted, () => `The key \`${value}\` does not exist.`)
return isDeleted
}

/**
* Update a key by id.
* Update a key by value.
*
* @param {string} keyId - The id of the plan.
* @param {string} value - The value of the plan.
* @param {Object} options - The options for updating a plan.
* @param {string} [options.name] - The name of the key.
* @param {string} [options.value] - The value of the key.
* @param {string} [options.description] - The description of the key.
* @param {string} [options.enabled] - Whether the key is enabled or not.
* @param {object} [options.metadata] - Any extra information can be attached here.
*
* @returns {Object} The updated plan.
*/
const update = async (keyId, opts) => {
const currentKey = await retrieve(keyId, { throwError: true })
const update = async (value, opts) => {
const currentKey = await retrieve(value, { throwError: true })
const metadata = Object.assign({}, currentKey.metadata, assertMetadata(opts.metadata))
const key = Object.assign(currentKey, pick(opts, KEY_FIELDS), {
updatedAt: Date.now()
})
const key = Object.assign(currentKey, { updatedAt: Date.now() }, pick(opts, ['enabled', 'value', 'plan']))
if (Object.keys(metadata).length) key.metadata = metadata
if (key.plan) await plans.retrieve(key.plan, { throwError: true })
return (await redis.set(keyId, serialize(key))) && key
return (await redis.set(prefixKey(value), serialize(key))) && key
}

/**
Expand All @@ -101,13 +83,12 @@ module.exports = ({ serialize, deserialize, plans, redis } = {}) => {
* @returns {Array} The list of keys.
*/
const list = async () => {
const keyIds = await redis.keys(`${KEY_PREFIX}*`)
return Promise.all(keyIds.map(keyIds => retrieve(keyIds, { validate: false })))
const allKeys = await redis.keys(prefixKey('*'))
const keyValues = allKeys.map(key => key.replace(prefixKey(''), ''))
return Promise.all(keyValues.map(keyValues => retrieve(keyValues)))
}

const getKey = validateKey({ prefix: KEY_PREFIX })
const prefixKey = key => `${prefix}key_${key}`

return { create, retrieve, del, update, list }
return { create, retrieve, del, update, list, prefixKey }
}

module.exports.KEY_PREFIX = KEY_PREFIX
133 changes: 75 additions & 58 deletions src/plans.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,119 @@
'use strict'

const { pick, uid, validateKey, assert, assertMetadata } = require('./util')
const { pick, assert, assertMetadata } = require('./util')

const PLAN_PREFIX = 'plan_'
const PLAN_QUOTA_PERIODS = ['day', 'week', 'month']
const PLAN_FIELDS = ['name', 'description']
const PLAN_FIELDS_OBJECT = ['quota', 'throttle', 'metadata']

module.exports = ({ serialize, deserialize, redis } = {}) => {
module.exports = ({ serialize, deserialize, redis, keys, prefix } = {}) => {
/**
* Create a plan.
*
* @param {Object} options - The options for creating a plan.
* @param {string} options.name - The name of the plan.
* @param {string} [options.description] - The description of the plan.
* @param {number} [options.quota] - The quota of the plan.
* @param {string} [options.quota.period] - The time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH".
* @param {number} [options.quota.limit] - The target maximum number of requests that can be made in a given time period.
* @param {Object} [options.throttle] - The throttle of the plan.
* @param {number} [options.throttle.burstLimit] - The burst limit of the plan.
* @param {number} [options.throttle.rateLimit] - The rate limit of the plan.
* @param {string} options.id - The id of the plan.
* @param {number} [options.limit] - The target maximum number of requests that can be made in a given time period.
* @param {string} [options.period] - The time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH".
* @param {number} [options.burst] - The burst limit of the plan.
* @param {number} [options.rate] - The rate limit of the plan.
* @param {Object} [options.metadata] - Any extra information can be attached here.
*
* @returns {Object} The plan object.
*/
const create = async (opts = {}) => {
assert(typeof opts.name === 'string' && opts.name.length > 0, 'The argument `name` is required.')
assert(
PLAN_QUOTA_PERIODS.includes(opts.quota?.period),
`The argument \`quota.period\` must be ${PLAN_QUOTA_PERIODS.map(period => `\`${period}\``).join(' or ')}.`
assert(typeof opts.id === 'string' && opts.id.length > 0, () => 'The argument `id` must be a string.')
assert(!/\s/.test(opts.id), () => 'The argument `id` cannot contain whitespace.')
const plan = Object.assign(
{
limit: assert(
typeof opts.limit === 'number' && opts.limit > 0 && opts.limit,
() => 'The argument `limit` must be a positive number.'
),
period: assert(
typeof opts.period === 'string' && opts.period.length > 0 && opts.period,
() => 'The argument `period` must be a string.'
)
},
pick(opts, ['rate', 'burst'])
)
assert(opts.quota.limit > 0, 'The argument `quota.limit` must be a positive number.')
opts.metadata = assertMetadata(opts.metadata)
const plan = pick(opts, PLAN_FIELDS.concat(PLAN_FIELDS_OBJECT))
plan.id = await uid({ redis, prefix: PLAN_PREFIX, size: 5 })
const metadata = assertMetadata(opts.metadata)
if (metadata) plan.metadata = metadata
plan.createdAt = plan.updatedAt = Date.now()
return (await redis.setnx(plan.id, serialize(plan))) && plan
await redis.set(prefixKey(opts.id), serialize(plan), 'NX')
return Object.assign({ id: opts.id }, plan)
}

/**
* Retrieve a plan by id
*
* @param {string} planId - The id of the plan.
* @param {string} id - The id of the plan.
* @param {Object} [options] - The options for retrieving a plan.
* @param {boolean} [options.validate=true] - Validate if the plan id is valid.
* @param {boolean} [options.throwError=false] - Throw an error if the plan does not exist.
*
* @returns {Object} The plan.
*/
const retrieve = async (planId, { throwError = false, validate = true } = {}) => {
const plan = await redis.get(getKey(planId, { validate }))
if (throwError) {
assert(plan !== null, `The plan \`${planId}\` does not exist.`)
}
return deserialize(plan)
const retrieve = async (id, { throwError = false } = {}) => {
const plan = await redis.get(prefixKey(id))
if (throwError) assert(plan !== null, () => `The plan \`${id}\` does not exist.`)
else if (plan === null) return null
return Object.assign({ id }, deserialize(plan))
}

/**
* Delete a plan by id.
*
* @param {string} planId - The id of the plan.
* @param {string} id - The id of the plan.
* @param {Object} [options] - The options for deleting a plan.
*
* @returns {boolean} Whether the plan was deleted or not.
*/
const del = async planId => {
const isDeleted = (await redis.del(getKey(planId, { validate: true }))) === 1
assert(isDeleted, `The plan \`${planId}\` does not exist.`)
const del = async id => {
const allKeys = await keys().list()
const key = allKeys.find(key => key.plan === id)
assert(key === undefined, () => `The plan \`${id}\` is associated with the key \`${key.value}\`.`)
const isDeleted = (await redis.del(prefixKey(id))) === 1
assert(isDeleted, () => `The plan \`${id}\` does not exist.`)
return isDeleted
}

/**
* Update a plan by id.
*
* @param {string} planId - The id of the plan.
* @param {string} id - The id of the plan.
* @param {Object} options - The options for updating a plan.
* @param {string} [options.name] - The name of the plan.
* @param {string} [options.description] - The description of the plan.
* @param {number} [options.quota] - The quota of the plan.
* @param {string} [options.quota.period] - The time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH".
* @param {number} [options.quota.limit] - The target maximum number of requests that can be made in a given time period.
* @param {Object} [options.throttle] - The throttle of the plan.
* @param {number} [options.throttle.burstLimit] - The burst limit of the plan.
* @param {number} [options.throttle.rateLimit] - The rate limit of the plan.
* @param {number} [options.limit] - The target maximum number of requests that can be made in a given time period.
* @param {string} [options.period] - The time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH".
* @param {number} [options.burst] - The burst limit of the plan.
* @param {number} [options.rate] - The rate limit of the plan.
* @param {object} [options.metadata] - Any extra information can be attached here.
*
* @returns {Object} The updated plan.
*/
const update = async (planId, opts) => {
const currentPlan = await retrieve(planId, { throwError: true })
const quota = Object.assign(currentPlan.quota, opts.quota)
const update = async (id, opts) => {
const currentPlan = await retrieve(id, { throwError: true })
const metadata = Object.assign({}, currentPlan.metadata, assertMetadata(opts.metadata))
const plan = Object.assign(currentPlan, pick(opts, PLAN_FIELDS), {
quota,
updatedAt: Date.now()
})

const plan = Object.assign(
currentPlan,
{
updatedAt: Date.now()
},
pick(opts, ['rate', 'burst'])
)

if (opts.limit) {
plan.limit = assert(
typeof opts.limit === 'number' && opts.limit > 0 && opts.limit,
() => 'The argument `limit` must be a positive number.'
)
}

if (opts.period) {
plan.period = assert(
typeof opts.period === 'string' && opts.period.length > 0 && opts.period,
() => 'The argument `period` must be a string.'
)
}

if (Object.keys(metadata).length) plan.metadata = metadata
return (await redis.set(planId, serialize(plan))) && plan
return (await redis.set(prefixKey(id), serialize(plan))) && plan
}

/**
Expand All @@ -105,13 +122,13 @@ module.exports = ({ serialize, deserialize, redis } = {}) => {
* @returns {Array} The list of plans.
*/
const list = async () => {
const planIds = await redis.keys(`${PLAN_PREFIX}*`)
return Promise.all(planIds.map(planId => retrieve(planId, { validate: false })))
const allPlans = await redis.keys(prefixKey('*'))
const planIds = allPlans.map(key => key.replace(prefixKey(''), ''))
const result = await Promise.all(planIds.map(planId => retrieve(planId)))
return result
}

const getKey = validateKey({ prefix: PLAN_PREFIX })
const prefixKey = key => `${prefix}plan_${key}`

return { create, del, retrieve, update, list }
return { create, del, retrieve, update, list, prefixKey }
}

module.exports.PLAN_PREFIX = PLAN_PREFIX
24 changes: 9 additions & 15 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,22 @@ const pick = (obj, keys) => {
return result
}

/**
* Assert a condition, or throw an error if the condition is falsy.
* @param {*} value - The value to assert.
* @param {string} message - The error message.
*/
const assert = (value, message) =>
value ||
(() => {
throw new TypeError(message)
throw new TypeError(message())
})()

const validateKey =
({ prefix }) =>
(id, { validate = true } = {}) => {
if (!validate) return id
if (!String(id).startsWith(prefix)) {
throw new TypeError(`The id \`${id}\` must to start with \`${prefix}\`.`)
}
return id
}

const assertMetadata = metadata => {
if (metadata) {
assert(isPlainObject(metadata), 'The metadata must be a flat object.')
assert(isPlainObject(metadata), () => 'The metadata must be a flat object.')
Object.keys(metadata).forEach(key => {
assert(!isPlainObject(metadata[key]), `The metadata field '${key}' can't be an object.`)
assert(!isPlainObject(metadata[key]), () => `The metadata field '${key}' can't be an object.`)
if (metadata[key] === undefined) delete metadata[key]
})
return Object.keys(metadata).length ? metadata : undefined
Expand All @@ -60,6 +55,5 @@ module.exports = {
assert,
assertMetadata,
pick,
uid,
validateKey
uid
}
Loading

0 comments on commit ec2181a

Please sign in to comment.