diff --git a/.github/bw-small.svg b/.github/bw-small.svg new file mode 100644 index 0000000..f0f211f --- /dev/null +++ b/.github/bw-small.svg @@ -0,0 +1,2 @@ + + diff --git a/.github/icon-white.png b/.github/icon-white.png new file mode 100644 index 0000000..13db99c Binary files /dev/null and b/.github/icon-white.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a032931..fb0c2f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 3edf3dd..8b149af 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +/db \ No newline at end of file diff --git a/README.md b/README.md index 744f5dc..432afb4 100644 --- a/README.md +++ b/README.md @@ -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``` @@ -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 @@ -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 @@ -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. diff --git a/cli.ts b/cli.ts index 0daac6b..075df39 100644 --- a/cli.ts +++ b/cli.ts @@ -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.", @@ -54,7 +59,9 @@ 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(); @@ -62,6 +69,8 @@ const run = async () => { 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(); diff --git a/cli/state.ts b/cli/state.ts index 12de7a4..5df102e 100644 --- a/cli/state.ts +++ b/cli/state.ts @@ -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; @@ -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, @@ -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 ) { @@ -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) { diff --git a/cli/templates/seed.ts b/cli/templates/seed.ts new file mode 100644 index 0000000..5ee9c69 --- /dev/null +++ b/cli/templates/seed.ts @@ -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 +}; diff --git a/clients/AbstractClient.ts b/clients/AbstractClient.ts index f7599c6..068e329 100644 --- a/clients/AbstractClient.ts +++ b/clients/AbstractClient.ts @@ -16,10 +16,14 @@ export type MigrationFile = { export interface ClientI { migrationFolder: string; + seedFolder: string; + migrationFiles: Deno.DirEntry[]; + seedFiles: Deno.DirEntry[]; prepare: () => Promise; close: () => Promise; migrate: (amount: amountMigrateT) => Promise; rollback: (amount: amountRollbackT) => Promise; + seed: (matcher?: string) => Promise; query: QueryHandler; setLogger: loggerFn; } @@ -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; @@ -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;`; @@ -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( @@ -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"); + } + } } diff --git a/clients/ClientMySQL.ts b/clients/ClientMySQL.ts index dc72fcf..de4caf2 100644 --- a/clients/ClientMySQL.ts +++ b/clients/ClientMySQL.ts @@ -5,6 +5,7 @@ import { amountRollbackT, ClientI, queryT, + ClientOptions, } from "./AbstractClient.ts"; export class ClientMySQL extends AbstractClient implements ClientI { @@ -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(); } @@ -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)); + } } diff --git a/clients/ClientPostgreSQL.ts b/clients/ClientPostgreSQL.ts index c66ee82..5f64f58 100644 --- a/clients/ClientPostgreSQL.ts +++ b/clients/ClientPostgreSQL.ts @@ -7,6 +7,7 @@ import { amountRollbackT, ClientI, queryT, + ClientOptions, } from "./AbstractClient.ts"; export class ClientPostgreSQL extends AbstractClient implements ClientI { @@ -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); } @@ -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)); + } } diff --git a/clients/ClientSQLite.ts b/clients/ClientSQLite.ts index 8308864..1b93153 100644 --- a/clients/ClientSQLite.ts +++ b/clients/ClientSQLite.ts @@ -5,6 +5,7 @@ import { amountRollbackT, ClientI, queryT, + ClientOptions, } from "./AbstractClient.ts"; import { resolve } from "../deps.ts"; @@ -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)); } @@ -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)); + } } diff --git a/examples/config-mysql.ts b/examples/config-mysql.ts index 4a36e15..dbd3dff 100644 --- a/examples/config-mysql.ts +++ b/examples/config-mysql.ts @@ -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", diff --git a/examples/config-postgres.ts b/examples/config-postgres.ts index 02b21ce..afefa02 100644 --- a/examples/config-postgres.ts +++ b/examples/config-postgres.ts @@ -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, diff --git a/examples/config-sqlite.ts b/examples/config-sqlite.ts index 665a93b..7e6e6c8 100644 --- a/examples/config-sqlite.ts +++ b/examples/config-sqlite.ts @@ -1,8 +1,11 @@ import { ClientSQLite } from "../clients/ClientSQLite.ts"; -const migrationFolder = "./migrations"; +const clientConfig = { + migrationFolder: "./tests/cli", + seedFolder: "./tests/cli", +}; const dbFile = "./sqlite.db"; export default { - client: new ClientSQLite(migrationFolder, dbFile), + client: new ClientSQLite(clientConfig, dbFile), }; diff --git a/examples/seed.ts b/examples/seed.ts new file mode 100644 index 0000000..ad7e41a --- /dev/null +++ b/examples/seed.ts @@ -0,0 +1,5 @@ +import { Seed } from "https://deno.land/x/nessie/mod.ts"; + +export const run: Seed = () => { + return "INSERT INTO table1 VALUES (1234)"; +}; diff --git a/nessie.config.ts b/nessie.config.ts index 87a48bc..094f81e 100644 --- a/nessie.config.ts +++ b/nessie.config.ts @@ -1,13 +1,18 @@ import { ClientPostgreSQL } from "./clients/ClientPostgreSQL.ts"; -const migrationFolder = "./migrations"; +const nessieOptions = { + migrationFolder: "./db/migrations", + seedFolder: "./db/seeds", +}; + +const connectionOptions = { + database: "nessie", + hostname: "localhost", + port: 5000, + user: "root", + password: "pwd", +}; export default { - client: new ClientPostgreSQL(migrationFolder, { - database: "nessie", - hostname: "localhost", - port: 5000, - user: "root", - password: "pwd", - }), + client: new ClientPostgreSQL(nessieOptions, connectionOptions), }; diff --git a/tests/cli/config/migration.config.ts b/tests/cli/config/migration.config.ts index 946f3a2..46cf88b 100644 --- a/tests/cli/config/migration.config.ts +++ b/tests/cli/config/migration.config.ts @@ -1,5 +1,6 @@ export const TYPE_MIGRATE = "migrate"; export const TYPE_ROLLBACK = "rollback"; +export const TYPE_SEED = "seed"; export const DIALECT_PG = "pg"; export const DIALECT_MYSQL = "mysql"; diff --git a/tests/cli/config/mysql.config.ts b/tests/cli/config/mysql.config.ts index e65d8f1..9c7c27e 100644 --- a/tests/cli/config/mysql.config.ts +++ b/tests/cli/config/mysql.config.ts @@ -1,10 +1,13 @@ import { ClientMySQL } from "../../../mod.ts"; export default { - client: new ClientMySQL("./tests/cli", { - "hostname": "localhost", - "port": 5001, - "username": "root", - "db": "nessie", - }), + client: new ClientMySQL( + { migrationFolder: "./tests/cli", seedFolder: "./tests/cli" }, + { + "hostname": "localhost", + "port": 5001, + "username": "root", + "db": "nessie", + }, + ), }; diff --git a/tests/cli/config/pg.config.ts b/tests/cli/config/pg.config.ts index f7c438a..8303d5e 100644 --- a/tests/cli/config/pg.config.ts +++ b/tests/cli/config/pg.config.ts @@ -1,11 +1,14 @@ import { ClientPostgreSQL } from "../../../mod.ts"; export default { - client: new ClientPostgreSQL("./tests/cli", { - "database": "nessie", - "hostname": "localhost", - "port": 5000, - "user": "root", - "password": "pwd", - }), + client: new ClientPostgreSQL( + { migrationFolder: "./tests/cli", seedFolder: "./tests/cli" }, + { + "database": "nessie", + "hostname": "localhost", + "port": 5000, + "user": "root", + "password": "pwd", + }, + ), }; diff --git a/tests/cli/config/sqlite.config.ts b/tests/cli/config/sqlite.config.ts index 815b45d..f3ffaf4 100644 --- a/tests/cli/config/sqlite.config.ts +++ b/tests/cli/config/sqlite.config.ts @@ -1,5 +1,8 @@ import { ClientSQLite } from "../../../mod.ts"; export default { - client: new ClientSQLite("./tests/cli", "./tests/data/sqlite.db"), + client: new ClientSQLite( + { migrationFolder: "./tests/cli", seedFolder: "./tests/cli" }, + "./tests/data/sqlite.db", + ), }; diff --git a/tests/cli/migration.test.ts b/tests/cli/migration.test.ts index 967a029..e40cff9 100644 --- a/tests/cli/migration.test.ts +++ b/tests/cli/migration.test.ts @@ -4,6 +4,7 @@ import { runner, TYPE_MIGRATE, TYPE_ROLLBACK, + TYPE_SEED, } from "./config/migration.config.ts"; const strings = [ @@ -29,6 +30,13 @@ const strings = [ "Migration complete", ], }, + { + name: "Seed", + string: [TYPE_SEED, "seed.ts"], + solution: [ + "Seeding complete", + ], + }, { name: "Migrate empty", string: [TYPE_MIGRATE], diff --git a/tests/cli/seed.ts b/tests/cli/seed.ts new file mode 100644 index 0000000..be1d265 --- /dev/null +++ b/tests/cli/seed.ts @@ -0,0 +1,5 @@ +import { Seed } from "../../mod.ts"; + +export const run: Seed = () => { + return "INSERT INTO testTable3 VALUES (1);"; +}; diff --git a/types.ts b/types.ts index 87daf48..642a500 100644 --- a/types.ts +++ b/types.ts @@ -1 +1,2 @@ export type Migration = () => string | string[] | Promise; +export type Seed = Migration;