@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 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 kysely
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 kysely
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 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.
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.5.12",
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 kysely is supplied by the server package in generated projects.
3
- import { KyselyWrapper } from '../../kysely/kysely-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
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 AuthenticationCoreKyselyCurrentUser {
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
- const db = KyselyWrapper.buildQueryCompiler(
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 compiledQuery = db
31
- .selectFrom(tableName)
32
- .select([
33
- KyselyWrapper.sql.ref(columns.isActive).as('isActive')
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.rows?.[0]) {
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,3 +1,8 @@
1
1
  {
2
- "addToServerPackage": false
2
+ "addToServerPackage": false,
3
+ "dependencies": [
4
+ "db-connection",
5
+ "drizzle",
6
+ "@tobi2409/authentication-core-lib"
7
+ ]
3
8
  }
@@ -1,6 +1,6 @@
1
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'
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 AuthenticationCoreKyselyLogin {
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
- const db = KyselyWrapper.buildQueryCompiler(
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 compiledQuery = db
46
- .selectFrom(tableName)
47
- .select([
48
- KyselyWrapper.sql.ref(columns.uuid).as('uuid'),
49
- KyselyWrapper.sql.ref(columns.mail).as('mail'),
50
- KyselyWrapper.sql.ref(columns.password).as('password'),
51
- KyselyWrapper.sql.ref(columns.isActive).as('isActive')
52
- ])
53
- .where(KyselyWrapper.sql.ref(columns.mail), '=', typedMail)
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.rows?.[0] as Record<string, unknown> | undefined
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 kysely is supplied by the server package in generated projects.
3
- import { KyselyWrapper } from '../../kysely/kysely-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
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 AuthenticationCoreKyselyRegister {
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
- const db = KyselyWrapper.buildQueryCompiler(
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
- // 'any' is used instead of 'unknown' because calling pool/client APIs on an unknown type requires
46
- // either a type assertion or a separate interface. DbConnection is driver-agnostic, so
47
- // the concrete pool/client type (e.g. pg.Pool/PoolClient) is not available here without adding a dependency.
48
- const pool = (await dbConnection.getConnection()) as any
49
- const transactionConnection = await pool.connect()
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
- const mailExistsRoutine = async (mail: string): Promise<boolean> => {
52
- const compiledQuery = db
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 result = await transactionConnection.query(
60
- compiledQuery.sql,
61
- compiledQuery.parameters
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
- return (result.rows?.length ?? 0) > 0
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
- const dataProcessing = async (
68
- identification: string,
69
- hashedPassword: string,
70
- inputData: Record<string, unknown>
71
- ): Promise<authenticationCoreLib.FetchedUser | undefined> => {
72
- const userData: Record<string, unknown> = {
73
- [columns.mail]: identification,
74
- [columns.password]: hashedPassword,
75
- [columns.isActive]: false,
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
- const compiledQuery = db
80
- .insertInto(tableName)
81
- .values(userData as any)
82
- .returning([
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
- const result = await transactionConnection.query(
91
- compiledQuery.sql,
92
- compiledQuery.parameters
103
+ return authenticationCoreLib.AuthenticationCoreRegister.register(
104
+ registrationInputData,
105
+ mailExistsRoutine,
106
+ customInputData,
107
+ dataProcessing,
108
+ verificationMail,
109
+ mailTransportConfig,
110
+ hashOptions
93
111
  )
94
- return mapFetchedUser(
95
- result.rows?.[0] as Record<string, unknown> | undefined
96
- )
97
- }
112
+ })
113
+ }
98
114
 
99
- try {
100
- await transactionConnection.query('BEGIN')
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
- const registeredUser =
103
- await authenticationCoreLib.AuthenticationCoreRegister.register(
104
- registrationInputData,
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
- await transactionConnection.query('COMMIT')
114
- return registeredUser
115
- } catch (error) {
116
- try {
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
- throw error
123
- } finally {
124
- if (typeof transactionConnection.release === 'function') {
125
- transactionConnection.release()
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 getConnection(): Promise<unknown> {
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 { PgConnectionWrapper } =
27
+ const createPgConnectionWrapper =
24
28
  await import('../pg/pg-wrapper.ts')
25
- this.connectionWrapper = new PgConnectionWrapper(
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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "addToServerPackage": true,
3
+ "packageName": "drizzle-orm",
4
+ "dependencies": ["db-connection"]
5
+ }
@@ -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
- export class PgConnectionWrapper {
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
+ }
@@ -1,3 +1,4 @@
1
1
  {
2
- "addToServerPackage": true
2
+ "addToServerPackage": true,
3
+ "dependencies": ["db-connection"]
3
4
  }
@@ -42,5 +42,6 @@ try {
42
42
  console.error(
43
43
  `\n❌ ${error instanceof Error ? error.message : String(error)}`
44
44
  )
45
+
45
46
  process.exit(1)
46
47
  }
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@tobi2409/mvvm-monster": "^0.9.13",
7
- "@tobi2409/layout": "^0.5.0"
6
+ "@tobi2409/mvvm-monster": "0.9.13",
7
+ "@tobi2409/layout": "0.5.0"
8
8
  }
9
9
  }
@@ -12,6 +12,6 @@
12
12
  "test": "echo \"Error: no test specified\" && exit 1"
13
13
  },
14
14
  "devDependencies": {
15
- "@tobi2409/proc2rest": "^0.5.0"
15
+ "@tobi2409/proc2rest": "0.8.2"
16
16
  }
17
17
  }
@@ -6,18 +6,18 @@
6
6
  "start": "tsx routes.generated.ts"
7
7
  },
8
8
  "dependencies": {
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.3.1",
14
- "helmet": "^8.3.0"
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": "^2.8.17",
18
- "@types/express": "^5.0.5",
19
- "@types/node": "^24.10.1",
20
- "tsx": "^4.20.6",
21
- "typescript": "^5.9.3"
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
  }
@@ -8,7 +8,7 @@
8
8
  <script type="importmap">
9
9
  {
10
10
  "imports": {
11
- "template-engine/": "./node_modules/@tobi2409/template-engine/src/"
11
+ "mvvm-monster/": "./node_modules/@tobi2409/mvvm-monster/src/"
12
12
  }
13
13
  }
14
14
  </script>
@@ -1,6 +1,6 @@
1
1
  // @server-import server.ts
2
2
 
3
- import TemplateEngine from 'template-engine/template-engine.js'
3
+ import TemplateEngine from 'mvvm-monster/template-engine.js'
4
4
 
5
5
  const data = TemplateEngine.reactive(
6
6
  {
@@ -21,28 +21,46 @@ export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
21
21
  json.dependencies = {}
22
22
  }
23
23
 
24
- for (const packageName of packageNames) {
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
- continue
38
+ return
32
39
  }
33
40
 
34
- if (shouldAddToServerPackage(wrapperDir)) {
35
- json.dependencies[packageName] = 'latest'
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,3 +0,0 @@
1
- {
2
- "addToServerPackage": true
3
- }
@@ -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
- }