Skip to content

Commit

Permalink
[db] Fix data backup and restore scripts (kriasoft#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya authored Feb 26, 2021
1 parent 71d2fb1 commit 9d7f8da
Show file tree
Hide file tree
Showing 17 changed files with 144 additions and 76 deletions.
42 changes: 24 additions & 18 deletions db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ assistance.

## Requirements

- [Node.js](https://nodejs.org/) v12 or higher, [Yarn](https://yarnpkg.com/) package manager
- [Node.js](https://nodejs.org/) v14, [Yarn](https://yarnpkg.com/) package manager
- Local or remote instance of [PostgreSQL](https://www.postgresql.org/) (see [Postgres.app](https://postgresapp.com/), [Google Cloud SQL](https://cloud.google.com/sql))
- Optionally, [`psql`](https://www.postgresql.org/docs/current/app-psql.html), [`pg_dump`](https://www.postgresql.org/docs/current/app-pgdump.html), [`pg_restore`](https://www.postgresql.org/docs/current/app-pgrestore.html) client utilities (`brew install libpq` [](https://stackoverflow.com/a/49689589/82686))

## How to open the database

You can access the database either by using a terminal window:

```bash
$ yarn db:repl # Launches Knex.js REPL shell
$ yarn db:psql # Launches PostgreSQL REPL shell
```
$ yarn db:repl [--env #0] # Launches Knex.js REPL shell
$ yarn db:psql [--env #0] # Launches PostgreSQL REPL shell
```

Or, by using a GUI such as [Postico](https://eggerapps.at/postico/). Find
Expand All @@ -61,37 +61,43 @@ and hit `TAB` which should insert a VS Code snippet.

## How to migrate database schema and data

```bash
$ yarn db:version # Prints the current schema version to the console
$ yarn db:migrate # Migrates database schema to the latest version
$ yarn db:seed # Seeds database with some reference data
```
$ yarn db:version [--env #0] # Prints the current schema version to the console
$ yarn db:migrate [--env #0] # Migrates database schema to the latest version
$ yarn db:seed [--env #0] # Seeds database with some reference data
```

While the app is in development, you can use a simplified migration workflow by
creating a backup of your existing database, making changes to the existing
migration file (`migrations/001_initial.ts`), re-apply the migration and restore
all the data (as opposed to re-seeding).
all the data (as opposed to re-seeding). For example:

```
$ yarn db:backup --env=prod
$ yarn db:reset --env=local --no-seed
$ yarn db:restore --env=local --from=prod
```

## How to re-apply the latest migration

```bash
$ yarn db:rollback # Rolls back the latest migration
$ yarn db:migrate # Migrates database schema to the latest version
$ yarn db:seed # Seeds database with some reference data
```
$ yarn db:rollback [--env #0] # Rolls back the latest migration
$ yarn db:migrate [--env #0] # Migrates database schema to the latest version
$ yarn db:seed [--env #0] # Seeds database with some reference data
```

Alternatively, you can drop and re-create the database, then apply all the
outstanding migrations and seed files over again by running:

```bash
$ yarn db:reset # Re-creates the database; applies migrations and seeds
```
$ yarn db:reset [--env #0] [--no-seed]
```

## How to backup and restore data

```bash
$ yarn db:backup # Exports database data to a backup file
$ yarn db:restore # Restores data from a backup file
```
$ yarn db:backup [--env #0]
$ yarn db:restore [--env #0] [--from #0]
```

You can find backup files inside of the [`/backups`](./backups) folder.
Expand Down
16 changes: 7 additions & 9 deletions db/scripts/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ const path = require("path");
const readline = require("readline");
const spawn = require("cross-spawn");
const cp = require("child_process");
const minimist = require("minimist");
const { EOL } = require("os");

// Load environment variables (PGHOST, PGUSER, etc.)
require("env");

const args = minimist(process.argv.slice(2), { default: { env: "dev" } });

// Get the list of database tables
let cmd = spawn.sync(
"psql",
Expand All @@ -26,9 +29,7 @@ let cmd = spawn.sync(
"--command",
"SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'",
],
{
stdio: ["inherit", "pipe", "inherit"],
},
{ stdio: ["inherit", "pipe", "inherit"] },
);

if (cmd.status !== 0) {
Expand Down Expand Up @@ -59,19 +60,16 @@ cmd = cp
"--exclude-table=migrations_lock",
"--exclude-table=migrations_id_seq",
"--exclude-table=migrations_lock_index_seq",
...process.argv.slice(2).filter((x) => !x.startsWith("--env")),
...args._,
],
{
stdio: ["pipe", "pipe", "inherit"],
},
{ stdio: ["pipe", "pipe", "inherit"] },
)
.on("exit", (code) => {
if (code !== 0) process.exit(code);
});

const env = process.env.ENV;
const timestamp = new Date().toISOString().replace(/(-|:|\.\d{3})/g, "");
const file = path.resolve(__dirname, `../backups/${timestamp}_${env}.sql`);
const file = path.resolve(__dirname, `../backups/${timestamp}_${args.env}.sql`);
const out = fs.createWriteStream(file, { encoding: "utf8" });
const rl = readline.createInterface({ input: cmd.stdout, terminal: false });

Expand Down
14 changes: 12 additions & 2 deletions db/scripts/reset.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
/**
* Resets database to its initial state (for local development). Usage:
*
* yarn db:reset [--env #0]
* yarn db:reset [--env #0] [--no-seed]
*
* @copyright 2016-present Kriasoft (https://git.io/Jt7GM)
*/

const knex = require("knex");
const spawn = require("cross-spawn");
const minimist = require("minimist");

const config = require("../knexfile");
const updateTypes = require("./update-types");

const args = minimist(process.argv.slice(2), {
boolean: ["seed"],
default: { env: "dev", seed: true },
});

async function reset() {
const db = knex({
...config,
Expand All @@ -33,7 +39,11 @@ async function reset() {

// Migrate database to the latest version
spawn.sync("yarn", ["knex", "migrate:latest"], { stdio: "inherit" });
spawn.sync("yarn", ["knex", "seed:run"], { stdio: "inherit" });

if (args.seed) {
spawn.sync("yarn", ["knex", "seed:run"], { stdio: "inherit" });
}

await updateTypes();
}

Expand Down
23 changes: 13 additions & 10 deletions db/scripts/restore.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
/**
* Restores database from a backup. Usage:
* Restores database from a backup file. Usage:
*
* yarn db:restore [--env #0]
* yarn db:restore [--env #0] [--from #0]
*
* @copyright 2016-present Kriasoft (https://git.io/Jt7GM)
*/

const fs = require("fs");
const path = require("path");
const spawn = require("cross-spawn");
const minimist = require("minimist");

// Load environment variables (PGHOST, PGUSER, etc.)
require("env");

const args = minimist(process.argv.slice(2));
const toEnv = args.env || "dev";
const fromEnv = args.from || toEnv;

// Find the latest backup file for the selected environment
const { ENV, PGDATABASE } = process.env;
const { PGDATABASE } = process.env;
const file = fs
.readdirSync(path.resolve(__dirname, "../backups"))
.sort()
.reverse()
.find((x) => x.endsWith(`_${ENV}.sql`));
.find((x) => x.endsWith(`_${fromEnv}.sql`));

if (!file) {
console.log(`Cannot find the SQL backup file for "${ENV}" environment.`);
console.log(`Cannot find the SQL backup file for "${fromEnv}" environment.`);
process.exit(1);
}

console.log(`Restoring ${file} to ${PGDATABASE} (${ENV})...`);
console.log(`Restoring ${file} to ${PGDATABASE} (${toEnv})...`);

spawn(
"psql",
Expand All @@ -35,9 +40,7 @@ spawn(
path.resolve(__dirname, `../backups/${file}`),
"--echo-errors",
"--no-readline",
...process.argv.slice(2).filter((x) => !x.startsWith("--env")),
...args._,
],
{
stdio: "inherit",
},
{ stdio: "inherit" },
).on("exit", process.exit);
17 changes: 4 additions & 13 deletions web/common/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Update, Action } from "history";
import { Environment } from "relay-runtime";
import { RelayEnvironmentProvider } from "react-relay/hooks";
import {
Container,
CssBaseline,
PaletteMode,
ThemeProvider,
Expand Down Expand Up @@ -88,7 +87,7 @@ export class App extends React.Component<AppProps> {
if (error) {
return (
<ThemeProvider theme={theme[this.state.theme]}>
<ErrorPage error={error} />;
<ErrorPage error={error} history={history} />;
</ThemeProvider>
);
}
Expand All @@ -101,17 +100,9 @@ export class App extends React.Component<AppProps> {
<CssBaseline />
<AppToolbar onChangeTheme={this.handleChangeTheme} />
<Toolbar />
<Container
maxWidth="md"
sx={{
marginTop: (x) => x.spacing(4),
marginBottom: (x) => x.spacing(4),
}}
>
{route?.component
? React.createElement(route.component, route.props)
: null}
</Container>
{route?.component
? React.createElement(route.component, route.props)
: null}
</LocationContext.Provider>
</HistoryContext.Provider>
</RelayEnvironmentProvider>
Expand Down
7 changes: 5 additions & 2 deletions web/common/AppToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ArrowDropDown, NotificationsNone } from "@material-ui/icons";

import { useAuth, useCurrentUser, useNavigate } from "../hooks";
import { UserMenu, NotificationsMenu } from "../menu";
import { Google } from "../icons";

type AppToolbarProps = AppBarProps & {
onChangeTheme: () => void;
Expand Down Expand Up @@ -126,10 +127,12 @@ export function AppToolbar(props: AppToolbarProps): JSX.Element {
)}
{!user && (
<Button
variant="outlined"
startIcon={<Google />}
href="/auth/google"
color="inherit"
color="primary"
onClick={signIn}
children="Sign In"
children="Sign in with Google"
/>
)}
</Toolbar>
Expand Down
9 changes: 5 additions & 4 deletions web/common/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
import * as React from "react";
import { CssBaseline, Container, Typography } from "@material-ui/core";

import type { History } from "../core/history";

export type ErrorPageProps = {
error: Error;
history: History;
};

export function ErrorPage(props: ErrorPageProps): JSX.Element {
const { error } = props;

return (
<React.Fragment>
<Container sx={{ marginTop: "43vh" }}>
<CssBaseline />
<Container maxWidth="sm">
<Typography
variant="h1"
align="center"
sx={{
padding: (theme) => theme.spacing(2),
marginTop: "43vh",
fontSize: "2em",
fontWeight: 300,
"& strong": {
Expand All @@ -33,6 +34,6 @@ export function ErrorPage(props: ErrorPageProps): JSX.Element {
<strong>Error {(error as any).status || 500}</strong>: {error.message}
</Typography>
</Container>
</React.Fragment>
</Container>
);
}
4 changes: 3 additions & 1 deletion web/core/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export async function resolveRoute(
]);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { component, ...route.response(data as any, ctx) };
const response = route.response(data as any, ctx);

if (response) return { component, ...response };
}

throw new NotFoundError();
Expand Down
38 changes: 38 additions & 0 deletions web/icons/Google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @copyright 2016-present Kriasoft (https://git.io/Jt7GM)
*/

import * as React from "react";
import { SvgIcon, SvgIconProps } from "@material-ui/core";

export function Google(props: SvgIconProps): JSX.Element {
return (
<SvgIcon
className="abcRioButtonSvg"
version="1.1"
role="img"
viewBox="0 0 48 48"
{...props}
>
<g>
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
/>
<path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
/>
<path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
/>
<path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
/>
<path fill="none" d="M0 0h48v48H0z" />
</g>
</SvgIcon>
);
}
1 change: 1 addition & 0 deletions web/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* @copyright 2016-present Kriasoft (https://git.io/Jt7GM)
*/

export { Google } from "./Google";
export { Logout } from "./Logout";
2 changes: 1 addition & 1 deletion web/menu/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function UserMenu(props: UserMenuProps): JSX.Element {
>
<MenuItem component={Link} href="/settings" onClick={handleClick}>
<ListItemIcon sx={{ minWidth: 40 }} children={<Settings />} />
<ListItemText primary="Account settings" />
<ListItemText primary="Account Settings" />
</MenuItem>

<MenuItem>
Expand Down
Loading

0 comments on commit 9d7f8da

Please sign in to comment.