diff --git a/playground/dico.config.json b/playground/dico.config.json index 699e84a..5479740 100644 --- a/playground/dico.config.json +++ b/playground/dico.config.json @@ -6,11 +6,46 @@ "src/**/*.vue" ], "schema": { - "foo": { + "js": { + "foo": "", "bar": "", - "baz": "", - "qux": {} + "nested": { + "foo": "", + "bar": "" + } + }, + "jsx": { + "foo": "", + "bar": "", + "nested": { + "foo": "", + "bar": "" + } + }, + "ts": { + "foo": "", + "bar": "", + "nested": { + "foo": "", + "bar": "" + } + }, + "tsx": { + "foo": "", + "bar": "", + "nested": { + "foo": "", + "bar": "" + } + }, + "vue": { + "foo": "", + "bar": "", + "nested": { + "foo": "", + "bar": "" + } } }, - "updated_at": "2021-06-01T10:00:08.413473+00:00" + "updated_at": "2021-06-03T08:19:40.516881+00:00" } diff --git a/src/cli.ts b/src/cli.ts index 41ee0b3..3f2afc4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -30,6 +30,7 @@ cli .command("init", "Init a dico in your project") .option("-f, --force", "Override existing `dico.config.json`") .action(async options => { + // TODO: Check role? / handle 401 await middlewares.signedInOnly(); await commands.init(cli, options); }); @@ -39,6 +40,19 @@ cli.command("build", "Build current project dico").action(async options => { await commands.build(cli, options); }); +cli + .command("push", "Push current dico to Dico.app") + .option( + "-b, --build", + "Also build current project dico (performs `dico build` before pushing)" + ) + .option("-f, --force", "Force push, even if not in sync (not recommended)") + .action(async options => { + // TODO: Check role? / handle 401 + await middlewares.signedInOnly(); + await commands.push(cli, options); + }); + cli.version(VERSION); cli.help(commands.help); diff --git a/src/commands/build.ts b/src/commands/build.ts index 2d867cc..2fed879 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -143,7 +143,7 @@ const buildDico = new Listr( export const build = async ( _: CAC, - __: { [key: string]: never } + options: { [key: string]: unknown } ): Promise => { if (!dicojson.exists()) { logger.error(messages.DicoJSONNotFound, dicojson.getPath()); diff --git a/src/commands/default.ts b/src/commands/default.ts index d44c25a..4fc5d3e 100644 --- a/src/commands/default.ts +++ b/src/commands/default.ts @@ -1,14 +1,13 @@ import { CAC } from "cac"; import exit from "exit"; -import { NAME } from "../const"; import { logger } from "../lib"; import * as messages from "../messages"; -export const _default = (cli: CAC, _: { [key: string]: never }): void => { +export const _default = (cli: CAC, _: { [key: string]: unknown }): void => { const command = cli.args.join(" "); if (command) { - logger.error(messages.InvalidCommand, command, NAME); + logger.error(messages.InvalidCommand, command); exit(1); } else { cli.outputHelp(); diff --git a/src/commands/index.ts b/src/commands/index.ts index 794c2a6..afc1c55 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -4,6 +4,7 @@ export * from "./whoami"; export * from "./init"; export * from "./build"; +export * from "./push"; export * from "./help"; export * from "./default"; diff --git a/src/commands/init.ts b/src/commands/init.ts index 5679935..ab8d842 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -113,7 +113,7 @@ const initConfig = { export const init = async ( _: CAC, - options: { [key: string]: never } + options: { [key: string]: unknown } ): Promise => { if (!options.force && dicojson.exists()) { logger.error(messages.DicoJSONAlreadyExists, dicojson.getPath()); diff --git a/src/commands/push.ts b/src/commands/push.ts new file mode 100644 index 0000000..79b3374 --- /dev/null +++ b/src/commands/push.ts @@ -0,0 +1,130 @@ +import chalk from "chalk"; +import { CAC } from "cac"; +// @ts-expect-error it's our fork and we don't care +import UpdateRenderer from "@lihbr/listr-update-renderer"; +import Listr, { ListrTaskWrapper } from "listr"; +import { Observable } from "rxjs"; + +import { lineBreak, logger } from "../lib"; +import * as dicojson from "../core/dicojson"; +import * as client from "../core/client"; +import * as messages from "../messages"; +import exit from "exit"; +import { DEFAULT_TIMEOUT, JSON_FILE } from "../const"; +import { ConfigJSON, ProjectKey } from "../types"; +import { build } from "./build"; + +class PushError extends Error {} + +interface PushDicoContext { + config: ConfigJSON; + force: boolean; +} + +const pushDico = new Listr( + [ + { + title: "Checking production dico status...", + // @ts-expect-error listr types are broken + task: (ctx: PushDicoContext, task: ListrTaskWrapper) => + new Observable(observer => { + Promise.all([ + client.structure.select.byDicoSlug.run(ctx.config.dico), + new Promise(resolve => setTimeout(resolve, DEFAULT_TIMEOUT)) + ]) + .then(([structure, _]) => { + if (structure.updated_at !== ctx.config.updated_at) { + if (!ctx.force) { + observer.error( + new PushError(messages.ProductionDicoNotInSync) + ); + } else { + task.title = `${messages.ProductionDicoNotInSync}, ignoring because of \`force\` flag!`; + } + } + + task.title = `Production dico is in sync with local \`${JSON_FILE}\``; + + observer.complete(); + }) + .catch(error => { + observer.error(error); + }); + }) + }, + { + title: "Uploading new schema to Dico.app...", + // @ts-expect-error listr types are broken + task: (ctx: PushDicoContext, task: ListrTaskWrapper) => + new Observable(observer => { + Promise.all([ + client.structure.update.byDicoSlug.schema.run( + ctx.config.dico, + ctx.config.schema + ), + new Promise(resolve => setTimeout(resolve, DEFAULT_TIMEOUT)) + ]) + .then(([structure, _]) => { + // Update config file + const config = dicojson.read(); + config.updated_at = structure.updated_at; + dicojson.write(config); + + task.title = "New schema uploaded"; + observer.complete(); + }) + .catch(error => { + observer.error(error); + }); + }) + } + ], + { renderer: UpdateRenderer } +); + +export const push = async ( + cli: CAC, + options: { [key: string]: unknown } +): Promise => { + if (!dicojson.exists()) { + logger.error(messages.DicoJSONNotFound, dicojson.getPath()); + exit(1); + } + + if (options.force) { + logger.warn(messages.CommandWithForce, "init"); + } else { + lineBreak(); + } + + if (options.build) { + logger.info(messages.CommandWithFlagCommand, "push", "build", "build"); + await build(cli, {}); + logger.info(messages.NowStartingCommand, "push"); + lineBreak(); + } + + try { + await pushDico.run({ + config: dicojson.read(), + force: !!options.force + }); + } catch (error) { + // Handle conflicts error + if (error instanceof PushError && error.message.includes("not in sync")) { + logger.error(`${error.message}:`); + + logger.info( + `Sync errors happen when your \`${JSON_FILE}\` file is older than the one available on Dico.app\n Try merging your branch with the most up-to-date one on git before trying again\n Alternatively you can use the \`force\` flag to bypass sync errors (not recommended)` + ); + lineBreak(); + + exit(1); + } else { + throw error; + } + } + + logger.success(messages.CommandSuccessful, "push"); + lineBreak(); +}; diff --git a/src/commands/signout.ts b/src/commands/signout.ts index ad56a1a..805947e 100644 --- a/src/commands/signout.ts +++ b/src/commands/signout.ts @@ -2,7 +2,7 @@ import { CAC } from "cac"; import { lineBreak, logger } from "../lib"; import * as user from "../core/user"; -export const signout = (_: CAC, __: { [key: string]: never }): void => { +export const signout = (_: CAC, __: { [key: string]: unknown }): void => { user.signout(); lineBreak(); logger.success("Logged out\n"); diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index db5b18f..ad1c013 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -6,7 +6,7 @@ import * as messages from "../messages"; export const whoami = async ( _: CAC, - __: { [key: string]: never } + __: { [key: string]: unknown } ): Promise => { if (await user.isSignedIn()) { const { user: signedInUser } = dicorc.read(); diff --git a/src/core/client.ts b/src/core/client.ts index 01bb9ad..8a68ea1 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -2,7 +2,7 @@ import _fetch from "node-fetch"; import { API_ENDPOINT } from "../const"; import * as dicorc from "./dicorc"; import * as messages from "../messages"; -import { ConfigRC, Dico, Structure } from "../types"; +import { ConfigJSON, ConfigRC, Dico, Structure } from "../types"; class ClientError extends Error {} @@ -23,6 +23,7 @@ export const fetch = async ( authorization: `Bearer ${token}` }; + // Populate headers with headers from options if (options && options.headers && typeof options.headers === "object") { Object.entries(options.headers).map(([k, v]) => { if (typeof v === "string") { @@ -31,6 +32,12 @@ export const fetch = async ( }); } + // Ensure body is JSON + if (options && options.body) { + options.body = JSON.stringify(options.body); + headers["content-type"] = "application/json"; + } + const response = await _fetch( `${dicorc.read().endpoint || API_ENDPOINT}${endpoint}`, { @@ -85,5 +92,23 @@ export const structure = { return data; } } + }, + update: { + byDicoSlug: { + schema: { + run: async ( + slug: string, + schema: ConfigJSON["schema"], + token?: string + ): Promise => { + const { data } = await fetch(`/structure/${slug}`, token, { + method: "PUT", + body: { schema } + }); + + return data; + } + } + } } }; diff --git a/src/messages.ts b/src/messages.ts index 7d6def2..65d37ca 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,7 +1,6 @@ -import { NAME } from "./const"; +import { JSON_FILE, NAME } from "./const"; -export const InvalidCommand = - "Invalid command: `%s`, run `%s --help` for all commands"; +export const InvalidCommand = `Invalid command: \`%s\`, run \`${NAME} --help\` for all commands`; export const NotSignedIn = "Not logged in! Log in with command `login `"; export const SignedInAs = "Logged in as `%s <%s>`"; @@ -13,12 +12,14 @@ export const InvalidTokenFormat = export const NoDicoFoundDeveloper = "No dico found where you at least have the `Developer` role"; -export const DicoJSONNotFound = "`dico.config.json` not found at `%s`"; -export const DicoJSONAlreadyExists = - "`dico.config.json` already exists at `%s`!"; +export const DicoJSONNotFound = `\`${JSON_FILE}\` not found at \`%s\``; +export const DicoJSONAlreadyExists = `\`${JSON_FILE}\` already exists at \`%s\`!`; -export const NoSourceFilesFound = - "No source files found with current `sources` option. Double-check your `dico.config.json` and try again!"; +export const NoSourceFilesFound = `No source files found with current \`sources\` option. Double-check your \`${JSON_FILE}\` and try again!`; export const CommandSuccessful = `\`${NAME} %s\` successful!`; export const CommandWithForce = `Running \`${NAME} %s\` with \`force\` flag`; +export const CommandWithFlagCommand = `Running \`${NAME} %s\` with \`%s\` flag, performing \`${NAME} %s\` first:`; +export const NowStartingCommand = `Now starting command \`${NAME} %s\`:`; + +export const ProductionDicoNotInSync = `Production dico is not in sync with local \`${JSON_FILE}\``;