@tobi2409/fullsty 0.5.0 → 0.5.2
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 +3 -3
- package/package.json +1 -1
- package/scripts/extension-wrappers/db-connection/db-connection-wrapper.ts +44 -0
- package/scripts/extension-wrappers/db-connection/extension.json +3 -0
- package/scripts/extension-wrappers/kysely/extension.json +3 -0
- package/scripts/extension-wrappers/kysely/kysely-wrapper.ts +29 -0
- package/scripts/extension-wrappers/pg/extension.json +3 -0
- package/scripts/extension-wrappers/pg/pg-wrapper.ts +54 -59
- package/scripts/extension-wrappers/sql-template-tag/extension.json +3 -0
- package/scripts/shared/file-copy.js +8 -2
- package/scripts/shared/server-package-utils.js +23 -29
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Run it from the project root, for example:
|
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
../../scripts/fullsty-server-pkg.js add pg
|
|
59
|
-
../../scripts/fullsty-server-pkg.js add
|
|
59
|
+
../../scripts/fullsty-server-pkg.js add kysely
|
|
60
60
|
../../scripts/fullsty-server-pkg.js remove pg
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -66,7 +66,7 @@ What it does:
|
|
|
66
66
|
|
|
67
67
|
- `add <package>`
|
|
68
68
|
- adds the package to the project's server package file, which is initially created from [project-frame/server-package.json](project-frame/server-package.json)
|
|
69
|
-
- copies a matching wrapper from [extension-wrappers](extension-wrappers) into [projects/demo1/src/server](projects/demo1/src/server)
|
|
69
|
+
- copies a matching wrapper from [scripts/extension-wrappers](scripts/extension-wrappers) into [projects/demo1/src/server](projects/demo1/src/server)
|
|
70
70
|
- `remove <package>`
|
|
71
71
|
- removes the package from the project's server package file
|
|
72
72
|
- removes the matching wrapper from [projects/demo1/src/server](projects/demo1/src/server)
|
|
@@ -75,7 +75,7 @@ The script does not manage the project-level [package.json](project-frame/packag
|
|
|
75
75
|
|
|
76
76
|
After `fullsty-server-pkg.js add <package>` you should run `npm run generate` again so the updated project server package file is copied into `generated/server/package.json`. After that, run `npm install` again inside [projects/demo1/generated/server](projects/demo1/generated/server) so the generated server gets the updated dependencies.
|
|
77
77
|
|
|
78
|
-
`fullsty-server-pkg.js add
|
|
78
|
+
`fullsty-server-pkg.js add kysely` copies [scripts/extension-wrappers/kysely/kysely-wrapper.ts](scripts/extension-wrappers/kysely/kysely-wrapper.ts) into [projects/demo1/src/server](projects/demo1/src/server). This keeps `kysely` visible as the recommended SQL style without coupling it to a specific DBMS wrapper. For PostgreSQL pooling and env handling you can add `pg` separately.
|
|
79
79
|
|
|
80
80
|
## 5) Start the generated server
|
|
81
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobi2409/fullsty",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Fullsty is a modular fullstack framework starter built from three core parts: template-engine, layout and proc2rest. It provides a project frame for building fullstack applications with a clear separation of client and server code.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Tobias Hollstein",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type DriverName = "pg" | "mysql" | "sqlite";
|
|
2
|
+
|
|
3
|
+
interface ConnectionData {
|
|
4
|
+
connectionName?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class DbConnection {
|
|
8
|
+
private connectionWrapper?: unknown;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly driverName: DriverName,
|
|
12
|
+
private readonly connectionData: ConnectionData = {}
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
getDriverName(): DriverName {
|
|
16
|
+
return this.driverName;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getConnection(): Promise<unknown> {
|
|
20
|
+
if (this.driverName === "pg") {
|
|
21
|
+
if (!this.connectionWrapper) {
|
|
22
|
+
// @ts-ignore pg-connection-wrapper is supplied by the pg extension in generated projects.
|
|
23
|
+
const { PgConnectionWrapper } = await import("../pg/pg-connection-wrapper.ts");
|
|
24
|
+
this.connectionWrapper = new PgConnectionWrapper(this.connectionData.connectionName ?? "default");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (this.connectionWrapper as any).getPgPool();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(`Driver is not integrated yet: ${this.driverName}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async close(): Promise<void> {
|
|
34
|
+
if (this.driverName === "pg") {
|
|
35
|
+
if (this.connectionWrapper) {
|
|
36
|
+
await (this.connectionWrapper as any).closePool();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error(`Driver is not integrated yet: ${this.driverName}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-ignore kysely is supplied by the server package in generated projects.
|
|
2
|
+
import * as k from "kysely";
|
|
3
|
+
|
|
4
|
+
type DriverName = "pg" | "mysql" | "sqlite";
|
|
5
|
+
|
|
6
|
+
export namespace KyselyWrapper {
|
|
7
|
+
export const sql = k.sql;
|
|
8
|
+
|
|
9
|
+
export function getKysely(): typeof k {
|
|
10
|
+
return k;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildQueryCompiler(driverName: DriverName): k.Kysely<any> {
|
|
14
|
+
const kysely = getKysely() as any;
|
|
15
|
+
|
|
16
|
+
if (driverName === "pg") {
|
|
17
|
+
return new kysely.Kysely({
|
|
18
|
+
dialect: {
|
|
19
|
+
createAdapter: () => new kysely.PostgresAdapter(),
|
|
20
|
+
createDriver: () => new kysely.DummyDriver(),
|
|
21
|
+
createIntrospector: (db: any) => new kysely.PostgresIntrospector(db),
|
|
22
|
+
createQueryCompiler: () => new kysely.PostgresQueryCompiler(),
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(`Driver is not integrated yet: ${driverName}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,78 +1,73 @@
|
|
|
1
|
+
// @ts-ignore pg dependencies are supplied by the server package in generated projects.
|
|
1
2
|
import dotenv from "dotenv";
|
|
3
|
+
// @ts-ignore node types are supplied by the server package in generated projects.
|
|
2
4
|
import fs from "fs";
|
|
5
|
+
// @ts-ignore node types are supplied by the server package in generated projects.
|
|
6
|
+
import path from "path";
|
|
7
|
+
// @ts-ignore pg dependencies are supplied by the server package in generated projects.
|
|
3
8
|
import { Pool } from "pg";
|
|
9
|
+
// @ts-ignore node types are supplied by the server package in generated projects.
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
export class PgConnectionWrapper {
|
|
13
|
+
private pool: Pool | null = null;
|
|
14
|
+
private pgEnv: Record<string, string> = {};
|
|
15
|
+
private pgEnvLoaded: boolean = false;
|
|
16
|
+
private readonly wrapperDir: string = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
private readonly envFilePath: string = path.join(this.wrapperDir, ".env.pg");
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
const prefix = connectionName.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
11
|
-
return `${prefix}_${key}`;
|
|
12
|
-
}
|
|
19
|
+
constructor(private readonly connectionName: string = "default") {}
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
21
|
+
private envKey(key: string): string {
|
|
22
|
+
const prefix = this.connectionName.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
23
|
+
return `${prefix}_${key}`;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: "";
|
|
26
|
+
private loadPgEnv(): void {
|
|
27
|
+
// only load the .env.pg file once
|
|
28
|
+
if (this.pgEnvLoaded) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// Load .env.pg into a local cache instead of writing into process.env.
|
|
33
|
+
// This keeps the pg wrapper isolated from other wrappers such as auth,
|
|
34
|
+
// which may load their own .env files later.
|
|
35
|
+
const envFileContent = fs.existsSync(this.envFilePath)
|
|
36
|
+
? fs.readFileSync(this.envFilePath, "utf8")
|
|
37
|
+
: "";
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
key: string,
|
|
33
|
-
fallback?: string,
|
|
34
|
-
): string {
|
|
35
|
-
loadPgEnv();
|
|
36
|
-
return pgEnv[envKey(connectionName, key)] ?? fallback ?? "";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getPgPool(connectionName: string = "default"): Pool {
|
|
40
|
-
const existing = pools.get(connectionName);
|
|
41
|
-
if (existing) {
|
|
42
|
-
return existing;
|
|
39
|
+
this.pgEnv = dotenv.parse(envFileContent);
|
|
40
|
+
this.pgEnvLoaded = true;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
password: getEnv(connectionName, "PGPASSWORD", "postgres"),
|
|
50
|
-
database: getEnv(connectionName, "PGDATABASE", "fullsty_demo"),
|
|
51
|
-
max: Number(getEnv(connectionName, "POOL_MAX", "10")),
|
|
52
|
-
});
|
|
43
|
+
private getEnv(key: string, fallback?: string): string {
|
|
44
|
+
this.loadPgEnv();
|
|
45
|
+
return this.pgEnv[this.envKey(key)] ?? fallback ?? "";
|
|
46
|
+
}
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
getPgPool(): Pool {
|
|
49
|
+
if (this.pool) {
|
|
50
|
+
return this.pool;
|
|
51
|
+
}
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
this.pool = new Pool({
|
|
54
|
+
host: this.getEnv("PGHOST", "127.0.0.1"),
|
|
55
|
+
port: Number(this.getEnv("PGPORT", "5432")),
|
|
56
|
+
user: this.getEnv("PGUSER", "postgres"),
|
|
57
|
+
password: this.getEnv("PGPASSWORD", "postgres"),
|
|
58
|
+
database: this.getEnv("PGDATABASE", "fullsty_demo"),
|
|
59
|
+
max: Number(this.getEnv("POOL_MAX", "10"))
|
|
60
|
+
});
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
return;
|
|
62
|
+
return this.pool;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export async function closeAllPgPools(): Promise<void> {
|
|
72
|
-
const all = [...pools.entries()];
|
|
65
|
+
async closePool(): Promise<void> {
|
|
66
|
+
if (!this.pool) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
pools.delete(name);
|
|
70
|
+
await this.pool.end();
|
|
71
|
+
this.pool = null;
|
|
77
72
|
}
|
|
78
|
-
}
|
|
73
|
+
}
|
|
@@ -2,16 +2,22 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
// Recursively copy a directory and all nested files/folders.
|
|
5
|
-
export function copyDirectory(sourceDir, targetDir) {
|
|
5
|
+
export function copyDirectory(sourceDir, targetDir, options = {}) {
|
|
6
|
+
const excludedNames = new Set(options.excludedNames ?? []);
|
|
7
|
+
|
|
6
8
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
7
9
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
8
10
|
|
|
9
11
|
for (const entry of entries) {
|
|
12
|
+
if (excludedNames.has(entry.name)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
11
17
|
const targetPath = path.join(targetDir, entry.name);
|
|
12
18
|
|
|
13
19
|
if (entry.isDirectory()) {
|
|
14
|
-
copyDirectory(sourcePath, targetPath);
|
|
20
|
+
copyDirectory(sourcePath, targetPath, options);
|
|
15
21
|
} else {
|
|
16
22
|
fs.copyFileSync(sourcePath, targetPath);
|
|
17
23
|
}
|
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
import fs
|
|
1
|
+
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { copyDirectory } from "./file-copy.js";
|
|
4
4
|
import { readJson, writeJson } from "./project-json-utils.js";
|
|
5
5
|
|
|
6
|
+
const EXTENSION_CONFIG_FILE = "extension.json";
|
|
7
|
+
|
|
6
8
|
function wrapperDirFromPackage(scriptDir, packageName) {
|
|
7
9
|
return path.join(scriptDir, "extension-wrappers", packageName);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
for (const entry of entries) {
|
|
14
|
-
const sourcePath = path.join(sourceWrapperDir, entry.name);
|
|
15
|
-
const targetPath = path.join(targetServerDir, entry.name);
|
|
16
|
-
|
|
17
|
-
if (entry.isDirectory()) {
|
|
18
|
-
if (fs.existsSync(targetPath)) {
|
|
19
|
-
removeWrapperFiles(sourcePath, targetPath);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (fs.existsSync(targetPath)) {
|
|
26
|
-
fs.rmSync(targetPath, { force: true });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
12
|
+
function shouldAddToServerPackage(wrapperDir) {
|
|
13
|
+
const extensionConfig = readJson(wrapperDir, EXTENSION_CONFIG_FILE);
|
|
14
|
+
return extensionConfig.addToServerPackage;
|
|
29
15
|
}
|
|
30
16
|
|
|
31
17
|
export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
|
|
@@ -37,12 +23,17 @@ export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
|
|
|
37
23
|
|
|
38
24
|
for (const packageName of packageNames) {
|
|
39
25
|
const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
|
|
26
|
+
const targetWrapperDir = path.join(serverDir, packageName);
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
if (shouldAddToServerPackage(wrapperDir)) {
|
|
29
|
+
json.dependencies[packageName] = "latest";
|
|
30
|
+
}
|
|
42
31
|
|
|
43
32
|
if (fs.existsSync(wrapperDir)) {
|
|
44
|
-
copyDirectory(wrapperDir,
|
|
45
|
-
|
|
33
|
+
copyDirectory(wrapperDir, targetWrapperDir, {
|
|
34
|
+
excludedNames: [EXTENSION_CONFIG_FILE],
|
|
35
|
+
});
|
|
36
|
+
console.log(`\n✅ Copied wrapper for ${packageName} to src/server/${packageName}`);
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
39
|
|
|
@@ -62,15 +53,18 @@ export function handleRemove(projectDir, scriptDir, serverDir, packageNames) {
|
|
|
62
53
|
|
|
63
54
|
for (const packageName of packageNames) {
|
|
64
55
|
const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
|
|
56
|
+
const targetWrapperDir = path.join(serverDir, packageName);
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
if (shouldAddToServerPackage(wrapperDir)) {
|
|
59
|
+
delete json.dependencies[packageName];
|
|
60
|
+
delete json.devDependencies[packageName];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (fs.existsSync(wrapperDir) && fs.existsSync(targetWrapperDir)) {
|
|
64
|
+
fs.rmSync(targetWrapperDir, { recursive: true, force: true });
|
|
68
65
|
|
|
69
|
-
if (fs.existsSync(wrapperDir)) {
|
|
70
|
-
removeWrapperFiles(wrapperDir, serverDir);
|
|
71
|
-
|
|
72
66
|
console.log(
|
|
73
|
-
`\n✅ Removed wrapper for ${packageName} from src/server`,
|
|
67
|
+
`\n✅ Removed wrapper for ${packageName} from src/server/${packageName}`,
|
|
74
68
|
);
|
|
75
69
|
}
|
|
76
70
|
}
|