feature: add db:create command (#8760)
This commit is contained in:
@@ -125,6 +125,28 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
desc: `Create a new Medusa project.`,
|
||||
handler: handlerP(newStarter),
|
||||
})
|
||||
.command({
|
||||
command: "db:create",
|
||||
desc: "Create the database used by your application",
|
||||
builder: (builder) => {
|
||||
builder.option("db", {
|
||||
type: "string",
|
||||
describe: "Specify the name of the database you want to create",
|
||||
})
|
||||
builder.option("interactive", {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
describe:
|
||||
"Display prompts. Use --no-interactive flag to run the command without prompts",
|
||||
})
|
||||
},
|
||||
handler: handlerP(
|
||||
getCommandHandler("db/create", (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
return cmd(args)
|
||||
})
|
||||
),
|
||||
})
|
||||
.command({
|
||||
command: `telemetry`,
|
||||
describe: `Enable or disable collection of anonymous usage data.`,
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.12.0",
|
||||
"pg-connection-string": "^2.6.4",
|
||||
"pluralize": "^8.0.0",
|
||||
"ulid": "^2.3.0"
|
||||
},
|
||||
|
||||
@@ -51,6 +51,26 @@ export class EnvEditor {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for an existing key from the
|
||||
* ".env" file
|
||||
*/
|
||||
get(key: string): string | null {
|
||||
const envFile = this.#files.find((file) => file.filePath.endsWith(".env"))
|
||||
if (!envFile) {
|
||||
return null
|
||||
}
|
||||
const matchingLine = envFile.contents.find((line) =>
|
||||
line.startsWith(`${key}=`)
|
||||
)
|
||||
if (!matchingLine) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [_, ...rest] = matchingLine.split("=")
|
||||
return rest.join("=")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set key-value pair to the dot-env files.
|
||||
*
|
||||
|
||||
@@ -26,5 +26,6 @@ export * from "./shipping"
|
||||
export * from "./totals"
|
||||
export * from "./totals/big-number"
|
||||
export * from "./user"
|
||||
export * from "./pg"
|
||||
|
||||
export const MedusaModuleType = Symbol.for("MedusaModule")
|
||||
|
||||
52
packages/core/utils/src/pg/index.ts
Normal file
52
packages/core/utils/src/pg/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PostgreSQL utilities
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| @note
|
||||
| These utlities does not rely on an MedusaJS application and neither
|
||||
| uses the Mikro ORM.
|
||||
| The goal here is to run DB operations without booting the application.
|
||||
|
|
||||
| For example:
|
||||
| Creating a database from CLI, or checking if a database exists
|
||||
|
|
||||
*/
|
||||
|
||||
import { Client, type ClientConfig } from "pg"
|
||||
import { parse } from "pg-connection-string"
|
||||
|
||||
/**
|
||||
* Parsers the database connection string into an object
|
||||
* of postgreSQL options
|
||||
*/
|
||||
export function parseConnectionString(connectionString: string) {
|
||||
return parse(connectionString)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PostgreSQL database client using the connection
|
||||
* string or database options
|
||||
*/
|
||||
export function createClient(options: string | ClientConfig) {
|
||||
return new Client(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database using the client. Make sure to call
|
||||
* `client.connect` before using this utility.
|
||||
*/
|
||||
export async function createDb(client: Client, databaseName: string) {
|
||||
await client.query(`CREATE DATABASE "${databaseName}"`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a database exists using the Client. Make sure to call
|
||||
* `client.connect` before using this utility.
|
||||
*/
|
||||
export async function dbExists(client: Client, databaseName: string) {
|
||||
const result = await client.query(
|
||||
`SELECT datname FROM pg_catalog.pg_database WHERE datname='${databaseName}';`
|
||||
)
|
||||
return !!result.rowCount
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { dropDatabase } from "pg-god"
|
||||
import {
|
||||
createClient,
|
||||
parseConnectionString,
|
||||
dbExists,
|
||||
createDb,
|
||||
} from "../../index"
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||
|
||||
export const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
describe("Pg | createClient", () => {
|
||||
test("create client connection from connection options", async () => {
|
||||
const client = createClient(pgGodCredentials)
|
||||
await client.connect()
|
||||
await client.end()
|
||||
})
|
||||
|
||||
test("create client connection from connectionString without db name", async () => {
|
||||
const client = createClient(
|
||||
`postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}`
|
||||
)
|
||||
await client.connect()
|
||||
await client.end()
|
||||
})
|
||||
|
||||
test("create client connection from connectionString with db name", async () => {
|
||||
const client = createClient(
|
||||
`postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/foo`
|
||||
)
|
||||
await expect(() => client.connect()).rejects.toMatchInlineSnapshot(
|
||||
`[error: database "foo" does not exist]`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Pg | parseConnectionString", () => {
|
||||
test("parse connection string without db name", async () => {
|
||||
const options = parseConnectionString(
|
||||
`postgres://${DB_USERNAME}:@${DB_HOST}`
|
||||
)
|
||||
expect(options).toEqual({
|
||||
user: DB_USERNAME,
|
||||
password: "",
|
||||
host: DB_HOST,
|
||||
port: "",
|
||||
database: null,
|
||||
})
|
||||
})
|
||||
|
||||
test("parse connection string with db name", async () => {
|
||||
const options = parseConnectionString(
|
||||
`postgres://${DB_USERNAME}:@${DB_HOST}/foo`
|
||||
)
|
||||
expect(options).toEqual({
|
||||
user: DB_USERNAME,
|
||||
password: "",
|
||||
host: DB_HOST,
|
||||
port: "",
|
||||
database: "foo",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Pg | dbExists", () => {
|
||||
beforeEach(async () => {
|
||||
await dropDatabase({ databaseName: "foo" }, pgGodCredentials)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await dropDatabase({ databaseName: "foo" }, pgGodCredentials)
|
||||
})
|
||||
|
||||
test("return false when db does not exist", async () => {
|
||||
const options = parseConnectionString(
|
||||
`postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/foo`
|
||||
)
|
||||
const client = createClient({
|
||||
host: options.host!,
|
||||
port: options.port ? Number(options.port) : undefined,
|
||||
user: options.user,
|
||||
password: options.password,
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const exists = await dbExists(client, options.database!)
|
||||
await client.end()
|
||||
|
||||
expect(exists).toBe(false)
|
||||
})
|
||||
|
||||
test("return true when db exist", async () => {
|
||||
const options = parseConnectionString(
|
||||
`postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/foo`
|
||||
)
|
||||
|
||||
const client = createClient({
|
||||
host: options.host!,
|
||||
port: options.port ? Number(options.port) : undefined,
|
||||
user: options.user,
|
||||
password: options.password,
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
await createDb(client, options.database!)
|
||||
const exists = await dbExists(client, options.database!)
|
||||
await client.end()
|
||||
|
||||
expect(exists).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -46,6 +46,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^2.3.11",
|
||||
"@inquirer/input": "^2.2.9",
|
||||
"@medusajs/admin-sdk": "0.0.1",
|
||||
"@medusajs/core-flows": "^0.0.9",
|
||||
"@medusajs/framework": "^0.0.1",
|
||||
@@ -75,6 +76,7 @@
|
||||
"node-schedule": "^2.1.1",
|
||||
"qs": "^6.11.2",
|
||||
"request-ip": "^3.3.0",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
|
||||
111
packages/medusa/src/commands/db/create.ts
Normal file
111
packages/medusa/src/commands/db/create.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import slugify from "slugify"
|
||||
import { basename } from "path"
|
||||
import input from "@inquirer/input"
|
||||
import { logger } from "@medusajs/framework"
|
||||
import {
|
||||
createDb,
|
||||
dbExists,
|
||||
EnvEditor,
|
||||
createClient,
|
||||
parseConnectionString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
const main = async function ({ directory, interactive, db }) {
|
||||
let dbName = db
|
||||
|
||||
/**
|
||||
* Loading the ".env" file in editor mode so that
|
||||
* we can read values from it and update its
|
||||
* contents.
|
||||
*/
|
||||
const envEditor = new EnvEditor(directory)
|
||||
await envEditor.load()
|
||||
|
||||
/**
|
||||
* Ensure the "DATABASE_URL" is defined before we attempt to
|
||||
* create the database.
|
||||
*
|
||||
* Also we will discard the database name from the connection
|
||||
* string because the mentioned database might not exist
|
||||
*/
|
||||
const dbConnectionString = envEditor.get("DATABASE_URL")
|
||||
if (!dbConnectionString) {
|
||||
logger.error(
|
||||
`Missing "DATABASE_URL" inside the .env file. The value is required to connect to the PostgreSQL server`
|
||||
)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Use default value + prompt only when the dbName is not
|
||||
* provided via a flag
|
||||
*/
|
||||
if (!dbName) {
|
||||
const defaultValue =
|
||||
envEditor.get("DB_NAME") ?? `medusa-${slugify(basename(directory))}`
|
||||
if (interactive) {
|
||||
try {
|
||||
dbName = await input({
|
||||
message: "Enter the database name",
|
||||
default: defaultValue,
|
||||
required: true,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.name === "ExitPromptError") {
|
||||
process.exit()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
dbName = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse connection string specified as "DATABASE_URL" inside the
|
||||
* .env file and create a client instance from it.
|
||||
*
|
||||
* Remember we should not specify the database name from the
|
||||
* connection string, because this database might not
|
||||
* exist
|
||||
*/
|
||||
const connectionOptions = parseConnectionString(dbConnectionString)
|
||||
const client = createClient({
|
||||
host: connectionOptions.host!,
|
||||
port: connectionOptions.port ? Number(connectionOptions.port) : undefined,
|
||||
user: connectionOptions.user,
|
||||
password: connectionOptions.password,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
logger.info(`Connection established with the database "${dbName}"`)
|
||||
} catch (error) {
|
||||
process.exitCode = 1
|
||||
logger.error(
|
||||
"Unable to establish database connection because of the following error"
|
||||
)
|
||||
logger.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
if (await dbExists(client, dbName)) {
|
||||
logger.info(`Database "${dbName}" already exists`)
|
||||
|
||||
envEditor.set("DB_NAME", dbName)
|
||||
await envEditor.save()
|
||||
logger.info(`Updated .env file with "DB_NAME=${dbName}"`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await createDb(client, dbName)
|
||||
logger.info(`Created database "${dbName}"`)
|
||||
|
||||
envEditor.set("DB_NAME", dbName)
|
||||
await envEditor.save()
|
||||
logger.info(`Updated .env file with "DB_NAME=${dbName}"`)
|
||||
}
|
||||
|
||||
export default main
|
||||
69
yarn.lock
69
yarn.lock
@@ -3669,6 +3669,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/core@npm:^9.0.10":
|
||||
version: 9.0.10
|
||||
resolution: "@inquirer/core@npm:9.0.10"
|
||||
dependencies:
|
||||
"@inquirer/figures": ^1.0.5
|
||||
"@inquirer/type": ^1.5.2
|
||||
"@types/mute-stream": ^0.0.4
|
||||
"@types/node": ^22.1.0
|
||||
"@types/wrap-ansi": ^3.0.0
|
||||
ansi-escapes: ^4.3.2
|
||||
cli-spinners: ^2.9.2
|
||||
cli-width: ^4.1.0
|
||||
mute-stream: ^1.0.0
|
||||
signal-exit: ^4.1.0
|
||||
strip-ansi: ^6.0.1
|
||||
wrap-ansi: ^6.2.0
|
||||
yoctocolors-cjs: ^2.1.2
|
||||
checksum: 117f50a55b5ebee8bfc62ea6adec87035f28ee7ace1efea67895c3d32ab50bf569ecd3ca33c457d0c7ae4240b9fe4d7b698ab70946ac561ab579d0920ddc98bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/core@npm:^9.0.3":
|
||||
version: 9.0.3
|
||||
resolution: "@inquirer/core@npm:9.0.3"
|
||||
@@ -3704,6 +3725,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/figures@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "@inquirer/figures@npm:1.0.5"
|
||||
checksum: ec9ba23db42cb33fa18eb919abf2a18e750e739e64c1883ce4a98345cd5711c60cac12d1faf56a859f52d387deb221c8d3dfe60344ee07955a9a262f8b821fe3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/input@npm:^2.2.9":
|
||||
version: 2.2.9
|
||||
resolution: "@inquirer/input@npm:2.2.9"
|
||||
dependencies:
|
||||
"@inquirer/core": ^9.0.10
|
||||
"@inquirer/type": ^1.5.2
|
||||
checksum: 0fcdc5d9c17712f9a2c79f39d1e03ed4a58cfe1dd1095209b4c83621dba2cb94db03b7df0df34e22584bce9e53403a284c76721def66a452e4751666d945d99f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/type@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@inquirer/type@npm:1.3.1"
|
||||
@@ -3720,6 +3758,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/type@npm:^1.5.2":
|
||||
version: 1.5.2
|
||||
resolution: "@inquirer/type@npm:1.5.2"
|
||||
dependencies:
|
||||
mute-stream: ^1.0.0
|
||||
checksum: e2c91562c07440620bd805a60438b78c188d2727d86f396a68c480e4357469a72cd80bd2c158faa6b987671911566bd4fc12976f4bdda1a3441594e318c40058
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@internationalized/date@npm:^3.5.4":
|
||||
version: 3.5.4
|
||||
resolution: "@internationalized/date@npm:3.5.4"
|
||||
@@ -4860,6 +4907,7 @@ __metadata:
|
||||
resolution: "@medusajs/medusa@workspace:packages/medusa"
|
||||
dependencies:
|
||||
"@inquirer/checkbox": ^2.3.11
|
||||
"@inquirer/input": ^2.2.9
|
||||
"@medusajs/admin-sdk": 0.0.1
|
||||
"@medusajs/core-flows": ^0.0.9
|
||||
"@medusajs/framework": ^0.0.1
|
||||
@@ -4899,6 +4947,7 @@ __metadata:
|
||||
qs: ^6.11.2
|
||||
request-ip: ^3.3.0
|
||||
rimraf: ^5.0.1
|
||||
slugify: ^1.6.6
|
||||
supertest: ^4.0.2
|
||||
typescript: ^5.3.3
|
||||
uuid: ^9.0.0
|
||||
@@ -5423,6 +5472,8 @@ __metadata:
|
||||
express: ^4.18.2
|
||||
jest: ^29.7.0
|
||||
jsonwebtoken: ^9.0.2
|
||||
pg: ^8.12.0
|
||||
pg-connection-string: ^2.6.4
|
||||
pluralize: ^8.0.0
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^29.1.1
|
||||
@@ -12160,6 +12211,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.1.0":
|
||||
version: 22.5.0
|
||||
resolution: "@types/node@npm:22.5.0"
|
||||
dependencies:
|
||||
undici-types: ~6.19.2
|
||||
checksum: 45aa75c5e71645fac42dced4eff7f197c3fdfff6e8a9fdacd0eb2e748ff21ee70ffb73982f068a58e8d73b2c088a63613142c125236cdcf3c072ea97eada1559
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/normalize-package-data@npm:^2.4.0":
|
||||
version: 2.4.4
|
||||
resolution: "@types/normalize-package-data@npm:2.4.4"
|
||||
@@ -24557,7 +24617,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-connection-string@npm:^2.6.2":
|
||||
"pg-connection-string@npm:^2.6.2, pg-connection-string@npm:^2.6.4":
|
||||
version: 2.6.4
|
||||
resolution: "pg-connection-string@npm:2.6.4"
|
||||
checksum: 0d0b617df0fc6507bf6a94bdcd56c7a305788a1402d69bff9773350947c8f525d6d8136128065370749a3325e99658ae40fbdcce620fb8e60126181f0591a6a6
|
||||
@@ -30181,6 +30241,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~6.19.2":
|
||||
version: 6.19.8
|
||||
resolution: "undici-types@npm:6.19.8"
|
||||
checksum: 078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"
|
||||
|
||||
Reference in New Issue
Block a user