Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Seed functionality #51

Merged
merged 8 commits into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/bw-small.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/icon-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ jobs:
env:
URL_PATH: ${{github.event.pull_request.head.repo.full_name||github.repository}}/${{github.event.pull_request.head.ref||'master'}}

- name: Create seed
run: deno run --allow-read --allow-write --allow-net https://raw.githubusercontent.com/$URL_PATH/cli.ts make:seed test
env:
URL_PATH: ${{github.event.pull_request.head.repo.full_name||github.repository}}/${{github.event.pull_request.head.ref||'master'}}

cli-migrations:
name: Test CLI Migrations
needs: get-diff
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,4 @@ $RECYCLE.BIN/
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
.vscode
data
/migrations
/db
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ You can see examples of how to make a client plugin in the [clients folder](./cl

```deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_users```

* `make:seed [name]`: Create seed

```deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make:seed create_users```

* `migrate [amount?]`: Run migration - will migrate your migrations in your migration folder (sorted by timestamp) newer than the latest migration in your db. Amount defines how many migrations, defaults to all available if not set.

```deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts migrate```
Expand All @@ -49,6 +53,14 @@ You can see examples of how to make a client plugin in the [clients folder](./cl

```deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts rollback all```

* `seed [matcher?]`: Seed - will seed your database. Optional matcher will match all files in your seed folder by string literal or RegExp.

```deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts seed```

```deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts seed seed_file.js```

```deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts seed ".+.ts"```

### Flags

* `-c, --config`: Path to config file, will default to ./nessie.config.ts
Expand Down Expand Up @@ -132,6 +144,16 @@ export const down: Migration = () => {
};
```

Seed file

```ts
import { Seed } from "https://deno.land/x/nessie/mod.ts";

export const run: Seed = () => {
return "INSERT INTO testTable VALUES (1)"
};
```

See the [example folder](./examples) for more

## How to make a client
Expand All @@ -146,4 +168,6 @@ A client needs to extend [AbstractClient](./clients/AbstractClient.ts) and imple

`rollback`: Takes a number as an optional input, will default to 1 if not set. Will run `Math.min(amount, numberOfFiles)` migration files. Only handles the `down` method.

`seed`: Takes an optional matcher as input. Matcher can be regex or string. Will seed the database. Handles the `run` method in seed files.

`close`: Will be the last method run before the program is finished. This should close the database connection.
13 changes: 11 additions & 2 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ const initDenomander = () => {
"Path to config file, will default to ./nessie.config.ts",
)
.command("init", "Generates the config file")
.command("make [migrationName]", "Creates a migration file with the name")
.command("make [fileName]", "Creates a migration file with the name")
.command("make:seed [fileName]", "Creates a seed file with the name")
.command(
"seed [matcher?]",
"Seeds the database with the files found with the matcher in the seed folder specified in the config file. Matcher is optional, and accepts string literals and RegExp",
)
.command(
"migrate [amount?]",
"Migrates migrations. Optional number of migrations. If not provided, it will do all available.",
Expand Down Expand Up @@ -54,14 +59,18 @@ const run = async () => {
const state = await new State(prog).init();

if (prog.make) {
await state.makeMigration(prog.migrationName);
await state.makeMigration(prog.fileName);
} else if (prog["make:seed"]) {
await state.makeSeed(prog.fileName);
} else {
await state.client!.prepare();

if (prog.migrate) {
await state.client!.migrate(prog.amount);
} else if (prog.rollback) {
await state.client!.rollback(prog.amount);
} else if (prog.seed) {
await state.client!.seed(prog.matcher);
}

await state.client!.close();
Expand Down
33 changes: 31 additions & 2 deletions cli/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export type loggerFn = (output?: any, title?: string) => void;

const STD_CONFIG_FILE = "nessie.config.ts";

const STD_CLIENT_OPTIONS = {
seedFolder: "./db/seeds",
migrationFolder: "./db/migrations",
};

export class State {
private enableDebug: boolean;
private configFile: string;
Expand All @@ -36,7 +41,7 @@ export class State {
if (!this.config?.client) {
this.logger("Using standard config");

this.client = new ClientPostgreSQL("./migrations", {
this.client = new ClientPostgreSQL(STD_CLIENT_OPTIONS, {
database: "nessie",
hostname: "localhost",
port: 5432,
Expand All @@ -52,7 +57,7 @@ export class State {
return this;
}

async makeMigration(migrationName: string) {
async makeMigration(migrationName: string = "migration") {
if (
migrationName.length > AbstractClient.MAX_FILE_NAME_LENGTH - 13
) {
Expand Down Expand Up @@ -82,6 +87,30 @@ export class State {
);
}

async makeSeed(seedName: string = "seed") {
const fileName = `${seedName}.ts`;
if (this.client?.seedFiles.find((el) => el.name === seedName)) {
console.info(`Seed with name '${seedName}' already exists.`);
}

this.logger(fileName, "Seed file name");

await Deno.mkdir(this.client!.seedFolder, { recursive: true });

const responseFile = await fetch(
"https://deno.land/x/nessie/cli/templates/seed.ts",
);

await Deno.writeTextFile(
`${this.client!.migrationFolder}/${fileName}`,
await responseFile.text(),
);

console.info(
`Created seed ${fileName} at ${this.client!.seedFolder}`,
);
}

logger(output?: any, title?: string): void {
try {
if (this.enableDebug) {
Expand Down
8 changes: 8 additions & 0 deletions cli/templates/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Seed } from "https://deno.land/x/nessie/mod.ts";
import { Schema } from "https://deno.land/x/nessie/qb.ts";
import Dex from "https://deno.land/x/dex/mod.ts";

export const run: Seed = () => {
// return new Schema()
// return Dex
};
54 changes: 51 additions & 3 deletions clients/AbstractClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ export type MigrationFile = {

export interface ClientI {
migrationFolder: string;
seedFolder: string;
migrationFiles: Deno.DirEntry[];
seedFiles: Deno.DirEntry[];
prepare: () => Promise<void>;
close: () => Promise<void>;
migrate: (amount: amountMigrateT) => Promise<void>;
rollback: (amount: amountRollbackT) => Promise<void>;
seed: (matcher?: string) => Promise<void>;
query: QueryHandler;
setLogger: loggerFn;
}
Expand All @@ -28,6 +32,12 @@ export interface nessieConfig {
client: ClientI;
}

export interface ClientOptions {
migrationFolder: string;
seedFolder: string;
[option: string]: any;
}

export class AbstractClient {
static readonly MAX_FILE_NAME_LENGTH = 100;

Expand All @@ -36,9 +46,11 @@ export class AbstractClient {
protected COL_CREATED_AT = "created_at";
protected REGEX_MIGRATION_FILE_NAME = /^\d{10,14}-.+.ts$/;
protected regexFileName = new RegExp(this.REGEX_MIGRATION_FILE_NAME);
protected migrationFiles: Deno.DirEntry[];
protected logger: loggerFn = () => undefined;
migrationFiles: Deno.DirEntry[];
seedFiles: Deno.DirEntry[];
migrationFolder: string;
seedFolder: string;

protected QUERY_GET_LATEST =
`SELECT ${this.COL_FILE_NAME} FROM ${this.TABLE_MIGRATIONS} ORDER BY ${this.COL_FILE_NAME} DESC LIMIT 1;`;
Expand All @@ -50,9 +62,21 @@ export class AbstractClient {
protected QUERY_MIGRATION_DELETE: QueryWithString = (fileName) =>
`DELETE FROM ${this.TABLE_MIGRATIONS} WHERE ${this.COL_FILE_NAME} = '${fileName}';`;

constructor(migrationFolder: string) {
this.migrationFolder = resolve(migrationFolder);
constructor(options: string | ClientOptions) {
if (typeof options === "string") {
console.info(
"DEPRECATED: Using string as the client option is deprecated, please use a config object instead.",
);
this.migrationFolder = resolve(options);
this.seedFolder = resolve("./db/seeds");
} else {
this.migrationFolder = resolve(
options.migrationFolder || "./db/migrations",
);
this.seedFolder = resolve(options.seedFolder || "./db/seeds");
}
this.migrationFiles = Array.from(Deno.readDirSync(this.migrationFolder));
this.seedFiles = Array.from(Deno.readDirSync(this.seedFolder));
}

protected async migrate(
Expand Down Expand Up @@ -155,4 +179,28 @@ export class AbstractClient {
setLogger(fn: loggerFn) {
this.logger = fn;
}

async seed(matcher: string = ".+.ts", queryHandler: QueryHandler) {
const files = this.seedFiles.filter((el) =>
el.isFile && (el.name === matcher || new RegExp(matcher).test(el.name))
);

if (!files) {
console.info(
`No seed file found at '${this.seedFolder}' with matcher '${matcher}'`,
);
return;
} else {
for await (const file of files) {
const filePath = parsePath(this.seedFolder, file.name);

const { run } = await import(filePath);
const sql = await run();

await queryHandler(sql);
}

console.info("Seeding complete");
}
}
}
12 changes: 10 additions & 2 deletions clients/ClientMySQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
amountRollbackT,
ClientI,
queryT,
ClientOptions,
} from "./AbstractClient.ts";

export class ClientMySQL extends AbstractClient implements ClientI {
Expand All @@ -17,8 +18,11 @@ export class ClientMySQL extends AbstractClient implements ClientI {
private QUERY_CREATE_MIGRATION_TABLE =
`CREATE TABLE ${this.TABLE_MIGRATIONS} (id bigint UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, ${this.COL_FILE_NAME} varchar(${AbstractClient.MAX_FILE_NAME_LENGTH}) NOT NULL UNIQUE, ${this.COL_CREATED_AT} datetime NOT NULL DEFAULT CURRENT_TIMESTAMP);`;

constructor(migrationFolder: string, connectionOptions: ClientConfig) {
super(migrationFolder);
constructor(
options: string | ClientOptions,
connectionOptions: ClientConfig,
) {
super(options);
this.clientOptions = connectionOptions;
this.client = new Client();
}
Expand Down Expand Up @@ -87,4 +91,8 @@ export class ClientMySQL extends AbstractClient implements ClientI {
this.query.bind(this),
);
}

async seed(matcher?: string) {
await super.seed(matcher, this.query.bind(this));
}
}
12 changes: 10 additions & 2 deletions clients/ClientPostgreSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
amountRollbackT,
ClientI,
queryT,
ClientOptions,
} from "./AbstractClient.ts";

export class ClientPostgreSQL extends AbstractClient implements ClientI {
Expand All @@ -18,8 +19,11 @@ export class ClientPostgreSQL extends AbstractClient implements ClientI {
private QUERY_CREATE_MIGRATION_TABLE =
`CREATE TABLE ${this.TABLE_MIGRATIONS} (id bigserial PRIMARY KEY, ${this.COL_FILE_NAME} varchar(${AbstractClient.MAX_FILE_NAME_LENGTH}) UNIQUE, ${this.COL_CREATED_AT} timestamp (0) default current_timestamp);`;

constructor(migrationFolder: string, connectionOptions: ConnectionOptions) {
super(migrationFolder);
constructor(
options: string | ClientOptions,
connectionOptions: ConnectionOptions,
) {
super(options);
this.client = new Client(connectionOptions);
}

Expand Down Expand Up @@ -75,4 +79,8 @@ export class ClientPostgreSQL extends AbstractClient implements ClientI {
this.query.bind(this),
);
}

async seed(matcher?: string) {
await super.seed(matcher, this.query.bind(this));
}
}
9 changes: 7 additions & 2 deletions clients/ClientSQLite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
amountRollbackT,
ClientI,
queryT,
ClientOptions,
} from "./AbstractClient.ts";
import { resolve } from "../deps.ts";

Expand All @@ -16,8 +17,8 @@ export class ClientSQLite extends AbstractClient implements ClientI {
private QUERY_CREATE_MIGRATION_TABLE =
`CREATE TABLE ${this.TABLE_MIGRATIONS} (id integer NOT NULL PRIMARY KEY autoincrement, ${this.COL_FILE_NAME} varchar(${AbstractClient.MAX_FILE_NAME_LENGTH}) UNIQUE, ${this.COL_CREATED_AT} datetime NOT NULL DEFAULT CURRENT_TIMESTAMP);`;

constructor(migrationFolder: string, connectionOptions: string) {
super(migrationFolder);
constructor(options: string | ClientOptions, connectionOptions: string) {
super(options);
this.client = new DB(resolve(connectionOptions));
}

Expand Down Expand Up @@ -74,4 +75,8 @@ export class ClientSQLite extends AbstractClient implements ClientI {
this.query.bind(this),
);
}

async seed(matcher?: string) {
await super.seed(matcher, this.query.bind(this));
}
}
7 changes: 5 additions & 2 deletions examples/config-mysql.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ClientMySQL } from "../clients/ClientMySQL.ts";

const migrationFolder = "./migrations";
const clientConfig = {
migrationFolder: "./tests/cli",
seedFolder: "./tests/cli",
};

export default {
client: new ClientMySQL(migrationFolder, {
client: new ClientMySQL(clientConfig, {
hostname: "localhost",
port: 3306,
username: "root",
Expand Down
7 changes: 5 additions & 2 deletions examples/config-postgres.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ClientPostgreSQL } from "../clients/ClientPostgreSQL.ts";

const migrationFolder = "./migrations";
const clientConfig = {
migrationFolder: "./tests/cli",
seedFolder: "./tests/cli",
};

export default {
client: new ClientPostgreSQL(migrationFolder, {
client: new ClientPostgreSQL(clientConfig, {
database: "nessie",
hostname: "localhost",
port: 5432,
Expand Down
Loading