Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bedrock/account

Package Overview
Dependencies
Maintainers
5
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bedrock/account - npm Package Compare versions

Comparing version 8.2.0 to 9.0.0

lib/logger.js

13

CHANGELOG.md
# bedrock-account ChangeLog
## 9.0.0 - 2023-01-24
### Changed
- **BREAKING**: The database record and layout for this module has changed in
ways that incompatible with any previous releases. Now the uniqueness
constraint for account email addresses is enforced via a proxy collection
and an internal transaction system. This enables the account collection (and
the proxy collection) to be sharded.
### Removed
- **BREAKING**: The `patch` feature in the update API has been removed. Only
overwrite w/sequence matching is permitted.
## 8.2.0 - 2022-12-11

@@ -4,0 +17,0 @@

342

lib/index.js
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
*/

@@ -8,8 +8,5 @@ import * as bedrock from '@bedrock/core';

import {klona} from 'klona';
import jsonpatch from 'fast-json-patch';
import {schema as accountSchema} from '../schemas/bedrock-account.js';
import {validateInstance} from '@bedrock/validation';
import {logger} from './logger.js';
import {RecordCollection} from './RecordCollection.js';
const {util: {BedrockError}} = bedrock;
// load config defaults

@@ -22,44 +19,11 @@ import './config.js';

const logger = bedrock.loggers.get('app').child('bedrock-account');
let ACCOUNT_STORAGE;
bedrock.events.on('bedrock-mongodb.ready', async () => {
await database.openCollections(['account']);
await database.createIndexes([{
collection: 'account',
fields: {id: 1},
options: {unique: true, background: false}
}, {
// cover common queries
collection: 'account',
fields: {id: 1, 'meta.status': 1},
options: {unique: true, background: false}
}, {
// `id` is a prefix to allow for sharding on `id` -- a collection
// cannot be sharded unless its unique indexes have the shard key
// as a prefix; a separate non-unique index is used for lookups
collection: 'account',
fields: {id: 1, 'account.email': 1, 'meta.status': 1},
options: {
partialFilterExpression: {'account.email': {$exists: true}},
unique: true,
background: false
}
}, {
collection: 'account',
fields: {'account.email': 1, 'meta.status': 1},
options: {
partialFilterExpression: {'account.email': {$exists: true}},
unique: true,
background: false
}
}, {
collection: 'account',
fields: {'account.email': 1},
options: {
partialFilterExpression: {'account.email': {$exists: true}},
unique: true,
background: false
}
}]);
ACCOUNT_STORAGE = new RecordCollection({
collectionName: 'account',
sequenceInData: false,
uniqueFields: ['email']
});
await ACCOUNT_STORAGE.initialize();
});

@@ -106,28 +70,10 @@

// insert the account and get the updated record
// prepare the account record
const now = Date.now();
meta.created = now;
meta.updated = now;
meta.sequence = 0;
let record = {
id: database.hash(account.id),
meta,
account
};
try {
const result = await database.collections.account.insertOne(
record, database.writeOptions);
record = result.ops[0];
} catch(e) {
if(!database.isDuplicateError(e)) {
throw e;
}
throw new BedrockError(
'Duplicate account.',
'DuplicateError', {
public: true,
httpStatusCode: 409
}, e);
}
meta = {...meta, created: now, updated: now, sequence: 0};
let record = {account, meta};
// insert the record
record = await ACCOUNT_STORAGE.insert({record});
// emit `postInsert` event with updated record data

@@ -160,13 +106,17 @@ eventData.account = klona(record.account);

const query = {'meta.status': status};
const projection = {_id: 0};
if(id) {
query.id = database.hash(id);
projection.id = 1;
const options = {id};
if(email !== undefined) {
options.uniqueField = 'email';
options.uniqueValue = email;
}
if(email) {
query['account.email'] = email;
projection['account.email'] = 1;
try {
// can't use `ACCOUNT_STORAGE.exists`; must check `meta.status` field
const record = await ACCOUNT_STORAGE.get(options);
return record.meta.status === status;
} catch(e) {
if(e.name === 'NotFoundError') {
return false;
}
throw e;
}
return !!await database.collections.account.findOne(query, {projection});
}

@@ -186,3 +136,3 @@

*/
export async function get({id, email, explain} = {}) {
export async function get({id, email, explain = false} = {}) {
assert.optionalString(id, 'id');

@@ -194,29 +144,16 @@ assert.optionalString(email, 'email');

const query = {};
if(id) {
query.id = database.hash(id);
}
if(email) {
query['account.email'] = email;
}
const projection = {_id: 0, account: 1, meta: 1};
const collection = database.collections.account;
if(explain) {
// 'find().limit(1)' is used here because 'findOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await collection.find(query, {projection}).limit(1);
return cursor.explain('executionStats');
if(email !== undefined) {
const proxyCollection = ACCOUNT_STORAGE.proxyCollections.get('email');
return proxyCollection.get({uniqueValue: email, explain});
}
return ACCOUNT_STORAGE.helper.get({id, explain});
}
const record = await collection.findOne(query, {projection});
if(!record) {
throw new BedrockError(
'Account not found.',
'NotFoundError',
{id, httpStatusCode: 404, public: true});
const options = {id};
if(email !== undefined) {
options.uniqueField = 'email';
options.uniqueValue = email;
}
return record;
return ACCOUNT_STORAGE.get(options);
}

@@ -230,6 +167,13 @@

* @param {object} [options.options={}] - The options (eg: 'sort', 'limit').
* @param {boolean} [options._allowPending=false] - For internal use only;
* allows finding records that are in the process of being created.
*
* @returns {Promise} Resolves to the records that matched the query.
*/
export async function getAll({query = {}, options = {}} = {}) {
export async function getAll({
query = {}, options = {}, _allowPending = false
} = {}) {
if(!_allowPending) {
query = {...query, 'meta.state': {$ne: 'pending'}};
}
return database.collections.account.find(query, options).toArray();

@@ -240,6 +184,5 @@ }

* Updates an account by overwriting it with new `account` and / or `meta`
* information or by providing a `patch`. In all cases, the expected
* `sequence` must match the existing account, but if `meta` is being
* overwritten, `sequence` can be omitted and will be auto-computed from
* `meta.sequence`.
* information. In both cases, the expected `sequence` must match the existing
* account, but if `meta` is being overwritten, `sequence` can be omitted and
* the value from `meta.sequence` will be used.
*

@@ -251,7 +194,5 @@ * @param {object} options - The options to use.

* @param {number} [options.sequence] - The sequence number that must match the
* current record prior to the patch if given; can be omitted if no `patch`
* is given and `meta` is given.
* @param {Array} [options.patch] - A JSON patch for performing the update.
* @param {boolean} [options.explain=false] - An optional explain boolean that
* may only be used if `patch` is not provided.
* current record prior to the update if given; can be omitted if `meta` is
* given and has, instead, the new `sequence` number (which must be one more
* than the existing `sequence` number).
*

@@ -261,12 +202,12 @@ * @returns {Promise | ExplainObject} - Returns a Promise that resolves to

*/
export async function update({
id, account, meta, sequence, explain, patch
} = {}) {
if(patch) {
if(explain) {
throw new TypeError('"explain" not supported when using "patch".');
}
return _patchUpdate({id, patch, sequence});
export async function update({id, account, meta, sequence} = {}) {
if(id === undefined) {
id = account?.id;
}
return _update({id, account, meta, sequence, explain});
assert.string(id, 'id');
if(account && account.id !== id) {
throw new TypeError('"id" must equal "account.id".');
}
return ACCOUNT_STORAGE.update(
{id, data: account, meta, expectedSequence: sequence});
}

@@ -287,157 +228,8 @@

const result = await database.collections.account.updateOne(
{id: database.hash(id)}, {
$set: {'meta.status': status},
$inc: {'meta.sequence': 1}
}, database.writeOptions);
if(result.result.n === 0) {
throw new BedrockError(
'Could not set account status. Account not found.',
'NotFoundError',
{httpStatusCode: 404, account: id, public: true});
}
const {meta} = await ACCOUNT_STORAGE.get({id});
meta.status = status;
meta.sequence++;
await ACCOUNT_STORAGE.update({id, meta});
}
export async function _update({id, account, meta, sequence, explain} = {}) {
// validate params
if(!(account || meta)) {
throw new TypeError('Either "account" or "meta" is required.');
}
assert.optionalObject(account, 'account');
assert.optionalObject(meta, 'meta');
if(id === undefined) {
id = account?.id;
}
assert.string(id, 'id');
if(account && account.id !== id) {
throw new TypeError('"id" must equal "account.id".');
}
if(sequence === undefined) {
// use sequence from `meta`
sequence = meta?.sequence - 1;
}
assert.number(sequence, 'sequence');
if(meta && meta.sequence !== (sequence + 1)) {
throw new TypeError('"sequence" must equal "meta.sequence - 1".');
}
if(sequence < 0) {
throw new TypeError('"sequence" must be a non-negative integer.');
}
// build update
const now = Date.now();
const update = {$set: {}};
if(account) {
update.$set.account = account;
}
if(meta) {
update.$set.meta = {...meta, updated: now};
} else {
update.$set['meta.updated'] = now;
update.$set['meta.sequence'] = sequence + 1;
}
const collection = database.collections.account;
const query = {
id: database.hash(id),
'meta.sequence': sequence
};
if(explain) {
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
// cursor which allows the use of the explain function.
const cursor = await collection.find(query).limit(1);
return cursor.explain('executionStats');
}
const result = await collection.updateOne(query, update);
if(result.result.n > 0) {
// record updated
return true;
}
// determine if sequence did not match; will throw if account does not exist
const record = await get({id});
if(record.meta.sequence !== sequence) {
throw new BedrockError(
'Could not update account. Sequence does not match.', {
name: 'InvalidStateError',
details: {
httpStatusCode: 409,
public: true,
expected: sequence
}
});
}
return false;
}
async function _patchUpdate({id, patch, sequence} = {}) {
assert.string(id, 'id');
assert.array(patch, 'patch');
assert.number(sequence, 'sequence');
if(sequence < 0) {
throw new TypeError('"sequence" must be a non-negative integer.');
}
const record = await get({id});
if(record.meta.sequence !== sequence) {
throw new BedrockError(
'Could not update account. Record sequence does not match.',
'InvalidStateError', {
httpStatusCode: 409,
public: true,
actual: sequence,
expected: record.meta.sequence
});
}
const customValidate = (operation, index, tree, existingPath) => {
jsonpatch.validator(operation, index, tree, existingPath);
const pathId = /^\/id$/i.test(existingPath);
if(pathId) {
throw new jsonpatch.JsonPatchError(
'"id" cannot be changed',
'OPERATION_OP_INVALID',
index, operation, tree);
}
};
const errors = jsonpatch.validate(patch, record.account, customValidate);
if(errors) {
throw new BedrockError(
'The given JSON patch is invalid.',
'ValidationError', {
httpStatusCode: 400,
public: true,
patch,
errors
});
}
// apply patch and validate result
const patched = jsonpatch.applyPatch(record.account, patch).newDocument;
const validationResult = validateInstance(
{instance: patched, schema: accountSchema});
if(!validationResult.valid) {
throw validationResult.error;
}
const result = await database.collections.account.updateOne({
id: database.hash(id),
'meta.sequence': sequence
}, {
$set: {account: patched},
$inc: {'meta.sequence': 1}
}, database.writeOptions);
if(result.result.n === 0) {
return new BedrockError(
'Could not update account. Record sequence does not match.',
'InvalidStateError', {httpStatusCode: 409, public: true});
}
}
/**

@@ -444,0 +236,0 @@ * An object containing information on the query plan.

{
"name": "@bedrock/account",
"version": "8.2.0",
"version": "9.0.0",
"type": "module",

@@ -30,4 +30,4 @@ "description": "User accounts for Bedrock applications",

"assert-plus": "^1.0.0",
"fast-json-patch": "^3.1.1",
"klona": "^2.0.5"
"klona": "^2.0.5",
"uuid": "^9.0.0"
},

@@ -43,7 +43,7 @@ "peerDependencies": {

"devDependencies": {
"eslint": "^7.32.0",
"eslint-config-digitalbazaar": "^2.8.0",
"eslint-plugin-jsdoc": "^37.9.7",
"jsdoc-to-markdown": "^7.1.1"
"eslint": "^8.30.0",
"eslint-config-digitalbazaar": "^4.2.0",
"eslint-plugin-jsdoc": "^39.6.4",
"jsdoc-to-markdown": "^8.0.0"
}
}

@@ -5,2 +5,23 @@ # bedrock-account

## API Reference
## Modules
<dl>
<dt><a href="#module_bedrock-account">bedrock-account</a></dt>
<dd></dd>
</dl>
## Typedefs
<dl>
<dt><a href="#ExplainObject">ExplainObject</a> : <code>object</code></dt>
<dd><p>An object containing information on the query plan.</p>
</dd>
<dt><a href="#ExplainObject">ExplainObject</a> : <code>object</code></dt>
<dd><p>An object containing information on the query plan.</p>
</dd>
<dt><a href="#ExplainObject">ExplainObject</a> : <code>object</code></dt>
<dd><p>An object containing information on the query plan.</p>
</dd>
</dl>
<a name="module_bedrock-account"></a>

@@ -13,5 +34,5 @@

* [.exists(options)](#module_bedrock-account.exists) ⇒ <code>Promise</code>
* [.get(options)](#module_bedrock-account.get) ⇒ <code>Promise</code>
* [.get(options)](#module_bedrock-account.get) ⇒ <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject)
* [.getAll(options)](#module_bedrock-account.getAll) ⇒ <code>Promise</code>
* [.update(options)](#module_bedrock-account.update) ⇒ <code>Promise</code>
* [.update(options)](#module_bedrock-account.update) ⇒ <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject)
* [.setStatus(options)](#module_bedrock-account.setStatus) ⇒ <code>Promise</code>

@@ -50,12 +71,16 @@

### bedrock-account.get(options) ⇒ <code>Promise</code>
Retrieves an account.
### bedrock-account.get(options) ⇒ <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject)
Retrieves an account by ID or email.
**Kind**: static method of [<code>bedrock-account</code>](#module_bedrock-account)
**Returns**: <code>Promise</code> - Resolves to `{account, meta}`.
**Returns**: <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject) - - Returns a Promise that resolves to
the account record (`{account, meta}`) or an ExplainObject if
`explain=true`.
| Param | Type | Description |
| --- | --- | --- |
| options | <code>object</code> | The options to use. |
| options.id | <code>string</code> | The ID of the account to retrieve. |
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| options | <code>object</code> | | The options to use. |
| [options.id] | <code>string</code> | | The ID of the account to retrieve. |
| [options.email] | <code>string</code> | | The email of the account to retrieve. |
| [options.explain] | <code>boolean</code> | <code>false</code> | An optional explain boolean. |

@@ -75,10 +100,15 @@ <a name="module_bedrock-account.getAll"></a>

| [options.options] | <code>object</code> | <code>{}</code> | The options (eg: 'sort', 'limit'). |
| [options._allowPending] | <code>boolean</code> | <code>false</code> | For internal use only; allows finding records that are in the process of being created. |
<a name="module_bedrock-account.update"></a>
### bedrock-account.update(options) ⇒ <code>Promise</code>
Updates an account.
### bedrock-account.update(options) ⇒ <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject)
Updates an account by overwriting it with new `account` and / or `meta`
information. In both cases, the expected `sequence` must match the existing
account, but if `meta` is being overwritten, `sequence` can be omitted and
the value from `meta.sequence` will be used.
**Kind**: static method of [<code>bedrock-account</code>](#module_bedrock-account)
**Returns**: <code>Promise</code> - Resolves once the operation completes.
**Returns**: <code>Promise</code> \| [<code>ExplainObject</code>](#ExplainObject) - - Returns a Promise that resolves to
`true` if the update succeeds or an ExplainObject if `explain=true`.

@@ -89,4 +119,5 @@ | Param | Type | Description |

| options.id | <code>string</code> | The ID of the account to update. |
| options.patch | <code>Array</code> | A JSON patch for performing the update. |
| options.sequence | <code>number</code> | The sequence number that must match the current record prior to the patch. |
| [options.account] | <code>object</code> | The new account information to use. |
| [options.meta] | <code>object</code> | The new meta information to use. |
| [options.sequence] | <code>number</code> | The sequence number that must match the current record prior to the update if given; can be omitted if `meta` is given and has, instead, the new `sequence` number (which must be one more than the existing `sequence` number). |

@@ -107,1 +138,19 @@ <a name="module_bedrock-account.setStatus"></a>

<a name="ExplainObject"></a>
## ExplainObject : <code>object</code>
An object containing information on the query plan.
**Kind**: global typedef
<a name="ExplainObject"></a>
## ExplainObject : <code>object</code>
An object containing information on the query plan.
**Kind**: global typedef
<a name="ExplainObject"></a>
## ExplainObject : <code>object</code>
An object containing information on the query plan.
**Kind**: global typedef
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
*/

@@ -10,3 +10,3 @@ import * as brAccount from '@bedrock/account';

const newAccount = {
id: 'urn:uuid:' + uuid(),
id: `urn:uuid:${uuid()}`,
email

@@ -17,2 +17,38 @@ };

export async function createFakeTransaction({
accountId, type, committed, _pending, ops = [], skipAccountRecord = false
} = {}) {
const txn = {id: uuid(), type, recordId: accountId};
if(committed) {
txn.committed = true;
}
if(!skipAccountRecord) {
const query = {'account.id': accountId};
const update = {$set: {_txn: txn}};
if(_pending !== undefined) {
update.$set._pending = _pending;
}
const result = await database.collections.account.updateOne(
query, update, {upsert: true});
result.result.n.should.equal(1);
}
for(const op of ops) {
const _txn = {...txn, op: op.type};
if(op.type === 'insert') {
const query = {email: op.email};
const update = {$set: {accountId, email: op.email, _txn}};
await database.collections['account-email'].updateOne(
query, update, {upsert: true});
} else {
const query = {email: op.email};
const update = {$set: {_txn}};
const result = await database.collections['account-email'].updateOne(
query, update, {upsert: true});
result.result.n.should.equal(1);
}
}
}
export async function prepareDatabase(mockData) {

@@ -23,3 +59,5 @@ await removeCollections();

export async function removeCollections(collectionNames = ['account']) {
export async function removeCollections(collectionNames = [
'account', 'account-email'
]) {
await database.openCollections(collectionNames);

@@ -26,0 +64,0 @@ for(const collectionName of collectionNames) {

/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
*/

@@ -4,0 +4,0 @@ import * as helpers from './helpers.js';

@@ -21,3 +21,2 @@ {

"cross-env": "^7.0.2",
"fast-json-patch": "^2.0.6",
"uuid": "^8.3.2"

@@ -24,0 +23,0 @@ },

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc