Skip to content

Commit

Permalink
feat: add initial askar package (#1211)
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
  • Loading branch information
genaris authored Feb 7, 2023
1 parent 115d897 commit f18d189
Show file tree
Hide file tree
Showing 35 changed files with 2,383 additions and 8 deletions.
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')
}
}

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
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.'
) {
return instances
}
throw new WalletError(`Error executing query`, { cause: error })
}
}
}
Loading

0 comments on commit f18d189

Please sign in to comment.