@tobi2409/fullsty 0.5.12 → 0.8.0
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 +8 -3
- package/package.json +1 -1
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/current-user.ts +19 -24
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/extension.json +6 -1
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/login.ts +24 -24
- package/scripts/extension-wrappers/@tobi2409/authentication-core-db/register.ts +88 -76
- package/scripts/extension-wrappers/db-connection/db-connection-wrapper.ts +9 -4
- package/scripts/extension-wrappers/db-connection/pg-wrapper-access.ts +1 -0
- package/scripts/extension-wrappers/drizzle/drizzle-wrapper.ts +89 -0
- package/scripts/extension-wrappers/drizzle/extension.json +5 -0
- package/scripts/extension-wrappers/pg/pg-wrapper.ts +14 -1
- package/scripts/extension-wrappers/sql-template-tag/extension.json +2 -1
- package/scripts/fullsty-server-pkg.js +1 -0
- package/scripts/project-frame/client-package.json +2 -2
- package/scripts/project-frame/package.json +1 -1
- package/scripts/project-frame/server-package.json +11 -11
- package/scripts/project-frame/src/client/index.html +1 -1
- package/scripts/project-frame/src/client/index.js +1 -1
- package/scripts/shared/server-package-utils.js +22 -4
- package/scripts/extension-wrappers/kysely/extension.json +0 -3
- package/scripts/extension-wrappers/kysely/kysely-wrapper.ts +0 -31
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ For the `db-test` demo, add the required server wrappers in your project root be
|
|
|
21
21
|
```bash
|
|
22
22
|
fullsty-server-pkg add pg
|
|
23
23
|
fullsty-server-pkg add db-connection
|
|
24
|
-
fullsty-server-pkg add
|
|
24
|
+
fullsty-server-pkg add drizzle
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## 1) Create a project
|
|
@@ -66,7 +66,7 @@ Run it from the project root, for example:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
fullsty-server-pkg add pg
|
|
69
|
-
fullsty-server-pkg add
|
|
69
|
+
fullsty-server-pkg add drizzle
|
|
70
70
|
fullsty-server-pkg remove pg
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -83,9 +83,14 @@ What it does:
|
|
|
83
83
|
|
|
84
84
|
The script does not manage the project-level [package.json](project-frame/package.json).
|
|
85
85
|
|
|
86
|
+
The wrapper in `scripts/extension-wrappers` is the single source of truth. The
|
|
87
|
+
copied files under `src/server` and the generated files under `generated/server`
|
|
88
|
+
are derived artifacts and should not be edited manually. Re-run the wrapper
|
|
89
|
+
command and `npm run generate` after changing an extension or its dependencies.
|
|
90
|
+
|
|
86
91
|
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.
|
|
87
92
|
|
|
88
|
-
`fullsty-server-pkg.js add
|
|
93
|
+
`fullsty-server-pkg.js 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.
|
|
89
94
|
|
|
90
95
|
## 5) Start the generated server
|
|
91
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobi2409/fullsty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
|
|
2
|
-
// @ts-ignore
|
|
3
|
-
import {
|
|
2
|
+
// @ts-ignore drizzle-orm is supplied by the server package in generated projects.
|
|
3
|
+
import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
|
|
4
4
|
import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_TABLE_NAME,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
UserColumn
|
|
9
9
|
} from './shared.ts'
|
|
10
10
|
|
|
11
|
-
export namespace
|
|
11
|
+
export namespace AuthenticationCoreDrizzleCurrentUser {
|
|
12
12
|
export async function getCurrentUser(
|
|
13
13
|
token: string,
|
|
14
14
|
jwtKey: Parameters<
|
|
@@ -22,36 +22,31 @@ export namespace AuthenticationCoreKyselyCurrentUser {
|
|
|
22
22
|
>[3] = {}
|
|
23
23
|
): Promise<string> {
|
|
24
24
|
try {
|
|
25
|
-
|
|
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(
|
|
26
28
|
dbConnection.getDriverName()
|
|
27
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
|
+
})
|
|
28
37
|
|
|
29
38
|
const isActiveCallback = async (uuid: string): Promise<boolean> => {
|
|
30
|
-
const
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
])
|
|
35
|
-
.where(KyselyWrapper.sql.ref(columns.uuid), '=', uuid)
|
|
39
|
+
const result = await db
|
|
40
|
+
.select({ isActive: users.isActive })
|
|
41
|
+
.from(users)
|
|
42
|
+
.where(DrizzleWrapper.eq(users.uuid, uuid))
|
|
36
43
|
.limit(1)
|
|
37
|
-
.compile()
|
|
38
|
-
|
|
39
|
-
// 'any' is used instead of 'unknown' because calling .query() on an unknown type requires
|
|
40
|
-
// either a type assertion or a separate interface. DbConnection is driver-agnostic, so
|
|
41
|
-
// the concrete pool type (e.g. pg.Pool) is not available here without adding a dependency.
|
|
42
|
-
const connection = (await dbConnection.getConnection()) as any
|
|
43
|
-
const result = await connection.query(
|
|
44
|
-
compiledQuery.sql,
|
|
45
|
-
compiledQuery.parameters
|
|
46
|
-
)
|
|
47
44
|
|
|
48
|
-
if (!result
|
|
45
|
+
if (!result[0]) {
|
|
49
46
|
return false
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
return Boolean(
|
|
53
|
-
(result.rows[0] as Record<string, unknown>).isActive
|
|
54
|
-
)
|
|
49
|
+
return Boolean(result[0].isActive)
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
return authenticationCoreLib.AuthenticationCoreCurrentUser.getCurrentUser(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
|
|
2
|
-
// @ts-ignore
|
|
3
|
-
import {
|
|
2
|
+
// @ts-ignore drizzle-orm is supplied by the server package in generated projects.
|
|
3
|
+
import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
|
|
4
4
|
import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_TABLE_NAME,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
UserColumn
|
|
9
9
|
} from './shared.ts'
|
|
10
10
|
|
|
11
|
-
export namespace
|
|
11
|
+
export namespace AuthenticationCoreDrizzleLogin {
|
|
12
12
|
function mapFetchedUser(
|
|
13
13
|
row: Record<string, unknown> | undefined
|
|
14
14
|
): authenticationCoreLib.FetchedUser | undefined {
|
|
@@ -38,34 +38,34 @@ export namespace AuthenticationCoreKyselyLogin {
|
|
|
38
38
|
>[4] = {}
|
|
39
39
|
): Promise<string> {
|
|
40
40
|
try {
|
|
41
|
-
|
|
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(
|
|
42
44
|
dbConnection.getDriverName()
|
|
43
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
|
+
})
|
|
44
53
|
|
|
45
|
-
const
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.where(
|
|
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))
|
|
54
63
|
.limit(1)
|
|
55
|
-
.compile()
|
|
56
|
-
|
|
57
|
-
// 'any' is used instead of 'unknown' because calling .query() on an unknown type requires
|
|
58
|
-
// either a type assertion or a separate interface. DbConnection is driver-agnostic, so
|
|
59
|
-
// the concrete pool type (e.g. pg.Pool) is not available here without adding a dependency.
|
|
60
|
-
const connection = (await dbConnection.getConnection()) as any
|
|
61
|
-
const result = await connection.query(
|
|
62
|
-
compiledQuery.sql,
|
|
63
|
-
compiledQuery.parameters
|
|
64
|
-
)
|
|
65
64
|
|
|
66
65
|
const fetchedUser = mapFetchedUser(
|
|
67
|
-
result
|
|
66
|
+
result[0] as Record<string, unknown> | undefined
|
|
68
67
|
)
|
|
68
|
+
|
|
69
69
|
return authenticationCoreLib.AuthenticationCoreLogin.login(
|
|
70
70
|
typedMail,
|
|
71
71
|
typedPassword,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DbConnection } from '../../db-connection/db-connection-wrapper.ts'
|
|
2
|
-
// @ts-ignore
|
|
3
|
-
import {
|
|
2
|
+
// @ts-ignore drizzle-orm is supplied by the server package in generated projects.
|
|
3
|
+
import { DrizzleWrapper } from '../../drizzle/drizzle-wrapper.ts'
|
|
4
4
|
import { authenticationCoreLib } from '../../@tobi2409/authentication-core-lib/authentication-core-lib-wrapper.ts'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_TABLE_NAME,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
UserColumn
|
|
9
9
|
} from './shared.ts'
|
|
10
10
|
|
|
11
|
-
export namespace
|
|
11
|
+
export namespace AuthenticationCoreDrizzleRegister {
|
|
12
12
|
function mapFetchedUser(
|
|
13
13
|
row: Record<string, unknown> | undefined
|
|
14
14
|
): authenticationCoreLib.FetchedUser | undefined {
|
|
@@ -38,92 +38,104 @@ export namespace AuthenticationCoreKyselyRegister {
|
|
|
38
38
|
typeof authenticationCoreLib.AuthenticationCoreRegister.register
|
|
39
39
|
>[6]
|
|
40
40
|
): Promise<authenticationCoreLib.FetchedUser> {
|
|
41
|
-
|
|
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(
|
|
42
44
|
dbConnection.getDriverName()
|
|
43
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
|
+
})
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
return db.transaction(async (transactionDb: any) => {
|
|
55
|
+
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))
|
|
60
|
+
.limit(1)
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.selectFrom(tableName)
|
|
54
|
-
.select((eb: any) => [eb.val(1).as('exists')])
|
|
55
|
-
.where(KyselyWrapper.sql.ref(columns.mail), '=', mail)
|
|
56
|
-
.limit(1)
|
|
57
|
-
.compile()
|
|
62
|
+
return result.length > 0
|
|
63
|
+
}
|
|
58
64
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const dataProcessing = async (
|
|
66
|
+
identification: string,
|
|
67
|
+
hashedPassword: string,
|
|
68
|
+
inputData: Record<string, unknown>
|
|
69
|
+
): Promise<authenticationCoreLib.FetchedUser | undefined> => {
|
|
70
|
+
const userData: Record<string, unknown> = {
|
|
71
|
+
[columns.mail]: identification,
|
|
72
|
+
[columns.password]: hashedPassword,
|
|
73
|
+
[columns.isActive]: false,
|
|
74
|
+
...inputData
|
|
75
|
+
}
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
)
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
...inputData
|
|
77
|
-
}
|
|
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
|
+
})
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
KyselyWrapper.sql.ref(columns.uuid).as('uuid'),
|
|
84
|
-
KyselyWrapper.sql.ref(columns.mail).as('mail'),
|
|
85
|
-
KyselyWrapper.sql.ref(columns.password).as('password'),
|
|
86
|
-
KyselyWrapper.sql.ref(columns.isActive).as('isActive')
|
|
87
|
-
])
|
|
88
|
-
.compile()
|
|
98
|
+
return mapFetchedUser(
|
|
99
|
+
result[0] as Record<string, unknown> | undefined
|
|
100
|
+
)
|
|
101
|
+
}
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
return authenticationCoreLib.AuthenticationCoreRegister.register(
|
|
104
|
+
registrationInputData,
|
|
105
|
+
mailExistsRoutine,
|
|
106
|
+
customInputData,
|
|
107
|
+
dataProcessing,
|
|
108
|
+
verificationMail,
|
|
109
|
+
mailTransportConfig,
|
|
110
|
+
hashOptions
|
|
93
111
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
export async function activateMail(
|
|
116
|
+
uuid: string,
|
|
117
|
+
dbConnection: DbConnection,
|
|
118
|
+
tableName: string = DEFAULT_TABLE_NAME,
|
|
119
|
+
columns: UserColumn = DEFAULT_USER_COLUMN
|
|
120
|
+
): 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
|
+
)
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
mailExistsRoutine,
|
|
106
|
-
customInputData,
|
|
107
|
-
dataProcessing,
|
|
108
|
-
verificationMail,
|
|
109
|
-
mailTransportConfig,
|
|
110
|
-
hashOptions
|
|
111
|
-
)
|
|
127
|
+
const db = drizzleWrapper.buildQueryBuilder(
|
|
128
|
+
await dbConnection.getPool()
|
|
129
|
+
)
|
|
112
130
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await transactionConnection.query('ROLLBACK')
|
|
118
|
-
} catch {
|
|
119
|
-
// Ignore rollback errors, original error is more relevant.
|
|
120
|
-
}
|
|
131
|
+
const users = drizzleWrapper.buildTable(tableName, columns, {
|
|
132
|
+
uuid: 'uuid',
|
|
133
|
+
isActive: 'boolean'
|
|
134
|
+
})
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
136
|
+
await db
|
|
137
|
+
.update(users)
|
|
138
|
+
.set({ [columns.isActive]: true })
|
|
139
|
+
.where(DrizzleWrapper.eq(users.uuid, uuid))
|
|
128
140
|
}
|
|
129
141
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
type DriverName = 'pg' | 'mysql' | 'sqlite'
|
|
2
2
|
|
|
3
|
+
import { pgWrapperAccessToken } from './pg-wrapper-access.ts'
|
|
4
|
+
|
|
5
|
+
export { pgWrapperAccessToken }
|
|
6
|
+
|
|
3
7
|
interface ConnectionData {
|
|
4
8
|
connectionName?: string
|
|
5
9
|
}
|
|
@@ -16,14 +20,15 @@ export class DbConnection {
|
|
|
16
20
|
return this.driverName
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
async
|
|
23
|
+
async getPool(): Promise<unknown> {
|
|
20
24
|
if (this.driverName === 'pg') {
|
|
21
25
|
if (!this.connectionWrapper) {
|
|
22
26
|
// @ts-ignore pg-wrapper is supplied by the pg extension in generated projects.
|
|
23
|
-
const
|
|
27
|
+
const createPgConnectionWrapper =
|
|
24
28
|
await import('../pg/pg-wrapper.ts')
|
|
25
|
-
this.connectionWrapper =
|
|
26
|
-
this.connectionData.connectionName ?? 'default'
|
|
29
|
+
this.connectionWrapper = createPgConnectionWrapper.default(
|
|
30
|
+
this.connectionData.connectionName ?? 'default',
|
|
31
|
+
pgWrapperAccessToken
|
|
27
32
|
)
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const pgWrapperAccessToken = Symbol('pg-wrapper-access')
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
@@ -8,8 +8,9 @@ import path from 'path'
|
|
|
8
8
|
import { Pool } from 'pg'
|
|
9
9
|
// @ts-ignore node types are supplied by the server package in generated projects.
|
|
10
10
|
import { fileURLToPath } from 'url'
|
|
11
|
+
import { pgWrapperAccessToken } from '../db-connection/pg-wrapper-access.ts'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
class PgConnectionWrapper {
|
|
13
14
|
private pool: Pool | null = null
|
|
14
15
|
private pgEnv: Record<string, string> = {}
|
|
15
16
|
private pgEnvLoaded: boolean = false
|
|
@@ -75,3 +76,15 @@ export class PgConnectionWrapper {
|
|
|
75
76
|
this.pool = null
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
export default function createPgConnectionWrapper(
|
|
81
|
+
connectionName: string = 'default',
|
|
82
|
+
accessToken?: symbol
|
|
83
|
+
): PgConnectionWrapper {
|
|
84
|
+
// only accessible by DbConnection
|
|
85
|
+
if (accessToken !== pgWrapperAccessToken) {
|
|
86
|
+
throw new Error('PgConnectionWrapper can only be created by DbConnection')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new PgConnectionWrapper(connectionName)
|
|
90
|
+
}
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
"start": "tsx routes.generated.ts"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@msgpack/msgpack": "
|
|
10
|
-
"cors": "
|
|
11
|
-
"dotenv": "
|
|
12
|
-
"express": "
|
|
13
|
-
"express-rate-limit": "
|
|
14
|
-
"helmet": "
|
|
9
|
+
"@msgpack/msgpack": "3.1.3",
|
|
10
|
+
"cors": "2.8.6",
|
|
11
|
+
"dotenv": "17.4.2",
|
|
12
|
+
"express": "5.2.1",
|
|
13
|
+
"express-rate-limit": "8.7.0",
|
|
14
|
+
"helmet": "8.3.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@types/cors": "
|
|
18
|
-
"@types/express": "
|
|
19
|
-
"@types/node": "
|
|
20
|
-
"tsx": "
|
|
21
|
-
"typescript": "
|
|
17
|
+
"@types/cors": "2.8.19",
|
|
18
|
+
"@types/express": "5.0.6",
|
|
19
|
+
"@types/node": "26.5.0",
|
|
20
|
+
"tsx": "4.23.13",
|
|
21
|
+
"typescript": "7.0.2"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -21,28 +21,46 @@ export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
|
|
|
21
21
|
json.dependencies = {}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const processedPackages = new Set()
|
|
25
|
+
|
|
26
|
+
function addPackage(packageName) {
|
|
27
|
+
if (processedPackages.has(packageName)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
processedPackages.add(packageName)
|
|
25
32
|
const wrapperDir = wrapperDirFromPackage(scriptDir, packageName)
|
|
26
33
|
const targetWrapperDir = path.join(serverDir, packageName)
|
|
27
34
|
const wrapperExists = fs.existsSync(wrapperDir)
|
|
28
35
|
|
|
29
36
|
if (!wrapperExists) {
|
|
30
37
|
json.dependencies[packageName] = 'latest'
|
|
31
|
-
|
|
38
|
+
return
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const extensionConfig = readJson(wrapperDir, EXTENSION_CONFIG_FILE)
|
|
42
|
+
for (const dependency of extensionConfig.dependencies ?? []) {
|
|
43
|
+
addPackage(dependency)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (extensionConfig.addToServerPackage) {
|
|
47
|
+
const serverPackageName = extensionConfig.packageName ?? packageName
|
|
48
|
+
json.dependencies[serverPackageName] = 'latest'
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
copyDirectory(wrapperDir, targetWrapperDir, {
|
|
39
52
|
excludedNames: [EXTENSION_CONFIG_FILE]
|
|
40
53
|
})
|
|
54
|
+
|
|
41
55
|
console.log(
|
|
42
56
|
`\n✅ Copied wrapper for ${packageName} to src/server/${packageName}`
|
|
43
57
|
)
|
|
44
58
|
}
|
|
45
59
|
|
|
60
|
+
for (const packageName of packageNames) {
|
|
61
|
+
addPackage(packageName)
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
writeJson(projectDir, 'server-package.json', json)
|
|
47
65
|
}
|
|
48
66
|
|
|
@@ -1,31 +0,0 @@
|
|
|
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) =>
|
|
22
|
-
new kysely.PostgresIntrospector(db),
|
|
23
|
-
createQueryCompiler: () =>
|
|
24
|
-
new kysely.PostgresQueryCompiler()
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
throw new Error(`Driver is not integrated yet: ${driverName}`)
|
|
30
|
-
}
|
|
31
|
-
}
|