Skip to content

Commit 256949e

Browse files
authored
feat(db-postgres, db-vercel-postgres): createDatabase, auto database create if it does not exist (#8655)
Adds `createDatabase` method to Postgres adapters which can be used either independently like this: ```ts payload.db.createDatabase({ name: "some-database", schemaName: "custom-schema" }) ``` Or ```ts payload.db.createDatabase() ``` Which creates a database from the current configuration, this is used in `connect` if `autoDatabaseCreate` is set to `true` (default). You can disable this behaviour with: ```ts postgresAdapter({ autoDatabaseCreate: false }) ``` Example: <img width="470" alt="image" src="https://github.com/user-attachments/assets/8d08c79d-9672-454c-af0f-eb802f9dcd99">
1 parent 8daac4e commit 256949e

File tree

10 files changed

+173
-3
lines changed

10 files changed

+173
-3
lines changed

docs/database/postgres.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export default buildConfig({
6060
| `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
6161
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
6262
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
63+
| `disableCreateDatabase` | Pass `true` to disale auto database creation if it doesn't exist. Defaults to `false`. |
6364
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
6465
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
6566
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |

packages/db-postgres/src/connect.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,21 @@ export const connect: Connect = async function connect(
7676
}
7777
}
7878
} catch (err) {
79-
this.payload.logger.error({ err, msg: `Error: cannot connect to Postgres: ${err.message}` })
79+
if (err.message?.match(/database .* does not exist/i) && !this.disableCreateDatabase) {
80+
// capitalize first char of the err msg
81+
this.payload.logger.info(
82+
`${err.message.charAt(0).toUpperCase() + err.message.slice(1)}, creating...`,
83+
)
84+
const isCreated = await this.createDatabase()
85+
86+
if (isCreated) {
87+
await this.connect(options)
88+
return
89+
}
90+
} else {
91+
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
92+
}
93+
8094
if (typeof this.rejectInitializing === 'function') {
8195
this.rejectInitializing()
8296
}

packages/db-postgres/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import {
3636
convertPathToJSONTraversal,
3737
countDistinct,
38+
createDatabase,
3839
createJSONQuery,
3940
createMigration,
4041
defaultDrizzleSnapshot,
@@ -78,7 +79,9 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
7879
name: 'postgres',
7980
afterSchemaInit: args.afterSchemaInit ?? [],
8081
beforeSchemaInit: args.beforeSchemaInit ?? [],
82+
createDatabase,
8183
defaultDrizzleSnapshot,
84+
disableCreateDatabase: args.disableCreateDatabase ?? false,
8285
drizzle: undefined,
8386
enums: {},
8487
features: {

packages/db-postgres/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export type Args = {
2424
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
2525
*/
2626
beforeSchemaInit?: PostgresSchemaHook[]
27+
/**
28+
* Pass `true` to disale auto database creation if it doesn't exist.
29+
* @default false
30+
*/
31+
disableCreateDatabase?: boolean
2732
idType?: 'serial' | 'uuid'
2833
localesSuffix?: string
2934
logger?: DrizzleConfig['logger']

packages/db-vercel-postgres/src/connect.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,21 @@ export const connect: Connect = async function connect(
3939
}
4040
}
4141
} catch (err) {
42-
this.payload.logger.error({ err, msg: `Error: cannot connect to Postgres: ${err.message}` })
42+
if (err.message?.match(/database .* does not exist/i) && !this.disableCreateDatabase) {
43+
// capitalize first char of the err msg
44+
this.payload.logger.info(
45+
`${err.message.charAt(0).toUpperCase() + err.message.slice(1)}, creating...`,
46+
)
47+
const isCreated = await this.createDatabase()
48+
49+
if (isCreated) {
50+
await this.connect(options)
51+
return
52+
}
53+
} else {
54+
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
55+
}
56+
4357
if (typeof this.rejectInitializing === 'function') {
4458
this.rejectInitializing()
4559
}

packages/db-vercel-postgres/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import {
3636
convertPathToJSONTraversal,
3737
countDistinct,
38+
createDatabase,
3839
createJSONQuery,
3940
createMigration,
4041
defaultDrizzleSnapshot,
@@ -78,7 +79,9 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
7879
name: 'postgres',
7980
afterSchemaInit: args.afterSchemaInit ?? [],
8081
beforeSchemaInit: args.beforeSchemaInit ?? [],
82+
createDatabase,
8183
defaultDrizzleSnapshot,
84+
disableCreateDatabase: args.disableCreateDatabase ?? false,
8285
drizzle: undefined,
8386
enums: {},
8487
features: {

packages/db-vercel-postgres/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export type Args = {
2525
*/
2626
beforeSchemaInit?: PostgresSchemaHook[]
2727
connectionString?: string
28+
/**
29+
* Pass `true` to disale auto database creation if it doesn't exist.
30+
* @default false
31+
*/
32+
disableCreateDatabase?: boolean
2833
idType?: 'serial' | 'uuid'
2934
localesSuffix?: string
3035
logger?: DrizzleConfig['logger']

packages/drizzle/src/exports/postgres.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { countDistinct } from '../postgres/countDistinct.js'
2+
export { createDatabase } from '../postgres/createDatabase.js'
23
export { convertPathToJSONTraversal } from '../postgres/createJSONQuery/convertPathToJSONTraversal.js'
34
export { createJSONQuery } from '../postgres/createJSONQuery/index.js'
45
export { createMigration } from '../postgres/createMigration.js'
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { ClientConfig } from 'pg'
2+
3+
import type { BasePostgresAdapter } from './types.js'
4+
5+
const setConnectionStringDatabase = ({
6+
connectionString,
7+
database,
8+
}: {
9+
connectionString: string
10+
database: string
11+
}): string => {
12+
const connectionURL = new URL(connectionString)
13+
const newConnectionURL = new URL(connectionURL)
14+
newConnectionURL.pathname = `/${database}`
15+
16+
return newConnectionURL.toString()
17+
}
18+
19+
type Args = {
20+
/**
21+
* Name of a database, defaults to the current one
22+
*/
23+
name?: string
24+
/**
25+
* Schema to create in addition to 'public'. Defaults to adapter.schemaName if exists.
26+
*/
27+
schemaName?: string
28+
}
29+
export const createDatabase = async function (this: BasePostgresAdapter, args: Args = {}) {
30+
// DATABASE_URL - default Vercel env
31+
const connectionString = this.poolOptions?.connectionString ?? process.env.DATABASE_URL
32+
let managementClientConfig: ClientConfig = {}
33+
let dbName = args.name
34+
const schemaName = this.schemaName || 'public'
35+
36+
if (connectionString) {
37+
if (!dbName) {
38+
dbName = new URL(connectionString).pathname.slice(1)
39+
}
40+
41+
managementClientConfig.connectionString = setConnectionStringDatabase({
42+
connectionString,
43+
database: 'postgres',
44+
})
45+
} else {
46+
if (!dbName) {
47+
dbName = this.poolOptions.database
48+
}
49+
50+
managementClientConfig = {
51+
...this.poolOptions,
52+
database: 'postgres',
53+
}
54+
}
55+
56+
// import pg only when createDatabase is used
57+
const pg = await import('pg')
58+
59+
const managementClient = new pg.Client(managementClientConfig)
60+
61+
try {
62+
await managementClient.connect()
63+
await managementClient.query(`CREATE DATABASE ${dbName}`)
64+
65+
this.payload.logger.info(`Created database "${dbName}"`)
66+
67+
if (schemaName !== 'public') {
68+
let createdDatabaseConfig: ClientConfig = {}
69+
70+
if (connectionString) {
71+
createdDatabaseConfig.connectionString = setConnectionStringDatabase({
72+
connectionString,
73+
database: dbName,
74+
})
75+
} else {
76+
createdDatabaseConfig = {
77+
...this.poolOptions,
78+
database: dbName,
79+
}
80+
}
81+
82+
const createdDatabaseClient = new pg.Client(createdDatabaseConfig)
83+
84+
try {
85+
await createdDatabaseClient.connect()
86+
87+
await createdDatabaseClient.query(`CREATE SCHEMA ${schemaName}`)
88+
this.payload.logger.info(`Created schema "${dbName}.${schemaName}"`)
89+
} catch (err) {
90+
this.payload.logger.error({
91+
err,
92+
msg: `Error: failed to create schema "${dbName}.${schemaName}". Details: ${err.message}`,
93+
})
94+
} finally {
95+
await createdDatabaseClient.end()
96+
}
97+
}
98+
99+
return true
100+
} catch (err) {
101+
this.payload.logger.error(
102+
`Error: failed to create database ${dbName}. Details: ${err.message}`,
103+
err,
104+
)
105+
106+
return false
107+
} finally {
108+
await managementClient.end()
109+
}
110+
}

packages/drizzle/src/postgres/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
} from 'drizzle-orm/pg-core'
2222
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
2323
import type { Payload, PayloadRequest } from 'payload'
24-
import type { QueryResult } from 'pg'
24+
import type { ClientConfig, QueryResult } from 'pg'
2525

2626
import type { extendDrizzleTable, Operators } from '../index.js'
2727
import type { BuildQueryJoinAliases, DrizzleAdapter, TransactionPg } from '../types.js'
@@ -92,6 +92,17 @@ export type Insert = (args: {
9292
values: Record<string, unknown> | Record<string, unknown>[]
9393
}) => Promise<Record<string, unknown>[]>
9494

95+
export type CreateDatabase = (args?: {
96+
/**
97+
* Name of a database, defaults to the current one
98+
*/
99+
name?: string
100+
/**
101+
* Schema to create in addition to 'public'. Defaults from adapter.schemaName if exists.
102+
*/
103+
schemaName?: string
104+
}) => Promise<boolean>
105+
95106
type Schema =
96107
| {
97108
enum: typeof pgEnum
@@ -119,8 +130,10 @@ export type BasePostgresAdapter = {
119130
afterSchemaInit: PostgresSchemaHook[]
120131
beforeSchemaInit: PostgresSchemaHook[]
121132
countDistinct: CountDistinct
133+
createDatabase: CreateDatabase
122134
defaultDrizzleSnapshot: DrizzleSnapshotJSON
123135
deleteWhere: DeleteWhere
136+
disableCreateDatabase: boolean
124137
drizzle: PostgresDB
125138
dropDatabase: DropDatabase
126139
enums: Record<string, GenericEnum>
@@ -137,6 +150,7 @@ export type BasePostgresAdapter = {
137150
logger: DrizzleConfig['logger']
138151
operators: Operators
139152
pgSchema?: Schema
153+
poolOptions?: ClientConfig
140154
prodMigrations?: {
141155
down: (args: MigrateDownArgs) => Promise<void>
142156
name: string

0 commit comments

Comments
 (0)