diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bde178..3b41d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.0-alpha.8] - 2021-04-08 + +- `kivik deploy local` deploys to a running Kivik instance, unless a deployment with key `local` exists in the RC file. +- `kivik start` does not deploy by default. +- RC file deployments can set a `fixtures` flag to determine if deployments should also deploy fixtures. This is false by default. +- `kivik deploy` checks this flag (as well as the `local.fixtures` for `kivik deploy local`) when deploying. +- `instance.deployDb(db: string, suffix?: string)` lets test suites deploy one database at a time. + ## [2.0.0-alpha.7] - 2021-04-08 - New CLI invocations: `kivik start`, `kivik watch`, `kivik stop`. `kivik inspect`, `kivik dev` and `kivik instance` are aliases of `kivik watch`, and nothing particularly new happens if you keep using those like you used to. `kivik start` and `kivik stop` start and stop the Kivik instance in the background. @@ -110,6 +118,7 @@ - Multiple design document support - View (map/reduce) and update function support within design documents +[2.0.0-alpha.8]: https://github.com/crkn-rcdr/kivik/releases/tag/v2.0.0-alpha.8 [2.0.0-alpha.7]: https://github.com/crkn-rcdr/kivik/releases/tag/v2.0.0-alpha.7 [2.0.0-alpha.6]: https://github.com/crkn-rcdr/kivik/releases/tag/v2.0.0-alpha.6 [2.0.0-alpha.5]: https://github.com/crkn-rcdr/kivik/releases/tag/v2.0.0-alpha.5 diff --git a/README.md b/README.md index 2f09755..3c941b2 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ export interface Deployment { }; /** Suffix to append to the end of each database name. Default: `undefined` */ suffix?: string; + /** Whether or not to deploy fixtures along with the design documents. */ + fixtures?: boolean; + /** List of databases to deploy. By default, all databases are deployed. */ + dbs?: string[]; } /** Configuration for Kivik instances. */ @@ -181,10 +185,12 @@ $ kivik deploy production import { createKivik } from "kivik"; const kivik = await createKivik("path/to/dir", "deploy"); -await kivik.deployTo("production"); +await kivik.deploy("production"); await kivik.close(); ``` +If you deploy to `local`, you will deploy to a running Kivik instance (see below), unless you have a deployment with key `local` in your RC file. + ### Instance ```shell @@ -235,7 +241,7 @@ test("Your fixture looks good", async (t) => { }); ``` -When a Kivik instance is running, Kivik saves its container's name to `$DIR/.kivik.tmp`. If this file is altered or deleted, Kivik won't be able to find the running instance it might be referring to. The file is deleted when a Kivik instance is stopped. +When a Kivik instance is running, Kivik saves its container's name to `$DIR/.kivik.tmp`. If this file is altered or deleted, Kivik won't be able to find the running instance it might be referring to. The file is deleted when a Kivik instance is stopped. Only one Kivik instance can run at a time. ## Testing Kivik diff --git a/package.json b/package.json index 9cd8855..c7405fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kivik", - "version": "2.0.0-alpha.7", + "version": "2.0.0-alpha.8", "description": "An opinionated library and command-line utility for configuration CouchDB endpoints, databases, and design documents", "keywords": [ "couchdb" @@ -19,9 +19,12 @@ } ], "license": "Unlicense", - "main": "./dist/index.js", + "files": [ + "dist/**" + ], + "main": "dist/index.js", "bin": { - "kivik": "./dist/bin.js" + "kivik": "dist/bin.js" }, "engines": { "node": ">=12.0.0" diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index 153a7a7..c2ff97a 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -13,27 +13,36 @@ export default (unloggedContext: UnloggedContext) => { command: "deploy ", describe: "Deploys design documents to a remote database", builder: (yargs: yargs.Argv) => - yargs - .positional("deployment", { - type: "string", - describe: "Key of a deployment object in your kivikrc file", - }) - .check((argv: DeployArgv): boolean => { - const key = argv.deployment as string; - const deployment = unloggedContext.deployments[key]; - - if (!deployment) - throw new Error(`No deployment in kivikrc for key ${key}`); - - return true; - }), + yargs.positional("deployment", { + type: "string", + describe: "Key of a deployment object in your kivikrc file", + }), handler: async (argv: DeployArgv) => { const context = unloggedContext.withArgv(argv); - const kivik = await createKivik(context, "deploy"); - await kivik.deployTo(argv.deployment as string); + let deployment; + try { + deployment = await context.getDeployment(argv.deployment || ""); + } catch (error) { + context.log("error", error.message); + process.exit(1); + } + + let kivik; + try { + kivik = await createKivik( + context, + deployment.fixtures ? "instance" : "deploy" + ); + await kivik.deploy(deployment); + await kivik.close(); + process.exit(0); + } catch (error) { + context.log("error", `Error deploying: ${error.message}`); + process.exitCode = 1; + } - await kivik.close(); + if (kivik) await kivik.close(); }, }; }; diff --git a/src/cli/start.ts b/src/cli/start.ts index 6cfb981..53b173d 100644 --- a/src/cli/start.ts +++ b/src/cli/start.ts @@ -10,8 +10,7 @@ export default (unloggedContext: UnloggedContext) => { const context = unloggedContext.withArgv(argv); try { - const instance = await createInstance(context, false); - await instance.deploy(); + const instance = await createInstance(context, { attach: false }); await instance.detach(); instance.announce(); } catch (error) { diff --git a/src/cli/stop.ts b/src/cli/stop.ts index 6e9ed10..23a81a5 100644 --- a/src/cli/stop.ts +++ b/src/cli/stop.ts @@ -12,6 +12,7 @@ export default (unloggedContext: UnloggedContext) => { try { const instance = await getInstance(context); await instance.stop(); + context.log("success", "Kivik instance stopped."); } catch (error) { context.log( "error", diff --git a/src/context/index.ts b/src/context/index.ts index 75fb9fc..4196700 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -5,12 +5,15 @@ import { sync as findUp } from "find-up"; import { CommonArgv } from "../cli"; import { createLogger, LogLevel } from "./logger"; -import { normalizeRc, NormalizedRc } from "./rc"; +import { normalizeRc, NormalizedRc, Deployment, NanoDeployment } from "./rc"; +import { get as remoteNano } from "@crkn-rcdr/nano"; +import { getInstance } from "../instance"; export { logLevels, LogLevel } from "./logger"; export { Deployment, + NanoDeployment, InstanceConfig, normalizeInstanceConfig, NormalizedInstanceConfig, @@ -24,6 +27,10 @@ export type UnloggedContext = NormalizedRc & { export type Context = Omit & { readonly log: (level: LogLevel, message: string) => void; + readonly getDeployment: ( + key: string, + suffix?: string + ) => Promise; readonly withDatabase: (db: string) => DatabaseContext; }; @@ -42,16 +49,44 @@ export const createContext = (directory: string): UnloggedContext => { return { directory, ...rc, - withArgv: function (argv: CommonArgv): Context { + withArgv: function (argv: CommonArgv) { // https://no-color.org if (process.env.hasOwnProperty("NO_COLOR")) argv.color = false; const logger = createLogger(argv); + if (!confPath) + logger.log( + "warn", + "No kivikrc file detected. Proceeding with defaults." + ); logger.log("info", "Logger initialized."); return { ...this, log: (level: LogLevel, message: string) => logger.log(level, message), + getDeployment: async function (key: string, suffix?: string) { + if (key in this.deployments) { + const deployment = this.deployments[key] as Deployment; + return { + nano: remoteNano(deployment.url, deployment.auth), + suffix: suffix || deployment.suffix, + fixtures: !!deployment.fixtures, + dbs: deployment.dbs || null, + }; + } else if (key === "local") { + const instance = await getInstance(this); + return { + nano: instance.nano, + suffix, + fixtures: this.local.fixtures, + dbs: null, + }; + } else { + throw new Error( + `Your kivikrc file does not have a deployment with key '${key}'` + ); + } + }, withDatabase: function (db: string): DatabaseContext { return { ...this, diff --git a/src/context/rc.ts b/src/context/rc.ts index 530c6cc..e9349a3 100644 --- a/src/context/rc.ts +++ b/src/context/rc.ts @@ -1,3 +1,5 @@ +import { ServerScope } from "nano"; + /** Kivik RC file configuration. */ export interface Rc { /** @@ -31,8 +33,19 @@ export interface Deployment { }; /** Suffix to append to the end of each database name. Default: `undefined` */ suffix?: string; + /** Whether or not to deploy fixtures along with the design documents Default: `false`. */ + fixtures?: boolean; + /** List of databases to deploy. By default, all databases are deployed. */ + dbs?: string[]; } +export type NanoDeployment = { + nano: ServerScope; + suffix?: string; + fixtures: boolean; + dbs: string[] | null; +}; + /** Configuration for Kivik instances. */ export interface InstanceConfig { /** Deploy fixtures when running `kivik dev`. Default: `true` */ diff --git a/src/instance/container.ts b/src/instance/container.ts index 533e4f8..344e21a 100644 --- a/src/instance/container.ts +++ b/src/instance/container.ts @@ -6,7 +6,7 @@ import { localhost as localNano } from "@crkn-rcdr/nano"; import { ServerScope } from "nano"; import pRetry from "p-retry"; -import { Context, NormalizedInstanceConfig } from "../context"; +import { Context } from "../context"; const tempfile = (directory: string) => pathJoin(directory, ".kivik.tmp"); const getNano = (port: number, context: Context) => @@ -50,11 +50,8 @@ export const getContainer = async (context: Context) => { } }; -export const createContainer = async ( - context: Context, - instanceConfig: NormalizedInstanceConfig -): Promise => { - const { port: desiredPort, image, user, password } = instanceConfig; +export const createContainer = async (context: Context): Promise => { + const { port: desiredPort, image, user, password } = context.local; const port = await getPort({ port: desiredPort }); diff --git a/src/instance/index.spec.ts b/src/instance/index.spec.ts index 5fbe7ae..a09874e 100644 --- a/src/instance/index.spec.ts +++ b/src/instance/index.spec.ts @@ -2,7 +2,6 @@ import anyTest, { TestInterface } from "ava"; import { createInstance, Instance } from "."; import { directory } from "../example"; -import { DatabaseHandler } from "../kivik"; interface LocalContext { instance: Instance; @@ -20,8 +19,7 @@ test("Can survive multiple deploys", async (t) => { ); await Promise.all( suffixes.map(async (suffix) => { - const handlers = await t.context.instance.deploy(suffix); - const testdb = handlers.get("testdb") as DatabaseHandler; + const testdb = await t.context.instance.deployDb("testdb", suffix); const pickwick = await testdb.get("pickwick-papers"); t.is(pickwick["_id"], "pickwick-papers"); }) diff --git a/src/instance/index.ts b/src/instance/index.ts index cefad1c..3a63d77 100644 --- a/src/instance/index.ts +++ b/src/instance/index.ts @@ -1,36 +1,50 @@ +import { ServerScope } from "nano"; + +import { Context, defaultContext } from "../context"; import { - Context, - defaultContext, - InstanceConfig, - NormalizedInstanceConfig, - normalizeInstanceConfig, -} from "../context"; -import { createKivik, DatabaseHandlerMap, Kivik } from "../kivik"; + createKivik, + DatabaseHandler, + DatabaseHandlerMap, + Kivik, +} from "../kivik"; import { getContainer, createContainer, Container } from "./container"; +export interface InstanceOptions { + /** + * Whether to attach to a running instance when trying to create one. + * Ignored when trying to get a running instance directly. + * Default: true + */ + attach?: boolean; + /** + * Whether to deploy fixtures to the instance when deploying design + * documents. Default: Whatever is set in `local.fixtures` in your RC file. + * If that's unset, the default is `true`. + */ + fixtures?: boolean; +} + /** * Gets a running Kivik instance. Throws an error if the instance cannot be * found. * @param directory The root directory for the files Kivik will manage. - * @param config Instance configuration. */ export async function getInstance( directory: string, - config?: InstanceConfig + options?: InstanceOptions ): Promise; /** * Gets a running Kivik instance. Throws an error if the instance cannot be * found. * @param context The Kivik context object to apply to the instance. - * @param config Instance configuration. */ export async function getInstance( context: Context, - config?: InstanceConfig + options?: InstanceOptions ): Promise; export async function getInstance( input: string | Context, - config: InstanceConfig = {} + options: InstanceOptions = {} ): Promise { const context = typeof input === "string" ? defaultContext(input) : input; const container = await getContainer(context); @@ -38,51 +52,43 @@ export async function getInstance( throw new Error( "Cannot find a running instance. Check that .kivik.tmp exists and contains the name of a running Docker container." ); - const nConfig = normalizeInstanceConfig( - Object.assign({}, context.local, config) - ); - return await instanceHelper(container, context, nConfig); + + return await instanceHelper(container, context, options); } /** * Creates a Kivik instance. * @param directory The root directory for the files Kivik will manage. - * @param attach Attach to a running instance, if it exists. - * @param config Instance configuration. * @returns The Kivik instance. File scan and load is complete, and the Docker * container running the instance's CouchDB endpoint is ready to go. */ export async function createInstance( directory: string, - attach?: boolean, - config?: InstanceConfig + options?: InstanceOptions ): Promise; /** * Creates a Kivik instance. * @param context The Kivik context object to apply to the instance. - * @param attach Attach to a running instance, if it exists. - * @param config Instance configuration. * @returns The Kivik instance. File scan and load is complete, and the Docker * container running the instance's CouchDB endpoint is ready to go. */ export async function createInstance( context: Context, - attach?: boolean, - config?: InstanceConfig + options?: InstanceOptions ): Promise; export async function createInstance( input: string | Context, - attach = true, - config: InstanceConfig = {} + options: InstanceOptions = {} ) { const context = typeof input === "string" ? defaultContext(input) : input; let instance: Instance | null = null; try { - instance = await getInstance(context, config); + instance = await getInstance(context, options); } catch (_) {} if (instance) { + const attach = "attach" in options ? !!options.attach : true; if (attach) { context.log("success", "Attaching to a running instance."); return instance; @@ -96,29 +102,27 @@ export async function createInstance( ); } - const nConfig = normalizeInstanceConfig( - Object.assign({}, context.local, config) - ); - const container = await createContainer(context, nConfig); - return await instanceHelper(container, context, nConfig); + const container = await createContainer(context); + return await instanceHelper(container, context, options); } async function instanceHelper( container: Container, context: Context, - config: NormalizedInstanceConfig + options: InstanceOptions ): Promise { - const nConfig = normalizeInstanceConfig( - Object.assign({}, context.local, config) - ); + const fixtures = + "fixtures" in options + ? !!options.fixtures + : "fixtures" in context.local + ? !!context.local.fixtures + : true; - const kivik = await createKivik( - context, - nConfig.fixtures ? "instance" : "deploy" - ); + const kivik = await createKivik(context, fixtures ? "instance" : "deploy"); return { kivik, + nano: container.nano, announce: () => { container.announce(); }, @@ -127,7 +131,22 @@ async function instanceHelper( kivik.deployOnChanges(container.nano); }, deploy: async (suffix?: string) => { - return await kivik.deploy(container.nano, suffix); + return await kivik.deploy({ + nano: container.nano, + suffix, + fixtures, + dbs: null, + }); + }, + deployDb: async (db: string, suffix?: string) => { + const handlers = await kivik.deploy({ + nano: container.nano, + suffix, + fixtures, + dbs: [db], + }); + if (!handlers.has(db)) throw new Error(`Database '${db}' not found.`); + return handlers.get(db) as DatabaseHandler; }, detach: async () => { await kivik.close(); @@ -142,9 +161,11 @@ async function instanceHelper( export interface Instance { readonly kivik: Kivik; + readonly nano: ServerScope; readonly announce: () => void; readonly attach: () => Promise; readonly deploy: (suffix?: string) => Promise; + readonly deployDb: (db: string, suffix?: string) => Promise; readonly detach: () => Promise; readonly stop: () => Promise; } diff --git a/src/kivik/database.ts b/src/kivik/database.ts index 3cb4f01..ee77eee 100644 --- a/src/kivik/database.ts +++ b/src/kivik/database.ts @@ -18,7 +18,7 @@ import { isValidateFile, } from "./file"; import { DesignDoc } from "./design-doc"; -import { Context, DatabaseContext } from "../context"; +import { Context, DatabaseContext, NanoDeployment } from "../context"; /** A `nano` DocumentScope object pointing to the deployed database. */ export type DatabaseHandler = DocumentScope; @@ -123,14 +123,12 @@ export interface Database { /** * Deploy the database's configuration and fixtures. - * @param nano A `nano` instance pointing to the CouchDB endpoint to deploy - * the database configuration to. - * @param suffix If set, the database's identifier will have the suffix - * appended to it, joined by a hyphen: ``${this.name}-${suffix}`` + * @param deployment A NanoDeployment object containing information required + * for the task. * @returns A promise resolving to a `nano` DocumentScope instance which * can perform further operations on database documents. */ - deploy: (nano: ServerScope, suffix?: string) => Promise; + deploy: (deployment: NanoDeployment) => Promise; } /** @@ -267,7 +265,8 @@ class DatabaseImpl implements Database { return errors; } - async deploy(nano: ServerScope, suffix?: string): Promise { + async deploy(deployment: NanoDeployment): Promise { + const { nano, suffix, fixtures } = deployment; const name = suffix ? `${this.name}-${suffix}` : this.name; this.logDeployAttempt(suffix ? `database (${name})` : "database"); @@ -306,7 +305,7 @@ class DatabaseImpl implements Database { } // Validate and deploy fixtures - if (this.fixtures.size > 0) { + if (fixtures && this.fixtures.size > 0) { this.validateFixtures(); const fixtures = [...this.fixtures.values()] diff --git a/src/kivik/index.ts b/src/kivik/index.ts index a3a769a..7a86722 100644 --- a/src/kivik/index.ts +++ b/src/kivik/index.ts @@ -1,11 +1,10 @@ import { watch, FSWatcher } from "chokidar"; import { join as joinPath } from "path"; import pEvent from "p-event"; -import { get as getNano } from "@crkn-rcdr/nano"; import { ServerScope } from "nano"; import { Mode } from ".."; -import { Context, defaultContext } from "../context"; +import { Context, defaultContext, NanoDeployment } from "../context"; import { KivikFile } from "./file"; import { createDatabase, @@ -132,16 +131,6 @@ export interface Kivik { */ validateFixtures: () => ValidationReport; - /** - * Deploys stored configuration and fixtures to a CouchDB endpoint. - * @param nano A `nano` instance pointing to the endpoint. - * @param suffix If set, each database at the endpoint will have the suffix - * appended to its name with a hyphen: ``${db.name}-${suffix}`` - * @returns A Map of each database name to a `nano` `DocumentScope` handler - * that permits further operations on it. - */ - deploy: (nano: ServerScope, suffix?: string) => Promise; - /** * Deploys stored configuration and fixtures to a CouchDB endpoint, * described by a Kivik RC deployment object. @@ -149,7 +138,7 @@ export interface Kivik { * @returns A Map of each database name to a `nano` `DocumentScope` handler * that permits further operations on it. */ - deployTo: (deployment: string) => Promise; + deploy: (deployment: string | NanoDeployment) => Promise; /** * Triggers updates to a CouchDB endpoint when files monitored by the Kivik @@ -211,23 +200,19 @@ class KivikImpl implements Kivik { } async deploy( - nano: ServerScope, - suffix?: string + deployment: string | NanoDeployment ): Promise { + if (typeof deployment === "string") + deployment = await this.context.getDeployment(deployment); const handlers: DatabaseHandlerMap = new Map(); for (const [name, db] of this.databases) { - handlers.set(name, await db.deploy(nano, suffix)); + if (!deployment.dbs || deployment.dbs.includes(name)) { + handlers.set(name, await db.deploy(deployment)); + } } return handlers; } - async deployTo(deployment: string) { - const dObj = this.context.deployments[deployment]; - if (!dObj) - throw new Error(`Deployment object for key ${deployment} not found.`); - return await this.deploy(getNano(dObj.url, dObj.auth), dObj.suffix); - } - deployOnChanges(nano: ServerScope) { this.watcher.on("all", async (listener, path) => { try {