acrylblends 0.7.3 → 0.7.5
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.js +5 -1
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +6 -4
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/getting-started/aws-lambda.md +115 -0
- package/docs/framework/getting-started/bun.md +1 -3
- package/docs/framework/getting-started/cloudflare-pages.md +53 -0
- package/docs/framework/getting-started/cloudflare-workers.md +67 -0
- package/docs/framework/getting-started/deploy.md +5 -5
- package/docs/framework/getting-started/lambda-edge.md +41 -0
- package/docs/framework/getting-started/netlify.md +25 -0
- package/docs/framework/getting-started/other-runtimes.md +20 -9
- package/docs/framework/getting-started/service-worker.md +33 -0
- package/docs/framework/getting-started/vercel.md +28 -0
- package/docs/framework/guides/create-acrylblends.md +40 -2
- package/docs/framework/guides/data.md +202 -0
- package/docs/framework/guides/faq.md +2 -2
- package/docs/framework/guides/forms-and-sessions.md +34 -30
- package/docs/framework/helpers/conninfo.md +1 -1
- package/docs/framework/helpers/ssg.md +1 -1
- package/docs/framework/helpers/streaming.md +3 -2
- package/docs/framework/helpers/websocket.md +2 -2
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/upstream.json +3 -3
- package/lib/aws-lambda.d.ts +324 -0
- package/lib/aws-lambda.js +357 -0
- package/lib/bun.d.ts +75 -0
- package/lib/bun.js +134 -0
- package/lib/cloudflare-pages.d.ts +58 -0
- package/lib/cloudflare-pages.js +78 -0
- package/lib/cloudflare-workers.d.ts +21 -0
- package/lib/cloudflare-workers.js +80 -0
- package/lib/db/sqlite.d.ts +27 -0
- package/lib/db/sqlite.js +197 -0
- package/lib/db.d.ts +2 -0
- package/lib/db.js +2 -0
- package/lib/index-CseujaYI.d.ts +31 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/index-DaJtL7ci.d.ts +156 -0
- package/lib/lambda-edge.d.ts +92 -0
- package/lib/lambda-edge.js +105 -0
- package/lib/netlify.d.ts +29 -0
- package/lib/netlify.js +30 -0
- package/lib/node-server/register.js +1 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.js +3 -3
- package/lib/serve-static-Cp5adzGo.js +77 -0
- package/lib/serve-static.d.ts +1 -30
- package/lib/serve-static.js +1 -76
- package/lib/service-worker.d.ts +48 -0
- package/lib/service-worker.js +38 -0
- package/lib/src-MWcsMS8i.js +85 -0
- package/lib/ssg.d.ts +1 -155
- package/lib/vercel.d.ts +10 -0
- package/lib/vercel.js +9 -0
- package/package.json +41 -1
- package/plugins/acryl-db/LICENSE +47 -0
- package/plugins/acryl-db/plugin.json +12 -0
- package/plugins/acryl-db/src/cordis.ts +13 -0
- package/plugins/acryl-db/src/facade/database.ts +41 -0
- package/plugins/acryl-db/src/facade/migrations.ts +50 -0
- package/plugins/acryl-db/src/facade/schema.ts +9 -0
- package/plugins/acryl-db/src/index.ts +19 -0
- package/plugins/acryl-db/src/migration-files.ts +31 -0
- package/plugins/acryl-db/upstream.json +18 -0
- package/plugins/acryl-db-sqlite/LICENSE +47 -0
- package/plugins/acryl-db-sqlite/plugin.json +13 -0
- package/plugins/acryl-db-sqlite/src/facade/node.ts +199 -0
- package/plugins/acryl-db-sqlite/src/index.ts +88 -0
- package/plugins/acryl-db-sqlite/src/open.ts +61 -0
- package/plugins/acryl-db-sqlite/upstream.json +21 -0
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/adapter/aws-lambda/conninfo.ts +73 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/handler.ts +682 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/index.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/aws-lambda/types.ts +173 -0
- package/plugins/acryl-router/src/core/adapter/bun/conninfo.ts +43 -0
- package/plugins/acryl-router/src/core/adapter/bun/index.ts +11 -0
- package/plugins/acryl-router/src/core/adapter/bun/serve-static.ts +32 -0
- package/plugins/acryl-router/src/core/adapter/bun/server.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/bun/ssg.ts +26 -0
- package/plugins/acryl-router/src/core/adapter/bun/websocket.ts +113 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/conninfo.ts +26 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/handler.ts +122 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-pages/index.ts +11 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/conninfo.ts +7 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/index.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static-module.ts +12 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static.ts +41 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/utils.ts +50 -0
- package/plugins/acryl-router/src/core/adapter/cloudflare-workers/websocket.ts +52 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/conninfo.ts +15 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/handler.ts +239 -0
- package/plugins/acryl-router/src/core/adapter/lambda-edge/index.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/netlify/conninfo.ts +57 -0
- package/plugins/acryl-router/src/core/adapter/netlify/handler.ts +10 -0
- package/plugins/acryl-router/src/core/adapter/netlify/index.ts +6 -0
- package/plugins/acryl-router/src/core/adapter/netlify/mod.ts +2 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/handler.ts +37 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/index.ts +36 -0
- package/plugins/acryl-router/src/core/adapter/service-worker/types.ts +14 -0
- package/plugins/acryl-router/src/core/adapter/vercel/conninfo.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/vercel/handler.ts +8 -0
- package/plugins/acryl-router/src/core/adapter/vercel/index.ts +7 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +13 -3
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/tsconfig.json +1 -0
- package/templates/todo/upstream.json +1 -1
- package/lib/{serve-static-B14MF1Va.js → serve-static-B849HlX2.js} +1 -1
- package/lib/{transpile-CTnWof4z.js → transpile-QUK9b3eG.js} +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Validate a prefix before using it as part of a SQL identifier. */
|
|
2
|
+
export function validateSchemaPrefix(prefix: string): void {
|
|
3
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
|
|
4
|
+
throw new Error(`Invalid SQL schema prefix ${JSON.stringify(prefix)}`);
|
|
5
|
+
}
|
|
6
|
+
if (prefix.toLowerCase().startsWith("sqlite_")) {
|
|
7
|
+
throw new Error(`SQL schema prefix ${JSON.stringify(prefix)} is reserved`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `db` port: what an app's code sees of its relational database, whichever provider (`acryl-db-sqlite`, `acryl-db-postgres`) serves
|
|
3
|
+
* it. Plain code (an acrylblends app) opens a database with a provider's `openDb`; a Cordis plugin declares `inject: ['db']`, imports
|
|
4
|
+
* `acryl-db/cordis` for the `ctx.db` type, and never imports a driver.
|
|
5
|
+
*
|
|
6
|
+
* Bindings are positional `?` placeholders on every provider; a provider whose database spells them differently translates them.
|
|
7
|
+
* This entry imports nothing from Cordis, at runtime or in its types.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The service name every `db` provider registers. */
|
|
11
|
+
export const DB_SERVICE = 'db'
|
|
12
|
+
|
|
13
|
+
/** The table that records an app's migration version. */
|
|
14
|
+
export const MIGRATIONS_TABLE = '_migrations'
|
|
15
|
+
|
|
16
|
+
export type { Db, DbExecutor, SqlValue } from './facade/database.ts'
|
|
17
|
+
export { applyMigrations, type Migration } from './facade/migrations.ts'
|
|
18
|
+
export { validateSchemaPrefix } from './facade/schema.ts'
|
|
19
|
+
export { readMigrationFiles } from './migration-files.ts'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app's migrations as numbered SQL files: `migrations/001-notes.sql`, `002-tags.sql`, ... Each file is one migration, run as one
|
|
3
|
+
* `exec` (so it may hold several statements), numbered from 1 with no gaps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import type { Migration } from './facade/migrations.ts'
|
|
10
|
+
|
|
11
|
+
const FILE = /^(\d+)-[a-z0-9][a-z0-9-]*\.sql$/u
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read every `NNN-name.sql` file of a folder, in number order.
|
|
15
|
+
* @throws when a `.sql` file is not named `NNN-name.sql`, or the numbers do not run 1, 2, 3, ... without gaps or repeats.
|
|
16
|
+
*/
|
|
17
|
+
export async function readMigrationFiles(folder: string | URL): Promise<Migration[]> {
|
|
18
|
+
const dir = typeof folder === 'string' ? folder : fileURLToPath(folder)
|
|
19
|
+
const files = (await readdir(dir)).filter(name => name.endsWith('.sql')).sort()
|
|
20
|
+
const numbered = files.map(name => {
|
|
21
|
+
const match = FILE.exec(name)
|
|
22
|
+
if (match?.[1] === undefined) throw new Error(`acryl-db: migration file ${name} in ${dir} must be named NNN-name.sql, like 001-notes.sql`)
|
|
23
|
+
return { name, version: Number(match[1]) }
|
|
24
|
+
}).sort((a, b) => a.version - b.version)
|
|
25
|
+
numbered.forEach((file, index) => {
|
|
26
|
+
if (file.version !== index + 1) {
|
|
27
|
+
throw new Error(`acryl-db: migration ${file.name} in ${dir} should be number ${String(index + 1)}; the files must be numbered 1, 2, 3, ... with no gaps or repeats`)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
return Promise.all(numbered.map(async file => ({ version: file.version, statements: [await readFile(join(dir, file.name), 'utf8')] })))
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"repo": "acryldev/acryl",
|
|
4
|
+
"path": "plugins/acryl-db",
|
|
5
|
+
"commit": "4721bb00960fa4e157e6070c330cf87c148739fc"
|
|
6
|
+
},
|
|
7
|
+
"vendored": [
|
|
8
|
+
"src",
|
|
9
|
+
"tests",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"rewrittenSpecifiers": 0,
|
|
13
|
+
"digest": "sha256:51e5613457498bfab7c281e0c6443c4f7535401c16520ac44624dc8791a90fa6",
|
|
14
|
+
"testsNotRun": {},
|
|
15
|
+
"notice": [
|
|
16
|
+
"LICENSE"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WebBoxes s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
The files under src/facade and tests/facade are derived from the SQLite facade of chord in pi (https://github.com/earendil-works/pi, packages/chord, commit d5b3de22d0e24a3e9c38a9410d3329b099717283), used under this license:
|
|
26
|
+
|
|
27
|
+
MIT License
|
|
28
|
+
|
|
29
|
+
Copyright (c) 2025 Mario Zechner
|
|
30
|
+
|
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
32
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
33
|
+
in the Software without restriction, including without limitation the rights
|
|
34
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
35
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
36
|
+
furnished to do so, subject to the following conditions:
|
|
37
|
+
|
|
38
|
+
The above copyright notice and this permission notice shall be included in all
|
|
39
|
+
copies or substantial portions of the Software.
|
|
40
|
+
|
|
41
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
42
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
43
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
44
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
45
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
46
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
47
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acryl-db-sqlite",
|
|
3
|
+
"description": "The SQLite provider of the db port: one node:sqlite file in the app home (WAL), the app's numbered migrations applied on start, db provided to plugins. Vendored from ACRYL.",
|
|
4
|
+
"requires": ["acryl-db", "instance"],
|
|
5
|
+
"inject": [],
|
|
6
|
+
"provides": ["db"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
9
|
+
"@deepseek-ai/schemastery": "3.18.4"
|
|
10
|
+
},
|
|
11
|
+
"mount": "mount it after instance: root.plugin(dbSqlite, { migrations: 'migrations' }) with the migrations/ folder (001-name.sql, ...) next to src/; then a plugin that declares inject: ['db'] calls await ctx.db.all('SELECT ...', value)",
|
|
12
|
+
"omit": ["**/*.test.*", "**/*.spec.*", "**/*.case.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { StatementSync } from "node:sqlite";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import type { Db, DbExecutor, SqlValue } from "../../../acryl-db/src/index.ts";
|
|
6
|
+
|
|
7
|
+
/** Node SQLite connection settings for a database file. */
|
|
8
|
+
export type NodeSqliteDatabaseOptions = {
|
|
9
|
+
/** SQLite WAL auto-checkpoint threshold. SQLite and this adapter default to 1,000 pages; 0 disables it. */
|
|
10
|
+
readonly walAutoCheckpointPages?: number;
|
|
11
|
+
/** Time SQLite waits for a competing file lock. SQLite defaults to 0; this adapter defaults to 5,000 ms. */
|
|
12
|
+
readonly busyTimeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_WAL_AUTO_CHECKPOINT_PAGES = 1_000;
|
|
16
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
17
|
+
|
|
18
|
+
type TransactionScope = { active: boolean };
|
|
19
|
+
|
|
20
|
+
const ignore = (): void => {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runs operations in call order. An operation starts immediately when nothing is running or waiting;
|
|
24
|
+
* otherwise it waits for everything before it. An asynchronous operation holds the queue until it settles.
|
|
25
|
+
*/
|
|
26
|
+
class SerialOperationQueue {
|
|
27
|
+
private tail: Promise<void> = Promise.resolve();
|
|
28
|
+
private pending = 0;
|
|
29
|
+
|
|
30
|
+
run<T>(operation: () => T): Promise<T> {
|
|
31
|
+
if (this.pending > 0) return this.enqueue(operation);
|
|
32
|
+
try {
|
|
33
|
+
return Promise.resolve(operation());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return Promise.reject(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
runAsync<T>(operation: () => Promise<T>): Promise<T> {
|
|
40
|
+
if (this.pending > 0) return this.enqueue(operation);
|
|
41
|
+
this.pending++;
|
|
42
|
+
// Publish the barrier before the operation starts, so calls it makes synchronously wait behind it.
|
|
43
|
+
const { promise: barrier, resolve: releaseBarrier } = Promise.withResolvers<void>();
|
|
44
|
+
this.tail = barrier;
|
|
45
|
+
let started: Promise<T>;
|
|
46
|
+
try {
|
|
47
|
+
started = operation();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
started = Promise.reject(error);
|
|
50
|
+
}
|
|
51
|
+
return started.finally(() => {
|
|
52
|
+
this.pending--;
|
|
53
|
+
releaseBarrier();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private enqueue<T>(operation: () => T | Promise<T>): Promise<T> {
|
|
58
|
+
this.pending++;
|
|
59
|
+
return this.release(this.tail.then(operation));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private release<T>(operation: Promise<T>): Promise<T> {
|
|
63
|
+
const settled = operation.finally(() => {
|
|
64
|
+
this.pending--;
|
|
65
|
+
});
|
|
66
|
+
this.tail = settled.then(ignore, ignore);
|
|
67
|
+
return settled;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Executes SQL on one connection. Prepared statements are cached per connection by SQL text, so the
|
|
73
|
+
* database and its transaction handles share them across transactions.
|
|
74
|
+
*/
|
|
75
|
+
abstract class NodeSqliteExecutor implements DbExecutor {
|
|
76
|
+
protected readonly database: DatabaseSync;
|
|
77
|
+
protected readonly statements: Map<string, StatementSync>;
|
|
78
|
+
|
|
79
|
+
constructor(database: DatabaseSync, statements: Map<string, StatementSync>) {
|
|
80
|
+
this.database = database;
|
|
81
|
+
this.statements = statements;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exec(sql: string): Promise<void> {
|
|
85
|
+
return this.runOperation(() => {
|
|
86
|
+
this.database.exec(sql);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
run(sql: string, ...params: SqlValue[]): Promise<{ readonly changes: number }> {
|
|
91
|
+
return this.runOperation(() => ({ changes: Number(this.statement(sql).run(...params).changes) }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get<T extends object>(sql: string, ...params: SqlValue[]): Promise<T | undefined> {
|
|
95
|
+
return this.runOperation(() => this.statement(sql).get(...params) as T | undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
all<T extends object>(sql: string, ...params: SqlValue[]): Promise<T[]> {
|
|
99
|
+
return this.runOperation(() => this.statement(sql).all(...params) as T[]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected abstract runOperation<T>(operation: () => T): Promise<T>;
|
|
103
|
+
|
|
104
|
+
private statement(sql: string): StatementSync {
|
|
105
|
+
let statement = this.statements.get(sql);
|
|
106
|
+
if (statement === undefined) {
|
|
107
|
+
statement = this.database.prepare(sql);
|
|
108
|
+
this.statements.set(sql, statement);
|
|
109
|
+
}
|
|
110
|
+
return statement;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class NodeSqliteTransaction extends NodeSqliteExecutor {
|
|
115
|
+
private readonly scope: TransactionScope;
|
|
116
|
+
|
|
117
|
+
constructor(database: DatabaseSync, statements: Map<string, StatementSync>, scope: TransactionScope) {
|
|
118
|
+
super(database, statements);
|
|
119
|
+
this.scope = scope;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected async runOperation<T>(operation: () => T): Promise<T> {
|
|
123
|
+
if (!this.scope.active) throw new Error("SQLite transaction handle is no longer active");
|
|
124
|
+
return operation();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `Db` adapter backed by Node's built-in `node:sqlite`. */
|
|
129
|
+
export class NodeSqliteDatabase extends NodeSqliteExecutor implements Db {
|
|
130
|
+
private readonly access = new SerialOperationQueue();
|
|
131
|
+
private closed = false;
|
|
132
|
+
|
|
133
|
+
constructor(database: DatabaseSync) {
|
|
134
|
+
super(database, new Map());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
transaction<T>(callback: (transaction: DbExecutor) => Promise<T>): Promise<T> {
|
|
138
|
+
return this.access.runAsync(async () => {
|
|
139
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
140
|
+
const scope = { active: true };
|
|
141
|
+
try {
|
|
142
|
+
const result = await callback(new NodeSqliteTransaction(this.database, this.statements, scope));
|
|
143
|
+
scope.active = false;
|
|
144
|
+
this.database.exec("COMMIT");
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
scope.active = false;
|
|
148
|
+
try {
|
|
149
|
+
this.database.exec("ROLLBACK");
|
|
150
|
+
} catch (rollbackError) {
|
|
151
|
+
throw new AggregateError([error, rollbackError], "SQLite transaction failed and rollback failed");
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close(): Promise<void> {
|
|
159
|
+
return this.access.run(() => {
|
|
160
|
+
if (this.closed) return;
|
|
161
|
+
this.closed = true;
|
|
162
|
+
this.statements.clear();
|
|
163
|
+
try {
|
|
164
|
+
this.database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
165
|
+
} finally {
|
|
166
|
+
this.database.close();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected runOperation<T>(operation: () => T): Promise<T> {
|
|
172
|
+
return this.access.run(operation);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Open and configure a Node-backed SQLite database facade. */
|
|
177
|
+
export async function openNodeSqliteDatabase(
|
|
178
|
+
path: string,
|
|
179
|
+
options: NodeSqliteDatabaseOptions = {},
|
|
180
|
+
): Promise<NodeSqliteDatabase> {
|
|
181
|
+
const checkpointPages = options.walAutoCheckpointPages ?? DEFAULT_WAL_AUTO_CHECKPOINT_PAGES;
|
|
182
|
+
const timeout = options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
183
|
+
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
184
|
+
const database = new DatabaseSync(path, { timeout });
|
|
185
|
+
const adapter = new NodeSqliteDatabase(database);
|
|
186
|
+
try {
|
|
187
|
+
await adapter.exec("PRAGMA journal_mode = WAL");
|
|
188
|
+
await adapter.exec("PRAGMA synchronous = NORMAL");
|
|
189
|
+
await adapter.exec(`PRAGMA wal_autocheckpoint = ${checkpointPages}`);
|
|
190
|
+
return adapter;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
try {
|
|
193
|
+
await adapter.close();
|
|
194
|
+
} catch {
|
|
195
|
+
// Preserve the configuration failure.
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite provider of the `db` port (spec 049 slice 1): one `node:sqlite` file, opened with the ported facade's settings (WAL,
|
|
3
|
+
* `synchronous = NORMAL`, a busy timeout), migrated, and provided as `db`. App plugins declare `inject: ['db']` and import types from
|
|
4
|
+
* `acryl-db` only, so replacing this row with another provider does not touch them. The plugin is a thin wrapper over `openDb`
|
|
5
|
+
* (`acryl-db-sqlite/open`), the same opening for code without Cordis.
|
|
6
|
+
*
|
|
7
|
+
* Unloading withdraws `db` first, so consumers stop calling, and then closes the file: the close waits for running operations and
|
|
8
|
+
* transactions, checkpoints the WAL and releases the file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
14
|
+
import z from '@deepseek-ai/schemastery'
|
|
15
|
+
import { DB_SERVICE } from '../../acryl-db/src/index.ts'
|
|
16
|
+
import type {} from '../../acryl-db/src/cordis.ts'
|
|
17
|
+
import { openDb } from './open.ts'
|
|
18
|
+
|
|
19
|
+
export { NodeSqliteDatabase, openNodeSqliteDatabase, type NodeSqliteDatabaseOptions } from './facade/node.ts'
|
|
20
|
+
export { openDb, type OpenDb, type OpenDbOptions } from './open.ts'
|
|
21
|
+
|
|
22
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
23
|
+
export const name = 'acryl-db-sqlite'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What this plugin reads of the runtime's optional `appInstance` service: its home folder. The service belongs to the runtime (and to an
|
|
27
|
+
* app's own instance plugin), so this plugin declares no type for it on Cordis' Context; it checks the one field it uses at runtime.
|
|
28
|
+
*/
|
|
29
|
+
interface AppHome {
|
|
30
|
+
readonly home: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isAppHome(value: unknown): value is AppHome {
|
|
34
|
+
return typeof value === 'object' && value !== null && 'home' in value && typeof value.home === 'string'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Config {
|
|
38
|
+
path?: string
|
|
39
|
+
migrations?: string
|
|
40
|
+
busyTimeoutMs?: number
|
|
41
|
+
walAutoCheckpointPages?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Config: z<Config> = z.object({
|
|
45
|
+
path: z.string().description('The database file, absolute or relative to the app. Defaults to data.sqlite in the ACRYL app home; required where there is none.'),
|
|
46
|
+
migrations: z.string().description('A folder of NNN-name.sql files numbered from 1, absolute or relative to the app, applied in order on start.'),
|
|
47
|
+
busyTimeoutMs: z.natural().description('How long SQLite waits for a competing file lock, in milliseconds. Defaults to 5000.'),
|
|
48
|
+
walAutoCheckpointPages: z.natural().description('The WAL auto-checkpoint threshold in pages; 0 disables it. Defaults to 1000.'),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** The file name used in the app home when `path` is not configured. */
|
|
52
|
+
export const DEFAULT_FILE = 'data.sqlite'
|
|
53
|
+
|
|
54
|
+
/** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise. */
|
|
55
|
+
function resolveFromApp(ctx: Context, path: string, option: string): string {
|
|
56
|
+
if (path.startsWith('file:')) return fileURLToPath(path)
|
|
57
|
+
if (isAbsolute(path)) return path
|
|
58
|
+
const base = ctx.baseUrl
|
|
59
|
+
if (base === undefined || !base.startsWith('file:')) {
|
|
60
|
+
throw new Error(`acryl-db-sqlite: ${option} ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`)
|
|
61
|
+
}
|
|
62
|
+
const appDir = base.endsWith('/') ? fileURLToPath(base) : dirname(fileURLToPath(base))
|
|
63
|
+
return resolve(appDir, path)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function apply(ctx: Context, rawConfig: Config = {}): Promise<void> {
|
|
67
|
+
const config = Config(rawConfig)
|
|
68
|
+
// The app home comes from the runtime's `appInstance` service when this runs inside an ACRYL app; a plain framework app names its file.
|
|
69
|
+
const appInstance: unknown = ctx.get('appInstance')
|
|
70
|
+
let file: string
|
|
71
|
+
if (config.path !== undefined) file = resolveFromApp(ctx, config.path, 'path')
|
|
72
|
+
else if (isAppHome(appInstance)) file = join(appInstance.home, DEFAULT_FILE)
|
|
73
|
+
else throw new Error('acryl-db-sqlite: no database path; set `path` in this row\'s config (there is no ACRYL app home to default to)')
|
|
74
|
+
const migrations = config.migrations === undefined ? undefined : resolveFromApp(ctx, config.migrations, 'migrations')
|
|
75
|
+
const options = {
|
|
76
|
+
path: file,
|
|
77
|
+
...(migrations === undefined ? {} : { migrations }),
|
|
78
|
+
...(config.busyTimeoutMs === undefined ? {} : { busyTimeoutMs: config.busyTimeoutMs }),
|
|
79
|
+
...(config.walAutoCheckpointPages === undefined ? {} : { walAutoCheckpointPages: config.walAutoCheckpointPages }),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Disposers run in reverse: the service is withdrawn before the file closes.
|
|
83
|
+
await ctx.effect(async function* () {
|
|
84
|
+
const opened = await openDb(options)
|
|
85
|
+
yield () => opened.close()
|
|
86
|
+
yield ctx.reflect.provide(DB_SERVICE, opened.db)
|
|
87
|
+
}, `acryl-db-sqlite: ${file}`)
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain way to open the SQLite provider, for code without Cordis (an acrylblends app): `openDb({ path, migrations })` opens one
|
|
3
|
+
* `node:sqlite` file with the ported facade's settings (WAL, `synchronous = NORMAL`, a busy timeout) and foreign keys enforced, applies the
|
|
4
|
+
* numbered migrations, and resolves the `db` port with the function that closes it. The Cordis plugin is a thin wrapper over this, so both
|
|
5
|
+
* run one code path.
|
|
6
|
+
* This entry imports nothing from Cordis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { applyMigrations, MIGRATIONS_TABLE, readMigrationFiles, type Db, type SqlValue } from '../../acryl-db/src/index.ts'
|
|
10
|
+
import { openNodeSqliteDatabase } from './facade/node.ts'
|
|
11
|
+
|
|
12
|
+
export type { Db, DbExecutor, SqlValue } from '../../acryl-db/src/index.ts'
|
|
13
|
+
|
|
14
|
+
export interface OpenDbOptions {
|
|
15
|
+
/** The database file, its folder created if missing; `:memory:` for a database that lives until `close`. */
|
|
16
|
+
readonly path: string
|
|
17
|
+
/** A folder of `NNN-name.sql` files numbered from 1, as a path or a file URL, applied in order before `openDb` resolves. */
|
|
18
|
+
readonly migrations?: string | URL
|
|
19
|
+
/** How long SQLite waits for a competing file lock, in milliseconds. Defaults to 5000. */
|
|
20
|
+
readonly busyTimeoutMs?: number
|
|
21
|
+
/** The WAL auto-checkpoint threshold in pages; 0 disables it. Defaults to 1000. */
|
|
22
|
+
readonly walAutoCheckpointPages?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An open database: the port, and the one function that closes it. */
|
|
26
|
+
export interface OpenDb {
|
|
27
|
+
readonly db: Db
|
|
28
|
+
/** Waits for running operations and transactions, checkpoints the WAL and releases the file. Calling it again does nothing. */
|
|
29
|
+
close(): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Open, configure and migrate a SQLite database.
|
|
34
|
+
* @throws when the migration files are misnamed or misnumbered, or a migration fails (the file is closed, and unchanged), or the
|
|
35
|
+
* database's schema is newer than the migrations.
|
|
36
|
+
*/
|
|
37
|
+
export async function openDb(options: OpenDbOptions): Promise<OpenDb> {
|
|
38
|
+
const migrations = options.migrations === undefined ? [] : await readMigrationFiles(options.migrations)
|
|
39
|
+
const database = await openNodeSqliteDatabase(options.path, {
|
|
40
|
+
...(options.busyTimeoutMs === undefined ? {} : { busyTimeoutMs: options.busyTimeoutMs }),
|
|
41
|
+
...(options.walAutoCheckpointPages === undefined ? {} : { walAutoCheckpointPages: options.walAutoCheckpointPages }),
|
|
42
|
+
})
|
|
43
|
+
try {
|
|
44
|
+
// Off by default in SQLite, per connection, and ignored inside a transaction: set here, before the migrations' transaction. The port
|
|
45
|
+
// promises it (`ON DELETE CASCADE` works on every provider).
|
|
46
|
+
await database.exec('PRAGMA foreign_keys = ON')
|
|
47
|
+
await applyMigrations(database, migrations, { table: MIGRATIONS_TABLE })
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await database.close()
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
// The port, not the adapter: the code that receives `db` gets no `close`, which belongs to whoever opened it.
|
|
53
|
+
const db: Db = {
|
|
54
|
+
exec: sql => database.exec(sql),
|
|
55
|
+
run: (sql, ...params) => database.run(sql, ...params),
|
|
56
|
+
get: <T extends object>(sql: string, ...params: SqlValue[]) => database.get<T>(sql, ...params),
|
|
57
|
+
all: <T extends object>(sql: string, ...params: SqlValue[]) => database.all<T>(sql, ...params),
|
|
58
|
+
transaction: work => database.transaction(work),
|
|
59
|
+
}
|
|
60
|
+
return { db, close: () => database.close() }
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"repo": "acryldev/acryl",
|
|
4
|
+
"path": "plugins/acryl-db-sqlite",
|
|
5
|
+
"commit": "4721bb00960fa4e157e6070c330cf87c148739fc"
|
|
6
|
+
},
|
|
7
|
+
"vendored": [
|
|
8
|
+
"src",
|
|
9
|
+
"tests",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"rewrittenSpecifiers": 9,
|
|
13
|
+
"digest": "sha256:853af986073e5721c9b8fb5626b46bcd1ca8a6c5f936c4ac11fc3c21f3c5c0ef",
|
|
14
|
+
"testsNotRun": {
|
|
15
|
+
"tests/loader.spec.ts": "loads the plugin through the real Loader (@deepseek-ai/cordis-plugin-loader), which generated apps do not install; the plain opener it wraps is tested here by open.spec.ts and conformance.spec.ts",
|
|
16
|
+
"tests/plain-entry-imports.spec.ts": "a copy vendored into an app (with relative imports, no build of its own) has neither `lib/`; the app's own typecheck is what proves the copy names no Cordis type it does not provide."
|
|
17
|
+
},
|
|
18
|
+
"notice": [
|
|
19
|
+
"LICENSE"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Context } from '../../context.ts'
|
|
2
|
+
import type { GetConnInfo } from '../../helper/conninfo/index.ts'
|
|
3
|
+
import type {
|
|
4
|
+
ApiGatewayRequestContext,
|
|
5
|
+
ApiGatewayRequestContextV2,
|
|
6
|
+
ALBRequestContext,
|
|
7
|
+
} from './types.ts'
|
|
8
|
+
|
|
9
|
+
type LambdaRequestContext =
|
|
10
|
+
| ApiGatewayRequestContext
|
|
11
|
+
| ApiGatewayRequestContextV2
|
|
12
|
+
| ALBRequestContext
|
|
13
|
+
|
|
14
|
+
type Env = {
|
|
15
|
+
Bindings: {
|
|
16
|
+
requestContext: LambdaRequestContext
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get connection information from AWS Lambda
|
|
22
|
+
*
|
|
23
|
+
* Extracts client IP from various Lambda event sources:
|
|
24
|
+
* - API Gateway v1 (REST API): requestContext.identity.sourceIp
|
|
25
|
+
* - API Gateway v2 (HTTP API/Function URLs): requestContext.http.sourceIp
|
|
26
|
+
* - ALB: Falls back to x-forwarded-for header
|
|
27
|
+
*
|
|
28
|
+
* @param c - Context
|
|
29
|
+
* @returns Connection information including remote address
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { Acryl } from 'acrylblends'
|
|
33
|
+
* import { handle, getConnInfo } from 'acrylblends/aws-lambda'
|
|
34
|
+
*
|
|
35
|
+
* const app = new Acryl()
|
|
36
|
+
*
|
|
37
|
+
* app.get('/', (c) => {
|
|
38
|
+
* const info = getConnInfo(c)
|
|
39
|
+
* return c.text(`Your IP: ${info.remote.address}`)
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* export const handler = handle(app)
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const getConnInfo: GetConnInfo = (c: Context<Env>) => {
|
|
46
|
+
const requestContext = c.env.requestContext
|
|
47
|
+
|
|
48
|
+
let address: string | undefined
|
|
49
|
+
|
|
50
|
+
// API Gateway v1 - has identity object
|
|
51
|
+
if ('identity' in requestContext && requestContext.identity?.sourceIp) {
|
|
52
|
+
address = requestContext.identity.sourceIp
|
|
53
|
+
}
|
|
54
|
+
// API Gateway v2 - has http object
|
|
55
|
+
else if ('http' in requestContext && requestContext.http?.sourceIp) {
|
|
56
|
+
address = requestContext.http.sourceIp
|
|
57
|
+
}
|
|
58
|
+
// ALB - use X-Forwarded-For header
|
|
59
|
+
else {
|
|
60
|
+
const xff = c.req.header('x-forwarded-for')
|
|
61
|
+
if (xff) {
|
|
62
|
+
const ips = xff.split(',')
|
|
63
|
+
// ALB appends the real client IP to the end of the header
|
|
64
|
+
address = ips[ips.length - 1].trim()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
remote: {
|
|
70
|
+
address,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|