acrylblends 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -7
- package/dist/index.js.map +1 -1
- package/lib/audit-BXnYsLge.d.ts +58 -0
- package/lib/cell-ChU41DZD.d.ts +32 -0
- package/lib/client-modules-CLvWOjbd.js +267 -0
- package/lib/connection-ZLEso7XL.d.ts +25 -0
- package/lib/conninfo.d.ts +1 -23
- package/lib/cordis/db-sqlite.d.ts +56 -0
- package/lib/cordis/db-sqlite.js +58 -0
- package/lib/cordis/db.d.ts +8 -0
- package/lib/cordis/db.js +1 -0
- package/lib/cordis/door.d.ts +159 -0
- package/lib/cordis/door.js +757 -0
- package/lib/cordis/inspect.d.ts +77 -0
- package/lib/cordis/inspect.js +149 -0
- package/lib/cordis/kv-db.d.ts +16 -0
- package/lib/cordis/kv-db.js +18 -0
- package/lib/cordis/kv-json.d.ts +16 -0
- package/lib/cordis/kv-json.js +44 -0
- package/lib/cordis/kv.d.ts +8 -0
- package/lib/cordis/kv.js +1 -0
- package/lib/cordis/node-server.d.ts +45 -0
- package/lib/cordis/node-server.js +93 -0
- package/lib/cordis/router-http.d.ts +56 -0
- package/lib/cordis/router-http.js +117 -0
- package/lib/cordis/router.d.ts +8 -0
- package/lib/cordis/router.js +7 -0
- package/lib/cordis/state/react.d.ts +6 -0
- package/lib/cordis/state/react.js +11 -0
- package/lib/cordis/state.d.ts +33 -0
- package/lib/cordis/state.js +163 -0
- package/lib/core-CaY8XDZ6.js +19 -0
- package/lib/database-DBHdwefi.d.ts +44 -0
- package/lib/db/conformance.d.ts +11 -0
- package/lib/db/conformance.js +267 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db/sqlite.js +1 -196
- package/lib/db.d.ts +2 -1
- package/lib/{src-0hI7M7cN.js → deprecated-BP4fPt38.js} +0 -17
- package/lib/door/host.d.ts +24 -0
- package/lib/door/host.js +2 -0
- package/lib/door/page.d.ts +105 -0
- package/lib/door/page.js +1030 -0
- package/lib/door/protocol.d.ts +2 -0
- package/lib/door/protocol.js +197 -0
- package/lib/error-t4N2js2I.d.ts +8 -0
- package/lib/etag-DaKMOjty.js +135 -0
- package/lib/etag.js +1 -134
- package/lib/helpers-CvTauQT5.js +5 -0
- package/lib/http-exception-kpGJkmpi.d.ts +57 -0
- package/lib/http-exception.d.ts +1 -56
- package/lib/{index-BLTYsyc1.d.ts → index-BnGgRKTv.d.ts} +3 -39
- package/lib/index-BqVQkTnu.d.ts +98 -0
- package/lib/index-Dmst2QCQ.d.ts +44 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/jwk-LkzeoaU8.js +117 -0
- package/lib/jwk.js +1 -116
- package/lib/jwt-CskdhEW9.js +115 -0
- package/lib/jwt.js +2 -114
- package/lib/kv/conformance.d.ts +13 -0
- package/lib/kv/conformance.js +301 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv-9f-AlRKN.d.ts +39 -0
- package/lib/kv.d.ts +2 -1
- package/lib/language-D9a4zqvn.js +198 -0
- package/lib/language.js +1 -197
- package/lib/middleware-CPQ8himN.js +12 -0
- package/lib/node-server/serve-static.d.ts +0 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +1 -7
- package/lib/node-server.js +3 -1586
- package/lib/open-CMhnJODn.js +197 -0
- package/lib/protocol-BizVt4J6.d.ts +144 -0
- package/lib/request-id-CKVvHOO1.js +34 -0
- package/lib/request-id.js +1 -33
- package/lib/secure-headers-mP3j6Hm6.js +163 -0
- package/lib/secure-headers.js +1 -162
- package/lib/{serve-static-B849HlX2.js → serve-static-3uo_9rjF.js} +1 -4
- package/lib/server-DvgMGtzo.js +1323 -0
- package/lib/snapshot-text-D3NspDyT.js +338 -0
- package/lib/src-CwABFr6r.js +328 -0
- package/lib/timeout.d.ts +1 -1
- package/lib/timing-Cky7C1Jf.js +183 -0
- package/lib/timing.js +1 -182
- package/package.json +89 -1
- package/plugins/acryl-db/plugin.json +6 -1
- package/plugins/acryl-db-sqlite/plugin.json +4 -0
- package/plugins/acryl-db-sqlite/src/facade/node.ts +1 -1
- package/plugins/acryl-db-sqlite/src/index.ts +2 -2
- package/plugins/acryl-db-sqlite/src/open.ts +2 -2
- package/plugins/acryl-door/plugin.json +4 -1
- package/plugins/acryl-door/src/host/http.ts +2 -2
- package/plugins/acryl-door/src/host/mcp-routes.ts +1 -1
- package/plugins/acryl-door/src/host/operator-routes.ts +1 -1
- package/plugins/acryl-door/src/host/page-routes.ts +3 -3
- package/plugins/acryl-door/src/index.ts +2 -2
- package/plugins/acryl-inspect/plugin.json +4 -1
- package/plugins/acryl-inspect/src/service.ts +2 -2
- package/plugins/acryl-kv/plugin.json +6 -1
- package/plugins/acryl-kv-db/plugin.json +4 -0
- package/plugins/acryl-kv-db/src/index.ts +1 -1
- package/plugins/acryl-kv-db/src/open.ts +3 -3
- package/plugins/acryl-kv-json/plugin.json +4 -0
- package/plugins/acryl-kv-json/src/index.ts +1 -1
- package/plugins/acryl-kv-json/src/open.ts +2 -2
- package/plugins/acryl-node-server/plugin.json +5 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +1 -1
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +1 -1
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +2 -2
- package/plugins/acryl-node-server/src/adapter/websocket.ts +2 -2
- package/plugins/acryl-node-server/src/client-modules.ts +2 -2
- package/plugins/acryl-node-server/src/index.ts +1 -1
- package/plugins/acryl-router/plugin.json +11 -1
- package/plugins/acryl-router/src/connection.d.ts +25 -0
- package/plugins/acryl-router/src/core/app-base.d.ts +215 -0
- package/plugins/acryl-router/src/core/app.d.ts +19 -0
- package/plugins/acryl-router/src/core/client/client.d.ts +4 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +35 -0
- package/plugins/acryl-router/src/core/client/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/client/types.d.ts +229 -0
- package/plugins/acryl-router/src/core/client/utils.d.ts +18 -0
- package/plugins/acryl-router/src/core/compose.d.ts +14 -0
- package/plugins/acryl-router/src/core/context.d.ts +443 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +30 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +36 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +25 -0
- package/plugins/acryl-router/src/core/helper/css/common.d.ts +59 -0
- package/plugins/acryl-router/src/core/helper/css/index.d.ts +80 -0
- package/plugins/acryl-router/src/core/helper/dev/index.d.ts +20 -0
- package/plugins/acryl-router/src/core/helper/factory/index.d.ts +82 -0
- package/plugins/acryl-router/src/core/helper/html/index.d.ts +8 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +73 -0
- package/plugins/acryl-router/src/core/helper/route/index.d.ts +75 -0
- package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +44 -0
- package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +27 -0
- package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +91 -0
- package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +16 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +8 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +13 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +3 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +3 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/helper/testing/index.d.ts +13 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +70 -0
- package/plugins/acryl-router/src/core/http-exception.d.ts +58 -0
- package/plugins/acryl-router/src/core/index.d.ts +37 -0
- package/plugins/acryl-router/src/core/jsx/base.d.ts +61 -0
- package/plugins/acryl-router/src/core/jsx/children.d.ts +9 -0
- package/plugins/acryl-router/src/core/jsx/components.d.ts +15 -0
- package/plugins/acryl-router/src/core/jsx/constants.d.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/context.d.ts +51 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +10 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +3 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +83 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +39 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +16 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +8 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +7 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +73 -0
- package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +47 -0
- package/plugins/acryl-router/src/core/jsx/index.d.ts +76 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +13 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +735 -0
- package/plugins/acryl-router/src/core/jsx/streaming.d.ts +36 -0
- package/plugins/acryl-router/src/core/jsx/types.d.ts +36 -0
- package/plugins/acryl-router/src/core/jsx/utils.d.ts +4 -0
- package/plugins/acryl-router/src/core/request/constants.d.ts +1 -0
- package/plugins/acryl-router/src/core/request.d.ts +306 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +6 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +13 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +17 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +11 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +8 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +11 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +5 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +7 -0
- package/plugins/acryl-router/src/core/router/utils.d.ts +1 -0
- package/plugins/acryl-router/src/core/router.d.ts +97 -0
- package/plugins/acryl-router/src/core/types.d.ts +573 -0
- package/plugins/acryl-router/src/core/utils/accept.d.ts +6 -0
- package/plugins/acryl-router/src/core/utils/body.d.ts +79 -0
- package/plugins/acryl-router/src/core/utils/buffer.d.ts +17 -0
- package/plugins/acryl-router/src/core/utils/color.d.ts +21 -0
- package/plugins/acryl-router/src/core/utils/concurrent.d.ts +11 -0
- package/plugins/acryl-router/src/core/utils/constants.d.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.d.ts +41 -0
- package/plugins/acryl-router/src/core/utils/crypto.d.ts +15 -0
- package/plugins/acryl-router/src/core/utils/handler.d.ts +6 -0
- package/plugins/acryl-router/src/core/utils/headers.d.ts +8 -0
- package/plugins/acryl-router/src/core/utils/html.d.ts +44 -0
- package/plugins/acryl-router/src/core/utils/http-status.d.ts +32 -0
- package/plugins/acryl-router/src/core/utils/mime.d.ts +70 -0
- package/plugins/acryl-router/src/core/utils/stream.d.ts +31 -0
- package/plugins/acryl-router/src/core/utils/types.d.ts +128 -0
- package/plugins/acryl-router/src/core/utils/url.d.ts +44 -0
- package/plugins/acryl-router/src/core/validator/index.d.ts +7 -0
- package/plugins/acryl-router/src/core/validator/utils.d.ts +29 -0
- package/plugins/acryl-router/src/core/validator/validator.d.ts +16 -0
- package/plugins/acryl-router/src/helpers/index.d.ts +17 -0
- package/plugins/acryl-router/src/helpers/schema-validator.d.ts +54 -0
- package/plugins/acryl-router-http/plugin.json +4 -1
- package/plugins/acryl-router-http/src/index.ts +1 -1
- package/plugins/acryl-state/plugin.json +5 -1
- package/plugins/acryl-state/src/service.ts +3 -3
- package/plugins/tsconfig.json +32 -20
- package/docs/framework/UPSTREAM.md +0 -3
- package/docs/framework/upstream.json +0 -9
- package/lib/index-D4FAsBZu.d.ts +0 -85
- package/plugins/acryl-db/upstream.json +0 -18
- package/plugins/acryl-db-sqlite/upstream.json +0 -21
- package/plugins/acryl-door/upstream.json +0 -28
- package/plugins/acryl-inspect/upstream.json +0 -18
- package/plugins/acryl-kv/upstream.json +0 -18
- package/plugins/acryl-kv-db/upstream.json +0 -21
- package/plugins/acryl-kv-json/upstream.json +0 -22
- package/plugins/acryl-node-server/upstream.json +0 -21
- package/plugins/acryl-router/upstream.json +0 -20
- package/plugins/acryl-router-http/upstream.json +0 -22
- package/plugins/acryl-state/NOTICE +0 -30
- package/plugins/acryl-state/upstream.json +0 -22
- package/plugins/desktop/upstream.json +0 -12
- package/plugins/desktop-web/upstream.json +0 -12
- package/templates/todo/upstream.json +0 -12
- /package/lib/{index-CTy4N7ng.d.ts → deprecated-CTy4N7ng.d.ts} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HTTPException } from "../http-exception.js";
|
|
2
|
+
import { t as Context } from "../context-DDO0cfuP.js";
|
|
3
|
+
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
4
|
+
import { t as App } from "../deprecated-BP4fPt38.js";
|
|
5
|
+
import { n as getConnInfo, t as connectionOf } from "../connection-C7xgP1hP.js";
|
|
6
|
+
import { i as name, n as Router, r as apply, t as Config } from "../src-CwABFr6r.js";
|
|
7
|
+
export { Acryl, App, Config, Context, HTTPException, Router, apply, connectionOf, getConnInfo, name };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
//#region ../../plugins/acryl-state/src/react.ts
|
|
3
|
+
/**
|
|
4
|
+
* The React binding: one hook that reads a cell and re-renders when it changes. `useSyncExternalStore` is React's own
|
|
5
|
+
* contract for exactly this, so it is tear-free under concurrent rendering. Import from `acryl-state/react`.
|
|
6
|
+
*/
|
|
7
|
+
function useCell(cell) {
|
|
8
|
+
return useSyncExternalStore((listener) => cell.subscribe(listener), cell.get, cell.get);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useCell };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { t as Context$1 } from "../context-DS3fYyG-.js";
|
|
2
|
+
import { a as createDerived, i as createCell, n as Listener, r as ReadonlyCell, t as Cell } from "../cell-ChU41DZD.js";
|
|
3
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region ../../plugins/acryl-state/src/service.d.ts
|
|
6
|
+
declare module '@deepseek-ai/cordis' {
|
|
7
|
+
interface Context {
|
|
8
|
+
state: State;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
12
|
+
declare const name = "acryl-state";
|
|
13
|
+
declare class State extends Service {
|
|
14
|
+
private readonly cells;
|
|
15
|
+
constructor(ctx: Context);
|
|
16
|
+
private claim;
|
|
17
|
+
private report;
|
|
18
|
+
/** Create a writable cell owned by the calling plugin. The key must be new. */
|
|
19
|
+
cell<T>(key: string, initial: T): Cell<T>;
|
|
20
|
+
/** Create a read-only cell computed from other cells, owned by the calling plugin. */
|
|
21
|
+
derive<D extends readonly ReadonlyCell<unknown>[], T>(key: string, inputs: D, compute: (...values: { [K in keyof D]: D[K] extends ReadonlyCell<infer V> ? V : never }) => T): ReadonlyCell<T>;
|
|
22
|
+
/** A cell by key, if one is live. */
|
|
23
|
+
find<T = unknown>(key: string): ReadonlyCell<T> | undefined;
|
|
24
|
+
keys(): string[];
|
|
25
|
+
/**
|
|
26
|
+
* The current value of `key` as a server-sent event stream: the value now, then one event per change, until the client cancels or the router
|
|
27
|
+
* is disposed (both abort the stream). A comment line goes out every 15 seconds so proxies and browsers keep an idle stream open.
|
|
28
|
+
*/
|
|
29
|
+
stream(key: string, c: Context$1): Response | undefined;
|
|
30
|
+
}
|
|
31
|
+
declare function apply(ctx: Context): void;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type Cell, type Listener, type ReadonlyCell, State, apply, createCell, createDerived, name };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
2
|
+
import { methodNotAllowed } from "../method-not-allowed.js";
|
|
3
|
+
import { r as streamSSE } from "../streaming-DHST3Scg.js";
|
|
4
|
+
import { loopbackOnly } from "../loopback-only.js";
|
|
5
|
+
import "../helpers-CvTauQT5.js";
|
|
6
|
+
import "../src-CwABFr6r.js";
|
|
7
|
+
import "../middleware-CPQ8himN.js";
|
|
8
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
9
|
+
//#region ../../plugins/acryl-state/src/cell.ts
|
|
10
|
+
/** A writable cell. `report` receives anything a listener throws; one failing listener never blocks the others. */
|
|
11
|
+
function createCell(key, initial, report = () => void 0) {
|
|
12
|
+
let value = initial;
|
|
13
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
14
|
+
const set = (next) => {
|
|
15
|
+
if (Object.is(next, value)) return;
|
|
16
|
+
const previous = value;
|
|
17
|
+
value = next;
|
|
18
|
+
for (const listener of [...listeners]) try {
|
|
19
|
+
listener(next, previous);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
report(error);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
key,
|
|
26
|
+
get: () => value,
|
|
27
|
+
set,
|
|
28
|
+
update: (change) => {
|
|
29
|
+
set(change(value));
|
|
30
|
+
},
|
|
31
|
+
subscribe: (listener) => {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(listener);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A read-only cell computed from other cells and recomputed whenever one of them changes. `dispose` detaches it from its
|
|
41
|
+
* inputs; after that it keeps its last value.
|
|
42
|
+
*/
|
|
43
|
+
function createDerived(key, inputs, compute, report = () => void 0) {
|
|
44
|
+
const read = () => compute(...inputs.map((input) => input.get()));
|
|
45
|
+
const inner = createCell(key, read(), report);
|
|
46
|
+
const stops = inputs.map((input) => input.subscribe(() => {
|
|
47
|
+
inner.set(read());
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
cell: {
|
|
51
|
+
key,
|
|
52
|
+
get: inner.get,
|
|
53
|
+
subscribe: inner.subscribe
|
|
54
|
+
},
|
|
55
|
+
dispose: () => {
|
|
56
|
+
for (const stop of stops) stop();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region ../../plugins/acryl-state/src/service.ts
|
|
62
|
+
/**
|
|
63
|
+
* The ACRYL state service (`ctx.state`): observable cells owned by plugins. A cell created from a plugin's context is removed
|
|
64
|
+
* when that plugin unloads, so a surface never keeps a value alive for a plugin that is gone. In process the cells are read
|
|
65
|
+
* and subscribed to directly (the terminal UI); with a router present they are also readable as JSON and streamable as
|
|
66
|
+
* server-sent events (Web and Desktop), read-only: only the Host plugin that owns a cell writes it.
|
|
67
|
+
*/
|
|
68
|
+
const KEEP_ALIVE_MS = 15e3;
|
|
69
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
70
|
+
const name = "acryl-state";
|
|
71
|
+
const KEY_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/;
|
|
72
|
+
var State = class extends Service {
|
|
73
|
+
cells = /* @__PURE__ */ new Map();
|
|
74
|
+
constructor(ctx) {
|
|
75
|
+
super(ctx, "state");
|
|
76
|
+
}
|
|
77
|
+
claim(key) {
|
|
78
|
+
if (!KEY_PATTERN.test(key)) throw new TypeError(`acryl-state: key "${key}" must be dotted lowercase words, like "market.sources"`);
|
|
79
|
+
if (this.cells.has(key)) throw new Error(`acryl-state: key "${key}" is already in use`);
|
|
80
|
+
}
|
|
81
|
+
report = (error) => {
|
|
82
|
+
this.ctx.logger.error(`acryl-state: a subscriber failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
};
|
|
84
|
+
/** Create a writable cell owned by the calling plugin. The key must be new. */
|
|
85
|
+
cell(key, initial) {
|
|
86
|
+
this.claim(key);
|
|
87
|
+
const cell = createCell(key, initial, this.report);
|
|
88
|
+
this.cells.set(key, cell);
|
|
89
|
+
this.ctx.effect(() => () => {
|
|
90
|
+
this.cells.delete(key);
|
|
91
|
+
}, `acryl-state: cell ${key}`);
|
|
92
|
+
return cell;
|
|
93
|
+
}
|
|
94
|
+
/** Create a read-only cell computed from other cells, owned by the calling plugin. */
|
|
95
|
+
derive(key, inputs, compute) {
|
|
96
|
+
this.claim(key);
|
|
97
|
+
const derived = createDerived(key, inputs, compute, this.report);
|
|
98
|
+
this.cells.set(key, derived.cell);
|
|
99
|
+
this.ctx.effect(() => () => {
|
|
100
|
+
derived.dispose();
|
|
101
|
+
this.cells.delete(key);
|
|
102
|
+
}, `acryl-state: derived ${key}`);
|
|
103
|
+
return derived.cell;
|
|
104
|
+
}
|
|
105
|
+
/** A cell by key, if one is live. */
|
|
106
|
+
find(key) {
|
|
107
|
+
return this.cells.get(key);
|
|
108
|
+
}
|
|
109
|
+
keys() {
|
|
110
|
+
return [...this.cells.keys()].sort();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The current value of `key` as a server-sent event stream: the value now, then one event per change, until the client cancels or the router
|
|
114
|
+
* is disposed (both abort the stream). A comment line goes out every 15 seconds so proxies and browsers keep an idle stream open.
|
|
115
|
+
*/
|
|
116
|
+
stream(key, c) {
|
|
117
|
+
const cell = this.find(key);
|
|
118
|
+
if (cell === void 0) return void 0;
|
|
119
|
+
return streamSSE(c, async (stream) => {
|
|
120
|
+
const ended = new Promise((resolve) => {
|
|
121
|
+
stream.onAbort(resolve);
|
|
122
|
+
});
|
|
123
|
+
let queue = Promise.resolve();
|
|
124
|
+
const send = (value) => {
|
|
125
|
+
queue = queue.then(() => stream.writeSSE({
|
|
126
|
+
event: "value",
|
|
127
|
+
data: typeof value === "string" ? value : JSON.stringify(value)
|
|
128
|
+
})).catch(() => {});
|
|
129
|
+
};
|
|
130
|
+
send(cell.get());
|
|
131
|
+
const unsubscribe = cell.subscribe(send);
|
|
132
|
+
const keepAlive = setInterval(() => {
|
|
133
|
+
queue = queue.then(() => stream.write(": keepalive\n\n")).catch(() => {});
|
|
134
|
+
}, KEEP_ALIVE_MS);
|
|
135
|
+
keepAlive.unref();
|
|
136
|
+
try {
|
|
137
|
+
await ended;
|
|
138
|
+
} finally {
|
|
139
|
+
clearInterval(keepAlive);
|
|
140
|
+
unsubscribe();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function apply(ctx) {
|
|
146
|
+
const state = new State(ctx);
|
|
147
|
+
ctx.inject(["router"], (inner) => {
|
|
148
|
+
const api = new Acryl();
|
|
149
|
+
api.use(loopbackOnly({ inProcess: "allow" }), methodNotAllowed({ app: api })).get("/", (c) => c.json({ keys: state.keys() })).get("/:key", (c) => {
|
|
150
|
+
const cell = state.find(c.req.param("key"));
|
|
151
|
+
return cell === void 0 ? c.json({ error: "no such state" }, 404) : c.json({
|
|
152
|
+
key: cell.key,
|
|
153
|
+
value: cell.get()
|
|
154
|
+
});
|
|
155
|
+
}).get("/:key/events", (c) => state.stream(c.req.param("key"), c) ?? c.json({ error: "no such state" }, 404));
|
|
156
|
+
inner.router.route("/api/state", api);
|
|
157
|
+
inner.inject(["routerHttp"], (http) => {
|
|
158
|
+
http.routerHttp.mount("/api/state");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { State, apply, createCell, createDerived, name };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "./app-BM0rTCp7.js";
|
|
2
|
+
//#region ../../plugins/acryl-router/src/core/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* @module
|
|
5
|
+
*
|
|
6
|
+
* Acryl - Web Framework built on Web Standards
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Acryl } from 'acrylblends'
|
|
11
|
+
* const app = new Acryl()
|
|
12
|
+
*
|
|
13
|
+
* app.get('/', (c) => c.text('Acryl!'))
|
|
14
|
+
*
|
|
15
|
+
* export default app
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
//#endregion
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region ../../plugins/acryl-db/src/facade/database.d.ts
|
|
2
|
+
/** Values supported by the portable SQL drivers. */
|
|
3
|
+
type SqlValue = null | number | bigint | string | Uint8Array;
|
|
4
|
+
/**
|
|
5
|
+
* Asynchronous SQL operations shared by a database and its transaction handles.
|
|
6
|
+
*
|
|
7
|
+
* `exec` runs SQL text without bindings and may contain several statements. `run`, `get`, and `all`
|
|
8
|
+
* execute one statement with positional bindings. Adapters may cache prepared statements by SQL text,
|
|
9
|
+
* so callers pass values as bindings instead of interpolating them. `run` resolves the number of rows
|
|
10
|
+
* the statement changed.
|
|
11
|
+
*/
|
|
12
|
+
interface DbExecutor {
|
|
13
|
+
exec(sql: string): Promise<void>;
|
|
14
|
+
run(sql: string, ...params: SqlValue[]): Promise<{
|
|
15
|
+
readonly changes: number;
|
|
16
|
+
}>;
|
|
17
|
+
get<T extends object>(sql: string, ...params: SqlValue[]): Promise<T | undefined>;
|
|
18
|
+
all<T extends object>(sql: string, ...params: SqlValue[]): Promise<T[]>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal portable SQL database facade.
|
|
22
|
+
*
|
|
23
|
+
* All operations are asynchronous so adapters may execute outside the caller's runtime.
|
|
24
|
+
*
|
|
25
|
+
* `transaction` passes the callback a transaction handle. All work in the transaction
|
|
26
|
+
* must use that handle; the handle is invalid after the callback settles. Adapters must
|
|
27
|
+
* queue unrelated operations and other transactions until the transaction finishes. The
|
|
28
|
+
* returned promise settles after commit or rollback. Calling the database itself (including
|
|
29
|
+
* `transaction`) from inside a callback therefore waits for that transaction and
|
|
30
|
+
* never settles.
|
|
31
|
+
*
|
|
32
|
+
* When the callback rejects, the adapter must roll the transaction back before rejecting
|
|
33
|
+
* with that same error. If rollback fails, it must reject with a different error (for
|
|
34
|
+
* example an `AggregateError`) so callers cannot mistake the callback error for a
|
|
35
|
+
* guaranteed rollback.
|
|
36
|
+
*
|
|
37
|
+
* Closing is the adapter's: the plugin that opened a database closes it, so the facade an
|
|
38
|
+
* app receives has no `close`.
|
|
39
|
+
*/
|
|
40
|
+
interface Db extends DbExecutor {
|
|
41
|
+
transaction<T>(callback: (transaction: DbExecutor) => Promise<T>): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { DbExecutor as n, SqlValue as r, Db as t };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { t as Db } from "../database-DBHdwefi.js";
|
|
2
|
+
|
|
3
|
+
//#region ../../plugins/acryl-db/src/conformance.d.ts
|
|
4
|
+
/** A fresh, empty database from the provider under test, and how to close it. */
|
|
5
|
+
interface DbUnderTest {
|
|
6
|
+
readonly db: Db;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
declare function describeDbConformance(provider: string, open: () => Promise<DbUnderTest>): void;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { DbUnderTest, describeDbConformance };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { i as applyMigrations, n as MIGRATIONS_TABLE, r as readMigrationFiles } from "../src-MWcsMS8i.js";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
//#region ../../plugins/acryl-db/src/conformance.ts
|
|
7
|
+
/**
|
|
8
|
+
* The `db` conformance suite: what every provider of the port must do, as Vitest tests. A provider passes it to be accepted
|
|
9
|
+
* (`describeDbConformance('acryl-db-sqlite', open)` in its own tests); the SQL is the portable subset, with `?` bindings, so the same
|
|
10
|
+
* suite runs unchanged on SQLite and Postgres.
|
|
11
|
+
*
|
|
12
|
+
* A `?` inside a string literal, a quoted identifier or a comment is text, not a binding. Foreign keys are enforced (`ON DELETE CASCADE` works) and `INSERT ... RETURNING` answers through `get`, on every provider.
|
|
13
|
+
*
|
|
14
|
+
* The transaction cases are the contract the SQL facade documents (and its SQLite tests check): other calls and transactions wait for
|
|
15
|
+
* an active transaction, a rejected callback rolls back and rejects with its own error, and a handle is dead once its transaction settles.
|
|
16
|
+
*/
|
|
17
|
+
/** A promise and the function that settles it, for holding a transaction open. */
|
|
18
|
+
function gate() {
|
|
19
|
+
let open;
|
|
20
|
+
return {
|
|
21
|
+
promise: new Promise((resolve) => {
|
|
22
|
+
open = resolve;
|
|
23
|
+
}),
|
|
24
|
+
open
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function describeDbConformance(provider, open) {
|
|
28
|
+
describe(`db conformance: ${provider}`, () => {
|
|
29
|
+
const opened = [];
|
|
30
|
+
const fresh = async () => {
|
|
31
|
+
const subject = await open();
|
|
32
|
+
opened.push(subject);
|
|
33
|
+
return subject.db;
|
|
34
|
+
};
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await Promise.all(opened.splice(0).map((subject) => subject.close()));
|
|
37
|
+
});
|
|
38
|
+
it("round-trips null, integers, bigints in the safe range, reals, text and bytes", async () => {
|
|
39
|
+
const db = await fresh();
|
|
40
|
+
await db.exec("CREATE TABLE value_probe (n INTEGER, i INTEGER, b BIGINT, r DOUBLE PRECISION, t TEXT, x BYTEA)");
|
|
41
|
+
const bytes = new Uint8Array([
|
|
42
|
+
0,
|
|
43
|
+
1,
|
|
44
|
+
254,
|
|
45
|
+
255
|
|
46
|
+
]);
|
|
47
|
+
await db.run("INSERT INTO value_probe (n, i, b, r, t, x) VALUES (?, ?, ?, ?, ?, ?)", null, 42, 9007199254740991n, 1.5, "café ✓", bytes);
|
|
48
|
+
const row = await db.get("SELECT n, i, b, r, t, x FROM value_probe");
|
|
49
|
+
expect(row).toEqual({
|
|
50
|
+
n: null,
|
|
51
|
+
i: 42,
|
|
52
|
+
b: 9007199254740991,
|
|
53
|
+
r: 1.5,
|
|
54
|
+
t: "café ✓",
|
|
55
|
+
x: bytes
|
|
56
|
+
});
|
|
57
|
+
expect(row?.x).toBeInstanceOf(Uint8Array);
|
|
58
|
+
});
|
|
59
|
+
it("answers get with undefined and all with [] when nothing matches", async () => {
|
|
60
|
+
const db = await fresh();
|
|
61
|
+
await db.exec("CREATE TABLE empty_probe (value INTEGER)");
|
|
62
|
+
expect(await db.get("SELECT value FROM empty_probe WHERE value = ?", 1)).toBeUndefined();
|
|
63
|
+
expect(await db.all("SELECT value FROM empty_probe")).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it("reports how many rows run changed", async () => {
|
|
66
|
+
const db = await fresh();
|
|
67
|
+
await db.exec("CREATE TABLE change_probe (value INTEGER)");
|
|
68
|
+
expect(await db.run("INSERT INTO change_probe (value) VALUES (?), (?), (?)", 1, 2, 3)).toEqual({ changes: 3 });
|
|
69
|
+
expect(await db.run("UPDATE change_probe SET value = value + 10 WHERE value < ?", 3)).toEqual({ changes: 2 });
|
|
70
|
+
expect(await db.run("DELETE FROM change_probe WHERE value = ?", 99)).toEqual({ changes: 0 });
|
|
71
|
+
});
|
|
72
|
+
it("runs several statements in one exec", async () => {
|
|
73
|
+
const db = await fresh();
|
|
74
|
+
await db.exec("CREATE TABLE multi_probe (value INTEGER); INSERT INTO multi_probe (value) VALUES (1); INSERT INTO multi_probe (value) VALUES (2);");
|
|
75
|
+
expect(await db.all("SELECT value FROM multi_probe ORDER BY value")).toEqual([{ value: 1 }, { value: 2 }]);
|
|
76
|
+
});
|
|
77
|
+
it("enforces foreign keys: ON DELETE CASCADE removes the children, and an orphan insert is refused", async () => {
|
|
78
|
+
const db = await fresh();
|
|
79
|
+
await db.exec("CREATE TABLE fk_parent (id INTEGER PRIMARY KEY); CREATE TABLE fk_child (id INTEGER PRIMARY KEY, parent INTEGER NOT NULL REFERENCES fk_parent (id) ON DELETE CASCADE)");
|
|
80
|
+
await db.run("INSERT INTO fk_parent (id) VALUES (?), (?)", 1, 2);
|
|
81
|
+
await db.run("INSERT INTO fk_child (id, parent) VALUES (?, ?), (?, ?), (?, ?)", 10, 1, 11, 1, 20, 2);
|
|
82
|
+
expect(await db.run("DELETE FROM fk_parent WHERE id = ?", 1)).toEqual({ changes: 1 });
|
|
83
|
+
expect(await db.all("SELECT id FROM fk_child ORDER BY id")).toEqual([{ id: 20 }]);
|
|
84
|
+
await expect(db.run("INSERT INTO fk_child (id, parent) VALUES (?, ?)", 30, 99)).rejects.toThrow();
|
|
85
|
+
expect(await db.all("SELECT id FROM fk_child ORDER BY id")).toEqual([{ id: 20 }]);
|
|
86
|
+
});
|
|
87
|
+
it("returns the inserted row through get with INSERT ... RETURNING, inside a transaction too", async () => {
|
|
88
|
+
const db = await fresh();
|
|
89
|
+
await db.exec("CREATE TABLE returning_probe (id INTEGER PRIMARY KEY, body TEXT NOT NULL)");
|
|
90
|
+
expect(await db.get("INSERT INTO returning_probe (id, body) VALUES (?, ?) RETURNING id, body", 1, "first")).toEqual({
|
|
91
|
+
id: 1,
|
|
92
|
+
body: "first"
|
|
93
|
+
});
|
|
94
|
+
expect(await db.transaction((tx) => tx.get("INSERT INTO returning_probe (id, body) VALUES (?, ?) RETURNING id", 2, "second"))).toEqual({ id: 2 });
|
|
95
|
+
expect(await db.all("SELECT body FROM returning_probe ORDER BY id")).toEqual([{ body: "first" }, { body: "second" }]);
|
|
96
|
+
});
|
|
97
|
+
it("binds only the ? outside string literals, quoted identifiers and comments", async () => {
|
|
98
|
+
const db = await fresh();
|
|
99
|
+
expect(await db.get("SELECT '?' AS q, CAST(? AS INTEGER) AS v, 'it''s ?' AS r", 7)).toEqual({
|
|
100
|
+
q: "?",
|
|
101
|
+
v: 7,
|
|
102
|
+
r: "it's ?"
|
|
103
|
+
});
|
|
104
|
+
expect(await db.get("SELECT CAST(? AS INTEGER) AS v /* not ? a binding */, CAST(? AS INTEGER) AS w -- nor ?\n", 1, 2)).toEqual({
|
|
105
|
+
v: 1,
|
|
106
|
+
w: 2
|
|
107
|
+
});
|
|
108
|
+
await db.exec("CREATE TABLE \"odd?name\" (\"col?\" INTEGER, body TEXT)");
|
|
109
|
+
await db.run("INSERT INTO \"odd?name\" (\"col?\", body) VALUES (?, 'what?')", 3);
|
|
110
|
+
expect(await db.all("SELECT \"col?\" AS c, body FROM \"odd?name\" WHERE \"col?\" = ?", 3)).toEqual([{
|
|
111
|
+
c: 3,
|
|
112
|
+
body: "what?"
|
|
113
|
+
}]);
|
|
114
|
+
});
|
|
115
|
+
it("commits work done through the transaction handle and resolves the callback value", async () => {
|
|
116
|
+
const db = await fresh();
|
|
117
|
+
expect(await db.transaction(async (tx) => {
|
|
118
|
+
await tx.exec("CREATE TABLE commit_probe (value INTEGER)");
|
|
119
|
+
await tx.run("INSERT INTO commit_probe (value) VALUES (?)", 1);
|
|
120
|
+
return "committed";
|
|
121
|
+
})).toBe("committed");
|
|
122
|
+
expect(await db.all("SELECT value FROM commit_probe")).toEqual([{ value: 1 }]);
|
|
123
|
+
});
|
|
124
|
+
it("rolls back when the callback rejects, and rejects with that same error", async () => {
|
|
125
|
+
const db = await fresh();
|
|
126
|
+
await db.exec("CREATE TABLE rollback_probe (value INTEGER)");
|
|
127
|
+
const failure = /* @__PURE__ */ new Error("roll back");
|
|
128
|
+
await expect(db.transaction(async (tx) => {
|
|
129
|
+
await tx.run("INSERT INTO rollback_probe (value) VALUES (?)", 1);
|
|
130
|
+
throw failure;
|
|
131
|
+
})).rejects.toBe(failure);
|
|
132
|
+
expect(await db.all("SELECT value FROM rollback_probe")).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
it("makes other calls and transactions wait for an active transaction, then runs them in call order", async () => {
|
|
135
|
+
const db = await fresh();
|
|
136
|
+
await db.exec("CREATE TABLE queue_probe (value INTEGER)");
|
|
137
|
+
const started = gate();
|
|
138
|
+
const release = gate();
|
|
139
|
+
const first = db.transaction(async (tx) => {
|
|
140
|
+
await tx.run("INSERT INTO queue_probe (value) VALUES (?)", 1);
|
|
141
|
+
started.open();
|
|
142
|
+
await release.promise;
|
|
143
|
+
});
|
|
144
|
+
await started.promise;
|
|
145
|
+
const settled = [];
|
|
146
|
+
const write = db.run("INSERT INTO queue_probe (value) VALUES (?)", 2).then(() => {
|
|
147
|
+
settled.push("write");
|
|
148
|
+
});
|
|
149
|
+
const second = db.transaction(async (tx) => {
|
|
150
|
+
await tx.run("INSERT INTO queue_probe (value) VALUES (?)", 3);
|
|
151
|
+
}).then(() => {
|
|
152
|
+
settled.push("second");
|
|
153
|
+
});
|
|
154
|
+
const read = db.all("SELECT value FROM queue_probe ORDER BY value").then((rows) => {
|
|
155
|
+
settled.push("read");
|
|
156
|
+
return rows;
|
|
157
|
+
});
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
159
|
+
expect(settled).toEqual([]);
|
|
160
|
+
release.open();
|
|
161
|
+
await Promise.all([
|
|
162
|
+
first,
|
|
163
|
+
write,
|
|
164
|
+
second
|
|
165
|
+
]);
|
|
166
|
+
expect(await read).toEqual([
|
|
167
|
+
{ value: 1 },
|
|
168
|
+
{ value: 2 },
|
|
169
|
+
{ value: 3 }
|
|
170
|
+
]);
|
|
171
|
+
expect(settled).toEqual([
|
|
172
|
+
"write",
|
|
173
|
+
"second",
|
|
174
|
+
"read"
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
it("keeps a rolled-back transaction from leaking into calls made while it ran", async () => {
|
|
178
|
+
const db = await fresh();
|
|
179
|
+
await db.exec("CREATE TABLE isolation_probe (value INTEGER)");
|
|
180
|
+
const transaction = db.transaction(async (tx) => {
|
|
181
|
+
await tx.run("INSERT INTO isolation_probe (value) VALUES (?)", 1);
|
|
182
|
+
await Promise.resolve();
|
|
183
|
+
throw new Error("roll back");
|
|
184
|
+
});
|
|
185
|
+
const before = db.all("SELECT value FROM isolation_probe");
|
|
186
|
+
const write = db.run("INSERT INTO isolation_probe (value) VALUES (?)", 2);
|
|
187
|
+
const after = db.all("SELECT value FROM isolation_probe");
|
|
188
|
+
await expect(transaction).rejects.toThrow("roll back");
|
|
189
|
+
await write;
|
|
190
|
+
expect(await before).toEqual([]);
|
|
191
|
+
expect(await after).toEqual([{ value: 2 }]);
|
|
192
|
+
});
|
|
193
|
+
it("rejects a transaction handle used after its transaction settles", async () => {
|
|
194
|
+
const db = await fresh();
|
|
195
|
+
await db.exec("CREATE TABLE stale_probe (value INTEGER)");
|
|
196
|
+
let handle;
|
|
197
|
+
await db.transaction(async (tx) => {
|
|
198
|
+
handle = tx;
|
|
199
|
+
await tx.run("INSERT INTO stale_probe (value) VALUES (?)", 1);
|
|
200
|
+
});
|
|
201
|
+
await expect(handle.run("INSERT INTO stale_probe (value) VALUES (?)", 2)).rejects.toThrow();
|
|
202
|
+
await expect(handle.get("SELECT value FROM stale_probe")).rejects.toThrow();
|
|
203
|
+
expect(await db.all("SELECT value FROM stale_probe")).toEqual([{ value: 1 }]);
|
|
204
|
+
});
|
|
205
|
+
it("applies migrations once and in order, and refuses a schema newer than the code", async () => {
|
|
206
|
+
const db = await fresh();
|
|
207
|
+
const first = [{
|
|
208
|
+
version: 1,
|
|
209
|
+
statements: ["CREATE TABLE notes (body TEXT)", "INSERT INTO notes (body) VALUES ('kept')"]
|
|
210
|
+
}];
|
|
211
|
+
await applyMigrations(db, first, { table: MIGRATIONS_TABLE });
|
|
212
|
+
await applyMigrations(db, first, { table: MIGRATIONS_TABLE });
|
|
213
|
+
await applyMigrations(db, [...first, {
|
|
214
|
+
version: 2,
|
|
215
|
+
statements: ["ALTER TABLE notes ADD COLUMN title TEXT"]
|
|
216
|
+
}], { table: MIGRATIONS_TABLE });
|
|
217
|
+
expect(await db.all("SELECT body, title FROM notes")).toEqual([{
|
|
218
|
+
body: "kept",
|
|
219
|
+
title: null
|
|
220
|
+
}]);
|
|
221
|
+
expect(await db.get(`SELECT version FROM ${MIGRATIONS_TABLE}`)).toEqual({ version: 2 });
|
|
222
|
+
await expect(applyMigrations(db, first, { table: MIGRATIONS_TABLE })).rejects.toThrow("Schema version 2 is newer than supported version 1");
|
|
223
|
+
});
|
|
224
|
+
it("rolls every pending migration back together when one fails", async () => {
|
|
225
|
+
const db = await fresh();
|
|
226
|
+
const failing = [{
|
|
227
|
+
version: 1,
|
|
228
|
+
statements: ["CREATE TABLE migration_first (value TEXT)", "INSERT INTO migration_first (value) VALUES ('retained')"]
|
|
229
|
+
}, {
|
|
230
|
+
version: 2,
|
|
231
|
+
statements: ["CREATE TABLE migration_second (value TEXT)", "THIS IS NOT SQL"]
|
|
232
|
+
}];
|
|
233
|
+
await expect(applyMigrations(db, failing, { table: MIGRATIONS_TABLE })).rejects.toThrow();
|
|
234
|
+
await expect(db.all("SELECT value FROM migration_first")).rejects.toThrow();
|
|
235
|
+
const version = await db.get(`SELECT version FROM ${MIGRATIONS_TABLE}`).catch(() => void 0);
|
|
236
|
+
expect(version === void 0 || Number(version.version) === 0).toBe(true);
|
|
237
|
+
await applyMigrations(db, [failing[0], {
|
|
238
|
+
version: 2,
|
|
239
|
+
statements: ["CREATE TABLE migration_second (value TEXT)"]
|
|
240
|
+
}], { table: MIGRATIONS_TABLE });
|
|
241
|
+
expect(await db.get(`SELECT version FROM ${MIGRATIONS_TABLE}`)).toEqual({ version: 2 });
|
|
242
|
+
expect(await db.all("SELECT value FROM migration_first")).toEqual([{ value: "retained" }]);
|
|
243
|
+
});
|
|
244
|
+
it("applies an app's numbered migration files", async () => {
|
|
245
|
+
const db = await fresh();
|
|
246
|
+
const folder = await mkdtemp(join(tmpdir(), "acryl-db-migrations-"));
|
|
247
|
+
try {
|
|
248
|
+
await writeFile(join(folder, "001-notes.sql"), "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);\nINSERT INTO notes (id, body) VALUES (1, 'first');\n");
|
|
249
|
+
await writeFile(join(folder, "002-tags.sql"), "CREATE TABLE tags (note INTEGER NOT NULL, tag TEXT NOT NULL);\n");
|
|
250
|
+
await applyMigrations(db, await readMigrationFiles(folder), { table: MIGRATIONS_TABLE });
|
|
251
|
+
await db.run("INSERT INTO tags (note, tag) VALUES (?, ?)", 1, "todo");
|
|
252
|
+
expect(await db.all("SELECT body, tag FROM notes JOIN tags ON tags.note = notes.id")).toEqual([{
|
|
253
|
+
body: "first",
|
|
254
|
+
tag: "todo"
|
|
255
|
+
}]);
|
|
256
|
+
expect(await db.get(`SELECT version FROM ${MIGRATIONS_TABLE}`)).toEqual({ version: 2 });
|
|
257
|
+
} finally {
|
|
258
|
+
await rm(folder, {
|
|
259
|
+
recursive: true,
|
|
260
|
+
force: true
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { describeDbConformance };
|
package/lib/db/sqlite.d.ts
CHANGED