Skip to content
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

Merged
merged 20 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/askar/README.md
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).
14 changes: 14 additions & 0 deletions packages/askar/jest.config.ts
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
41 changes: 41 additions & 0 deletions packages/askar/package.json
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"
}
}
33 changes: 33 additions & 0 deletions packages/askar/src/AskarModule.ts
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')
}
}
Comment on lines +10 to +19
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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


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)
}
}
}
44 changes: 44 additions & 0 deletions packages/askar/src/AskarModuleConfig.ts
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
}
}
9 changes: 9 additions & 0 deletions packages/askar/src/index.ts
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'
177 changes: 177 additions & 0 deletions packages/askar/src/storage/AskarStorageService.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

        try:
            item = await self._session.handle.fetch(
                record_type, record_id, for_update=for_update
            )
        except AskarError as err:
            raise StorageError("Error when fetching storage record") from err
        if not item:
            raise StorageNotFoundError(f"Record not found: {record_type}/{record_id}")

Will it sometimes throw an error instead of not returning a record?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.'
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 })
}
}
}
Loading