@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 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 sql-template-tag
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 sql-template-tag` copies [extension-wrappers/sql-template-tag/sql-template-tag-wrapper.ts](extension-wrappers/sql-template-tag/sql-template-tag-wrapper.ts) into [projects/demo1/src/server](projects/demo1/src/server). This keeps `sql-template-tag` 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.
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.0",
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,3 @@
1
+ {
2
+ "addToServerPackage": false
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "addToServerPackage": true
3
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "addToServerPackage": true
3
+ }
@@ -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
- const pools = new Map<string, Pool>();
6
- let pgEnv: Record<string, string> = {};
7
- let pgEnvLoaded = false;
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
- function envKey(connectionName: string, key: string): string {
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
- function loadPgEnv(): void {
15
- if (pgEnvLoaded) {
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
- // Load .env.pg into a local cache instead of writing into process.env.
20
- // This keeps the pg wrapper isolated from other wrappers such as auth,
21
- // which may load their own .env files later.
22
- const envFileContent = fs.existsSync(".env.pg")
23
- ? fs.readFileSync(".env.pg", "utf8")
24
- : "";
26
+ private loadPgEnv(): void {
27
+ // only load the .env.pg file once
28
+ if (this.pgEnvLoaded) {
29
+ return;
30
+ }
25
31
 
26
- pgEnv = dotenv.parse(envFileContent);
27
- pgEnvLoaded = true;
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
- function getEnv(
31
- connectionName: string,
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
- const pool = new Pool({
46
- host: getEnv(connectionName, "PGHOST", "127.0.0.1"),
47
- port: Number(getEnv(connectionName, "PGPORT", "5432")),
48
- user: getEnv(connectionName, "PGUSER", "postgres"),
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
- pools.set(connectionName, pool);
55
- return pool;
56
- }
48
+ getPgPool(): Pool {
49
+ if (this.pool) {
50
+ return this.pool;
51
+ }
57
52
 
58
- export async function closePgPool(
59
- connectionName: string = "default",
60
- ): Promise<void> {
61
- const pool = pools.get(connectionName);
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
- if (!pool) {
64
- return;
62
+ return this.pool;
65
63
  }
66
64
 
67
- await pool.end();
68
- pools.delete(connectionName);
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
- for (const [name, pool] of all) {
75
- await pool.end();
76
- pools.delete(name);
70
+ await this.pool.end();
71
+ this.pool = null;
77
72
  }
78
- }
73
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "addToServerPackage": true
3
+ }
@@ -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, { write } from "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 removeWrapperFiles(sourceWrapperDir, targetServerDir) {
11
- const entries = fs.readdirSync(sourceWrapperDir, { withFileTypes: true });
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
- json.dependencies[packageName] = "latest";
28
+ if (shouldAddToServerPackage(wrapperDir)) {
29
+ json.dependencies[packageName] = "latest";
30
+ }
42
31
 
43
32
  if (fs.existsSync(wrapperDir)) {
44
- copyDirectory(wrapperDir, serverDir);
45
- console.log(`\n✅ Copied wrapper for ${packageName} to src/server`);
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
- delete json.dependencies[packageName];
67
- delete json.devDependencies[packageName];
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
  }