@tobi2409/fullsty 0.8.3 → 0.8.4
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/@tobi2409/authentication-core-db/current-user.ts +13 -21
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/extension.json +1 -1
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/login.ts +17 -26
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/register.ts +48 -62
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/shared.ts +7 -0
- package/scripts/extension-wrappers/@tobi2409/sql-query-builder/extension.json +4 -0
- package/scripts/extension-wrappers/drizzle/drizzle-wrapper.ts +0 -89
- package/scripts/extension-wrappers/drizzle/extension.json +0 -5
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
|
|
28
|
+
fullsty-server-pkg add @tobi2409/sql-query-builder
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## 1) Create a project
|
|
@@ -70,7 +70,7 @@ Run it from the project root, for example:
|
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
fullsty-server-pkg add pg
|
|
73
|
-
fullsty-server-pkg add
|
|
73
|
+
fullsty-server-pkg add @tobi2409/sql-query-builder
|
|
74
74
|
fullsty-server-pkg remove pg
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -94,7 +94,7 @@ command and `npm run generate` after changing an extension or its dependencies.
|
|
|
94
94
|
|
|
95
95
|
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
96
|
|
|
97
|
-
`fullsty-server-pkg add
|
|
97
|
+
`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
98
|
|
|
99
99
|
## 5) Start the generated server
|
|
100
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobi2409/fullsty",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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
|
|
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
|
-
|
|
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
|
|
40
|
-
.select({
|
|
41
|
-
.from(
|
|
42
|
-
.where(
|
|
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(
|
|
@@ -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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
57
|
-
.select(
|
|
58
|
-
.from(
|
|
59
|
-
.where(
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Object.keys(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
}
|