-
Notifications
You must be signed in to change notification settings - Fork 200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add initial askar package #1211
Changes from 16 commits
87b39d5
3465764
ae35f4b
ed302c4
17b6a73
d6f348b
0216718
b8adc3b
7f2577d
93a9617
d8b1e60
dd1ce82
44e65db
27c3c47
2641f84
b44647f
c06f1ef
fd0dff8
746f3f9
e3fde36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<p align="center"> | ||
<br /> | ||
<img | ||
alt="Hyperledger Aries logo" | ||
src="https://raw.githubusercontent.com/hyperledger/aries-framework-javascript/aa31131825e3331dc93694bc58414d955dcb1129/images/aries-logo.png" | ||
height="250px" | ||
/> | ||
</p> | ||
<h1 align="center"><b>Aries Framework JavaScript Askar Module</b></h1> | ||
<p align="center"> | ||
<a | ||
href="https://raw.githubusercontent.com/hyperledger/aries-framework-javascript/main/LICENSE" | ||
><img | ||
alt="License" | ||
src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" | ||
/></a> | ||
<a href="https://www.typescriptlang.org/" | ||
><img | ||
alt="typescript" | ||
src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg" | ||
/></a> | ||
<a href="https://www.npmjs.com/package/@aries-framework/askar" | ||
><img | ||
alt="@aries-framework/askar version" | ||
src="https://img.shields.io/npm/v/@aries-framework/askar" | ||
/></a> | ||
|
||
</p> | ||
<br /> | ||
|
||
Askar module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript.git). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import type { Config } from '@jest/types' | ||
|
||
import base from '../../jest.config.base' | ||
|
||
import packageJson from './package.json' | ||
|
||
const config: Config.InitialOptions = { | ||
...base, | ||
name: packageJson.name, | ||
displayName: packageJson.name, | ||
setupFilesAfterEnv: ['./tests/setup.ts'], | ||
} | ||
|
||
export default config |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "@aries-framework/askar", | ||
"main": "build/index", | ||
"types": "build/index", | ||
"version": "0.3.3", | ||
"private": true, | ||
"files": [ | ||
"build" | ||
], | ||
"license": "Apache-2.0", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/askar", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/hyperledger/aries-framework-javascript", | ||
"directory": "packages/askar" | ||
}, | ||
"scripts": { | ||
"build": "yarn run clean && yarn run compile", | ||
"clean": "rimraf ./build", | ||
"compile": "tsc -p tsconfig.build.json", | ||
"prepublishOnly": "yarn run build", | ||
"test": "jest" | ||
}, | ||
"dependencies": { | ||
"@aries-framework/core": "0.3.3", | ||
"@hyperledger/aries-askar-shared": "^0.1.0-dev.1", | ||
"class-transformer": "^0.5.1", | ||
"class-validator": "^0.14.0", | ||
"rxjs": "^7.2.0", | ||
"tsyringe": "^4.7.0" | ||
}, | ||
"devDependencies": { | ||
"@hyperledger/aries-askar-nodejs": "^0.1.0-dev.1", | ||
"reflect-metadata": "^0.1.13", | ||
"rimraf": "^4.0.7", | ||
"typescript": "~4.9.4" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { DependencyManager, Module } from '@aries-framework/core' | ||
|
||
import { AriesFrameworkError, InjectionSymbols } from '@aries-framework/core' | ||
|
||
import { AskarStorageService } from './storage' | ||
import { AskarWallet } from './wallet' | ||
|
||
export class AskarModule implements Module { | ||
public register(dependencyManager: DependencyManager) { | ||
try { | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
require('@hyperledger/aries-askar-nodejs') | ||
} catch (error) { | ||
try { | ||
require('@hyperledger/aries-askar-react-native') | ||
} catch (error) { | ||
throw new Error('Could not load aries-askar bindings') | ||
} | ||
} | ||
|
||
if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { | ||
throw new AriesFrameworkError('There is an instance of Wallet already registered') | ||
} else { | ||
dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) | ||
} | ||
|
||
if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { | ||
throw new AriesFrameworkError('There is an instance of StorageService already registered') | ||
} else { | ||
dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import type { AriesAskar } from './types' | ||
|
||
/** | ||
* AskarModuleConfigOptions defines the interface for the options of the AskarModuleConfig class. | ||
*/ | ||
export interface AskarModuleConfigOptions { | ||
/** | ||
* Implementation of the Askar interface according to aries-askar JavaScript wrapper. | ||
* | ||
* | ||
* ## Node.JS | ||
* | ||
* ```ts | ||
* import { NodeJSAriesAskar } from 'aries-askar-nodejs' | ||
* | ||
* const askarModule = new AskarModule({ | ||
* askar: new NodeJSAriesAskar() | ||
* }) | ||
* ``` | ||
* | ||
* ## React Native | ||
* | ||
* ```ts | ||
* import { ReactNativeAriesAskar } from 'aries-askar-react-native' | ||
* | ||
* const askarModule = new AskarModule({ | ||
* askar: new ReactNativeAriesAskar() | ||
* }) | ||
* ``` | ||
*/ | ||
askar: AriesAskar | ||
} | ||
|
||
export class AskarModuleConfig { | ||
private options: AskarModuleConfigOptions | ||
|
||
public constructor(options: AskarModuleConfigOptions) { | ||
this.options = options | ||
} | ||
|
||
public get askar() { | ||
return this.options.askar | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Wallet | ||
export { AskarWallet } from './wallet' | ||
|
||
// Storage | ||
export { AskarStorageService } from './storage' | ||
|
||
// Module | ||
export { AskarModule } from './AskarModule' | ||
export { AskarModuleConfig } from './AskarModuleConfig' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import type { BaseRecordConstructor, AgentContext, BaseRecord, Query, StorageService } from '@aries-framework/core' | ||
|
||
import { | ||
RecordDuplicateError, | ||
WalletError, | ||
RecordNotFoundError, | ||
injectable, | ||
JsonTransformer, | ||
} from '@aries-framework/core' | ||
import { Scan } from '@hyperledger/aries-askar-shared' | ||
|
||
import { askarErrors, isAskarError } from '../utils/askarError' | ||
import { assertAskarWallet } from '../utils/assertAskarWallet' | ||
|
||
import { askarQueryFromSearchQuery, recordToInstance, transformFromRecordTagValues } from './utils' | ||
|
||
@injectable() | ||
export class AskarStorageService<T extends BaseRecord> implements StorageService<T> { | ||
/** @inheritDoc */ | ||
public async save(agentContext: AgentContext, record: T) { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
const value = JsonTransformer.serialize(record) | ||
const tags = transformFromRecordTagValues(record.getTags()) as Record<string, string> | ||
|
||
try { | ||
await session.insert({ category: record.type, name: record.id, value, tags }) | ||
} catch (error) { | ||
if (isAskarError(error) && error.code === askarErrors.Duplicate) { | ||
throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) | ||
} | ||
|
||
throw new WalletError('Error saving record', { cause: error }) | ||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async update(agentContext: AgentContext, record: T): Promise<void> { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
const value = JsonTransformer.serialize(record) | ||
const tags = transformFromRecordTagValues(record.getTags()) as Record<string, string> | ||
|
||
try { | ||
await session.replace({ category: record.type, name: record.id, value, tags }) | ||
} catch (error) { | ||
if (isAskarError(error) && error.code === askarErrors.NotFound) { | ||
throw new RecordNotFoundError(`record with id ${record.id} not found.`, { | ||
recordType: record.type, | ||
cause: error, | ||
}) | ||
} | ||
|
||
throw new WalletError('Error updating record', { cause: error }) | ||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async delete(agentContext: AgentContext, record: T) { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
try { | ||
await session.remove({ category: record.type, name: record.id }) | ||
} catch (error) { | ||
if (isAskarError(error) && error.code === askarErrors.NotFound) { | ||
throw new RecordNotFoundError(`record with id ${record.id} not found.`, { | ||
recordType: record.type, | ||
cause: error, | ||
}) | ||
} | ||
throw new WalletError('Error deleting record', { cause: error }) | ||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async deleteById( | ||
agentContext: AgentContext, | ||
recordClass: BaseRecordConstructor<T>, | ||
id: string | ||
): Promise<void> { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
try { | ||
await session.remove({ category: recordClass.type, name: id }) | ||
} catch (error) { | ||
if (isAskarError(error) && error.code === askarErrors.NotFound) { | ||
throw new RecordNotFoundError(`record with id ${id} not found.`, { | ||
recordType: recordClass.type, | ||
cause: error, | ||
}) | ||
} | ||
throw new WalletError('Error deleting record', { cause: error }) | ||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, id: string): Promise<T> { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
try { | ||
const record = await session.fetch({ category: recordClass.type, name: id }) | ||
if (!record) { | ||
throw new RecordNotFoundError(`record with id ${id} not found.`, { | ||
recordType: recordClass.type, | ||
}) | ||
} | ||
return recordToInstance(record, recordClass) | ||
} catch (error) { | ||
if ( | ||
isAskarError(error) && | ||
(error.code === askarErrors.NotFound || | ||
// FIXME: this is current output from askar wrapper but does not describe specifically a not found scenario | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems Askar also just returns not found if the record wasn't found:
Will it sometimes throw an error instead of not returning a record? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not 100% sure but I'd say that it always sends the error.code Custom (100). Let's analyze on Askar side to see where the problem is. |
||
error.message === 'Received null pointer. The native library could not find the value.') | ||
) { | ||
throw new RecordNotFoundError(`record with id ${id} not found.`, { | ||
recordType: recordClass.type, | ||
cause: error, | ||
}) | ||
} | ||
throw new WalletError(`Error getting record`, { cause: error }) | ||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>): Promise<T[]> { | ||
assertAskarWallet(agentContext.wallet) | ||
const session = agentContext.wallet.session | ||
|
||
const records = await session.fetchAll({ category: recordClass.type }) | ||
|
||
const instances = [] | ||
for (const record of records) { | ||
instances.push(recordToInstance(record, recordClass)) | ||
} | ||
return instances | ||
} | ||
|
||
/** @inheritDoc */ | ||
public async findByQuery( | ||
agentContext: AgentContext, | ||
recordClass: BaseRecordConstructor<T>, | ||
query: Query<T> | ||
): Promise<T[]> { | ||
assertAskarWallet(agentContext.wallet) | ||
const store = agentContext.wallet.store | ||
|
||
const askarQuery = askarQueryFromSearchQuery(query) | ||
|
||
const scan = new Scan({ | ||
category: recordClass.type, | ||
store, | ||
tagFilter: askarQuery, | ||
}) | ||
|
||
const instances = [] | ||
try { | ||
const records = await scan.fetchAll() | ||
for (const record of records) { | ||
instances.push(recordToInstance(record, recordClass)) | ||
} | ||
return instances | ||
} catch (error) { | ||
if ( | ||
isAskarError(error) && // FIXME: this is current output from askar wrapper but does not describe specifically a 0 length scenario | ||
error.message === 'Received null pointer. The native library could not find the value.' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this something we can fix in the wrapper? So the wrapper should jus allow a null pointer as return value? Maybe @blu3beri knows There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah if that is the intended behaviour we can fix that in the wrapper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added openwallet-foundation/askar#77 to follow this issue. I'm not absolutely sure that is a specific problem for JS, as a preliminar quick-debugging I've done showed me that the wrapper is getting an Error Code: 0 in those cases so it tries to create an EntryList from a null pointer (and that's why it shows that custom error). |
||
) { | ||
return instances | ||
} | ||
throw new WalletError(`Error executing query`, { cause: error }) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 nice. We could probably improve this at some point by detecting the env and loading packages based on that. I think it could succeed in requireing the nodejs package even though we're in a RN env?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced about this way of importing it but I wanted to left it consistent with the way we are using other shared components. And it's nice that it attempts to make life easier to developers using the framework.. unless they import nodejs package when on react native environment 😮 . Would be nice to investigate if there is a reliable way to get the JS environment nowadays.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Also not 100% convinced yet. I think it's just another thing the user needs to provide to the agent config, while we could just infer it. Maybe we only try to import it if it's not provided yet? This way it 'works' out of the box. but you can also manually configure it