2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 19:39:07 +00:00
books/backend/patches/updateSchemas.ts
2022-05-31 11:39:45 +05:30

405 lines
9.7 KiB
TypeScript

import fs from 'fs/promises';
import { RawValueMap } from 'fyo/core/types';
import { Knex } from 'knex';
import path from 'path';
import { changeKeys, deleteKeys, getIsNullOrUndef, invertMap } from 'utils';
import { getCountryCodeFromCountry } from 'utils/misc';
import { Version } from 'utils/version';
import { ModelNameEnum } from '../../models/types';
import { FieldTypeEnum, Schema, SchemaMap } from '../../schemas/types';
import { DatabaseManager } from '../database/manager';
const ignoreColumns = ['keywords'];
const columnMap = { creation: 'created', owner: 'createdBy' };
const childTableColumnMap = {
parenttype: 'parentSchemaName',
parentfield: 'parentFieldname',
};
const defaultNumberSeriesMap = {
[ModelNameEnum.Payment]: 'PAY-',
[ModelNameEnum.JournalEntry]: 'JE-',
[ModelNameEnum.SalesInvoice]: 'SINV-',
[ModelNameEnum.PurchaseInvoice]: 'PINV-',
} as Record<ModelNameEnum, string>;
async function execute(dm: DatabaseManager) {
if (dm.db?.dbPath === ':memory:') {
return;
}
const sourceKnex = dm.db!.knex!;
const version = (
await sourceKnex('SingleValue')
.select('value')
.where({ fieldname: 'version' })
)?.[0]?.value;
/**
* Versions after this should have the new schemas
*/
if (version && Version.gt(version, '0.4.3-beta.0')) {
return;
}
/**
* Initialize a different db to copy all the updated
* data into.
*/
const countryCode = await getCountryCode(sourceKnex);
const destDm = await getDestinationDM(dm.db!.dbPath, countryCode);
/**
* Copy data from all the relevant tables
* the other tables will be empty cause unused.
*/
try {
await copyData(sourceKnex, destDm);
} catch (err) {
const destPath = destDm.db!.dbPath;
destDm.db!.close();
await fs.unlink(destPath);
throw err;
}
/**
* Version will update when migration completes, this
* is set to prevent this patch from running again.
*/
await destDm.db!.update(ModelNameEnum.SystemSettings, {
version: '0.5.0-beta.0',
});
/**
* Replace the database with the new one.
*/
await replaceDatabaseCore(dm, destDm);
}
async function replaceDatabaseCore(
dm: DatabaseManager,
destDm: DatabaseManager
) {
const newDbPath = destDm.db!.dbPath; // new db with new schema
const oldDbPath = dm.db!.dbPath; // old db to be replaced
await dm.db!.close();
await destDm.db!.close();
await fs.unlink(oldDbPath);
await fs.rename(newDbPath, oldDbPath);
await dm._connect(oldDbPath);
}
async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
const destKnex = destDm.db!.knex!;
const schemaMap = destDm.getSchemaMap();
await destKnex!.raw('PRAGMA foreign_keys=OFF');
await copySingleValues(sourceKnex, destKnex, schemaMap);
await copyParty(sourceKnex, destKnex, schemaMap[ModelNameEnum.Party]!);
await copyItem(sourceKnex, destKnex, schemaMap[ModelNameEnum.Item]!);
await copyChildTables(sourceKnex, destKnex, schemaMap);
await copyOtherTables(sourceKnex, destKnex, schemaMap);
await copyTransactionalTables(sourceKnex, destKnex, schemaMap);
await copyLedgerEntries(
sourceKnex,
destKnex,
schemaMap[ModelNameEnum.AccountingLedgerEntry]!
);
await copyNumberSeries(
sourceKnex,
destKnex,
schemaMap[ModelNameEnum.NumberSeries]!
);
await destKnex!.raw('PRAGMA foreign_keys=ON');
}
async function copyNumberSeries(
sourceKnex: Knex,
destKnex: Knex,
schema: Schema
) {
const values = (await sourceKnex(
ModelNameEnum.NumberSeries
)) as RawValueMap[];
const refMap = invertMap(defaultNumberSeriesMap);
for (const value of values) {
if (value.referenceType) {
continue;
}
const name = value.name as string;
const referenceType = refMap[name];
if (!referenceType) {
delete value.name;
continue;
}
const indices = await sourceKnex.raw(
`
select cast(substr(name, ??) as int) as idx
from ??
order by idx desc
limit 1`,
[name.length + 1, referenceType]
);
value.start = 1001;
value.current = indices[0]?.idx ?? value.current ?? value.start;
value.referenceType = referenceType;
}
await copyValues(
destKnex,
ModelNameEnum.NumberSeries,
values.filter((v) => v.name),
[],
{},
schema
);
}
async function copyLedgerEntries(
sourceKnex: Knex,
destKnex: Knex,
schema: Schema
) {
const values = (await sourceKnex(
ModelNameEnum.AccountingLedgerEntry
)) as RawValueMap[];
await copyValues(
destKnex,
ModelNameEnum.AccountingLedgerEntry,
values,
['description', 'againstAccount', 'balance'],
{},
schema
);
}
async function copyOtherTables(
sourceKnex: Knex,
destKnex: Knex,
schemaMap: SchemaMap
) {
const schemaNames = [
ModelNameEnum.Account,
ModelNameEnum.Currency,
ModelNameEnum.Address,
ModelNameEnum.Color,
ModelNameEnum.Tax,
ModelNameEnum.PatchRun,
];
for (const sn of schemaNames) {
const values = (await sourceKnex(sn)) as RawValueMap[];
await copyValues(destKnex, sn, values, [], {}, schemaMap[sn]);
}
}
async function copyTransactionalTables(
sourceKnex: Knex,
destKnex: Knex,
schemaMap: SchemaMap
) {
const schemaNames = [
ModelNameEnum.JournalEntry,
ModelNameEnum.Payment,
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
];
for (const sn of schemaNames) {
const values = (await sourceKnex(sn)) as RawValueMap[];
values.forEach((v) => {
if (!v.submitted) {
v.submitted = 0;
}
if (!v.cancelled) {
v.cancelled = 0;
}
if (!v.numberSeries) {
v.numberSeries = defaultNumberSeriesMap[sn];
}
if (v.customer) {
v.party = v.customer;
}
if (v.supplier) {
v.party = v.supplier;
}
});
await copyValues(
destKnex,
sn,
values,
[],
childTableColumnMap,
schemaMap[sn]
);
}
}
async function copyChildTables(
sourceKnex: Knex,
destKnex: Knex,
schemaMap: SchemaMap
) {
const childSchemaNames = Object.keys(schemaMap).filter(
(sn) => schemaMap[sn]?.isChild
);
for (const sn of childSchemaNames) {
const values = (await sourceKnex(sn)) as RawValueMap[];
await copyValues(
destKnex,
sn,
values,
[],
childTableColumnMap,
schemaMap[sn]
);
}
}
async function copyItem(sourceKnex: Knex, destKnex: Knex, schema: Schema) {
const values = (await sourceKnex(ModelNameEnum.Item)) as RawValueMap[];
values.forEach((value) => {
value.for = 'Both';
});
await copyValues(destKnex, ModelNameEnum.Item, values, [], {}, schema);
}
async function copyParty(sourceKnex: Knex, destKnex: Knex, schema: Schema) {
const values = (await sourceKnex(ModelNameEnum.Party)) as RawValueMap[];
values.forEach((value) => {
// customer will be mapped onto role
if (Number(value.supplier) === 1) {
value.customer = 'Supplier';
} else {
value.customer = 'Customer';
}
});
await copyValues(
destKnex,
ModelNameEnum.Party,
values,
['supplier', 'addressDisplay'],
{ customer: 'role' },
schema
);
}
async function copySingleValues(
sourceKnex: Knex,
destKnex: Knex,
schemaMap: SchemaMap
) {
const singleSchemaNames = Object.keys(schemaMap).filter(
(k) => schemaMap[k]?.isSingle
);
const singleValues = (await sourceKnex(ModelNameEnum.SingleValue).whereIn(
'parent',
singleSchemaNames
)) as RawValueMap[];
await copyValues(destKnex, ModelNameEnum.SingleValue, singleValues);
}
async function copyValues(
destKnex: Knex,
destTableName: string,
values: RawValueMap[],
keysToDelete: string[] = [],
keyMap: Record<string, string> = {},
schema?: Schema
) {
keysToDelete = [...keysToDelete, ...ignoreColumns];
keyMap = { ...keyMap, ...columnMap };
values = values.map((sv) => deleteKeys(sv, keysToDelete));
values = values.map((sv) => changeKeys(sv, keyMap));
if (schema) {
values.forEach((v) => notNullify(v, schema));
}
if (schema) {
const newKeys = schema?.fields.map((f) => f.fieldname);
values.forEach((v) => deleteOldKeys(v, newKeys));
}
await destKnex.batchInsert(destTableName, values, 100);
}
async function getDestinationDM(sourceDbPath: string, countryCode: string) {
/**
* This is where all the stuff from the old db will be copied.
* That won't be altered cause schema update will cause data loss.
*/
const dir = path.parse(sourceDbPath).dir;
const dbPath = path.join(dir, '__update_schemas_temp.db');
const dm = new DatabaseManager();
await dm._connect(dbPath, countryCode);
await dm.db!.migrate();
await dm.db!.truncate();
return dm;
}
async function getCountryCode(knex: Knex) {
/**
* Need to account for schema changes, in 0.4.3-beta.0
*/
const country = (
await knex('SingleValue').select('value').where({ fieldname: 'country' })
)?.[0]?.value;
if (!country) {
return '';
}
return getCountryCodeFromCountry(country);
}
function notNullify(map: RawValueMap, schema: Schema) {
for (const field of schema.fields) {
if (!field.required || !getIsNullOrUndef(map[field.fieldname])) {
continue;
}
switch (field.fieldtype) {
case FieldTypeEnum.Float:
case FieldTypeEnum.Int:
case FieldTypeEnum.Check:
map[field.fieldname] = 0;
break;
case FieldTypeEnum.Currency:
map[field.fieldname] = '0.00000000000';
break;
case FieldTypeEnum.Table:
continue;
default:
map[field.fieldname] = '';
}
}
}
function deleteOldKeys(map: RawValueMap, newKeys: string[]) {
for (const key of Object.keys(map)) {
if (newKeys.includes(key)) {
continue;
}
delete map[key];
}
}
export default { execute, beforeMigrate: true };