@devxcrew/framework 0.1.10 → 0.1.12
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/README.md +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modules/database/common/database.schema.d.ts +44 -0
- package/dist/modules/database/common/database.schema.js +49 -0
- package/dist/modules/database/common/database.types.d.ts +26 -0
- package/dist/modules/database/common/database.types.js +1 -0
- package/dist/modules/database/common/index.d.ts +2 -0
- package/dist/modules/database/common/index.js +1 -0
- package/dist/modules/database/database.connections.d.ts +45 -0
- package/dist/modules/database/database.connections.js +196 -0
- package/dist/modules/database/database.controller.d.ts +1 -0
- package/dist/modules/database/database.controller.js +1 -0
- package/dist/modules/database/database.execution.d.ts +30 -0
- package/dist/modules/database/database.execution.js +109 -0
- package/dist/modules/database/database.migration.d.ts +11 -0
- package/dist/modules/database/database.migration.js +80 -0
- package/dist/modules/database/database.provider.d.ts +38 -0
- package/dist/modules/database/database.provider.js +39 -0
- package/dist/modules/database/database.repository.d.ts +1 -0
- package/dist/modules/database/database.repository.js +1 -0
- package/dist/modules/database/database.routes.d.ts +1 -0
- package/dist/modules/database/database.routes.js +1 -0
- package/dist/modules/database/database.schema.d.ts +1 -0
- package/dist/modules/database/database.schema.js +1 -0
- package/dist/modules/database/database.seed.d.ts +3 -0
- package/dist/modules/database/database.seed.js +7 -0
- package/dist/modules/database/database.service.d.ts +7 -0
- package/dist/modules/database/database.service.js +24 -0
- package/dist/modules/database/database.transfer.d.ts +17 -0
- package/dist/modules/database/database.transfer.js +79 -0
- package/dist/modules/database/database.types.d.ts +1 -0
- package/dist/modules/database/database.types.js +1 -0
- package/dist/modules/database/drivers/mariadb/index.d.ts +5 -0
- package/dist/modules/database/drivers/mariadb/index.js +5 -0
- package/dist/modules/database/drivers/mariadb/mariadb.backup.d.ts +3 -0
- package/dist/modules/database/drivers/mariadb/mariadb.backup.js +141 -0
- package/dist/modules/database/drivers/mariadb/mariadb.compatibility.d.ts +6 -0
- package/dist/modules/database/drivers/mariadb/mariadb.compatibility.js +88 -0
- package/dist/modules/database/drivers/mariadb/mariadb.connection.d.ts +3 -0
- package/dist/modules/database/drivers/mariadb/mariadb.connection.js +22 -0
- package/dist/modules/database/drivers/mariadb/mariadb.schema.d.ts +14 -0
- package/dist/modules/database/drivers/mariadb/mariadb.schema.js +29 -0
- package/dist/modules/database/drivers/sqlite/index.d.ts +3 -0
- package/dist/modules/database/drivers/sqlite/index.js +3 -0
- package/dist/modules/database/drivers/sqlite/sqlite.backup.d.ts +2 -0
- package/dist/modules/database/drivers/sqlite/sqlite.backup.js +45 -0
- package/dist/modules/database/drivers/sqlite/sqlite.connection.d.ts +13 -0
- package/dist/modules/database/drivers/sqlite/sqlite.connection.js +41 -0
- package/dist/modules/database/drivers/sqlite/sqlite.schema.d.ts +3 -0
- package/dist/modules/database/drivers/sqlite/sqlite.schema.js +9 -0
- package/dist/modules/database/index.d.ts +12 -0
- package/dist/modules/database/index.js +11 -0
- package/dist/modules/database/operations/database.backup.d.ts +3 -0
- package/dist/modules/database/operations/database.backup.js +24 -0
- package/dist/modules/database/operations/sqlite-import.d.ts +8 -0
- package/dist/modules/database/operations/sqlite-import.js +96 -0
- package/dist/modules/settings/index.d.ts +2 -0
- package/dist/modules/settings/index.js +1 -0
- package/dist/modules/settings/settings.controller.d.ts +1 -0
- package/dist/modules/settings/settings.controller.js +1 -0
- package/dist/modules/settings/settings.migration.d.ts +1 -0
- package/dist/modules/settings/settings.migration.js +1 -0
- package/dist/modules/settings/settings.provider.d.ts +2 -0
- package/dist/modules/settings/settings.provider.js +9 -0
- package/dist/modules/settings/settings.repository.d.ts +1 -0
- package/dist/modules/settings/settings.repository.js +1 -0
- package/dist/modules/settings/settings.routes.d.ts +1 -0
- package/dist/modules/settings/settings.routes.js +1 -0
- package/dist/modules/settings/settings.schema.d.ts +1 -0
- package/dist/modules/settings/settings.schema.js +1 -0
- package/dist/modules/settings/settings.seed.d.ts +1 -0
- package/dist/modules/settings/settings.seed.js +1 -0
- package/dist/modules/settings/settings.service.d.ts +13 -0
- package/dist/modules/settings/settings.service.js +93 -0
- package/dist/modules/settings/settings.types.d.ts +11 -0
- package/dist/modules/settings/settings.types.js +1 -0
- package/dist/runtime/config.js +3 -2
- package/package.json +5 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SqliteDialect, type SqliteDatabase } from "kysely";
|
|
2
|
+
export declare function createSqliteDialect(path: string): SqliteDialect;
|
|
3
|
+
export declare class NodeSqliteDatabase implements SqliteDatabase {
|
|
4
|
+
private readonly database;
|
|
5
|
+
constructor(path: string);
|
|
6
|
+
prepare(query: string): {
|
|
7
|
+
reader: boolean;
|
|
8
|
+
all: (parameters: readonly unknown[]) => Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
9
|
+
run: (parameters: readonly unknown[]) => import("node:sqlite").StatementResultingChanges;
|
|
10
|
+
iterate: (parameters: readonly unknown[]) => NodeJS.Iterator<Record<string, import("node:sqlite").SQLOutputValue>, undefined, any>;
|
|
11
|
+
};
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { SqliteDialect } from "kysely";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
export function createSqliteDialect(path) {
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
7
|
+
return new SqliteDialect({
|
|
8
|
+
database: async () => new NodeSqliteDatabase(path),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export class NodeSqliteDatabase {
|
|
12
|
+
database;
|
|
13
|
+
constructor(path) {
|
|
14
|
+
this.database = new DatabaseSync(path);
|
|
15
|
+
this.database.exec("PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL;");
|
|
16
|
+
}
|
|
17
|
+
prepare(query) {
|
|
18
|
+
const statement = this.database.prepare(query);
|
|
19
|
+
statement.setReadBigInts(true);
|
|
20
|
+
return {
|
|
21
|
+
reader: statement.columns().length > 0,
|
|
22
|
+
all: (parameters) => statement.all(...bind(parameters)),
|
|
23
|
+
run: (parameters) => statement.run(...bind(parameters)),
|
|
24
|
+
iterate: (parameters) => statement.iterate(...bind(parameters)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
close() {
|
|
28
|
+
this.database.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function bind(parameters) {
|
|
32
|
+
return parameters.map((value) => {
|
|
33
|
+
if (value === null ||
|
|
34
|
+
typeof value === "string" ||
|
|
35
|
+
typeof value === "number" ||
|
|
36
|
+
typeof value === "bigint" ||
|
|
37
|
+
value instanceof Uint8Array)
|
|
38
|
+
return value;
|
|
39
|
+
throw new TypeError("Unsupported SQLite parameter type.");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export function sqliteConfig(environment) {
|
|
3
|
+
const path = environment.DB_SQLITE_PATH ?? "storage/private/data/identity.sqlite";
|
|
4
|
+
if (!path.trim())
|
|
5
|
+
throw new Error("DB_SQLITE_PATH must not be empty.");
|
|
6
|
+
if (path === ":memory:" || path.startsWith("file:"))
|
|
7
|
+
throw new Error("DB_SQLITE_PATH must identify a persisted SQLite file.");
|
|
8
|
+
return { path: resolve(path) };
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createDatabaseProvider, type DatabaseProvider, } from "./database.provider.js";
|
|
2
|
+
export { DatabaseExecution, DatabaseValidationError, } from "./database.execution.js";
|
|
3
|
+
export { ResumableTransfer } from "./database.transfer.js";
|
|
4
|
+
export { DatabaseCapacityError } from "./database.connections.js";
|
|
5
|
+
export { createDatabaseMigrator, prepareMigrationStorage, databaseMetadataMigration, transferCheckpointsMigration, storageMigrations, } from "./database.migration.js";
|
|
6
|
+
export { readDatabaseConfiguration, databaseCommandSchema, } from "./common/index.js";
|
|
7
|
+
export type { DatabaseSchema, DatabaseInfrastructureSchema, DatabaseProviderOptions, ConnectionTarget, } from "./common/database.types.js";
|
|
8
|
+
export { smokeDatabase } from "./database.service.js";
|
|
9
|
+
export { backupConnection, checkConnectionBackup, } from "./operations/database.backup.js";
|
|
10
|
+
export { SqliteDataTransfer } from "./operations/sqlite-import.js";
|
|
11
|
+
export { createDatabaseBackup, verifyDatabaseBackup, } from "./drivers/sqlite/index.js";
|
|
12
|
+
export { createMariaDbBackup, verifyMariaDbBackup, rehearseMariaDbRestore, } from "./drivers/mariadb/index.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createDatabaseProvider, } from "./database.provider.js";
|
|
2
|
+
export { DatabaseExecution, DatabaseValidationError, } from "./database.execution.js";
|
|
3
|
+
export { ResumableTransfer } from "./database.transfer.js";
|
|
4
|
+
export { DatabaseCapacityError } from "./database.connections.js";
|
|
5
|
+
export { createDatabaseMigrator, prepareMigrationStorage, databaseMetadataMigration, transferCheckpointsMigration, storageMigrations, } from "./database.migration.js";
|
|
6
|
+
export { readDatabaseConfiguration, databaseCommandSchema, } from "./common/index.js";
|
|
7
|
+
export { smokeDatabase } from "./database.service.js";
|
|
8
|
+
export { backupConnection, checkConnectionBackup, } from "./operations/database.backup.js";
|
|
9
|
+
export { SqliteDataTransfer } from "./operations/sqlite-import.js";
|
|
10
|
+
export { createDatabaseBackup, verifyDatabaseBackup, } from "./drivers/sqlite/index.js";
|
|
11
|
+
export { createMariaDbBackup, verifyMariaDbBackup, rehearseMariaDbRestore, } from "./drivers/mariadb/index.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ConnectionTarget } from "../common/index.js";
|
|
2
|
+
export declare function backupConnection(environment: NodeJS.ProcessEnv, input: ConnectionTarget, path: string): Promise<void>;
|
|
3
|
+
export declare function checkConnectionBackup(environment: NodeJS.ProcessEnv, input: ConnectionTarget, path: string, restore?: boolean): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createMariaDbBackup, verifyMariaDbBackup, rehearseMariaDbRestore, } from "../drivers/mariadb/index.js";
|
|
2
|
+
import { createDatabaseBackup, verifyDatabaseBackup, } from "../drivers/sqlite/index.js";
|
|
3
|
+
import { connectionTargetSchema, } from "../common/index.js";
|
|
4
|
+
export async function backupConnection(environment, input, path) {
|
|
5
|
+
const target = connectionTargetSchema.parse(input);
|
|
6
|
+
if (target.driver === "mariadb")
|
|
7
|
+
await createMariaDbBackup({ ...environment, DB_MASTER_NAME: target.databaseName }, path);
|
|
8
|
+
else
|
|
9
|
+
await createDatabaseBackup(target.sqlitePath, path);
|
|
10
|
+
}
|
|
11
|
+
export async function checkConnectionBackup(environment, input, path, restore = false) {
|
|
12
|
+
const target = connectionTargetSchema.parse(input);
|
|
13
|
+
if (target.driver === "mariadb") {
|
|
14
|
+
if (restore)
|
|
15
|
+
await rehearseMariaDbRestore({ ...environment, DB_MASTER_NAME: target.databaseName }, path);
|
|
16
|
+
else
|
|
17
|
+
await verifyMariaDbBackup(path);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (restore)
|
|
21
|
+
throw new Error("SQLite backups are verified directly as restored snapshots.");
|
|
22
|
+
verifyDatabaseBackup(path);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type Kysely } from "kysely";
|
|
3
|
+
export declare class SqliteDataTransfer {
|
|
4
|
+
private readonly source;
|
|
5
|
+
private readonly allowed;
|
|
6
|
+
constructor(source: DatabaseSync, allowed: readonly string[]);
|
|
7
|
+
execute<Schema>(target: Kysely<Schema>, signal?: AbortSignal): Promise<Record<string, number>>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
export class SqliteDataTransfer {
|
|
3
|
+
source;
|
|
4
|
+
allowed;
|
|
5
|
+
constructor(source, allowed) {
|
|
6
|
+
this.source = source;
|
|
7
|
+
this.allowed = allowed;
|
|
8
|
+
}
|
|
9
|
+
async execute(target, signal) {
|
|
10
|
+
if (this.source.prepare("PRAGMA integrity_check").get()?.integrity_check !==
|
|
11
|
+
"ok")
|
|
12
|
+
throw new Error("SQLite integrity failed.");
|
|
13
|
+
if (this.source.prepare("PRAGMA foreign_key_check").all().length)
|
|
14
|
+
throw new Error("SQLite foreign keys failed.");
|
|
15
|
+
this.source.exec("BEGIN");
|
|
16
|
+
try {
|
|
17
|
+
const existing = new Set(this.source
|
|
18
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
19
|
+
.all()
|
|
20
|
+
.map((row) => String(row.name)));
|
|
21
|
+
const remaining = new Set(this.allowed.filter((name) => existing.has(name)));
|
|
22
|
+
const targetTables = new Set((await target.introspection.getTables()).map((table) => table.name));
|
|
23
|
+
const ordered = [];
|
|
24
|
+
while (remaining.size) {
|
|
25
|
+
const ready = [...remaining].filter((table) => {
|
|
26
|
+
assertIdentifier(table);
|
|
27
|
+
return this.source
|
|
28
|
+
.prepare(`PRAGMA foreign_key_list("${table}")`)
|
|
29
|
+
.all()
|
|
30
|
+
.every((fk) => !remaining.has(String(fk.table)) || fk.table === table);
|
|
31
|
+
});
|
|
32
|
+
if (!ready.length)
|
|
33
|
+
throw new Error("Cyclic source table dependencies.");
|
|
34
|
+
for (const table of ready) {
|
|
35
|
+
if (!targetTables.has(table))
|
|
36
|
+
throw new Error("Destination schema is incomplete.");
|
|
37
|
+
remaining.delete(table);
|
|
38
|
+
ordered.push(table);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return await target.transaction().execute(async (transaction) => {
|
|
42
|
+
const report = {};
|
|
43
|
+
for (const table of ordered) {
|
|
44
|
+
const primary = this.source
|
|
45
|
+
.prepare(`PRAGMA table_info("${table}")`)
|
|
46
|
+
.all()
|
|
47
|
+
.filter((column) => Number(column.pk) > 0)
|
|
48
|
+
.map((column) => String(column.name));
|
|
49
|
+
if (!primary.length)
|
|
50
|
+
throw new Error("Transfer tables require a primary key.");
|
|
51
|
+
const statement = this.source.prepare(`SELECT * FROM "${table}"`);
|
|
52
|
+
statement.setReadBigInts(true);
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const row of statement.iterate()) {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
const columns = Object.keys(row);
|
|
57
|
+
columns.forEach(assertIdentifier);
|
|
58
|
+
const values = columns.map((key) => typeof row[key] === "bigint" ? String(row[key]) : row[key]);
|
|
59
|
+
await sql `INSERT INTO ${sql.table(table)} (${sql.join(columns.map((column) => sql.ref(column)))}) VALUES (${sql.join(values.map((value) => sql `${value}`))})
|
|
60
|
+
ON DUPLICATE KEY UPDATE ${sql.join(columns.map((column) => sql `${sql.ref(column)}=VALUES(${sql.ref(column)})`))}`.execute(transaction);
|
|
61
|
+
const found = await sql `SELECT * FROM ${sql.table(table)} WHERE ${sql.join(primary.map((key) => sql `${sql.ref(key)}=${typeof row[key] === "bigint" ? String(row[key]) : row[key]}`), sql ` AND `)}`.execute(transaction);
|
|
62
|
+
if (found.rows.length !== 1 ||
|
|
63
|
+
canonical(row) !== canonical(found.rows[0]))
|
|
64
|
+
throw new Error(`Import verification failed for ${table}.`);
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
const total = await sql `SELECT COUNT(*) AS total FROM ${sql.table(table)}`.execute(transaction);
|
|
68
|
+
if (BigInt(total.rows[0].total) !== BigInt(count))
|
|
69
|
+
throw new Error(`Destination records differ for ${table}.`);
|
|
70
|
+
report[table] = count;
|
|
71
|
+
}
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
return report;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
this.source.exec("ROLLBACK");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function assertIdentifier(name) {
|
|
82
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name))
|
|
83
|
+
throw new Error("Invalid transfer identifier.");
|
|
84
|
+
}
|
|
85
|
+
function canonical(row) {
|
|
86
|
+
return JSON.stringify(Object.keys(row)
|
|
87
|
+
.sort()
|
|
88
|
+
.map((key) => [
|
|
89
|
+
key,
|
|
90
|
+
row[key] === null
|
|
91
|
+
? null
|
|
92
|
+
: row[key] instanceof Uint8Array
|
|
93
|
+
? Buffer.from(row[key]).toString("hex")
|
|
94
|
+
: String(row[key]),
|
|
95
|
+
]));
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSettingsProvider } from "./settings.provider.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SettingsService } from "./settings.service.js";
|
|
2
|
+
export function createSettingsProvider(options = {}) {
|
|
3
|
+
const service = new SettingsService(options);
|
|
4
|
+
return {
|
|
5
|
+
current: () => service.current(),
|
|
6
|
+
load: (root, overrides) => service.load(root, overrides),
|
|
7
|
+
write: (root, patch) => service.write(root, patch),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SettingsOptions } from "./settings.types.js";
|
|
2
|
+
export declare class SettingsService {
|
|
3
|
+
private readonly options;
|
|
4
|
+
private snapshot;
|
|
5
|
+
private readonly writable;
|
|
6
|
+
private writes;
|
|
7
|
+
constructor(options: SettingsOptions);
|
|
8
|
+
current(): Readonly<NodeJS.ProcessEnv>;
|
|
9
|
+
load(root?: string, overrides?: NodeJS.ProcessEnv): Readonly<NodeJS.ProcessEnv>;
|
|
10
|
+
write(root: string, patch: Readonly<NodeJS.ProcessEnv>): Promise<void>;
|
|
11
|
+
private validate;
|
|
12
|
+
private persist;
|
|
13
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, rename, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
export class SettingsService {
|
|
7
|
+
options;
|
|
8
|
+
snapshot;
|
|
9
|
+
writable;
|
|
10
|
+
writes = Promise.resolve();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.snapshot = Object.freeze({ ...options.defaults });
|
|
14
|
+
this.writable = new Set(options.writableKeys ?? []);
|
|
15
|
+
}
|
|
16
|
+
current() {
|
|
17
|
+
return this.snapshot;
|
|
18
|
+
}
|
|
19
|
+
load(root = process.cwd(), overrides = process.env) {
|
|
20
|
+
let file = {};
|
|
21
|
+
try {
|
|
22
|
+
file = parseEnv(readFileSync(resolve(root, ".env"), "utf8"));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT")
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const environment = { ...this.options.defaults, ...file, ...overrides };
|
|
29
|
+
this.validate(environment);
|
|
30
|
+
// The provider never changes process.env or another application's snapshot.
|
|
31
|
+
this.snapshot = Object.freeze(environment);
|
|
32
|
+
return this.snapshot;
|
|
33
|
+
}
|
|
34
|
+
write(root, patch) {
|
|
35
|
+
if (Object.entries(patch).some(([key, value]) => !this.writable.has(key) ||
|
|
36
|
+
value === undefined ||
|
|
37
|
+
/[\r\n\0]/.test(value)))
|
|
38
|
+
return Promise.reject(new Error("The settings update contains unsupported values."));
|
|
39
|
+
this.validate(patch);
|
|
40
|
+
const result = this.writes.then(() => this.persist(root, patch));
|
|
41
|
+
this.writes = result.then(() => undefined, () => undefined);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
validate(environment) {
|
|
45
|
+
if (this.options.schema &&
|
|
46
|
+
!this.options.schema.safeParse(environment).success)
|
|
47
|
+
throw new Error("Invalid application settings environment.");
|
|
48
|
+
}
|
|
49
|
+
async persist(root, patch) {
|
|
50
|
+
const path = resolve(root, ".env");
|
|
51
|
+
let source = "";
|
|
52
|
+
try {
|
|
53
|
+
source = await readFile(path, "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code !== "ENOENT")
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const written = new Set();
|
|
60
|
+
const lines = source.split(/\r?\n/).map((line) => {
|
|
61
|
+
const key = /^(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/.exec(line)?.[1];
|
|
62
|
+
if (!key || !Object.hasOwn(patch, key))
|
|
63
|
+
return line;
|
|
64
|
+
if (written.has(key))
|
|
65
|
+
return "";
|
|
66
|
+
written.add(key);
|
|
67
|
+
return `${key}=${encodeValue(patch[key])}`;
|
|
68
|
+
});
|
|
69
|
+
for (const [key, value] of Object.entries(patch))
|
|
70
|
+
if (!written.has(key))
|
|
71
|
+
lines.push(`${key}=${encodeValue(value)}`);
|
|
72
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
73
|
+
try {
|
|
74
|
+
await writeFile(temporary, `${lines.join("\n").replace(/\n+$/, "")}\n`, {
|
|
75
|
+
mode: 0o600,
|
|
76
|
+
flag: "wx",
|
|
77
|
+
});
|
|
78
|
+
await rename(temporary, path);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await rm(temporary, { force: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function encodeValue(value) {
|
|
86
|
+
// Node's .env parser preserves backslashes literally; choose a quote it can read.
|
|
87
|
+
if (!/[\s#"'`]/.test(value))
|
|
88
|
+
return value;
|
|
89
|
+
const quote = ['"', "'"].find((candidate) => !value.includes(candidate));
|
|
90
|
+
if (!quote)
|
|
91
|
+
throw new Error("The settings value cannot be represented in an environment file.");
|
|
92
|
+
return `${quote}${value}${quote}`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
export interface SettingsOptions {
|
|
3
|
+
defaults?: Readonly<NodeJS.ProcessEnv>;
|
|
4
|
+
schema?: z.ZodType;
|
|
5
|
+
writableKeys?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface SettingsProvider {
|
|
8
|
+
current(): Readonly<NodeJS.ProcessEnv>;
|
|
9
|
+
load(root?: string, overrides?: NodeJS.ProcessEnv): Readonly<NodeJS.ProcessEnv>;
|
|
10
|
+
write(root: string, patch: Readonly<NodeJS.ProcessEnv>): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/runtime/config.js
CHANGED
|
@@ -15,8 +15,9 @@ export function readApplicationConfig(env) {
|
|
|
15
15
|
url.search ||
|
|
16
16
|
url.hash)
|
|
17
17
|
throw new Error("APP_URL must be an HTTP origin without credentials or a path.");
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const publicPort = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
19
|
+
if (mode === "development" && publicPort !== port)
|
|
20
|
+
throw new Error("APP_URL must use APP_PORT in development.");
|
|
20
21
|
return {
|
|
21
22
|
name,
|
|
22
23
|
port,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devxcrew/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@devxcrew/tools": "0.1.8",
|
|
28
28
|
"@types/node": "^26.0.0",
|
|
29
|
+
"tsx": "4.23.15",
|
|
29
30
|
"typescript": "6.0.3"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
33
|
+
"kysely": "0.29.6",
|
|
34
|
+
"mysql2": "3.23.4",
|
|
32
35
|
"zod": "4.6.5"
|
|
33
36
|
},
|
|
34
37
|
"scripts": {
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
"build": "tsc -p tsconfig.json",
|
|
52
55
|
"prepack": "npm run build",
|
|
53
56
|
"release:check": "npm run check && npm run build && npm pack --dry-run --ignore-scripts",
|
|
54
|
-
"test": "npm run build && node --test"
|
|
57
|
+
"test": "npm run build && node --test tests/*.test.mjs src/modules/*/*.test.mjs && tsx --test src/modules/database/tests/*.test.ts src/modules/settings/*.test.ts"
|
|
55
58
|
},
|
|
56
59
|
"publishConfig": {
|
|
57
60
|
"access": "public",
|