Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
echo-bravo-yahoo committed Dec 19, 2024
1 parent ac7438f commit c9cb0b7
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 264 deletions.
10 changes: 9 additions & 1 deletion src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
44 changes: 28 additions & 16 deletions src/commands/init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.");

Expand All @@ -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",
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
Expand All @@ -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,
});

Expand Down Expand Up @@ -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"),
"",
),
]);
Expand All @@ -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,
};
116 changes: 0 additions & 116 deletions src/lib/schema/demo-collection-schema.fsl
Original file line number Diff line number Diff line change
@@ -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<Order> = ( 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<Category>
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<Product> = (category => Product.byCategory(category))

unique [.name]

index byName {
terms [.name]
}
}

collection Order {
customer: Ref<Customer>
status: "cart" | "processing" | "shipped" | "delivered"
createdAt: Time

compute items: Set<OrderItem> = (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<Order>
product: Ref<Product>
quantity: Int

unique [.order, .product]
check positiveQuantity (orderItem => orderItem.quantity > 0)

index byOrder {
terms [.order]
values [.product, .quantity]
}

index byOrderAndProduct {
terms [.order, .product]
}
}
131 changes: 0 additions & 131 deletions src/lib/schema/demo-function-schema.fsl
Original file line number Diff line number Diff line change
@@ -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.")
}
}

0 comments on commit c9cb0b7

Please sign in to comment.