diff --git a/argument-checking.ts b/argument-checking.ts new file mode 100644 index 0000000..168f255 --- /dev/null +++ b/argument-checking.ts @@ -0,0 +1,22 @@ +import { CommanderStatic } from "commander"; + +export const assertModelArgument = (program: CommanderStatic) => { + if (!program.model) { + throw new Error("You must pass a path to the model with the -m command"); + } + return true; +} + +export const assertUrlArgument = (program: CommanderStatic) => { + if (!program.url) { + throw new Error("You must pass an url for the Zap server with the -u command"); + } + return true; +} + +export const assertKeyArgument = (program: CommanderStatic) => { + if (!program.adminKey) { + console.error("You must pass an admin key with the -k command"); + } + return true; +} diff --git a/hasura/hasura.ts b/hasura/hasura.ts deleted file mode 100644 index 84b7d00..0000000 --- a/hasura/hasura.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { readFileSync } from "fs"; -import { - isMetadata, - mergePermissionsFromModel, - replaceMetadata, - getMetadata -} from "./metadata"; -import { trackTables } from "./track"; - -const updateHasuraMetadata = async ( - model: { [key: string]: any }, - url: string, - adminKey: string -) => { - const metadata = await getMetadata(url, adminKey); - if (!isMetadata(metadata)) { - throw new TypeError( - "metadata retrieved from Hasura does not conform to expected type" - ); - } - try { - console.log( - await replaceMetadata( - url, - mergePermissionsFromModel(metadata, model), - adminKey - ) - ); - } catch (err) { - console.error("Error trying to replace metadata", err); - } -}; - -const main = async () => { - const modelFile = process.argv[process.argv.length - 3]; - const url = process.argv[process.argv.length - 2]; - const adminKey = process.argv[process.argv.length - 1]; - if (!modelFile || !url) { - console.error(` - Please provide path to model file and Hasura server root URL as arguments. - Example: ts-node index.ts model.json http://localhost:8080 - `); - } - const model = JSON.parse(readFileSync(modelFile).toString()); - - try { - await trackTables( - model.application.name, - Object.keys(model.model.tables), - url, - adminKey - ); - console.log("Tables tracked"); - } catch (err) { - console.error("Error tracking tables: ", err); - } - try { - await updateHasuraMetadata(model, url, adminKey); - console.log("Metadatas updated"); - } catch (err) { - console.error("Error updating metadatas: ", err); - } -}; - -main(); diff --git a/hasura/metadata.ts b/hasura/metadata.ts index 8aa94f4..fe7b020 100644 --- a/hasura/metadata.ts +++ b/hasura/metadata.ts @@ -1,5 +1,6 @@ import fetch from "node-fetch"; import { isArray } from "util"; +import { readFileSync } from "fs"; export type HasuraMetadata = { functions: any[]; // anys for now @@ -25,7 +26,31 @@ export type HasuraMetadataTable = { }[]; }; -export const getMetadata = async (url: string, adminKey: string) => { +export const updateMetadata = async ( + modelPath: string, + url: string, + adminKey: string +) => { + const model = JSON.parse(readFileSync(modelPath).toString()); + const metadata = await getMetadata(url, adminKey); + if (!isMetadata(metadata)) { + throw new TypeError( + "metadata retrieved from Hasura does not conform to expected type" + ); + } + try { + await replaceMetadata( + url, + mergePermissionsFromModel(metadata, model), + adminKey + ) + console.log("Metadatas updated sucessfuly"); + } catch (err) { + throw new Error(`Error trying to replace metadata: ${err}`); + } +}; + +const getMetadata = async (url: string, adminKey: string) => { const reponse = await fetch(`${url}/v1/query`, { method: "POST", headers: { @@ -43,7 +68,7 @@ export const getMetadata = async (url: string, adminKey: string) => { return await reponse.json(); }; -export const mergePermissionsFromModel = ( +const mergePermissionsFromModel = ( metadata: HasuraMetadata, model: any ): HasuraMetadata => { @@ -66,7 +91,7 @@ export const mergePermissionsFromModel = ( }; }; -export const replaceMetadata = async ( +const replaceMetadata = async ( url: string, newMetadata: HasuraMetadata, adminKey: string @@ -89,7 +114,6 @@ export const replaceMetadata = async ( )}` ); } - return await response.json(); }; export const isMetadata = (metadata: any): metadata is HasuraMetadata => diff --git a/hasura/track.ts b/hasura/track.ts index a35580b..97f2efb 100644 --- a/hasura/track.ts +++ b/hasura/track.ts @@ -1,35 +1,42 @@ import fetch from "node-fetch"; +import { readFileSync } from "fs"; export const trackTables = async ( - schema: string, - tables: string[], + modelPath: string, url: string, adminKey: string ) => { - return Promise.all( - tables.map(async table => { - const response = await fetch(`${url}/v1/query`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-hasura-admin-secret": adminKey - }, - body: JSON.stringify({ - type: "track_table", - args: { - schema: schema, - name: table - } - }) - }); - if (!response.ok) { - throw new Error( - `${response.status} -> ${response.statusText} : ${JSON.stringify( - await response.json() - )}` - ); - } - return await response.json(); - }) - ); + const model = JSON.parse(readFileSync(modelPath).toString()); + const schema = model.application.name; + try { + await Promise.all( + Object.keys(model.model.tables).map(async table => { + const response = await fetch(`${url}/v1/query`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-hasura-admin-secret": adminKey + }, + body: JSON.stringify({ + type: "track_table", + args: { + schema: schema, + name: table + } + }) + }); + if (!response.ok) { + throw new Error( + `${response.status} -> ${response.statusText} : ${JSON.stringify( + await response.json() + )}` + ); + } + return await response.json(); + }) + ); + console.log("Tables tracked sucessfuly"); + } catch (err) { + throw new Error(`Error trying to track tables ${err}`); + } }; diff --git a/index.ts b/index.ts index 0fda504..d1b27ec 100644 --- a/index.ts +++ b/index.ts @@ -1,76 +1,66 @@ -import { readFileSync } from "fs"; -import { Client } from "pg"; -import { write } from "./write"; -import { read } from "./read"; -import { diff, DiffResult } from "./diff"; -import { prepare } from "./prepare"; -import { ModelDefinition } from "./sql"; +import program from "commander"; +import { applyModel } from "./model/model"; +import { trackTables } from "./hasura/track"; +import { + assertModelArgument, + assertUrlArgument, + assertKeyArgument +} from "./argument-checking"; +import { updateMetadata } from "./hasura/metadata"; -const applyModel = async (model: any, options: { drop?: boolean } = {}) => { - const liveDatabaseConfig = { - client: "pg", - connection: { - host: process.env.PGHOST || "", - port: Number(process.env.PGPORT) || 5432, - user: process.env.PGUSER || "", - password: process.env.PASSWORD || "", - database: process.env.PGDATABASE || "" - } - }; - - const client = new Client(liveDatabaseConfig.connection); - await client.connect(); +const main = async () => { + program + .option( + "--apply-model", + "Apply model of the json file passed as argument in the -m command" + ) + .option( + "--drop", + "Specify wether to drop the database before applying the model" + ) + .option("--track-tables", "Track tables from the model json file") + .option( + "--update-metadatas", + "Updates metadatas of the Zap server with permissions defined in the model json file" + ) + .option("-m, --model ", "Path to the model json file") + .option("-u, --url ", "URL of the Zap server") + .option( + "-k, --admin-key ", + "Admin key used to update the Zap server" + ); try { - await prepare(client); - const live: ModelDefinition = options.drop - ? { tables: {} } - : await read(client, model.application.name); - const diffResult = diff(live, model.model); - logDiffResult(model, diffResult); - if (diffResult.problems.length > 0) { - return; + program.parse(process.argv); + if (program.applyModel) { + if (!assertModelArgument(program)) { + return; + } + await applyModel(program.model, { drop: program.drop }); } - await write( - client, - model.application.name, - model.model, - diffResult.operations, - { drop: options.drop } - ); - } finally { - await client.end(); - } -}; - -const logDiffResult = (model: any, { operations, problems }: DiffResult) => { - if (operations.length === 0 && problems.length === 0) { - console.log(model.application.name, "no operations to apply"); - } - for (const operation of operations) { - console.log( - model.application.name, - operation.type, - operation.type === "createTable" - ? operation.name - : `${operation.table}.${operation.name}` - ); - } - for (const problem of problems) { - console.error( - model.application.name, - "problem", - problem.kind, - problem.entityName, - problem.problem - ); + if (program.updateMetadatas) { + if ( + !assertModelArgument(program) || + !assertUrlArgument(program) || + !assertKeyArgument(program) + ) { + return; + } + await updateMetadata(program.model, program.url, program.adminKey); + } + if (program.trackTables) { + if ( + !assertModelArgument(program) || + !assertUrlArgument(program) || + !assertKeyArgument(program) + ) { + return; + } + await trackTables(program.model, program.url, program.adminKey); + } + console.log("All done") + } catch (err) { + console.error(err); } }; -const main = async () => { - const modelFile = process.argv[process.argv.length - 1]; - const drop = process.argv.includes("--drop"); - const model = JSON.parse(readFileSync(modelFile).toString()); - await applyModel(model, { drop }); -}; - main(); diff --git a/diff.ts b/model/diff.ts similarity index 100% rename from diff.ts rename to model/diff.ts diff --git a/model/model.ts b/model/model.ts new file mode 100644 index 0000000..21ff260 --- /dev/null +++ b/model/model.ts @@ -0,0 +1,72 @@ +import { readFileSync } from "fs"; +import { Client } from "pg"; +import { write } from "./write"; +import { read } from "./read"; +import { diff, DiffResult } from "./diff"; +import { prepare } from "./prepare"; +import { ModelDefinition } from "./sql"; + +export const applyModel = async ( + modelPath: string, + options: { drop?: boolean } = {} +) => { + const model = JSON.parse(readFileSync(modelPath).toString()); + const liveDatabaseConfig = { + client: "pg", + connection: { + host: process.env.PGHOST || "", + port: Number(process.env.PGPORT) || 5432, + user: process.env.PGUSER || "", + password: process.env.PASSWORD || "", + database: process.env.PGDATABASE || "" + } + }; + const client = new Client(liveDatabaseConfig.connection); + try { + await client.connect(); + await prepare(client); + const live: ModelDefinition = options.drop + ? { tables: {} } + : await read(client, model.application.name); + const diffResult = diff(live, model.model); + logDiffResult(model, diffResult); + if (diffResult.problems.length > 0) { + return; + } + await write( + client, + model.application.name, + model.model, + diffResult.operations, + { drop: options.drop } + ); + await client.end(); + console.log("Model successfuly applied"); + } catch (err) { + throw new Error(`Error trying to apply model ${err}`); + } +}; + +const logDiffResult = (model: any, { operations, problems }: DiffResult) => { + if (operations.length === 0 && problems.length === 0) { + console.log(model.application.name, "no operations to apply"); + } + for (const operation of operations) { + console.log( + model.application.name, + operation.type, + operation.type === "createTable" + ? operation.name + : `${operation.table}.${operation.name}` + ); + } + for (const problem of problems) { + console.error( + model.application.name, + "problem", + problem.kind, + problem.entityName, + problem.problem + ); + } +}; diff --git a/prepare.ts b/model/prepare.ts similarity index 100% rename from prepare.ts rename to model/prepare.ts diff --git a/read.ts b/model/read.ts similarity index 100% rename from read.ts rename to model/read.ts diff --git a/sql.test.ts b/model/sql.test.ts similarity index 100% rename from sql.test.ts rename to model/sql.test.ts diff --git a/sql.ts b/model/sql.ts similarity index 100% rename from sql.ts rename to model/sql.ts diff --git a/write.ts b/model/write.ts similarity index 100% rename from write.ts rename to model/write.ts diff --git a/package-lock.json b/package-lock.json index 88a5f2a..b58d406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,11 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -125,7 +130,8 @@ "dotenv": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", - "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==" + "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==", + "dev": true }, "es-abstract": { "version": "1.13.0", diff --git a/package.json b/package.json index 8552195..5015fa4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "ts-node -r dotenv/config index.ts", - "start:hasura": "ts-node -r dotenv/config ./hasura/hasura.ts" + "start": "ts-node -r dotenv/config index.ts" }, "keywords": [], "author": "", @@ -22,6 +21,7 @@ "dotenv": "^8.1.0" }, "dependencies": { + "commander": "^3.0.2", "node-fetch": "^2.6.0", "pg": "^7.11.0" }