From c9cb0b715884275ec600da9985cc5ecfb274c459 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 18 Dec 2024 16:39:21 -0800 Subject: [PATCH] wip --- src/cli.mjs | 10 +- src/commands/init.mjs | 44 +++++--- src/lib/schema/demo-collection-schema.fsl | 116 ------------------- src/lib/schema/demo-function-schema.fsl | 131 ---------------------- 4 files changed, 37 insertions(+), 264 deletions(-) diff --git a/src/cli.mjs b/src/cli.mjs index 50e78add..34802b00 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -202,7 +202,15 @@ function buildYargs(argvInput) { "Components to emit logs for. Overrides the --verbosity flag. Pass values as a space-separated list. Ex: --verbose-component fetch error.", type: "array", default: [], - choices: ["argv", "completion", "config", "creds", "error", "fetch"], + choices: [ + "argv", + "command", + "completion", + "config", + "creds", + "error", + "fetch", + ], group: "Debug:", }, verbosity: { diff --git a/src/commands/init.mjs b/src/commands/init.mjs index 4c77d52f..c37014cf 100644 --- a/src/commands/init.mjs +++ b/src/commands/init.mjs @@ -18,15 +18,24 @@ import { listDatabasesWithAccountAPI } from "./database/list.mjs"; // TODO: handle error/exit case cleanly async function doInit(argv) { + const logger = container.resolve("logger"); const getters = [getDatabaseRunnable, getProjectRunnable, getKeyRunnable]; const runnables = []; let allChoices = {}; // in order, gather user input (choices) - for (const [index, getter] of Object.entries(getters)) { - // eslint-disable-next-line no-await-in-loop - runnables[index] = await getter(argv, allChoices); - allChoices = { ...allChoices, ...runnables[index].choices }; + try { + for (const [index, getter] of Object.entries(getters)) { + // eslint-disable-next-line no-await-in-loop + runnables[index] = await getter(argv, allChoices); + allChoices = { ...allChoices, ...runnables[index].choices }; + } + } catch (e) { + logger.stderr( + `Failed while gathering user input. No changes have been made to the filesystem or any Fauna databases.`, + "command", + ); + throw e; } // in order, do tasks based on user input (choices) @@ -70,7 +79,10 @@ async function getDatabaseRunnable(argv /*, priorChoices*/) { ], }); - if (runnable.choices.createNewDb !== "new") return runnable; + if (runnable.choices.createNewDb !== "new") { + runnable.choices.dbName = runnable.choices.createNewDb.split("/")[1]; + return runnable; + } logger.stdout("Ok! We'll create a new database."); @@ -94,7 +106,7 @@ async function getDatabaseRunnable(argv /*, priorChoices*/) { const otherSettings = await inquirer.checkbox({ message: "Configure any other settings", choices: [ - // TODO: this could fail with role issues? + // TODO: this could fail with plan / role issues? skipping. // { // name: "Backups", // value: "backup", @@ -195,9 +207,6 @@ async function getKeyRunnable(argv, priorChoices) { ], }); - // this is a little white lie - we don't actually run this FQL since we call frontdoor instead - // but we run the equivalent of it - // actually, do we just... run the FQL? let's just run the FQL runnable.fql = `Key.create({ role: "${runnable.choices.role}", data: { @@ -206,14 +215,16 @@ async function getKeyRunnable(argv, priorChoices) { })`; runnable.runner = async ({ choices, fql }) => { - logger.stdout(`Creating key ${choices.dbName} by running FQL query:`); + logger.stdout(`Creating key "${choices.keyName}" by running FQL query:`); logger.stdout(fql); - await runQueryFromString({ + const response = await runQueryFromString({ expression: fql, secret: await getSecret(), url: "https://db.fauna.com", }); - logger.stdout(`Created key ${choices.dbName}.`); + logger.stdout( + `Created key "${choices.keyName}" with value "${response.data.secret}".`, + ); }; return runnable; @@ -238,7 +249,7 @@ async function getProjectRunnable(argv, priorChoices) { if (!shouldCreateProjectDirectory) return runnable; runnable.choices.dirName = await inquirer.input({ - message: `FSL files are stored in a project directory and are specific to the database "${priorChoices.dbName}". What would you like to name this project directory?`, + message: `FSL files are stored in a project directory and are specific to the database "${priorChoices.dbName}". What would you like to name this project directory? In the next step, you will choose where to put the directory.`, default: priorChoices.dbName, }); @@ -327,11 +338,11 @@ async function getProjectRunnable(argv, priorChoices) { // new db with no demo data? create two blank schema files await Promise.all([ fsp.writeFile( - path.join(__dirname, "../lib/schema/demo-collection-schema.fsl"), + path.join(__dirname, "../src/lib/schema/demo-collection-schema.fsl"), "", ), fsp.writeFile( - path.join(__dirname, "../lib/schema/demo-function-schema.fsl"), + path.join(__dirname, "../src/lib/schema/demo-function-schema.fsl"), "", ), ]); @@ -346,7 +357,8 @@ async function getProjectRunnable(argv, priorChoices) { export default { command: "init", - describe: "Init!!", + describe: + "Configure an existing database or create a new one. Optionally creates demo data, an FSL directory, and a database key.", builder: buildInitCommand, handler: doInit, }; diff --git a/src/lib/schema/demo-collection-schema.fsl b/src/lib/schema/demo-collection-schema.fsl index e64c5cac..e69de29b 100644 --- a/src/lib/schema/demo-collection-schema.fsl +++ b/src/lib/schema/demo-collection-schema.fsl @@ -1,116 +0,0 @@ -collection Customer { - name: String - email: String - address: { - street: String, - city: String, - state: String, - postalCode: String, - country: String - } - - compute cart: Order? = (customer => Order.byCustomerAndStatus(customer, 'cart').first()) - - // Use a computed field to get the set of Orders for a customer. - compute orders: Set = ( customer => Order.byCustomer(customer)) - - // Use a unique constraint to ensure no two customers have the same email. - unique [.email] - - index byEmail { - terms [.email] - } -} - -collection Product { - name: String - description: String - // Use an Integer to represent cents. - // This avoids floating-point precision issues. - price: Int - category: Ref - stock: Int - - // Use a unique constraint to ensure no two products have the same name. - unique [.name] - check stockIsValid (product => product.stock >= 0) - check priceIsValid (product => product.price > 0) - - index byCategory { - terms [.category] - } - - index sortedByCategory { - values [.category] - } - - index byName { - terms [.name] - } - - index sortedByPriceLowToHigh { - values [.price, .name, .description, .stock] - } -} - -collection Category { - name: String - description: String - compute products: Set = (category => Product.byCategory(category)) - - unique [.name] - - index byName { - terms [.name] - } -} - -collection Order { - customer: Ref - status: "cart" | "processing" | "shipped" | "delivered" - createdAt: Time - - compute items: Set = (order => OrderItem.byOrder(order)) - compute total: Number = (order => order.items.fold(0, (sum, orderItem) => { - let orderItem: Any = orderItem - if (orderItem.product != null) { - sum + orderItem.product.price * orderItem.quantity - } else { - sum - } - })) - payment: { *: Any } - - check oneOrderInCart (order => { - Order.byCustomerAndStatus(order.customer, "cart").count() <= 1 - }) - - // Define an index to get all orders for a customer. Orders will be sorted by - // createdAt in descending order. - index byCustomer { - terms [.customer] - values [desc(.createdAt), .status] - } - - index byCustomerAndStatus { - terms [.customer, .status] - } -} - -collection OrderItem { - order: Ref - product: Ref - quantity: Int - - unique [.order, .product] - check positiveQuantity (orderItem => orderItem.quantity > 0) - - index byOrder { - terms [.order] - values [.product, .quantity] - } - - index byOrderAndProduct { - terms [.order, .product] - } -} diff --git a/src/lib/schema/demo-function-schema.fsl b/src/lib/schema/demo-function-schema.fsl index c2b8e311..e69de29b 100644 --- a/src/lib/schema/demo-function-schema.fsl +++ b/src/lib/schema/demo-function-schema.fsl @@ -1,131 +0,0 @@ -function createOrUpdateCartItem(customerId, productName, quantity) { - // Find the customer by id, using the ! operator to assert that the customer exists. - // If the customer does not exist, fauna will throw a document_not_found error. - let customer = Customer.byId(customerId)! - // There is a unique constraint on [.name] so this will return at most one result. - let product = Product.byName(productName).first() - - // Check if the product exists. - if (product == null) { - abort("Product does not exist.") - } - - // Check that the quantity is valid. - if (quantity < 0) { - abort("Quantity must be a non-negative integer.") - } - - // Create a new cart for the customer if they do not have one. - if (customer!.cart == null) { - Order.create({ - status: "cart", - customer: customer, - createdAt: Time.now(), - payment: {} - }) - } - - // Check that the product has the requested quantity in stock. - if (product!.stock < quantity) { - abort("Product does not have the requested quantity in stock.") - } - - // Attempt to find an existing order item for the order, product pair. - // There is a unique constraint on [.order, .product] so this will return at most one result. - let orderItem = OrderItem.byOrderAndProduct(customer!.cart, product).first() - - if (orderItem == null) { - // If the order item does not exist, create a new one. - OrderItem.create({ - order: Order(customer!.cart!.id), - product: product, - quantity: quantity, - }) - } else { - // If the order item exists, update the quantity. - orderItem!.update({ quantity: quantity }) - } -} - -function getOrCreateCart(id) { - // Find the customer by id, using the ! operator to assert that the customer exists. - // If the customer does not exist, fauna will throw a document_not_found error. - let customer = Customer.byId(id)! - - if (customer!.cart == null) { - // Create a cart if the customer does not have one. - Order.create({ - status: 'cart', - customer: Customer.byId(id), - createdAt: Time.now(), - payment: {} - }) - } else { - // Return the cart if it already exists. - customer!.cart - } -} - -function checkout(orderId, status, payment) { - // Find the order by id, using the ! operator to assert that the order exists. - let order = Order.byId(orderId)! - - // Check that we are setting the order to the processing status. If not, we should - // not be calling this function. - if (status != "processing") { - abort("Can not call checkout with status other than processing.") - } - - // Check that the order can be transitioned to the processing status. - validateOrderStatusTransition(order!.status, "processing") - - // Check that the order has at least one order item. - if (order!.items.isEmpty()) { - abort("Order must have at least one item.") - } - - // Check that customer has a valid address. - if (order!.customer!.address == null) { - abort("Customer must have a valid address.") - } - - // Check that the order has a payment method if not provided as an argument. - if (order!.payment == null && payment == null) { - abort("Order must have a valid payment method.") - } - - // Check that the order items are still in stock. - order!.items.forEach((item) => { - let product: Any = item.product - if (product.stock < item.quantity) { - abort("One of the selected products does not have the requested quantity in stock.") - } - }) - - // Decrement the stock of each product in the order. - order!.items.forEach((item) => { - let product: Any = item.product - product.update({ stock: product.stock - item.quantity }) - }) - - // Transition the order to the processing status, update the payment if provided. - if (payment != null) { - order!.update({ status: "processing", payment: payment }) - } else { - order!.update({ status: "processing" }) - } -} - -function validateOrderStatusTransition(oldStatus, newStatus) { - if (oldStatus == "cart" && newStatus != "processing") { - // The order can only transition from cart to processing. - abort("Invalid status transition.") - } else if (oldStatus == "processing" && newStatus != "shipped") { - // The order can only transition from processing to shipped. - abort("Invalid status transition.") - } else if (oldStatus == "shipped" && newStatus != "delivered") { - // The order can only transition from shipped to delivered. - abort("Invalid status transition.") - } -} -