Skip to content

Commit

Permalink
Implemented Storage Versioning (#54)
Browse files Browse the repository at this point in the history
* Implemented Storage Versioning

* Added Unit test

* Removed experimental coverage
  • Loading branch information
mlhaufe authored Dec 17, 2023
1 parent 547ece4 commit 2bf15da
Show file tree
Hide file tree
Showing 58 changed files with 376 additions and 690 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"plugin:@typescript-eslint/eslint-recommended"
],
"ignorePatterns": [
"package.json",
"node_modules",
"dist",
"webpack.config.mjs"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
run: npm run build:prod
- name: Test
run: npm run test
- name: Upload artifact
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ jobs:
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
run: npm run build:dev
- name: Test
run: npm run test
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
"author": "Michael L Haufe <[email protected]> (https://final-hill.com)",
"license": "AGPL-3.0-only",
"scripts": {
"build": "webpack",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production",
"lint": "eslint src/ --ext .mts --fix",
"serve": "webpack serve",
"test": "globstar -- node --experimental-test-coverage --import tsx --test \"src/**/*.test.mts\""
"serve": "webpack serve --mode development",
"test": "globstar -- node --import tsx --test \"src/**/*.test.mts\""
},
"devDependencies": {
"@types/dom-navigation": "^1.0.3",
Expand Down Expand Up @@ -49,4 +50,4 @@
"mermaid": "^10.6.1",
"style-loader": "^3.3.3"
}
}
}
6 changes: 4 additions & 2 deletions src/data/BehaviorRepository.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Behavior } from '~/domain/Behavior.mjs';
import Behavior from '~/domain/Behavior.mjs';
import { LocalStorageRepository } from './LocalStorageRepository.mjs';
import BehaviorToJsonMapper from '~/mappers/BehaviorToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class BehaviorRepository extends LocalStorageRepository<Behavior> {
constructor() { super('behavior', Behavior); }
constructor() { super('behavior', new BehaviorToJsonMapper(pkg.version)); }
}
6 changes: 4 additions & 2 deletions src/data/EnvironmentRepository.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Environment } from '~/domain/Environment.mjs';
import Environment from '~/domain/Environment.mjs';
import { PEGSRepository } from './PEGSRepository.mjs';
import EnvironmentToJsonMapper from '~/mappers/EnvironmentToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class EnvironmentRepository extends PEGSRepository<Environment> {
constructor() { super('environments', Environment); }
constructor() { super('environments', new EnvironmentToJsonMapper(pkg.version)); }
}
6 changes: 4 additions & 2 deletions src/data/GlossaryRepository.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { GlossaryTerm } from '~/domain/GlossaryTerm.mjs';
import GlossaryTerm from '~/domain/GlossaryTerm.mjs';
import { LocalStorageRepository } from './LocalStorageRepository.mjs';
import GlossaryTermToJsonMapper from '~/mappers/GlossaryTermToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class GlossaryRepository extends LocalStorageRepository<GlossaryTerm> {
constructor() {
super('glossary', GlossaryTerm);
super('glossary', new GlossaryTermToJsonMapper(pkg.version));
}
}
6 changes: 4 additions & 2 deletions src/data/GoalsRepository.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Goals } from '~/domain/Goals.mjs';
import Goals from '~/domain/Goals.mjs';
import { PEGSRepository } from './PEGSRepository.mjs';
import GoalsToJsonMapper from '~/mappers/GoalsToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class GoalsRepository extends PEGSRepository<Goals> {
constructor() { super('goals', Goals); }
constructor() { super('goals', new GoalsToJsonMapper(pkg.version)); }
}
48 changes: 22 additions & 26 deletions src/data/LocalStorageRepository.mts
Original file line number Diff line number Diff line change
@@ -1,83 +1,79 @@
import { type Entity } from '~/domain/Entity.mjs';
import type Entity from '~/domain/Entity.mjs';
import type { EntityJson } from '~/mappers/EntityToJsonMapper.mjs';
import type Mapper from '~/usecases/Mapper.mjs';
import Repository from '~/usecases/Repository.mjs';

export class LocalStorageRepository<E extends Entity> extends Repository<E> {
private _storageKey;
private _fromJSON;

constructor(storageKey: string, EntityConstructor: typeof Entity) {
super(EntityConstructor);

this._storageKey = storageKey;
this._fromJSON = EntityConstructor.fromJSON as (json: any) => E;
constructor(readonly storageKey: string, mapper: Mapper<E, EntityJson>) {
super(mapper);
}

get storage(): Storage {
return localStorage;
}

get(id: E['id']): Promise<E | undefined> {
const data = this.storage.getItem(this._storageKey),
json: E[] = data ? JSON.parse(data) : [],
const data = this.storage.getItem(this.storageKey),
json: EntityJson[] = data ? JSON.parse(data) : [],
result = json.find(item => item.id === id);

return Promise.resolve(
result ? this._fromJSON(result) : undefined
result ? this.mapper.mapFrom(result) : undefined
);
}

getAll(filter: (entity: E) => boolean = _ => true): Promise<E[]> {
const data = this.storage.getItem(this._storageKey),
json: E[] = data ? JSON.parse(data) : [],
result = json.filter(filter).map(this._fromJSON);
const data = this.storage.getItem(this.storageKey),
json: EntityJson[] = data ? JSON.parse(data) : [],
result = json.map(this.mapper.mapFrom).filter(filter);

return Promise.resolve(result);
}

add(item: E): Promise<void> {
const data = this.storage.getItem(this._storageKey),
const data = this.storage.getItem(this.storageKey),
json: E[] = data ? JSON.parse(data) : [];
json.push(item.toJSON() as E);
this.storage.setItem(this._storageKey, JSON.stringify(json));
json.push(this.mapper.mapTo(item));
this.storage.setItem(this.storageKey, JSON.stringify(json));

this.dispatchEvent(new CustomEvent('update'));

return Promise.resolve();
}

clear(): Promise<void> {
this.storage.removeItem(this._storageKey);
this.storage.removeItem(this.storageKey);
this.dispatchEvent(new CustomEvent('update'));

return Promise.resolve();
}

update(item: E): Promise<void> {
const data = this.storage.getItem(this._storageKey),
json: E[] = data ? JSON.parse(data) : [],
const data = this.storage.getItem(this.storageKey),
json: EntityJson[] = data ? JSON.parse(data) : [],
index = json.findIndex(e => e.id === item.id);

if (index === -1)
throw new Error('Not found');

json[index] = item.toJSON() as E;
this.storage.setItem(this._storageKey, JSON.stringify(json));
json[index] = this.mapper.mapTo(item);
this.storage.setItem(this.storageKey, JSON.stringify(json));

this.dispatchEvent(new CustomEvent('update'));

return Promise.resolve();
}

delete(id: E['id']): Promise<void> {
const data = this.storage.getItem(this._storageKey),
json: E[] = data ? JSON.parse(data) : [],
const data = this.storage.getItem(this.storageKey),
json: EntityJson[] = data ? JSON.parse(data) : [],
index = json.findIndex(item => item.id === id);

if (index === -1)
throw new Error('Not found');

json.splice(index, 1);
this.storage.setItem(this._storageKey, JSON.stringify(json));
this.storage.setItem(this.storageKey, JSON.stringify(json));

this.dispatchEvent(new CustomEvent('update'));

Expand Down
10 changes: 6 additions & 4 deletions src/data/LocalStorageRepository.test.mts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { Behavior } from '~/domain/Behavior.mjs';
import { LocalStorageRepository } from './LocalStorageRepository.mjs';
import type { Entity } from '~/domain/Entity.mjs';
import Behavior from '~/domain/Behavior.mjs';
// @ts-expect-error: No typings available
import DomStorage from 'dom-storage';
import BehaviorToJsonMapper from '~/mappers/BehaviorToJsonMapper.mjs';

const fakeStorage: Storage = new DomStorage(null, { strict: true });

class TestLocalStorageRepository<E extends Entity> extends LocalStorageRepository<E> {
class TestLocalStorageRepository<B extends Behavior> extends LocalStorageRepository<B> {
override get storage() {
return fakeStorage;
}
}

describe('LocalStorageRepository', () => {
const repository = new TestLocalStorageRepository<Behavior>('localstorage-test', Behavior);
const repository = new TestLocalStorageRepository<Behavior>(
'localstorage-test', new BehaviorToJsonMapper('0.3.0')
);

test('storage property', () => {
assert(repository.storage instanceof DomStorage);
Expand Down
8 changes: 5 additions & 3 deletions src/data/PEGSRepository.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { PEGS } from '~/domain/PEGS.mjs';
import type PEGS from '~/domain/PEGS.mjs';
import { LocalStorageRepository } from './LocalStorageRepository.mjs';
import type Mapper from '~/usecases/Mapper.mjs';
import type { EntityJson } from '~/mappers/EntityToJsonMapper.mjs';

export abstract class PEGSRepository<E extends PEGS> extends LocalStorageRepository<E> {
constructor(storageKey: string, EntityConstructor: typeof PEGS) {
super(storageKey, EntityConstructor);
constructor(storageKey: string, mapper: Mapper<E, EntityJson>) {
super(storageKey, mapper);
}

async getBySlug(slug: string): Promise<E | undefined> {
Expand Down
34 changes: 0 additions & 34 deletions src/data/PEGSRepository.test.mts

This file was deleted.

6 changes: 4 additions & 2 deletions src/data/ProjectRepository.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Project } from '~/domain/Project.mjs';
import Project from '~/domain/Project.mjs';
import { PEGSRepository } from './PEGSRepository.mjs';
import ProjectToJsonMapper from '~/mappers/ProjectToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class ProjectRepository extends PEGSRepository<Project> {
constructor() { super('projects', Project); }
constructor() { super('projects', new ProjectToJsonMapper(pkg.version)); }
}
6 changes: 4 additions & 2 deletions src/data/StakeholderRepository.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Stakeholder } from '~/domain/Stakeholder.mjs';
import Stakeholder from '~/domain/Stakeholder.mjs';
import { LocalStorageRepository } from './LocalStorageRepository.mjs';
import StakeholderToJsonMapper from '~/mappers/StakeholderToJsonMapper.mjs';
import pkg from '~/../package.json' with { type: 'json' };

export class StakeholderRepository extends LocalStorageRepository<Stakeholder> {
constructor() { super('stakeholder', Stakeholder); }
constructor() { super('stakeholder', new StakeholderToJsonMapper(pkg.version)); }
}
10 changes: 2 additions & 8 deletions src/domain/Behavior.mts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import type { Properties } from '~/types/Properties.mjs';
import { Requirement, type RequirementJson } from './Requirement.mjs';

export interface BehaviorJson extends RequirementJson { }

export class Behavior extends Requirement {
static override fromJSON({ id, statement }: BehaviorJson): Behavior {
return new Behavior({ id, statement });
}
import Requirement from './Requirement.mjs';

export default class Behavior extends Requirement {
constructor(options: Properties<Behavior>) {
super(options);
}
Expand Down
22 changes: 0 additions & 22 deletions src/domain/Behavior.test.mts

This file was deleted.

Loading

0 comments on commit 2bf15da

Please sign in to comment.