Port to deno
This commit is contained in:
parent
e83a1fda32
commit
33bc8f1863
|
@ -1,2 +0,0 @@
|
||||||
node_modules
|
|
||||||
.venv
|
|
|
@ -1,10 +1,9 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*.{ts,json}]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
|
||||||
end_of_line = lf
|
|
||||||
max_line_length = off
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
.venv
|
deno.d.ts
|
||||||
node_modules
|
.vscode
|
||||||
.rts2_cache_*
|
.local
|
||||||
.coverage
|
|
||||||
/dist/
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
image: node:latest
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
|
|
||||||
test:
|
|
||||||
before_script:
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- npm test
|
|
|
@ -1,3 +1,7 @@
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
}
|
"deno.enable": true,
|
||||||
|
"deno.suggest.imports.hosts": {
|
||||||
|
"https://deno.land": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -1,11 +0,0 @@
|
||||||
FROM alpine
|
|
||||||
|
|
||||||
RUN apk add --no-cache nodejs npm
|
|
||||||
|
|
||||||
ADD . /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
EXPOSE 5001
|
|
||||||
CMD npm run storageserver
|
|
68
README.md
68
README.md
|
@ -1,67 +1,3 @@
|
||||||
tk-storage
|
# typescript/storage
|
||||||
==========
|
|
||||||
|
|
||||||
[![pipeline status](https://gitlab.com/thunderk/tk-storage/badges/master/pipeline.svg)](https://gitlab.com/thunderk/tk-storage/commits/master)
|
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/storage?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=storage)
|
||||||
[![coverage report](https://gitlab.com/thunderk/tk-storage/badges/master/coverage.svg)](https://gitlab.com/thunderk/tk-storage/commits/master)
|
|
||||||
[![npm version](https://img.shields.io/npm/v/tk-storage.svg)](https://npmjs.com/tk-storage)
|
|
||||||
[![npm size](https://img.shields.io/bundlephobia/min/tk-storage.svg)](https://bundlephobia.com/result?p=tk-storage)
|
|
||||||
|
|
||||||
About
|
|
||||||
-----
|
|
||||||
|
|
||||||
Javascript/Typescript persistent storage, with key-value stores as foundation.
|
|
||||||
|
|
||||||
Typescript definitions are included.
|
|
||||||
|
|
||||||
Issues can be reported on [GitLab](https://gitlab.com/thunderk/tk-storage/issues).
|
|
||||||
|
|
||||||
Install
|
|
||||||
-------
|
|
||||||
|
|
||||||
Import in node:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm install tk-storage
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { getLocalStorage } from "tk-storage";
|
|
||||||
const storage = getLocalStorage("myapp");
|
|
||||||
```
|
|
||||||
|
|
||||||
Import in browser:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://unpkg.com/tk-storage"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const storage = tkStorage.getLocalStorage("myapp");
|
|
||||||
```
|
|
||||||
|
|
||||||
Use
|
|
||||||
---
|
|
||||||
|
|
||||||
To get a storage locally persistent (saved in browser data or on disk for Node.js):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const storage = getLocalStorage("myapp");
|
|
||||||
await storage.get("key"); // => null
|
|
||||||
await storage.set("key", "value");
|
|
||||||
await storage.get("key"); // => "value"
|
|
||||||
```
|
|
||||||
|
|
||||||
To get a storage remotely persistent (saved on a compliant server):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const storage = getRemoteStorage("myapp", "https://tk-storage.example.io/", { shared: true });
|
|
||||||
await storage.get("key"); // => null
|
|
||||||
await storage.set("key", "value");
|
|
||||||
await storage.get("key"); // => "value"
|
|
||||||
```
|
|
||||||
|
|
||||||
Run a server for remote storage:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm run storageserver
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Activation script for virtual nodejs environment
|
|
||||||
# Usage:
|
|
||||||
# source activate_node
|
|
||||||
|
|
||||||
vdir="./.venv"
|
|
||||||
expected="12.13.0"
|
|
||||||
|
|
||||||
if [ \! -f "./activate_node" ]
|
|
||||||
then
|
|
||||||
echo "Not in project directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
test -x "${vdir}/bin/nodeenv" || ( python3 -m venv "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
|
|
||||||
test "$(${vdir}/node/bin/nodejs --version)" = "v${expected}" || "${vdir}/bin/nodeenv" --node=${expected} --force "${vdir}/node"
|
|
||||||
source "${vdir}/node/bin/activate"
|
|
|
@ -1,13 +1,5 @@
|
||||||
import { KeyValueStorage, MemoryStorage, RefScopedStorage } from "./basic";
|
import { basicCheck, describe, expect, it } from "./testing.ts";
|
||||||
|
import { MemoryStorage, RefScopedStorage } from "./basic.ts";
|
||||||
export async function basicCheck(storage: KeyValueStorage): Promise<void> {
|
|
||||||
expect(await storage.get("test")).toBe(null);
|
|
||||||
await storage.set("test", "val");
|
|
||||||
expect(await storage.get("test")).toBe("val");
|
|
||||||
expect(await storage.get("notest")).toBe(null);
|
|
||||||
await storage.set("test", null);
|
|
||||||
expect(await storage.get("test")).toBe(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(MemoryStorage, () => {
|
describe(MemoryStorage, () => {
|
||||||
it("stores values in memory", async () => {
|
it("stores values in memory", async () => {
|
|
@ -1,18 +1,18 @@
|
||||||
import uuid1 from "uuid/v1";
|
import { uuid1 } from "./deps.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard interface for the simplest key-value store
|
* Standard interface for the simplest key-value store
|
||||||
*/
|
*/
|
||||||
export interface KeyValueStorage {
|
export interface KeyValueStorage {
|
||||||
get(key: string): Promise<string | null>
|
get(key: string): Promise<string | null>;
|
||||||
set(key: string, value: string | null): Promise<void>
|
set(key: string, value: string | null): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic memory implementation
|
* Basic memory implementation
|
||||||
*/
|
*/
|
||||||
export class MemoryStorage implements KeyValueStorage {
|
export class MemoryStorage implements KeyValueStorage {
|
||||||
private store: { [key: string]: string } = {}
|
private store: { [key: string]: string } = {};
|
||||||
|
|
||||||
async get(key: string): Promise<string | null> {
|
async get(key: string): Promise<string | null> {
|
||||||
const result = this.store[key];
|
const result = this.store[key];
|
||||||
|
@ -28,7 +28,7 @@ export class MemoryStorage implements KeyValueStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageDelegate = KeyValueStorage | (() => KeyValueStorage)
|
type StorageDelegate = KeyValueStorage | (() => KeyValueStorage);
|
||||||
|
|
||||||
function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
|
function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
|
||||||
return (typeof delegate === "function") ? delegate() : delegate;
|
return (typeof delegate === "function") ? delegate() : delegate;
|
||||||
|
@ -38,7 +38,7 @@ function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
|
||||||
* Wrap a storage, scoping the keys using a suffix
|
* Wrap a storage, scoping the keys using a suffix
|
||||||
*/
|
*/
|
||||||
export class ScopedStorage implements KeyValueStorage {
|
export class ScopedStorage implements KeyValueStorage {
|
||||||
private readonly target: KeyValueStorage
|
private readonly target: KeyValueStorage;
|
||||||
|
|
||||||
constructor(target: StorageDelegate, private readonly suffix: string) {
|
constructor(target: StorageDelegate, private readonly suffix: string) {
|
||||||
this.target = fromDelegate(target);
|
this.target = fromDelegate(target);
|
||||||
|
@ -59,9 +59,9 @@ export class ScopedStorage implements KeyValueStorage {
|
||||||
* The namespace is persisted in a reference storage (used unscoped)
|
* The namespace is persisted in a reference storage (used unscoped)
|
||||||
*/
|
*/
|
||||||
export class RefScopedStorage implements KeyValueStorage {
|
export class RefScopedStorage implements KeyValueStorage {
|
||||||
private readonly reference: KeyValueStorage
|
private readonly reference: KeyValueStorage;
|
||||||
private readonly target: KeyValueStorage
|
private readonly target: KeyValueStorage;
|
||||||
private inner?: KeyValueStorage
|
private inner?: KeyValueStorage;
|
||||||
|
|
||||||
constructor(reference: StorageDelegate, target: StorageDelegate) {
|
constructor(reference: StorageDelegate, target: StorageDelegate) {
|
||||||
this.reference = fromDelegate(reference);
|
this.reference = fromDelegate(reference);
|
||||||
|
@ -74,7 +74,7 @@ export class RefScopedStorage implements KeyValueStorage {
|
||||||
if (suffix) {
|
if (suffix) {
|
||||||
return new ScopedStorage(this.target, suffix);
|
return new ScopedStorage(this.target, suffix);
|
||||||
} else {
|
} else {
|
||||||
const suffix = uuid1();
|
const suffix = uuid1.generate().toString();
|
||||||
await this.reference.set(refkey, suffix);
|
await this.reference.set(refkey, suffix);
|
||||||
return new ScopedStorage(this.target, suffix);
|
return new ScopedStorage(this.target, suffix);
|
||||||
}
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { v1 as uuid1 } from "https://deno.land/std@0.99.0/uuid/mod.ts";
|
||||||
|
export { json, opine } from "https://deno.land/x/opine@1.5.3/mod.ts";
|
||||||
|
export { opineCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
|
||||||
|
export { readAll } from "https://deno.land/std@0.99.0/io/util.ts";
|
|
@ -1,11 +0,0 @@
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<script src="tk-storage.umd.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,27 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
transform: {
|
|
||||||
"^.+\\.ts$": "ts-jest"
|
|
||||||
},
|
|
||||||
moduleFileExtensions: [
|
|
||||||
"ts",
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"node"
|
|
||||||
],
|
|
||||||
watchPathIgnorePatterns: [
|
|
||||||
"<rootDir>/dist/",
|
|
||||||
"<rootDir>/node_modules/",
|
|
||||||
],
|
|
||||||
restoreMocks: true,
|
|
||||||
collectCoverage: true,
|
|
||||||
collectCoverageFrom: [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"!src/**/*.test.ts",
|
|
||||||
],
|
|
||||||
coverageDirectory: ".coverage",
|
|
||||||
coverageReporters: [
|
|
||||||
"lcovonly",
|
|
||||||
"html",
|
|
||||||
"text-summary"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {
|
||||||
|
basicCheck,
|
||||||
|
describe,
|
||||||
|
disableLocalStorage,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
} from "./testing.ts";
|
||||||
|
import { LocalStorage } from "./local.ts";
|
||||||
|
|
||||||
|
describe(LocalStorage, () => {
|
||||||
|
it("uses localStorage as storage", async () => {
|
||||||
|
const storage = new LocalStorage();
|
||||||
|
await basicCheck(storage);
|
||||||
|
|
||||||
|
await disableLocalStorage(async () => {
|
||||||
|
expect(() => new LocalStorage()).toThrow(
|
||||||
|
"localStorage not available",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
import { KeyValueStorage } from "./basic";
|
import { KeyValueStorage } from "./basic.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key-value store using localStorage
|
* Key-value store using localStorage
|
||||||
*/
|
*/
|
||||||
export class BrowserLocalStorage implements KeyValueStorage {
|
export class LocalStorage implements KeyValueStorage {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (typeof localStorage == "undefined" || !localStorage) {
|
if (typeof localStorage == "undefined" || !localStorage) {
|
||||||
throw new Error("localStorage not available");
|
throw new Error("localStorage not available");
|
|
@ -1,19 +1,8 @@
|
||||||
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from ".";
|
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./mod.ts";
|
||||||
import { MemoryStorage, ScopedStorage } from "./basic";
|
import { MemoryStorage, ScopedStorage } from "./basic.ts";
|
||||||
import { BrowserLocalStorage } from "./browser";
|
import { describe, expect, it, runTestServer } from "./testing.ts";
|
||||||
import { NodeDirectoryStorage } from "./node";
|
|
||||||
import { forceNodeStoragesInTempDir } from "./node.test";
|
|
||||||
import { startTestServer } from "./remote.test";
|
|
||||||
|
|
||||||
const localStorage = (window as any).localStorage;
|
|
||||||
|
|
||||||
describe(getLocalStorage, () => {
|
|
||||||
forceNodeStoragesInTempDir();
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
(window as any).localStorage = localStorage;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/*describe(getLocalStorage, () => {
|
||||||
it("returns the best storage available", async () => {
|
it("returns the best storage available", async () => {
|
||||||
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
|
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
|
||||||
|
|
||||||
|
@ -35,32 +24,36 @@ describe(getLocalStorage, () => {
|
||||||
storage = getLocalStorage("app1");
|
storage = getLocalStorage("app1");
|
||||||
expect(storage).toBeInstanceOf(NodeDirectoryStorage);
|
expect(storage).toBeInstanceOf(NodeDirectoryStorage);
|
||||||
|
|
||||||
(NodeDirectoryStorage.findUserData as any).mockImplementation(() => { throw new Error() });
|
(NodeDirectoryStorage.findUserData as any).mockImplementation(() => {
|
||||||
|
throw new Error();
|
||||||
|
});
|
||||||
|
|
||||||
storage = getLocalStorage("app1");
|
storage = getLocalStorage("app1");
|
||||||
expect(storage).toBeInstanceOf(MemoryStorage);
|
expect(storage).toBeInstanceOf(MemoryStorage);
|
||||||
expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage");
|
expect(mockWarn).toHaveBeenCalledWith(
|
||||||
|
"No persistent storage available, using in-memory volatile storage",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});*/
|
||||||
|
|
||||||
describe(getRemoteStorage, () => {
|
describe(getRemoteStorage, () => {
|
||||||
const server = startTestServer();
|
|
||||||
|
|
||||||
it("returns a scoped storage", async () => {
|
it("returns a scoped storage", async () => {
|
||||||
let storage = getRemoteStorage("app1", `http://localhost:${server.port()}`);
|
await runTestServer(async (url) => {
|
||||||
|
let storage = getRemoteStorage("app1", url);
|
||||||
|
|
||||||
await storage.set("testkey", "testvalue");
|
await storage.set("testkey", "testvalue");
|
||||||
expect(await storage.get("testkey")).toBe("testvalue");
|
expect(await storage.get("testkey")).toBe("testvalue");
|
||||||
|
|
||||||
storage = getRemoteStorage("app1", `http://localhost:${server.port()}`);
|
storage = getRemoteStorage("app1", url);
|
||||||
expect(await storage.get("testkey")).toBe("testvalue");
|
expect(await storage.get("testkey")).toBe("testvalue");
|
||||||
|
|
||||||
storage = getRemoteStorage("app2", `http://localhost:${server.port()}`);
|
storage = getRemoteStorage("app2", url);
|
||||||
expect(await storage.get("testkey")).toBeNull();
|
expect(await storage.get("testkey")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scopes in a private namespace on shared mode", async () => {
|
it("scopes in a private namespace on shared mode", async () => {
|
||||||
|
// TODO
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import {
|
||||||
|
KeyValueStorage,
|
||||||
|
MemoryStorage,
|
||||||
|
RefScopedStorage,
|
||||||
|
ScopedStorage,
|
||||||
|
} from "./basic.ts";
|
||||||
|
import { LocalStorage } from "./local.ts";
|
||||||
|
import { RestRemoteStorage } from "./remote.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base type for storage usage
|
||||||
|
*/
|
||||||
|
export type TKStorage = KeyValueStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed classes
|
||||||
|
*/
|
||||||
|
export {
|
||||||
|
LocalStorage,
|
||||||
|
MemoryStorage,
|
||||||
|
RefScopedStorage,
|
||||||
|
RestRemoteStorage,
|
||||||
|
ScopedStorage,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the best "local" storage available
|
||||||
|
*/
|
||||||
|
export function getLocalStorage(appname: string): TKStorage {
|
||||||
|
try {
|
||||||
|
return new ScopedStorage(new LocalStorage(), appname);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
"No persistent storage available, using in-memory volatile storage",
|
||||||
|
);
|
||||||
|
return new MemoryStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a remote storage, based on an URI
|
||||||
|
*
|
||||||
|
* If *shared* is set to true, a namespace will be used to avoid collisions, using getLocalStorage to persist it
|
||||||
|
*/
|
||||||
|
export function getRemoteStorage(
|
||||||
|
appname: string,
|
||||||
|
uri: string,
|
||||||
|
options = { shared: false },
|
||||||
|
): TKStorage {
|
||||||
|
let storage: TKStorage = new RestRemoteStorage(appname, uri);
|
||||||
|
storage = new ScopedStorage(storage, appname);
|
||||||
|
if (options.shared) {
|
||||||
|
storage = new RefScopedStorage(getLocalStorage(appname), storage);
|
||||||
|
}
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a in-memory volatile storage
|
||||||
|
*/
|
||||||
|
export function getMemoryStorage(): TKStorage {
|
||||||
|
return new MemoryStorage();
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
|
@ -1,60 +0,0 @@
|
||||||
{
|
|
||||||
"name": "tk-storage",
|
|
||||||
"version": "0.3.0",
|
|
||||||
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
|
|
||||||
"main": "dist/tk-storage.umd.js",
|
|
||||||
"source": "src/index.ts",
|
|
||||||
"module": "dist/tk-storage.modern.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"test": "jest",
|
|
||||||
"normalize": "tk-base",
|
|
||||||
"build": "microbundle build -f modern,umd",
|
|
||||||
"dev": "run-p dev:*",
|
|
||||||
"dev:test": "jest --watchAll",
|
|
||||||
"dev:build": "microbundle watch -f modern,umd",
|
|
||||||
"storageserver": "ts-node src/server.ts",
|
|
||||||
"docker:build": "docker build -t thunderk/tk-storage .",
|
|
||||||
"docker:push": "docker push thunderk/tk-storage",
|
|
||||||
"docker:run": "docker run -it --rm thunderk/tk-storage",
|
|
||||||
"docker:publish": "run-s docker:build docker:push",
|
|
||||||
"prepare": "npm run build",
|
|
||||||
"prepublishOnly": "npm test",
|
|
||||||
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"xmlhttprequest": "^1.8.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/cors": "^2.8.6",
|
|
||||||
"@types/express": "^4.17.2",
|
|
||||||
"@types/uuid": "^3.4.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.17.1",
|
|
||||||
"get-port": "^5.0.0",
|
|
||||||
"tk-base": "^0.2.5",
|
|
||||||
"uuid": "^3.3.3"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"/src",
|
|
||||||
"/dist"
|
|
||||||
],
|
|
||||||
"author": {
|
|
||||||
"name": "Michaël Lemaire",
|
|
||||||
"url": "https://thunderk.net"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://code.thunderk.net/tslib/tk-storage.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://gitlab.com/thunderk/tk-storage/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://code.thunderk.net/tslib/tk-storage",
|
|
||||||
"license": "ISC",
|
|
||||||
"keywords": [
|
|
||||||
"typescript",
|
|
||||||
"storage",
|
|
||||||
"database"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { basicCheck, describe, it, runTestServer } from "./testing.ts";
|
||||||
|
import { RestRemoteStorage } from "./remote.ts";
|
||||||
|
|
||||||
|
describe(RestRemoteStorage, () => {
|
||||||
|
it("communicates with the server", async () => {
|
||||||
|
await runTestServer(async (url) => {
|
||||||
|
const storage = new RestRemoteStorage("test", url);
|
||||||
|
await basicCheck(storage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { KeyValueStorage } from "./basic.ts";
|
||||||
|
|
||||||
|
function protectKey(key: string): string {
|
||||||
|
return encodeURIComponent(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
type RestClient = (
|
||||||
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
|
uri: string,
|
||||||
|
body?: string,
|
||||||
|
) => Promise<string | null>;
|
||||||
|
export const HEADER_REQUESTER = "X-TK-Storage-App";
|
||||||
|
export const HEADER_REPLYIER = "X-TK-Storage-Control";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage on a remote server, directly usable using HTTP REST verbs
|
||||||
|
*/
|
||||||
|
export class RestRemoteStorage implements KeyValueStorage {
|
||||||
|
readonly appname: string;
|
||||||
|
readonly base_url: string;
|
||||||
|
readonly client: RestClient;
|
||||||
|
|
||||||
|
constructor(appname: string, url: string) {
|
||||||
|
this.appname = appname;
|
||||||
|
this.base_url = (url[url.length - 1] != "/") ? url + "/" : url;
|
||||||
|
this.client = this.createClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<string | null> {
|
||||||
|
key = protectKey(key);
|
||||||
|
return await this.client("GET", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: string | null): Promise<void> {
|
||||||
|
key = protectKey(key);
|
||||||
|
if (value === null) {
|
||||||
|
await this.client("DELETE", key);
|
||||||
|
} else {
|
||||||
|
await this.client("PUT", key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createClient(): RestClient {
|
||||||
|
const client: RestClient = async (method, path, body?) => {
|
||||||
|
const response = await fetch(`${this.base_url}${path}`, {
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
headers: {
|
||||||
|
[HEADER_REQUESTER]: this.appname,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.headers.get(HEADER_REPLYIER) != "ok") {
|
||||||
|
throw new Error("storage not compatible with tk-storage");
|
||||||
|
} else if (response.status == 200) {
|
||||||
|
return await response.text();
|
||||||
|
} else if (response.status == 404) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`http error ${response.status}: ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env -S deno run --allow-read --allow-net
|
||||||
|
import { getLocalStorage } from "./mod.ts";
|
||||||
|
import { KeyValueStorage } from "./basic.ts";
|
||||||
|
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote.ts";
|
||||||
|
import { json, opine, opineCors, readAll } from "./deps.ts";
|
||||||
|
|
||||||
|
const PORT = 5001;
|
||||||
|
|
||||||
|
type Server = {
|
||||||
|
close: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a server compliant with RestRemoteStorage
|
||||||
|
*/
|
||||||
|
export function startRestServer(
|
||||||
|
port: number,
|
||||||
|
storage: KeyValueStorage,
|
||||||
|
): Promise<Server> {
|
||||||
|
const app = opine();
|
||||||
|
app.use(
|
||||||
|
opineCors({ exposedHeaders: [HEADER_REPLYIER] }),
|
||||||
|
json(),
|
||||||
|
(req, res, next) => {
|
||||||
|
if (req.get(HEADER_REQUESTER)) {
|
||||||
|
res.set(HEADER_REPLYIER, "ok");
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.setStatus(400).send();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
app.get("*", (req, res, next) => {
|
||||||
|
storage.get(req.path).then((result) => {
|
||||||
|
if (result === null) {
|
||||||
|
res.setStatus(404).send();
|
||||||
|
} else {
|
||||||
|
res.send(result);
|
||||||
|
}
|
||||||
|
}).catch(next);
|
||||||
|
});
|
||||||
|
app.put("*", (req, res, next) => {
|
||||||
|
readAll(req.body).then((body) => {
|
||||||
|
const value = new TextDecoder().decode(body);
|
||||||
|
storage.set(req.path, value).then(() => {
|
||||||
|
res.send();
|
||||||
|
}).catch(next);
|
||||||
|
}).catch(next);
|
||||||
|
});
|
||||||
|
app.delete("*", (req, res, next) => {
|
||||||
|
storage.set(req.path, null).then(() => {
|
||||||
|
res.send();
|
||||||
|
}).catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const server = app.listen(port, () => {
|
||||||
|
resolve({
|
||||||
|
close: () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
server.close();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await startRestServer(PORT, getLocalStorage("tk-storage-server"));
|
||||||
|
console.log(`Server running, use http://localhost:${PORT} to connect`);
|
||||||
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
import { basicCheck } from "./basic.test";
|
|
||||||
import { BrowserLocalStorage } from "./browser";
|
|
||||||
|
|
||||||
const localStorage = (window as any).localStorage;
|
|
||||||
|
|
||||||
describe(BrowserLocalStorage, () => {
|
|
||||||
it("uses localStorage as storage", async () => {
|
|
||||||
const storage = new BrowserLocalStorage();
|
|
||||||
await basicCheck(storage);
|
|
||||||
|
|
||||||
delete (window as any)["localStorage"];
|
|
||||||
expect(() => new BrowserLocalStorage()).toThrowError("localStorage not available");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
(window as any).localStorage = localStorage;
|
|
||||||
});
|
|
51
src/index.ts
51
src/index.ts
|
@ -1,51 +0,0 @@
|
||||||
import { KeyValueStorage, MemoryStorage, RefScopedStorage, ScopedStorage } from "./basic";
|
|
||||||
import { BrowserLocalStorage } from "./browser";
|
|
||||||
import { NodeDirectoryStorage } from "./node";
|
|
||||||
import { RestRemoteStorage } from "./remote";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base type for storage usage
|
|
||||||
*/
|
|
||||||
export type TKStorage = KeyValueStorage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposed classes
|
|
||||||
*/
|
|
||||||
export { MemoryStorage, ScopedStorage, RefScopedStorage, BrowserLocalStorage, NodeDirectoryStorage, RestRemoteStorage };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the best "local" storage available
|
|
||||||
*/
|
|
||||||
export function getLocalStorage(appname: string): TKStorage {
|
|
||||||
try {
|
|
||||||
return new ScopedStorage(new BrowserLocalStorage(), appname);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
return new NodeDirectoryStorage(appname);
|
|
||||||
} catch {
|
|
||||||
console.warn("No persistent storage available, using in-memory volatile storage");
|
|
||||||
return new MemoryStorage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a remote storage, based on an URI
|
|
||||||
*
|
|
||||||
* If *shared* is set to true, a namespace will be used to avoid collisions, using getLocalStorage to persist it
|
|
||||||
*/
|
|
||||||
export function getRemoteStorage(appname: string, uri: string, options = { shared: false }): TKStorage {
|
|
||||||
let storage: TKStorage = new RestRemoteStorage(appname, uri);
|
|
||||||
storage = new ScopedStorage(storage, appname);
|
|
||||||
if (options.shared) {
|
|
||||||
storage = new RefScopedStorage(getLocalStorage(appname), storage);
|
|
||||||
}
|
|
||||||
return storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a in-memory volatile storage
|
|
||||||
*/
|
|
||||||
export function getMemoryStorage(): TKStorage {
|
|
||||||
return new MemoryStorage();
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { basicCheck } from "./basic.test";
|
|
||||||
import { NodeDirectoryStorage } from "./node";
|
|
||||||
|
|
||||||
let tempdir: string | null = null
|
|
||||||
|
|
||||||
export function forceNodeStoragesInTempDir() {
|
|
||||||
beforeEach(() => {
|
|
||||||
const fs = require("fs");
|
|
||||||
const os = require("os");
|
|
||||||
const path = require("path");
|
|
||||||
const tmpdir = tempdir = fs.mkdtempSync(path.join(os.tmpdir(), "tk-storage-testing"));
|
|
||||||
jest.spyOn(NodeDirectoryStorage, "findUserData").mockReturnValue(tmpdir);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (tempdir) {
|
|
||||||
// TODO remove directory
|
|
||||||
tempdir = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(NodeDirectoryStorage, () => {
|
|
||||||
forceNodeStoragesInTempDir();
|
|
||||||
|
|
||||||
it("uses a directory as storage", async () => {
|
|
||||||
const storage = new NodeDirectoryStorage("test-tk-storage");
|
|
||||||
await basicCheck(storage);
|
|
||||||
});
|
|
||||||
});
|
|
82
src/node.ts
82
src/node.ts
|
@ -1,82 +0,0 @@
|
||||||
import { KeyValueStorage } from "./basic";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key-value store, using local file-system
|
|
||||||
*/
|
|
||||||
export class NodeDirectoryStorage implements KeyValueStorage {
|
|
||||||
readonly directory: string
|
|
||||||
private checked = false
|
|
||||||
|
|
||||||
constructor(appname: string) {
|
|
||||||
if (typeof require === "undefined") {
|
|
||||||
throw new Error("Storage only available in nodejs");
|
|
||||||
}
|
|
||||||
const path = require("path");
|
|
||||||
this.directory = path.join(NodeDirectoryStorage.findUserData(), "tk-storage", appname);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the writable user data directory
|
|
||||||
*/
|
|
||||||
static findUserData(): string {
|
|
||||||
const result = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
|
|
||||||
if (!result) {
|
|
||||||
throw new Error("Cannot locate user data directory");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file path for a key
|
|
||||||
*/
|
|
||||||
getPath(key: string): string {
|
|
||||||
const path = require("path");
|
|
||||||
return path.join(this.directory, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(key: string): Promise<string | null> {
|
|
||||||
const fs = require("fs");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readFile(this.getPath(key), { encoding: 'utf8' }, (err: any, data: string) => {
|
|
||||||
resolve(err ? null : data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(key: string, value: string | null): Promise<void> {
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
if (!this.checked) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
fs.mkdir(this.directory, { recursive: true }, (err: any) => {
|
|
||||||
if (err && err.code !== 'EEXIST') {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (value === null) {
|
|
||||||
fs.unlink(this.getPath(key), (err: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fs.writeFile(this.getPath(key), value, { encoding: 'utf8' }, (err: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import getPort from "get-port";
|
|
||||||
import { MemoryStorage } from "./basic";
|
|
||||||
import { basicCheck } from "./basic.test";
|
|
||||||
import { RestRemoteStorage } from "./remote";
|
|
||||||
import { startRestServer } from "./server";
|
|
||||||
|
|
||||||
export function startTestServer(): { port: () => number } {
|
|
||||||
let app: { close: Function } | undefined;
|
|
||||||
let port: number;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
port = await getPort();
|
|
||||||
app = await startRestServer(port, new MemoryStorage());
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
if (app) {
|
|
||||||
await app.close();
|
|
||||||
app = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { port: () => port };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(RestRemoteStorage, () => {
|
|
||||||
const server = startTestServer();
|
|
||||||
|
|
||||||
it("communicates with the server", async () => {
|
|
||||||
const storage = new RestRemoteStorage("test", `http://localhost:${server.port()}`);
|
|
||||||
await basicCheck(storage);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,64 +0,0 @@
|
||||||
import { KeyValueStorage } from "./basic";
|
|
||||||
|
|
||||||
function protectKey(key: string): string {
|
|
||||||
return encodeURIComponent(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
type RestClient = (method: "GET" | "POST" | "PUT" | "DELETE", uri: string, body?: string) => Promise<string | null>
|
|
||||||
export const HEADER_REQUESTER = "X-TK-Storage-App"
|
|
||||||
export const HEADER_REPLYIER = "X-TK-Storage-Control"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage on a remote server, directly usable using HTTP REST verbs
|
|
||||||
*/
|
|
||||||
export class RestRemoteStorage implements KeyValueStorage {
|
|
||||||
readonly appname: string
|
|
||||||
readonly base_url: string
|
|
||||||
readonly client: RestClient
|
|
||||||
|
|
||||||
constructor(appname: string, url: string) {
|
|
||||||
this.appname = appname;
|
|
||||||
this.base_url = (url[url.length - 1] != "/") ? url + "/" : url;
|
|
||||||
this.client = this.createClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(key: string): Promise<string | null> {
|
|
||||||
key = protectKey(key);
|
|
||||||
return await this.client("GET", key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(key: string, value: string | null): Promise<void> {
|
|
||||||
key = protectKey(key);
|
|
||||||
if (value === null) {
|
|
||||||
await this.client("DELETE", key);
|
|
||||||
} else {
|
|
||||||
await this.client("PUT", key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createClient(): RestClient {
|
|
||||||
const Request: typeof XMLHttpRequest = (typeof XMLHttpRequest === "undefined") ? require("xmlhttprequest").XMLHttpRequest : XMLHttpRequest;
|
|
||||||
const client: RestClient = (method, path, body?) => new Promise((resolve, reject) => {
|
|
||||||
const xhr = new Request();
|
|
||||||
xhr.open(method, `${this.base_url}${path}`);
|
|
||||||
xhr.responseType = 'text';
|
|
||||||
xhr.setRequestHeader(HEADER_REQUESTER, this.appname);
|
|
||||||
xhr.onload = function () {
|
|
||||||
if (xhr.getResponseHeader(HEADER_REPLYIER) != "ok") {
|
|
||||||
reject("storage not compatible with tk-storage");
|
|
||||||
} else if (xhr.status == 200) {
|
|
||||||
resolve(xhr.responseText);
|
|
||||||
} else if (xhr.status == 404) {
|
|
||||||
resolve(null);
|
|
||||||
} else {
|
|
||||||
reject(`http error ${xhr.status}: ${xhr.statusText}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = function () {
|
|
||||||
reject("unknown error");
|
|
||||||
};
|
|
||||||
xhr.send(body);
|
|
||||||
});
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
import bodyParser from "body-parser";
|
|
||||||
import cors from "cors";
|
|
||||||
import express from "express";
|
|
||||||
import { getLocalStorage } from ".";
|
|
||||||
import { KeyValueStorage } from "./basic";
|
|
||||||
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote";
|
|
||||||
|
|
||||||
type Server = {
|
|
||||||
close: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a server compliant with RestRemoteStorage
|
|
||||||
*/
|
|
||||||
export function startRestServer(port: number, storage: KeyValueStorage): Promise<Server> {
|
|
||||||
const app = express();
|
|
||||||
app.use(
|
|
||||||
cors({ exposedHeaders: [HEADER_REPLYIER] }),
|
|
||||||
bodyParser.text(),
|
|
||||||
(req, res, next) => {
|
|
||||||
if (req.header(HEADER_REQUESTER)) {
|
|
||||||
res.set(HEADER_REPLYIER, "ok");
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(400).send();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
app.get("*", (req, res, next) => {
|
|
||||||
storage.get(req.path).then(result => {
|
|
||||||
if (result === null) {
|
|
||||||
res.status(404).send();
|
|
||||||
} else {
|
|
||||||
res.send(result);
|
|
||||||
}
|
|
||||||
}).catch(next);
|
|
||||||
});
|
|
||||||
app.put("*", (req, res, next) => {
|
|
||||||
storage.set(req.path, req.body).then(() => {
|
|
||||||
res.send();
|
|
||||||
}).catch(next);
|
|
||||||
});
|
|
||||||
app.delete("*", (req, res, next) => {
|
|
||||||
storage.set(req.path, null).then(() => {
|
|
||||||
res.send();
|
|
||||||
}).catch(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const server = app.listen(port, () => {
|
|
||||||
resolve({
|
|
||||||
close: () => new Promise((resolve, reject) => {
|
|
||||||
server.close((err) => err ? reject(err) : resolve());
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof require !== "undefined" && require.main === module) {
|
|
||||||
startRestServer(5001, getLocalStorage("tk-storage-server")).then(() => {
|
|
||||||
console.log(`Server running, use http://localhost:5001 to connect`);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { KeyValueStorage, MemoryStorage } from "./basic.ts";
|
||||||
|
import { expect } from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts";
|
||||||
|
export {
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts";
|
||||||
|
import { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
|
||||||
|
import { startRestServer } from "./server.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic high-level test suite for any kind storage
|
||||||
|
*/
|
||||||
|
export async function basicCheck(storage: KeyValueStorage): Promise<void> {
|
||||||
|
expect(await storage.get("test")).toBe(null);
|
||||||
|
await storage.set("test", "val");
|
||||||
|
expect(await storage.get("test")).toBe("val");
|
||||||
|
expect(await storage.get("notest")).toBe(null);
|
||||||
|
await storage.set("test", null);
|
||||||
|
expect(await storage.get("test")).toBe(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporarily disable localStorage (constructor will throw error),
|
||||||
|
* for the duration of the callback
|
||||||
|
*/
|
||||||
|
export async function disableLocalStorage(
|
||||||
|
body: () => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
const ns = window as any;
|
||||||
|
const removed = ns["localStorage"];
|
||||||
|
delete ns["localStorage"];
|
||||||
|
try {
|
||||||
|
await body;
|
||||||
|
} finally {
|
||||||
|
ns["localStorage"] = removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a temporary test server, with memory storage
|
||||||
|
*/
|
||||||
|
export async function runTestServer(
|
||||||
|
body: (url: string) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
if (!port) {
|
||||||
|
throw new Error("No port available for test server");
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await startRestServer(port, new MemoryStorage());
|
||||||
|
try {
|
||||||
|
await body(`http://localhost:${port}`);
|
||||||
|
} finally {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,10 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"module": "esnext",
|
||||||
"esModuleInterop": true,
|
"target": "ESNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true
|
||||||
"declaration": true,
|
}
|
||||||
"target": "es6",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"es6"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
"src/**/*.test.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue