@tobi2409/fullsty 0.8.3 → 0.8.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
@@ -25,7 +25,7 @@ For the `db-test` demo, add the required server wrappers in your project root be
25
25
  ```bash
26
26
  fullsty-server-pkg add pg
27
27
  fullsty-server-pkg add db-connection
28
- fullsty-server-pkg add drizzle
28
+ fullsty-server-pkg add @tobi2409/sql-query-builder
29
29
  ```
30
30
 
31
31
  ## 1) Create a project
@@ -58,10 +58,12 @@ npm run generate
58
58
  This runs:
59
59
 
60
60
  - `generate-server` (creates server output in [projects/demo1/generated/server](projects/demo1/generated/server))
61
- - `generate-client` (creates client output in [projects/demo1/generated/client](projects/demo1/generated/client))
61
+ - `generate-client` (creates client output with `proc2rest-client`, then recursively processes its HTML files with `layout` in [projects/demo1/generated/client](projects/demo1/generated/client))
62
62
 
63
63
  It also copies [project-frame/server-package.json](project-frame/server-package.json) to `generated/server/package.json` and [project-frame/client-package.json](project-frame/client-package.json) to `generated/client/package.json`.
64
64
 
65
+ The layout step processes all HTML files below `generated/client` in place and adds a matching `*-layout.css` and `*-layout.js` beside each one. The source files under `src/client` remain unchanged.
66
+
65
67
  ## 4) Use `fullsty-server-pkg` for wrapper packages
66
68
 
67
69
  `fullsty-server-pkg` is a project helper for wrapper-aware server package changes.
@@ -70,7 +72,7 @@ Run it from the project root, for example:
70
72
 
71
73
  ```bash
72
74
  fullsty-server-pkg add pg
73
- fullsty-server-pkg add drizzle
75
+ fullsty-server-pkg add @tobi2409/sql-query-builder
74
76
  fullsty-server-pkg remove pg
75
77
  ```
76
78
 
@@ -94,7 +96,7 @@ command and `npm run generate` after changing an extension or its dependencies.
94
96
 
95
97
  After `fullsty-server-pkg 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.
96
98
 
97
- `fullsty-server-pkg add drizzle` copies [scripts/extension-wrappers/drizzle/drizzle-wrapper.ts](scripts/extension-wrappers/drizzle/drizzle-wrapper.ts) into [projects/demo1/src/server](projects/demo1/src/server). This keeps Drizzle 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.
99
+ `fullsty-server-pkg add @tobi2409/sql-query-builder` adds the SQL query builder to the generated server dependencies. For PostgreSQL pooling and env handling, add `pg` separately.
98
100
 
99
101
  ## 5) Start the generated server
100
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tobi2409/fullsty",
3
- "version": "0.8.3",
3
+ "version": "0.8.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",
@@ -1,14 +1,14 @@
1
+ import SQLQueryBuilder from '@tobi2409/sql-query-builder'
1
2
  import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
2
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
3
- import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
4
3
  import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
5
4
  import {
6
5
  DEFAULT_TABLE_NAME,
7
6
  DEFAULT_USER_COLUMN,
7
+ quoteIdentifier,
8
8
  UserColumn
9
9
  } from './shared.ts'
10
10
 
11
- export namespace AuthenticationCoreDrizzleCurrentUser {
11
+ export namespace AuthenticationCoreDbCurrentUser {
12
12
  export async function getCurrentUser(
13
13
  token: string,
14
14
  jwtKey: Parameters<
@@ -22,31 +22,23 @@ export namespace AuthenticationCoreDrizzleCurrentUser {
22
22
  >[3] = {}
23
23
  ): Promise<string> {
24
24
  try {
25
- // The wrapper only stores the driver name and is very lightweight;
26
- // the pool is supplied per query context, so repeated initialization is safe.
27
- const drizzleWrapper = new DrizzleWrapper(
28
- dbConnection.getDriverName()
29
- )
30
- const db = drizzleWrapper.buildQueryBuilder(
31
- await dbConnection.getPool()
32
- )
33
- const users = drizzleWrapper.buildTable(tableName, columns, {
34
- uuid: 'uuid',
35
- isActive: 'boolean'
36
- })
25
+ const connection = (await dbConnection.getPool()) as any
37
26
 
38
27
  const isActiveCallback = async (uuid: string): Promise<boolean> => {
39
- const result = await db
40
- .select({ isActive: users.isActive })
41
- .from(users)
42
- .where(DrizzleWrapper.eq(users.uuid, uuid))
28
+ const query = new SQLQueryBuilder(dbConnection.getDriverName())
29
+ .select([`${quoteIdentifier(columns.isActive)} AS "isActive"`])
30
+ .from(quoteIdentifier(tableName))
31
+ .where(`${quoteIdentifier(columns.uuid)} = ?`, [uuid])
43
32
  .limit(1)
33
+ .toSQL()
34
+
35
+ const result = await connection.query(query.text, query.values)
44
36
 
45
- if (!result[0]) {
37
+ if (!result.rows[0]) {
46
38
  return false
47
39
  }
48
40
 
49
- return Boolean(result[0].isActive)
41
+ return Boolean(result.rows[0].isActive)
50
42
  }
51
43
 
52
44
  return authenticationCoreLib.AuthenticationCoreCurrentUser.getCurrentUser(
@@ -2,7 +2,7 @@
2
2
  "addToServerPackage": false,
3
3
  "dependencies": [
4
4
  "db-connection",
5
- "drizzle",
5
+ "@tobi2409/sql-query-builder",
6
6
  "@tobi2409/authentication-core-lib"
7
7
  ]
8
8
  }
@@ -1,14 +1,14 @@
1
+ import SQLQueryBuilder from '@tobi2409/sql-query-builder'
1
2
  import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
2
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
3
- import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
4
3
  import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
5
4
  import {
6
5
  DEFAULT_TABLE_NAME,
7
6
  DEFAULT_USER_COLUMN,
7
+ quoteIdentifier,
8
8
  UserColumn
9
9
  } from './shared.ts'
10
10
 
11
- export namespace AuthenticationCoreDrizzleLogin {
11
+ export namespace AuthenticationCoreDbLogin {
12
12
  function mapFetchedUser(
13
13
  row: Record<string, unknown> | undefined
14
14
  ): authenticationCoreLib.FetchedUser | undefined {
@@ -38,32 +38,23 @@ export namespace AuthenticationCoreDrizzleLogin {
38
38
  >[4] = {}
39
39
  ): Promise<string> {
40
40
  try {
41
- // The wrapper only stores the driver name and is very lightweight;
42
- // the pool is supplied per query context, so repeated initialization is safe.
43
- const drizzleWrapper = new DrizzleWrapper(
44
- dbConnection.getDriverName()
45
- )
46
- const db = drizzleWrapper.buildQueryBuilder(
47
- await dbConnection.getPool()
48
- )
49
- const users = drizzleWrapper.buildTable(tableName, columns, {
50
- uuid: 'uuid',
51
- isActive: 'boolean'
52
- })
53
-
54
- const result = await db
55
- .select({
56
- uuid: users.uuid,
57
- mail: users.mail,
58
- password: users.password,
59
- isActive: users.isActive
60
- })
61
- .from(users)
62
- .where(DrizzleWrapper.eq(users.mail, typedMail))
41
+ const query = new SQLQueryBuilder(dbConnection.getDriverName())
42
+ .select([
43
+ `${quoteIdentifier(columns.uuid)} AS "uuid"`,
44
+ `${quoteIdentifier(columns.mail)} AS "mail"`,
45
+ `${quoteIdentifier(columns.password)} AS "password"`,
46
+ `${quoteIdentifier(columns.isActive)} AS "isActive"`
47
+ ])
48
+ .from(quoteIdentifier(tableName))
49
+ .where(`${quoteIdentifier(columns.mail)} = ?`, [typedMail])
63
50
  .limit(1)
51
+ .toSQL()
52
+
53
+ const connection = (await dbConnection.getPool()) as any
54
+ const result = await connection.query(query.text, query.values)
64
55
 
65
56
  const fetchedUser = mapFetchedUser(
66
- result[0] as Record<string, unknown> | undefined
57
+ result.rows[0] as Record<string, unknown> | undefined
67
58
  )
68
59
 
69
60
  return authenticationCoreLib.AuthenticationCoreLogin.login(
@@ -1,14 +1,14 @@
1
+ import SQLQueryBuilder from '@tobi2409/sql-query-builder'
1
2
  import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
2
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
3
- import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
4
3
  import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
5
4
  import {
6
5
  DEFAULT_TABLE_NAME,
7
6
  DEFAULT_USER_COLUMN,
7
+ quoteIdentifier,
8
8
  UserColumn
9
9
  } from './shared.ts'
10
10
 
11
- export namespace AuthenticationCoreDrizzleRegister {
11
+ export namespace AuthenticationCoreDbRegister {
12
12
  function mapFetchedUser(
13
13
  row: Record<string, unknown> | undefined
14
14
  ): authenticationCoreLib.FetchedUser | undefined {
@@ -38,28 +38,23 @@ export namespace AuthenticationCoreDrizzleRegister {
38
38
  typeof authenticationCoreLib.AuthenticationCoreRegister.register
39
39
  >[6]
40
40
  ): Promise<authenticationCoreLib.FetchedUser> {
41
- // The wrapper only stores the driver name and is very lightweight;
42
- // the pool is supplied per query context, so repeated initialization is safe.
43
- const drizzleWrapper = new DrizzleWrapper(
44
- dbConnection.getDriverName()
45
- )
46
- const db = drizzleWrapper.buildQueryBuilder(
47
- await dbConnection.getPool()
48
- )
49
- const users = drizzleWrapper.buildTable(tableName, columns, {
50
- uuid: 'uuid',
51
- isActive: 'boolean'
52
- })
41
+ const pool = (await dbConnection.getPool()) as any
42
+ const transaction = await pool.connect()
43
+
44
+ try {
45
+ await transaction.query('BEGIN')
53
46
 
54
- return db.transaction(async (transactionDb: any) => {
55
47
  const mailExistsRoutine = async (mail: string): Promise<boolean> => {
56
- const result = await transactionDb
57
- .select({ mail: users.mail })
58
- .from(users)
59
- .where(DrizzleWrapper.eq(users.mail, mail))
48
+ const query = new SQLQueryBuilder(dbConnection.getDriverName())
49
+ .select([quoteIdentifier(columns.mail)])
50
+ .from(quoteIdentifier(tableName))
51
+ .where(`${quoteIdentifier(columns.mail)} = ?`, [mail])
60
52
  .limit(1)
53
+ .toSQL()
54
+
55
+ const result = await transaction.query(query.text, query.values)
61
56
 
62
- return result.length > 0
57
+ return result.rows.length > 0
63
58
  }
64
59
 
65
60
  const dataProcessing = async (
@@ -74,33 +69,28 @@ export namespace AuthenticationCoreDrizzleRegister {
74
69
  ...inputData
75
70
  }
76
71
 
77
- const insertUsers = drizzleWrapper.buildTable(
78
- tableName,
79
- Object.fromEntries(
80
- Object.keys({ ...columns, ...userData }).map((column) => [
81
- column,
82
- columns[column as keyof UserColumn] ?? column
83
- ])
84
- ),
85
- { uuid: 'uuid', isActive: 'boolean' }
86
- )
72
+ const query = new SQLQueryBuilder(dbConnection.getDriverName())
73
+ .insertInto(
74
+ quoteIdentifier(tableName),
75
+ Object.keys(userData).map(quoteIdentifier)
76
+ )
77
+ .values(Object.values(userData))
78
+ .returning([
79
+ `${quoteIdentifier(columns.uuid)} AS "uuid"`,
80
+ `${quoteIdentifier(columns.mail)} AS "mail"`,
81
+ `${quoteIdentifier(columns.password)} AS "password"`,
82
+ `${quoteIdentifier(columns.isActive)} AS "isActive"`
83
+ ])
84
+ .toSQL()
87
85
 
88
- const result = await transactionDb
89
- .insert(insertUsers)
90
- .values(userData)
91
- .returning({
92
- uuid: insertUsers.uuid,
93
- mail: insertUsers.mail,
94
- password: insertUsers.password,
95
- isActive: insertUsers.isActive
96
- })
86
+ const result = await transaction.query(query.text, query.values)
97
87
 
98
88
  return mapFetchedUser(
99
- result[0] as Record<string, unknown> | undefined
89
+ result.rows[0] as Record<string, unknown> | undefined
100
90
  )
101
91
  }
102
92
 
103
- return authenticationCoreLib.AuthenticationCoreRegister.register(
93
+ const registeredUser = await authenticationCoreLib.AuthenticationCoreRegister.register(
104
94
  registrationInputData,
105
95
  mailExistsRoutine,
106
96
  customInputData,
@@ -109,7 +99,14 @@ export namespace AuthenticationCoreDrizzleRegister {
109
99
  mailTransportConfig,
110
100
  hashOptions
111
101
  )
112
- })
102
+ await transaction.query('COMMIT')
103
+ return registeredUser
104
+ } catch (error) {
105
+ await transaction.query('ROLLBACK')
106
+ throw error
107
+ } finally {
108
+ transaction.release()
109
+ }
113
110
  }
114
111
 
115
112
  export async function activateMail(
@@ -118,24 +115,13 @@ export namespace AuthenticationCoreDrizzleRegister {
118
115
  tableName: string = DEFAULT_TABLE_NAME,
119
116
  columns: UserColumn = DEFAULT_USER_COLUMN
120
117
  ): Promise<void> {
121
- // The wrapper only stores the driver name and is very lightweight;
122
- // the pool is supplied per query context, so repeated initialization is safe.
123
- const drizzleWrapper = new DrizzleWrapper(
124
- dbConnection.getDriverName()
125
- )
126
-
127
- const db = drizzleWrapper.buildQueryBuilder(
128
- await dbConnection.getPool()
129
- )
130
-
131
- const users = drizzleWrapper.buildTable(tableName, columns, {
132
- uuid: 'uuid',
133
- isActive: 'boolean'
134
- })
135
-
136
- await db
137
- .update(users)
138
- .set({ [columns.isActive]: true })
139
- .where(DrizzleWrapper.eq(users.uuid, uuid))
118
+ const query = new SQLQueryBuilder(dbConnection.getDriverName())
119
+ .update(quoteIdentifier(tableName))
120
+ .setRaw(`${quoteIdentifier(columns.isActive)} = ?`, [true])
121
+ .where(`${quoteIdentifier(columns.uuid)} = ?`, [uuid])
122
+ .toSQL()
123
+
124
+ const connection = (await dbConnection.getPool()) as any
125
+ await connection.query(query.text, query.values)
140
126
  }
141
127
  }
@@ -13,3 +13,10 @@ export const DEFAULT_USER_COLUMN: UserColumn = {
13
13
  password: 'password',
14
14
  isActive: 'isActive'
15
15
  }
16
+
17
+ export function quoteIdentifier(identifier: string): string {
18
+ return identifier
19
+ .split('.')
20
+ .map((part) => `"${part.replaceAll('"', '""')}"`)
21
+ .join('.')
22
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "addToServerPackage": true,
3
+ "dependencies": []
4
+ }
@@ -4,6 +4,6 @@
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@tobi2409/mvvm-monster": "0.9.16",
7
- "@tobi2409/layout": "0.5.0"
7
+ "@tobi2409/layout": "0.9.1"
8
8
  }
9
9
  }
@@ -7,11 +7,12 @@
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "generate-server": "npx proc2rest-server && node -e \"require('fs').copyFileSync('server-package.json','generated/server/package.json'); require('fs').copyFileSync('server-tsconfig.json','generated/server/tsconfig.json')\"",
10
- "generate-client": "npx proc2rest-client && node -e \"require('fs').copyFileSync('client-package.json','generated/client/package.json');\"",
10
+ "generate-client": "npx proc2rest-client && npx layout generated/client --output-dir . && node -e \"require('fs').copyFileSync('client-package.json','generated/client/package.json');\"",
11
11
  "generate": "npm run generate-server && npm run generate-client",
12
12
  "test": "echo \"Error: no test specified\" && exit 1"
13
13
  },
14
14
  "devDependencies": {
15
+ "@tobi2409/layout": "0.9.1",
15
16
  "@tobi2409/proc2rest": "0.8.2"
16
17
  }
17
18
  }
@@ -13,10 +13,6 @@
13
13
  }
14
14
  </script>
15
15
 
16
- <link
17
- rel="stylesheet"
18
- href="node_modules/@tobi2409/layout/src/layout.css"
19
- />
20
16
  <link rel="stylesheet" href="index.css" />
21
17
  </head>
22
18
  <body>
@@ -1,89 +0,0 @@
1
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
2
- import { eq, sql as drizzleSql, type SQL } from 'drizzle-orm'
3
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
4
- import { drizzle } from 'drizzle-orm/node-postgres'
5
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
6
- import { boolean as pgBoolean, integer, pgTable, text, uuid as pgUuid } from 'drizzle-orm/pg-core'
7
- // @ts-ignore drizzle-orm is supplied by the server package in generated projects.
8
- import { PgDialect } from 'drizzle-orm/pg-core'
9
-
10
- type DriverName = 'pg' | 'mysql' | 'sqlite'
11
- type ColumnType = 'text' | 'boolean' | 'integer' | 'uuid'
12
-
13
- export class DrizzleWrapper {
14
- static readonly sql = drizzleSql
15
- static readonly eq = eq
16
-
17
- // The pool connection belongs to the current query or transaction and can
18
- // therefore change between calls. Apart from the driver name, this wrapper
19
- // holds no state and is very lightweight, so it can safely be initialized
20
- // more than once. buildQueryBuilder receives the appropriate connection.
21
- constructor(private readonly driverName: DriverName) {}
22
-
23
- buildQueryBuilder(connection: unknown): any {
24
- this.assertPostgresDriver()
25
-
26
- // Creating the Drizzle instance does not execute a query. It only creates
27
- // a lightweight executable facade around the supplied pool or transaction
28
- // connection; database work starts when a built query is awaited.
29
- return drizzle(connection as any)
30
- }
31
-
32
- buildTable(
33
- tableName: string,
34
- columns: Record<string, string>,
35
- columnTypes: Record<string, ColumnType> = {}
36
- ): any {
37
- this.assertPostgresDriver()
38
-
39
- // pgTable only creates lightweight metadata for an existing table; it does
40
- // not open a connection, query, create, or migrate anything. Missing tables
41
- // or columns fail when the query is executed. Columns default to text unless
42
- // a PostgreSQL type is explicitly supplied.
43
- return pgTable(
44
- tableName,
45
- Object.fromEntries(
46
- Object.entries(columns).map(([propertyName, columnName]) => [
47
- propertyName,
48
- this.buildColumn(columnName, columnTypes[propertyName])
49
- ])
50
- ) as any
51
- )
52
- }
53
-
54
- private buildColumn(columnName: string, columnType: ColumnType = 'text'): any {
55
- switch (columnType) {
56
- case 'boolean':
57
- return pgBoolean(columnName)
58
- case 'integer':
59
- return integer(columnName)
60
- case 'uuid':
61
- return pgUuid(columnName)
62
- default:
63
- return text(columnName)
64
- }
65
- }
66
-
67
- buildQueryCompiler(): {
68
- compile(query: SQL): { sql: string; parameters: unknown[] }
69
- } {
70
- this.assertPostgresDriver()
71
-
72
- const dialect = new PgDialect()
73
- return {
74
- compile(query) {
75
- const compiledQuery = dialect.sqlToQuery(query)
76
- return {
77
- sql: compiledQuery.sql,
78
- parameters: compiledQuery.params
79
- }
80
- }
81
- }
82
- }
83
-
84
- private assertPostgresDriver(): void {
85
- if (this.driverName !== 'pg') {
86
- throw new Error(`Driver is not integrated yet: ${this.driverName}`)
87
- }
88
- }
89
- }
@@ -1,5 +0,0 @@
1
- {
2
- "addToServerPackage": true,
3
- "packageName": "drizzle-orm",
4
- "dependencies": ["db-connection"]
5
- }