feature: add db:create command (#8760)

This commit is contained in:
Harminder Virk
2024-08-26 15:04:52 +05:30
committed by GitHub
parent cd4063de74
commit 7c2cfc132a
9 changed files with 396 additions and 1 deletions
+22
View File
@@ -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.`,
+2
View File
@@ -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.
*
+1
View File
@@ -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
View 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)
})
})
+2
View File
@@ -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
View 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