diff --git a/README.md b/README.md index a309993..fb8aabe 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The **options** accepted are: - `id`string: The id of the plan, it cannot contain whitespaces. - `period`string: The time window which the limit applies. It accepts [ms](https://www.npmjs.com/package/ms) syntax. - `limit`number: The target maximum number of requests that can be made in a given time period. -- `metadata`object: A flat object containing additional information. +- `metadata`object: A flat object containing additional information. Pass `null` or `''` to remove all the metadata fields. Any other field provided will be omitted. @@ -142,7 +142,7 @@ The **options** accepted are: - `value`string: The value of the key, being a base58 16 length key generated by default. - `enabled`string: It determines if the key is active, being `true` by default. -- `metadata`object: A flat object containing additional information. +- `metadata`object: A flat object containing additional information. Pass `null` or `''` to remove all the metadata fields. Any other field provided will be omitted. diff --git a/src/assert.js b/src/assert.js new file mode 100644 index 0000000..c28a5d3 --- /dev/null +++ b/src/assert.js @@ -0,0 +1,10 @@ +const { errors } = require('./error') + +module.exports = (condition, code, args = () => []) => { + return ( + condition || + (() => { + throw errors[code](args) + })() + ) +} diff --git a/src/error.js b/src/error.js index 2a74dd0..5115b92 100644 --- a/src/error.js +++ b/src/error.js @@ -1,7 +1,5 @@ 'use strict' -const { isPlainObject } = require('./util') - class OpenKeyError extends Error { constructor (props) { super() @@ -26,24 +24,4 @@ const errors = [ return acc }, {}) -const assert = (condition, code, args = () => []) => { - return ( - condition || - (() => { - throw errors[code](args) - })() - ) -} - -const assertMetadata = metadata => { - if (metadata) { - assert(isPlainObject(metadata), 'ERR_METADATA_NOT_FLAT_OBJECT') - Object.keys(metadata).forEach(key => { - assert(!isPlainObject(metadata[key]), 'ERR_METADATA_INVALID', () => [key]) - if (metadata[key] === undefined) delete metadata[key] - }) - return Object.keys(metadata).length ? metadata : false - } -} - -module.exports = { errors, assert, assertMetadata } +module.exports = { errors } diff --git a/src/keys.js b/src/keys.js index aa9f82f..c5c0cdf 100644 --- a/src/keys.js +++ b/src/keys.js @@ -1,7 +1,8 @@ 'use strict' const { pick, uid } = require('./util') -const { assert, assertMetadata } = require('./error') +const metadata = require('./metadata') +const assert = require('./assert') module.exports = ({ serialize, deserialize, plans, redis, prefix } = {}) => { /** @@ -17,8 +18,8 @@ module.exports = ({ serialize, deserialize, plans, redis, prefix } = {}) => { */ const create = async (opts = {}) => { const key = { enabled: opts.enabled ?? true } - const metadata = assertMetadata(opts.metadata) if (metadata) key.metadata = metadata + metadata(key, opts) key.createdAt = key.updatedAt = Date.now() const value = opts.value ?? (await uid({ redis, size: 16 })) if (opts.plan) { @@ -70,10 +71,8 @@ module.exports = ({ serialize, deserialize, plans, redis, prefix } = {}) => { * @returns {Object} The updated plan. */ 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, { updatedAt: Date.now() }, pick(opts, ['enabled', 'value', 'plan'])) - if (Object.keys(metadata).length) key.metadata = metadata + let key = await retrieve(value, { throwError: true }) + key = Object.assign(metadata(key, opts), { updatedAt: Date.now() }, pick(opts, ['enabled', 'value', 'plan'])) if (key.plan) await plans.retrieve(key.plan, { throwError: true }) return (await redis.set(prefixKey(value), await serialize(key))) && key } diff --git a/src/metadata.js b/src/metadata.js new file mode 100644 index 0000000..7669aa0 --- /dev/null +++ b/src/metadata.js @@ -0,0 +1,38 @@ +'use strict' + +const { isPlainObject } = require('./util') +const assert = require('./assert') + +const assertMetadata = metadata => { + if (metadata) { + assert(isPlainObject(metadata), 'ERR_METADATA_NOT_FLAT_OBJECT') + Object.keys(metadata).forEach(key => { + assert(!isPlainObject(metadata[key]), 'ERR_METADATA_INVALID', () => [key]) + if (metadata[key] === undefined) delete metadata[key] + }) + + return Object.keys(metadata).length ? metadata : false + } +} + +const clean = metadata => { + for (const [key, value] of Object.entries(metadata)) { + if ([null, ''].includes(value)) delete metadata[key] + } + return metadata +} + +const merge = (metadata, newMetadata) => { + if (newMetadata === null) return null + const mergedMetadata = Object.assign({}, metadata, assertMetadata(newMetadata)) + return clean(mergedMetadata) +} + +const mutate = (obj, opts) => { + const metadata = merge(obj.metadata, opts.metadata) + if (metadata === null || Object.keys(metadata).length === 0) delete obj.metadata + else obj.metadata = metadata + return obj +} + +module.exports = mutate diff --git a/src/plans.js b/src/plans.js index acd1d85..35d509c 100644 --- a/src/plans.js +++ b/src/plans.js @@ -1,6 +1,7 @@ 'use strict' -const { assert, assertMetadata } = require('./error') +const assert = require('./assert') +const metadata = require('./metadata') module.exports = ({ serialize, deserialize, redis, keys, prefix } = {}) => { /** @@ -24,8 +25,7 @@ module.exports = ({ serialize, deserialize, redis, keys, prefix } = {}) => { 'ERR_PLAN_INVALID_PERIOD' ) } - const metadata = assertMetadata(opts.metadata) - if (metadata) plan.metadata = metadata + metadata(plan, opts) plan.createdAt = plan.updatedAt = Date.now() const isCreated = (await redis.set(prefixKey(opts.id), await serialize(plan), 'NX')) === 'OK' assert(isCreated, 'ERR_PLAN_ALREADY_EXIST', () => [opts.id]) @@ -78,8 +78,7 @@ module.exports = ({ serialize, deserialize, redis, keys, prefix } = {}) => { * @returns {Object} The updated plan. */ const update = async (id, opts) => { - const plan = await retrieve(id, { throwError: true }) - const metadata = Object.assign({}, plan.metadata, assertMetadata(opts.metadata)) + let plan = await retrieve(id, { throwError: true }) if (opts.limit) { plan.limit = assert(typeof opts.limit === 'number' && opts.limit > 0 && opts.limit, 'ERR_PLAN_INVALID_LIMIT') @@ -92,9 +91,8 @@ module.exports = ({ serialize, deserialize, redis, keys, prefix } = {}) => { ) } - plan.updatedAt = Date.now() + plan = Object.assign(metadata(plan, opts), { updatedAt: Date.now() }) - if (Object.keys(metadata).length) plan.metadata = metadata return (await redis.set(prefixKey(id), await serialize(plan))) && plan } diff --git a/test/keys.js b/test/keys.js index 5c4e6d2..6cfa0f9 100644 --- a/test/keys.js +++ b/test/keys.js @@ -109,7 +109,7 @@ test('.update # error if plan does not exist', async t => { t.is(error.code, 'ERR_PLAN_NOT_EXIST') }) -test('.update # add a plan', async t => { +test('.update # plan', async t => { const plan = await openkey.plans.create({ id: randomUUID(), limit: 3, @@ -120,7 +120,7 @@ test('.update # add a plan', async t => { t.is(key.plan, plan.id) }) -test('.update # add metadata', async t => { +test('.update # metadata', async t => { { const { value } = await openkey.keys.create() const key = await openkey.keys.update(value, { metadata: { cc: 'hello@microlink.io' } }) @@ -134,6 +134,19 @@ test('.update # add metadata', async t => { t.is(key.metadata.cc, 'hello@microlink.io') t.is(key.metadata.version, 2) } + { + const { value } = await openkey.keys.create() + const metadata = { tier: 'free', null: 'null', undefined: 'undefined', empty: '' } + let key = await openkey.keys.update(value, { metadata }) + t.is(key.metadata.null, 'null') + t.is(key.metadata.undefined, 'undefined') + t.is(key.metadata.empty, undefined) + key = await openkey.keys.update(value, { metadata: { ...metadata, null: null, undefined: '' } }) + t.is(key.metadata.null, undefined) + t.is(key.metadata.undefined, undefined) + key = await openkey.keys.update(value, { metadata: null }) + t.is(key.metadata, undefined) + } }) test('.update # error is metadata is not a flat object', async t => { diff --git a/test/plans.js b/test/plans.js index fe9fdf9..b7386aa 100644 --- a/test/plans.js +++ b/test/plans.js @@ -328,6 +328,24 @@ test('.update # metadata', async t => { t.is(plan.metadata.tier, 'free') t.is(plan.metadata.version, 2) } + { + const { id } = await openkey.plans.create({ + id: randomUUID(), + limit: 1, + period: '1s' + }) + + const metadata = { tier: 'free', null: 'null', undefined: 'undefined', empty: '' } + let plan = await openkey.plans.update(id, { metadata }) + t.is(plan.metadata.null, 'null') + t.is(plan.metadata.undefined, 'undefined') + t.is(plan.metadata.empty, undefined) + plan = await openkey.plans.update(id, { metadata: { ...metadata, null: null, undefined: '' } }) + t.is(plan.metadata.null, undefined) + t.is(plan.metadata.undefined, undefined) + plan = await openkey.plans.update(id, { metadata: null }) + t.is(plan.metadata, undefined) + } }) test('.update # error is metadata is not a flat object', async t => {