Skip to content

Commit

Permalink
fix: support minified class names (#19)
Browse files Browse the repository at this point in the history
* chore: add this.setMinifiedName and remove Type suffix

* fix: allow developers to indicate the class name for minification

* docs: separate and expand existing docs

* ci: upload code coverage to deepsource
  • Loading branch information
acodeninja authored Sep 30, 2024
1 parent 2d4c028 commit 85d884a
Show file tree
Hide file tree
Showing 32 changed files with 2,226 additions and 384 deletions.
28 changes: 9 additions & 19 deletions .github/workflows/quality-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,13 @@ jobs:
- uses: actions/checkout@v4
- uses: asdf-vm/actions/install@v3
- run: make init
- run: make test/coverage

dry-run-release:
name: 📋 dry run release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: asdf-vm/actions/install@v3
- run: npm clean-install
- run: npx semantic-release --dry-run
- run: make test/coverage/report
- run: curl https://deepsource.io/cli | sh
- name: deepsource report
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }}
run: |
./bin/deepsource report \
--analyzer test-coverage \
--key javascript \
--value-file "$(realpath ./coverage/lcov.info)"
256 changes: 21 additions & 235 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,243 +1,29 @@
# @acodeninja/persist

A JSON based data modelling and persistence library with alternate storage mechanisms.
A JSON based data modelling and persistence library with alternate storage mechanisms, designed with static site generation in mind.

## Models
![NPM Version](https://img.shields.io/npm/v/%40acodeninja%2Fpersist)
![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40acodeninja%2Fpersist)
![GitHub top language](https://img.shields.io/github/languages/top/acodeninja/persist)
![NPM Downloads](https://img.shields.io/npm/dw/%40acodeninja%2Fpersist)

The `Model` and `Type` classes allow creating representations of data objects
[![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=active+issues&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/)
[![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=code+coverage&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/)

### Defining Models
## Features

##### A model using all available basic types
- Data modelling with relationships
- Data validation
- Data querying
- Fuzzy search
- Storage with: S3, HTTP and Filesystem

```javascript
import Persist from "@acodeninja/persist";
## Find out more

export class SimpleModel extends Persist.Type.Model {
static boolean = Persist.Type.Boolean;
static string = Persist.Type.String;
static number = Persist.Type.Number;
static date = Persist.Type.Date;
}
```

##### A simple model using required types

```javascript
import Persist from "@acodeninja/persist";

export class SimpleModel extends Persist.Type.Model {
static requiredBoolean = Persist.Type.Boolean.required;
static requiredString = Persist.Type.String.required;
static requiredNumber = Persist.Type.Number.required;
static requiredDate = Persist.Type.Date.required;
}
```

##### A simple model using arrays of basic types

```javascript
import Persist from "@acodeninja/persist";

export class SimpleModel extends Persist.Type.Model {
static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean);
static arrayOfStrings = Persist.Type.Array.of(Type.String);
static arrayOfNumbers = Persist.Type.Array.of(Type.Number);
static arrayOfDates = Persist.Type.Array.of(Type.Date);
static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required;
static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required;
static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required;
static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required;
}
```

<details>
<summary>Complex relationships are also supported</summary>

#### One-to-One Relationships

##### A one-to-one relationship

```javascript
import Persist from "@acodeninja/persist";

export class ModelB extends Persist.Type.Model {
}

export class ModelA extends Persist.Type.Model {
static linked = ModelB;
}
```

##### A circular one-to-one relationship

```javascript
import Persist from "@acodeninja/persist";

export class ModelA extends Persist.Type.Model {
static linked = () => ModelB;
}

export class ModelB extends Persist.Type.Model {
static linked = ModelA;
}
```

#### One-to-Many Relationships

##### A one-to-many relationship

```javascript
import Persist from "@acodeninja/persist";

export class ModelB extends Persist.Type.Model {
}

export class ModelA extends Persist.Type.Model {
static linked = Persist.Type.Array.of(ModelB);
}
```

##### A circular one-to-many relationship

```javascript
import Persist from "@acodeninja/persist";

export class ModelA extends Persist.Type.Model {
static linked = () => Type.Array.of(ModelB);
}

export class ModelB extends Persist.Type.Model {
static linked = ModelA;
}
```

#### Many-to-Many Relationships

##### A many-to-many relationship

```javascript
import Persist from "@acodeninja/persist";

export class ModelA extends Persist.Type.Model {
static linked = Persist.Type.Array.of(ModelB);
}

export class ModelB extends Persist.Type.Model {
static linked = Persist.Type.Array.of(ModelA);
}
```
</details>

## Find and Search

Models may expose a `searchProperties()` and `indexProperties()` static method to indicate which
fields should be indexed for storage engine `find()` and `search()` methods.

Use `find()` for a low usage exact string match on any indexed attribute of a model.

Use `search()` for a medium usage fuzzy string match on any search indexed attribute of a model.

```javascript
import Persist from "@acodeninja/persist";
import FileEngine from "@acodeninja/persist/engine/file";

export class Tag extends Persist.Type.Model {
static tag = Persist.Type.String.required;
static description = Persist.Type.String;
static searchProperties = () => ['tag', 'description'];
static indexProperties = () => ['tag'];
}

const tag = new Tag({tag: 'documentation', description: 'How to use the persist library'});

await FileEngine.find(Tag, {tag: 'documentation'});
// [Tag {tag: 'documentation', description: 'How to use the persist library'}]

await FileEngine.search(Tag, 'how to');
// [Tag {tag: 'documentation', description: 'How to use the persist library'}]
```

## Storage

### Filesystem Storage Engine

To store models using the local file system, use the `File` storage engine.

```javascript
import Persist from "@acodeninja/persist";
import FileEngine from "@acodeninja/persist/engine/file";

Persist.addEngine('local', FileEngine, {
path: '/app/storage',
});

export class Tag extends Persist.Type.Model {
static tag = Persist.Type.String.required;
}

await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
```

### HTTP Storage Engine

To store models using an S3 Bucket, use the `S3` storage engine.

```javascript
import Persist from "@acodeninja/persist";
import HTTPEngine from "@acodeninja/persist/engine/http";

Persist.addEngine('remote', HTTPEngine, {
host: 'https://api.example.com',
});

export class Tag extends Persist.Type.Model {
static tag = Persist.Type.String.required;
}

await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'}));
```

### S3 Storage Engine

To store models using an S3 Bucket, use the `S3` storage engine.

```javascript
import Persist from "@acodeninja/persist";
import S3Engine from "@acodeninja/persist/engine/s3";

Persist.addEngine('remote', S3Engine, {
bucket: 'test-bucket',
client: new S3Client(),
});

export class Tag extends Persist.Type.Model {
static tag = Persist.Type.String.required;
}

await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
```

## Transactions

Create transactions to automatically roll back on failure to update.

```javascript
import Persist from "@acodeninja/persist";
import S3Engine from "@acodeninja/persist/engine/s3";

Persist.addEngine('remote', S3Engine, {
bucket: 'test-bucket',
client: new S3Client(),
transactions: true,
});

export class Tag extends Persist.Type.Model {
static tag = Persist.Type.String.required;
}

const transaction = Persist.getEngine('remote', S3Engine).start();

await transaction.put(new Tag({tag: 'documentation'}));
await transaction.commit();
```
- [Model Property Types](./docs/model-property-types.md)
- [Models as Properties](./docs/models-as-properties.md)
- [Structured Queries](./docs/structured-queries.md)
- [Search Queries](./docs/search-queries.md)
- [Storage Engines](./docs/storage-engines.md)
- [Transactions](./docs/transactions.md)
- [Quirks](./docs/code-quirks.md)
6 changes: 6 additions & 0 deletions ava.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ export default {
'exports/**/*.test.js',
'test/acceptance/**/*.test.js',
],
watchMode: {
ignoreChanges: [
'coverage',
'test/fixtures/minified/*',
],
},
};
71 changes: 71 additions & 0 deletions docs/code-quirks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Code Quirks

When using Persist in a minified or bundled codebase, it's important to be aware of two key quirks: handling class names during minification and managing reference errors when working with model relationships.

## Class Names and Minification

When you bundle or minify JavaScript code for production, class names are often altered, which can cause issues. Specifically, models may lose their original class names, which we rely on for storing data in the correct namespace.

To avoid this problem, you have two options:

1. Disable class name mangling in your minifier.
2. Use `this.setMinifiedName(name)` to manually specify the model's name.

```javascript
import Persist from "@acodeninja/persist";

export class Person extends Persist.Type.Model {
static {
this.setMinifiedName('Person');
this.name = Persist.Type.String.required;
}
}
```

If you don't set the minified name, the wrong namespace may be used when saving models, leading to unexpected behavior.

## Reference Errors

When defining relationships between models, especially circular relationships (e.g., `Person` references `Address`, and `Address` references `Person`), the order of declarations matters. If the models are referenced before they are initialized, you'll encounter `ReferenceError` messages, like:

```console
ReferenceError: Cannot access 'Address' before initialization
```

To avoid these errors, always define model relationships using arrow functions. For example:

```javascript
import Persist from "@acodeninja/persist";

export class Person extends Persist.Type.Model {
static {
this.address = () => Address;
}
}

export class Address extends Persist.Type.Model {
static {
this.person = () => Person;
this.address = Persist.Type.String.required;
this.postcode = Persist.Type.String.required;
}
}
```

By doing this, you ensure that model references are evaluated lazily, after all models have been initialized, preventing `ReferenceError` issues.

## Using `HTTP` Engine in Browser

When implementing thee `HTTP` engine for code that runs in the web browser, you must pass `fetch` into the engine configuration and bind it to the `window` object.

```javascript
import Persist from "@acodeninja/persist";
import HTTPEngine from "@acodeninja/persist/engine/http";

Persist.addEngine('remote', HTTPEngine, {
host: 'https://api.example.com',
fetch: fetch.bind(window),
});
```

This will ensure that `fetch` can access the window context which is required for it to function.
Loading

0 comments on commit 85d884a

Please sign in to comment.