You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
5.5 KiB
173 lines
5.5 KiB
// you may not need all of these but they come up a lot
|
|
import fs from "fs";
|
|
import assert from "assert";
|
|
import logging from '../lib/logging.js';
|
|
import { mkdir, write } from "../lib/builderator.js";
|
|
import glob from "fast-glob";
|
|
import { Database } from "duckdb-async";
|
|
import { UserPaymentProduct, User, Product, Payment } from "../lib/models.js";
|
|
import slugify from "slugify";
|
|
|
|
const log = logging.create(import.meta.url);
|
|
|
|
export const description = "Is used to load the data found in the django dump to the database."
|
|
|
|
// your command uses the npm package commander's options format
|
|
export const options = [
|
|
["--mode-split", "Split a django dump into multiple files per table with pk=id"],
|
|
["--mode-analyze", "Run in analysis mode, which loads the tables into duckdb."],
|
|
["--mode-import", "Use the split files in --split-dir to import"],
|
|
["--input <string>", "A string option with a default."],
|
|
]
|
|
|
|
// put required options in the required variable
|
|
export const required = [
|
|
["--split-dir <string>", "Directory where the split .json files go", "data_dump"],
|
|
["--duckdb <string>", "The duckdb to create.", "dump.duckdb"],
|
|
]
|
|
|
|
// handy function for checking things are good and aborting
|
|
const check = (test, fail_message) => {
|
|
if(!test) {
|
|
log.error(fail_message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export const mode_split = async (opts) => {
|
|
// it's easier to debug options with console
|
|
const raw_data = JSON.parse(fs.readFileSync(opts.input));
|
|
|
|
const tables = {};
|
|
|
|
for(let row of raw_data) {
|
|
assert(row.fields.id === undefined, `Bad ID ${JSON.stringify(row)}`);
|
|
row.fields.id = row.pk; // why did django do this? so weird
|
|
|
|
if(tables[row.model] === undefined) {
|
|
// first one
|
|
tables[row.model] = [row.fields];
|
|
} else {
|
|
tables[row.model].push(row.fields);
|
|
}
|
|
}
|
|
|
|
console.log(opts);
|
|
mkdir(opts.splitDir);
|
|
|
|
for(let [name, data] of Object.entries(tables)) {
|
|
const out_file = `${opts.splitDir}/${name}.json`;
|
|
console.log("WRITE", out_file);
|
|
write(out_file, JSON.stringify(data, null, 4));
|
|
}
|
|
|
|
// due to how async/await works it's just easier to manually exit with exit codes
|
|
process.exit(0);
|
|
}
|
|
|
|
export const mode_analyze = async (opts) => {
|
|
console.log("ANALYZE", opts);
|
|
const db = new Database(opts.duckdb);
|
|
|
|
// glob the files in the splitDir
|
|
const split_dir = await glob(`${opts.splitDir}/*.json`);
|
|
|
|
for(let file_name of split_dir) {
|
|
const [junk, table_name, ext] = file_name.split('.');
|
|
|
|
console.log(">> TABLE", table_name, "CREATE FROM", file_name);
|
|
await db.exec(`CREATE TABLE "${table_name}" AS SELECT * FROM "${file_name}"`);
|
|
// syntax doesn't follow SQL for replace ? with variable
|
|
}
|
|
|
|
await db.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
export const mode_import = async (opts) => {
|
|
console.log("IMPORT", opts);
|
|
|
|
const db = new Database(opts.duckdb);
|
|
// DDB go through each product
|
|
|
|
const products = await db.all("SELECT * FROM PRODUCT");
|
|
|
|
for(let product of products) {
|
|
const my_product = await Product.insert({
|
|
created_at: product.created_on,
|
|
title: product.title,
|
|
description: product.description,
|
|
price: product.base_price,
|
|
currency: "USD",
|
|
currency_symbol: "$",
|
|
active: product.active,
|
|
slug: slugify(product.title, {lower: true, strict: true}),
|
|
short_blurb: product.description,
|
|
docs_url: product.private_location,
|
|
poster: product.poster,
|
|
category: "Python",
|
|
created_by: "Zed A. Shaw",
|
|
preorder: 1
|
|
});
|
|
|
|
const purchases = await db.all(`SELECT * FROM PURCHASE WHERE product=${product.id} AND state='PAID'`);
|
|
|
|
console.log("PRODUCT", my_product.title, "PURCHASES", purchases.length);
|
|
|
|
for(let purchase of purchases) {
|
|
const cust_q = await db.all(`SELECT * FROM CUSTOMER WHERE id=${purchase.customer}`);
|
|
const customer = cust_q[0];
|
|
const fake_password = User.random_hex(10);
|
|
|
|
let user = await User.register({
|
|
created_at: customer.created_on,
|
|
initials: "",
|
|
full_name: customer.full_name,
|
|
password: fake_password,
|
|
password_repeat: fake_password,
|
|
email: customer.email,
|
|
unsubscribe: !customer.promotable,
|
|
unsubscribed_on: null,
|
|
});
|
|
|
|
if(!user) {
|
|
console.log("USER EXISTS LOADING", customer.email);
|
|
user = await User.first({email: customer.email});
|
|
} else {
|
|
console.log("ADDED", user.email, user.id, "PASS", fake_password);
|
|
}
|
|
|
|
// create payment and then connect with upp
|
|
const payment = await Payment.insert({
|
|
created_at: purchase.created_on,
|
|
system: purchase.purchase_system.toLowerCase(),
|
|
status: "complete",
|
|
internal_id: Payment.gen_internal_id(),
|
|
sys_created_on: purchase.ended_on ? purchase.ended_on : purchase.created_on,
|
|
sys_primary_id: purchase.service_data || "",
|
|
sys_secondary_id: purchase.fsm_state,
|
|
user_id: purchase.customer,
|
|
status_reason: "imported",
|
|
});
|
|
|
|
const upp = await UserPaymentProduct.finish_purchase(user, payment, my_product);
|
|
}
|
|
}
|
|
|
|
await db.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
export const main = async (opts) => {
|
|
if(opts.modeSplit) {
|
|
check(opts.input !== undefined, "--input required for --mode-split");
|
|
await mode_split(opts);
|
|
} else if(opts.modeAnalyze) {
|
|
await mode_analyze(opts);
|
|
} else if(opts.modeImport) {
|
|
await mode_import(opts);
|
|
} else {
|
|
console.error("USAGE: need one of --mode-analyze, --mode-import, or --mode-sync. BACKUP THE DB FIRST.");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|