Skip to content

Commit

Permalink
feat(strongbox): create @appium/strongbox
Browse files Browse the repository at this point in the history
This PR creates a new package `@appium/strongbox`, which provides a generic persistence store for Appium extensions.
  • Loading branch information
boneskull committed Apr 7, 2023
1 parent 1d5070e commit fd91234
Show file tree
Hide file tree
Showing 12 changed files with 747 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,8 @@ labels:
sync: true
matcher:
files: ['packages/universal-xml-plugin/**']

- label: '@appium/strongbox'
sync: true
matcher:
files: ['packages/strongbox/**']
34 changes: 32 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 96 additions & 0 deletions packages/strongbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# @appium/strongbox

> Persistent storage for Appium extensions
## Summary

This package is intended to be used in [Appium](https://appium.io) extensions which need to persist data between Appium runs. An example of such data may be a device token or key.

`@appium/strongbox` provides a simple and extensible API for managing such data, while abstracting the underlying storage mechanism.

_Note:_ This module is not intended for storing sensitive data.

## Usage

First, create an instance of `Strongbox`:

```ts
import {strongbox} from '@appium/strongbox';

const box = strongbox('my-pkg');
```

This instance corresponds to a unique collection of data.

From here, create a placeholder for data (you will need to provide the type of data you intend to store):

```ts
const item = await box.createItem<string>('my unique name');
```

...or, if you already have the data on-hand:

```ts
const item: Buffer|string = getSomeData();

const item = await box.createItemWithContents('my unique name', data);
```

Either way, you can read its contents:

```ts
// if the item doesn't exist, this result will be undefined
const contents = await item.read();
```

Or write new data to the item:

```ts
await item.write('new stuff');
```

The last-read contents of the `Item` will be available on the `contents` property, but the value of this property is only current as of the last `read()`:

```ts
const {contents} = item;
```

## API

In lieu of actual documentation, look at the type definitions that this package ships.

## Customization

1. Create a class that implements the `Item` interface:

```ts
import {strongbox, Item} from '@appium/strongbox';
import {Foo, getFoo} from 'somewhere/else';

class FooItem implements Item<Foo> {
// ...
}
```

2. Provide this class as the `defaultCtor` option to `strongbox()`:

```ts
const box = strongbox('my-pkg', {defaultCtor: FooItem});
```

3. Use like you would any other `Strongbox` instance:

```ts
const foo: Foo = getFoo();
const item = await box.createItemWithValue('my unique name', Foo);
```

## Default Behavior, For the Curious

Out-of-the-box, a `Strongbox` instance corresponds to a directory on-disk, and each `Item` (returned by `createItem()/createItemWithContents()`) corresponds to a file within that directory.

The directory of the `Strongbox` instance is determined by the [env-paths](https://www.npmjs.com/package/env-paths) package, and is platform-specific.

## License

Copyright © 2023 OpenJS Foundation. Licensed Apache-2.0
95 changes: 95 additions & 0 deletions packages/strongbox/lib/base-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
import path from 'node:path';
import type {Item, ItemEncoding, Value} from '.';
import {slugify} from './util';

/**
* Base item implementation
*
* @remarks This class is not intended to be instantiated directly
* @typeParam T - Type of data stored in the `Item`
*/
export class BaseItem<T extends Value> implements Item<T> {
/**
* {@inheritdoc Item.value}
*/
protected _value?: T | undefined;

/**
* Unique slugified identifier
*/
public readonly id: string;

/**
* {@inheritdoc Item.value}
*/
public readonly value: T | undefined;

/**
* Slugifies the name
* @param name Name of instance
* @param container Slugified name of container
* @param encoding Defaults to `utf8`
*/
constructor(
public readonly name: string,
public readonly container: string,
public readonly encoding: ItemEncoding = 'utf8'
) {
this.id = path.join(container, slugify(name));

Object.defineProperties(this, {
value: {
get() {
return this._value;
},
enumerable: true,
},
_value: {
enumerable: false,
writable: true,
},
});
}

/**
* {@inheritdoc Item.read}
*/
public async read(): Promise<T | undefined> {
try {
this._value = (await readFile(this.id, {
encoding: this.encoding,
})) as T;
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
throw e;
}
}
return this._value;
}

/**
* {@inheritdoc Item.write}
*/
public async write(value: T): Promise<void> {
if (this._value !== value) {
await mkdir(path.dirname(this.id), {recursive: true});
await writeFile(this.id, value, this.encoding);
this._value = value;
}
}

/**
* {@inheritdoc Item.clear}
*/
public async clear(): Promise<void> {
try {
await unlink(this.id);
this._value = undefined;
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
throw e;
}
}
}
}
Loading

0 comments on commit fd91234

Please sign in to comment.