@tobi2409/fullsty 0.5.3 → 0.5.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/README.md CHANGED
@@ -14,12 +14,20 @@ Minimal Fullsty starter workflow.
14
14
 
15
15
  A sample application can be found at https://github.com/tobi2409/fullsty/tree/main/projects/db-test
16
16
 
17
+ For the `db-test` demo, add the required server wrappers in your project root before generating/running:
18
+
19
+ ```bash
20
+ fullsty-server-pkg add pg
21
+ fullsty-server-pkg add db-connection
22
+ fullsty-server-pkg add kysely
23
+ ```
24
+
17
25
  ## 1) Create a project
18
26
 
19
27
  From the repository root:
20
28
 
21
29
  ```bash
22
- npx create-fullsty-project projects/demo1
30
+ create-fullsty-project projects/demo1
23
31
  ```
24
32
 
25
33
  This creates a new folder (for example [projects/demo1](projects/demo1)).
@@ -55,12 +63,12 @@ It also copies [project-frame/server-package.json](project-frame/server-package.
55
63
  Run it from the project root, for example:
56
64
 
57
65
  ```bash
58
- ../../scripts/fullsty-server-pkg.js add pg
59
- ../../scripts/fullsty-server-pkg.js add kysely
60
- ../../scripts/fullsty-server-pkg.js remove pg
66
+ fullsty-server-pkg add pg
67
+ fullsty-server-pkg add kysely
68
+ fullsty-server-pkg remove pg
61
69
  ```
62
70
 
63
- Alternatively, you can also use npx fullsty-server-pkg.
71
+ Alternatively, you can also use `npx fullsty-server-pkg`.
64
72
 
65
73
  What it does:
66
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tobi2409/fullsty",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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,47 @@
1
+ import { DbConnection } from "../../db-connection/db-connection-wrapper.ts";
2
+ // @ts-ignore kysely is supplied by the server package in generated projects.
3
+ import { KyselyWrapper } from "../../kysely/kysely-wrapper.ts";
4
+ import { authenticationCoreLib } from "../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts";
5
+ import { DEFAULT_TABLE_NAME, DEFAULT_USER_COLUMN, UserColumn } from "./shared.ts";
6
+
7
+ export namespace AuthenticationCoreKyselyCurrentUser {
8
+ export async function getCurrentUser(
9
+ token: string,
10
+ jwtKey: Parameters<typeof authenticationCoreLib.AuthenticationCoreCurrentUser.getCurrentUser>[1],
11
+ dbConnection: DbConnection,
12
+ tableName: string = DEFAULT_TABLE_NAME,
13
+ columns: UserColumn = DEFAULT_USER_COLUMN,
14
+ verifyOptions: Parameters<typeof authenticationCoreLib.AuthenticationCoreCurrentUser.getCurrentUser>[3] = {}
15
+ ): Promise<string> {
16
+ try {
17
+ const db = KyselyWrapper.buildQueryCompiler(dbConnection.getDriverName());
18
+
19
+ const isActiveCallback = async (uuid: string): Promise<boolean> => {
20
+ const compiledQuery = db
21
+ .selectFrom(tableName)
22
+ .select([
23
+ KyselyWrapper.sql.ref(columns.isActive).as("isActive")
24
+ ])
25
+ .where(KyselyWrapper.sql.ref(columns.uuid), "=", uuid)
26
+ .limit(1)
27
+ .compile();
28
+
29
+ // 'any' is used instead of 'unknown' because calling .query() on an unknown type requires
30
+ // either a type assertion or a separate interface. DbConnection is driver-agnostic, so
31
+ // the concrete pool type (e.g. pg.Pool) is not available here without adding a dependency.
32
+ const connection = await dbConnection.getConnection() as any;
33
+ const result = await connection.query(compiledQuery.sql, compiledQuery.parameters);
34
+
35
+ if (!result.rows?.[0]) {
36
+ return false;
37
+ }
38
+
39
+ return Boolean((result.rows[0] as Record<string, unknown>).isActive);
40
+ };
41
+
42
+ return authenticationCoreLib.AuthenticationCoreCurrentUser.getCurrentUser(token, jwtKey, isActiveCallback, verifyOptions);
43
+ } catch (error) {
44
+ throw error;
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "addToServerPackage": false
3
+ }
@@ -0,0 +1,57 @@
1
+ import { DbConnection } from "../../db-connection/db-connection-wrapper.ts";
2
+ // @ts-ignore kysely is supplied by the server package in generated projects.
3
+ import { KyselyWrapper } from "../../kysely/kysely-wrapper.ts";
4
+ import { authenticationCoreLib } from "../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts";
5
+ import { DEFAULT_TABLE_NAME, DEFAULT_USER_COLUMN, UserColumn } from "./shared.ts";
6
+
7
+ export namespace AuthenticationCoreKyselyLogin {
8
+ function mapFetchedUser(row: Record<string, unknown> | undefined): authenticationCoreLib.FetchedUser | undefined {
9
+ if (!row) {
10
+ return undefined;
11
+ }
12
+
13
+ return {
14
+ uuid: String(row.uuid),
15
+ mail: String(row.mail),
16
+ password: String(row.password),
17
+ isActive: Boolean(row.isActive)
18
+ };
19
+ }
20
+
21
+ export async function login(
22
+ typedMail: string,
23
+ typedPassword: string,
24
+ jwtKey: Parameters<typeof authenticationCoreLib.AuthenticationCoreLogin.login>[3],
25
+ dbConnection: DbConnection,
26
+ tableName: string = DEFAULT_TABLE_NAME,
27
+ columns: UserColumn = DEFAULT_USER_COLUMN,
28
+ jwtOptions: Parameters<typeof authenticationCoreLib.AuthenticationCoreLogin.login>[4] = {}
29
+ ): Promise<string> {
30
+ try {
31
+ const db = KyselyWrapper.buildQueryCompiler(dbConnection.getDriverName());
32
+
33
+ const compiledQuery = db
34
+ .selectFrom(tableName)
35
+ .select([
36
+ KyselyWrapper.sql.ref(columns.uuid).as("uuid"),
37
+ KyselyWrapper.sql.ref(columns.mail).as("mail"),
38
+ KyselyWrapper.sql.ref(columns.password).as("password"),
39
+ KyselyWrapper.sql.ref(columns.isActive).as("isActive")
40
+ ])
41
+ .where(KyselyWrapper.sql.ref(columns.mail), "=", typedMail)
42
+ .limit(1)
43
+ .compile();
44
+
45
+ // 'any' is used instead of 'unknown' because calling .query() on an unknown type requires
46
+ // either a type assertion or a separate interface. DbConnection is driver-agnostic, so
47
+ // the concrete pool type (e.g. pg.Pool) is not available here without adding a dependency.
48
+ const connection = await dbConnection.getConnection() as any;
49
+ const result = await connection.query(compiledQuery.sql, compiledQuery.parameters);
50
+
51
+ const fetchedUser = mapFetchedUser(result.rows?.[0] as Record<string, unknown> | undefined);
52
+ return authenticationCoreLib.AuthenticationCoreLogin.login(typedMail, typedPassword, fetchedUser, jwtKey, jwtOptions);
53
+ } catch (error) {
54
+ throw error;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,108 @@
1
+ import { DbConnection } from "../../db-connection/db-connection-wrapper.ts";
2
+ // @ts-ignore kysely is supplied by the server package in generated projects.
3
+ import { KyselyWrapper } from "../../kysely/kysely-wrapper.ts";
4
+ import { authenticationCoreLib } from "../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts";
5
+ import { DEFAULT_TABLE_NAME, DEFAULT_USER_COLUMN, UserColumn } from "./shared.ts";
6
+
7
+ export namespace AuthenticationCoreKyselyRegister {
8
+ function mapFetchedUser(row: Record<string, unknown> | undefined): authenticationCoreLib.FetchedUser | undefined {
9
+ if (!row) {
10
+ return undefined;
11
+ }
12
+
13
+ return {
14
+ uuid: String(row.uuid),
15
+ mail: String(row.mail),
16
+ password: String(row.password),
17
+ isActive: Boolean(row.isActive)
18
+ };
19
+ }
20
+
21
+ export async function register(
22
+ registrationInputData: authenticationCoreLib.RegistrationInputData,
23
+ customInputData: Record<string, unknown>,
24
+ verificationMail: authenticationCoreLib.VerificationMail,
25
+ dbConnection: DbConnection,
26
+ tableName: string = DEFAULT_TABLE_NAME,
27
+ columns: UserColumn = DEFAULT_USER_COLUMN,
28
+ mailTransportConfig?: Parameters<typeof authenticationCoreLib.AuthenticationCoreRegister.register>[5],
29
+ hashOptions?: Parameters<typeof authenticationCoreLib.AuthenticationCoreRegister.register>[6],
30
+ ): Promise<authenticationCoreLib.FetchedUser> {
31
+ const db = KyselyWrapper.buildQueryCompiler(dbConnection.getDriverName());
32
+
33
+ // 'any' is used instead of 'unknown' because calling pool/client APIs on an unknown type requires
34
+ // either a type assertion or a separate interface. DbConnection is driver-agnostic, so
35
+ // the concrete pool/client type (e.g. pg.Pool/PoolClient) is not available here without adding a dependency.
36
+ const pool = await dbConnection.getConnection() as any;
37
+ const transactionConnection = await pool.connect();
38
+
39
+ const mailExistsRoutine = async (mail: string): Promise<boolean> => {
40
+ const compiledQuery = db
41
+ .selectFrom(tableName)
42
+ .select((eb: any) => [eb.val(1).as("exists")])
43
+ .where(KyselyWrapper.sql.ref(columns.mail), "=", mail)
44
+ .limit(1)
45
+ .compile();
46
+
47
+ const result = await transactionConnection.query(compiledQuery.sql, compiledQuery.parameters);
48
+
49
+ return (result.rows?.length ?? 0) > 0;
50
+ };
51
+
52
+ const dataProcessing = async (
53
+ identification: string,
54
+ hashedPassword: string,
55
+ inputData: Record<string, unknown>
56
+ ): Promise<authenticationCoreLib.FetchedUser | undefined> => {
57
+ const userData: Record<string, unknown> = {
58
+ [columns.mail]: identification,
59
+ [columns.password]: hashedPassword,
60
+ [columns.isActive]: false,
61
+ ...inputData
62
+ };
63
+
64
+ const compiledQuery = db
65
+ .insertInto(tableName)
66
+ .values(userData as any)
67
+ .returning([
68
+ KyselyWrapper.sql.ref(columns.uuid).as("uuid"),
69
+ KyselyWrapper.sql.ref(columns.mail).as("mail"),
70
+ KyselyWrapper.sql.ref(columns.password).as("password"),
71
+ KyselyWrapper.sql.ref(columns.isActive).as("isActive")
72
+ ])
73
+ .compile();
74
+
75
+ const result = await transactionConnection.query(compiledQuery.sql, compiledQuery.parameters);
76
+ return mapFetchedUser(result.rows?.[0] as Record<string, unknown> | undefined);
77
+ };
78
+
79
+ try {
80
+ await transactionConnection.query("BEGIN");
81
+
82
+ const registeredUser = await authenticationCoreLib.AuthenticationCoreRegister.register(
83
+ registrationInputData,
84
+ mailExistsRoutine,
85
+ customInputData,
86
+ dataProcessing,
87
+ verificationMail,
88
+ mailTransportConfig,
89
+ hashOptions
90
+ );
91
+
92
+ await transactionConnection.query("COMMIT");
93
+ return registeredUser;
94
+ } catch (error) {
95
+ try {
96
+ await transactionConnection.query("ROLLBACK");
97
+ } catch {
98
+ // Ignore rollback errors, original error is more relevant.
99
+ }
100
+
101
+ throw error;
102
+ } finally {
103
+ if (typeof transactionConnection.release === "function") {
104
+ transactionConnection.release();
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,15 @@
1
+ export type UserColumn = {
2
+ uuid: string;
3
+ mail: string;
4
+ password: string;
5
+ isActive: string;
6
+ };
7
+
8
+ export const DEFAULT_TABLE_NAME = "users";
9
+
10
+ export const DEFAULT_USER_COLUMN: UserColumn = {
11
+ uuid: "uuid",
12
+ mail: "mail",
13
+ password: "password",
14
+ isActive: "isActive"
15
+ };
@@ -0,0 +1,2 @@
1
+ // @ts-ignore package is supplied by the server package in generated projects.
2
+ export * as authenticationCoreLib from "@tobi2409/authentication-core-lib";
@@ -0,0 +1,3 @@
1
+ {
2
+ "addToServerPackage": true
3
+ }
@@ -24,17 +24,21 @@ export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
24
24
  for (const packageName of packageNames) {
25
25
  const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
26
26
  const targetWrapperDir = path.join(serverDir, packageName);
27
+ const wrapperExists = fs.existsSync(wrapperDir);
27
28
 
28
- if (shouldAddToServerPackage(wrapperDir)) {
29
+ if (!wrapperExists) {
29
30
  json.dependencies[packageName] = "latest";
31
+ continue;
30
32
  }
31
33
 
32
- if (fs.existsSync(wrapperDir)) {
33
- copyDirectory(wrapperDir, targetWrapperDir, {
34
- excludedNames: [EXTENSION_CONFIG_FILE],
35
- });
36
- console.log(`\n✅ Copied wrapper for ${packageName} to src/server/${packageName}`);
34
+ if (shouldAddToServerPackage(wrapperDir)) {
35
+ json.dependencies[packageName] = "latest";
37
36
  }
37
+
38
+ copyDirectory(wrapperDir, targetWrapperDir, {
39
+ excludedNames: [EXTENSION_CONFIG_FILE],
40
+ });
41
+ console.log(`\n✅ Copied wrapper for ${packageName} to src/server/${packageName}`);
38
42
  }
39
43
 
40
44
  writeJson(projectDir, "server-package.json", json);
@@ -54,13 +58,20 @@ export function handleRemove(projectDir, scriptDir, serverDir, packageNames) {
54
58
  for (const packageName of packageNames) {
55
59
  const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
56
60
  const targetWrapperDir = path.join(serverDir, packageName);
61
+ const wrapperExists = fs.existsSync(wrapperDir);
62
+
63
+ if (!wrapperExists) {
64
+ delete json.dependencies[packageName];
65
+ delete json.devDependencies[packageName];
66
+ continue;
67
+ }
57
68
 
58
69
  if (shouldAddToServerPackage(wrapperDir)) {
59
70
  delete json.dependencies[packageName];
60
71
  delete json.devDependencies[packageName];
61
72
  }
62
73
 
63
- if (fs.existsSync(wrapperDir) && fs.existsSync(targetWrapperDir)) {
74
+ if (fs.existsSync(targetWrapperDir)) {
64
75
  fs.rmSync(targetWrapperDir, { recursive: true, force: true });
65
76
 
66
77
  console.log(