2
0
mirror of https://github.com/frappe/books.git synced 2024-11-15 01:44:04 +00:00
books/backend/database/tests/testCore.spec.ts
2023-06-27 13:11:15 +05:30

627 lines
17 KiB
TypeScript

import { FieldTypeEnum, RawValue, Schema } from 'schemas/types';
import test from 'tape';
import { getMapFromList, getValueMapFromList, sleep } from 'utils';
import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../helpers';
import DatabaseCore from '../core';
import { FieldValueMap, SqliteTableInfo } from '../types';
import {
assertDoesNotThrow,
assertThrows,
BaseMetaKey,
getBuiltTestSchemaMap,
} from './helpers';
/**
* Note: these tests have a strange structure where multiple tests are
* inside a `specify`, this is cause `describe` doesn't support `async` or waiting
* on promises.
*
* Due to this `async` db operations need to be handled in `specify`. And `specify`
* can't be nested in the `describe` can, hence the strange structure.
*
* This also implies that assert calls should have discriptive
*/
const schemaMap = getBuiltTestSchemaMap();
async function getDb(shouldMigrate: boolean = true): Promise<DatabaseCore> {
const db = new DatabaseCore();
await db.connect();
db.setSchemaMap(schemaMap);
if (shouldMigrate) {
await db.migrate();
}
return db;
}
test('db init, migrate, close', async (t) => {
const db = new DatabaseCore();
t.equal(db.dbPath, ':memory:');
const schemaMap = getBuiltTestSchemaMap();
db.setSchemaMap(schemaMap);
// Same Object
t.equal(schemaMap, db.schemaMap);
await assertDoesNotThrow(async () => await db.connect());
t.notEqual(db.knex, undefined);
await assertDoesNotThrow(async () => await db.migrate());
await assertDoesNotThrow(async () => await db.close());
});
/**
* DatabaseCore: Migrate and Check Db
*/
test(`Pre Migrate TableInfo`, async function (t) {
const db = await getDb(false);
for (const schemaName in schemaMap) {
const columns = await db.knex?.raw('pragma table_info(??)', schemaName);
t.equal(columns.length, 0, `column count ${schemaName}`);
}
await db.close();
});
test('Post Migrate TableInfo', async function (t) {
const db = await getDb();
for (const schemaName in schemaMap) {
const schema = schemaMap[schemaName] as Schema;
const fieldMap = getMapFromList(schema.fields, 'fieldname');
const columns: SqliteTableInfo[] = await db.knex!.raw(
'pragma table_info(??)',
schemaName
);
let columnCount = schema.fields.filter(
(f) => f.fieldtype !== FieldTypeEnum.Table
).length;
if (schema.isSingle) {
columnCount = 0;
}
t.equal(
columns.length,
columnCount,
`${schemaName}:: column count: ${columns.length}, ${columnCount}`
);
for (const column of columns) {
const field = fieldMap[column.name];
const dbColType = sqliteTypeMap[field.fieldtype];
t.equal(
column.name,
field.fieldname,
`${schemaName}.${column.name}:: name check: ${column.name}, ${field.fieldname}`
);
t.equal(
column.type.toLowerCase(),
dbColType,
`${schemaName}.${column.name}:: type check: ${column.type}, ${dbColType}`
);
if (field.required !== undefined) {
t.equal(
!!column.notnull,
field.required,
`${schemaName}.${column.name}:: iotnull iheck: ${column.notnull}, ${field.required}`
);
} else {
t.equal(
column.notnull,
0,
`${schemaName}.${column.name}:: notnull check: ${column.notnull}, ${field.required}`
);
}
if (column.dflt_value === null) {
t.equal(
field.default,
undefined,
`${schemaName}.${column.name}:: dflt_value check: ${column.dflt_value}, ${field.default}`
);
} else {
t.equal(
column.dflt_value.slice(1, -1),
String(field.default),
`${schemaName}.${column.name}:: dflt_value check: ${column.type}, ${dbColType}`
);
}
}
}
await db.close();
});
test('exists() before insertion', async function (t) {
const db = await getDb();
for (const schemaName in schemaMap) {
const doesExist = await db.exists(schemaName);
if (['SingleValue', 'SystemSettings'].includes(schemaName)) {
t.equal(doesExist, true, `${schemaName} exists`);
} else {
t.equal(doesExist, false, `${schemaName} exists`);
}
}
await db.close();
});
test('CRUD single values', async function (t) {
const db = await getDb();
/**
* Checking default values which are created when db.migrate
* takes place.
*/
let rows: Record<string, RawValue>[] = await db.knex!.raw(
'select * from SingleValue'
);
const defaultMap = getValueMapFromList(
(schemaMap.SystemSettings as Schema).fields,
'fieldname',
'default'
);
for (const row of rows) {
t.equal(
row.value,
defaultMap[row.fieldname as string],
`${row.fieldname} default values equality`
);
}
/**
* Insertion and updation for single values call the same function.
*
* Insert
*/
let localeRow = rows.find((r) => r.fieldname === 'locale');
const localeEntryName = localeRow?.name as string;
const localeEntryCreated = localeRow?.created as string;
let locale = 'hi-IN';
await db.insert('SystemSettings', { locale });
rows = await db.knex!.raw('select * from SingleValue');
localeRow = rows.find((r) => r.fieldname === 'locale');
t.notEqual(localeEntryName, undefined, 'localeEntryName');
t.equal(rows.length, 2, 'rows length insert');
t.equal(
localeRow?.name as string,
localeEntryName,
`localeEntryName ${localeRow?.name}, ${localeEntryName}`
);
t.equal(localeRow?.value, locale, `locale ${localeRow?.value}, ${locale}`);
t.equal(
localeRow?.created,
localeEntryCreated,
`locale ${localeRow?.value}, ${locale}`
);
/**
* Update
*/
locale = 'ca-ES';
await db.update('SystemSettings', { locale });
rows = await db.knex!.raw('select * from SingleValue');
localeRow = rows.find((r) => r.fieldname === 'locale');
t.notEqual(localeEntryName, undefined, 'localeEntryName');
t.equal(rows.length, 2, 'rows length update');
t.equal(
localeRow?.name as string,
localeEntryName,
`localeEntryName ${localeRow?.name}, ${localeEntryName}`
);
t.equal(localeRow?.value, locale, `locale ${localeRow?.value}, ${locale}`);
t.equal(
localeRow?.created,
localeEntryCreated,
`locale ${localeRow?.value}, ${locale}`
);
/**
* Delete
*/
await db.delete('SystemSettings', 'locale');
rows = await db.knex!.raw('select * from SingleValue');
t.equal(rows.length, 1, 'delete one');
await db.delete('SystemSettings', 'dateFormat');
rows = await db.knex!.raw('select * from SingleValue');
t.equal(rows.length, 0, 'delete two');
const dateFormat = 'dd/mm/yy';
await db.insert('SystemSettings', { locale, dateFormat });
rows = await db.knex!.raw('select * from SingleValue');
t.equal(rows.length, 2, 'delete two');
/**
* Read
*
* getSingleValues
*/
const svl = await db.getSingleValues('locale', 'dateFormat');
t.equal(svl.length, 2, 'getSingleValues length');
for (const sv of svl) {
t.equal(sv.parent, 'SystemSettings', `singleValue parent ${sv.parent}`);
t.equal(
sv.value,
{ locale, dateFormat }[sv.fieldname],
`singleValue value ${sv.value}`
);
/**
* get
*/
const svlMap = await db.get('SystemSettings');
t.equal(Object.keys(svlMap).length, 2, 'get key length');
t.equal(svlMap.locale, locale, 'get locale');
t.equal(svlMap.dateFormat, dateFormat, 'get locale');
}
await db.close();
});
test('CRUD nondependent schema', async function (t) {
const db = await getDb();
const schemaName = 'Customer';
let rows = await db.knex!(schemaName);
t.equal(rows.length, 0, 'rows length before insertion');
/**
* Insert
*/
const metaValues = getDefaultMetaFieldValueMap();
const name = 'John Thoe';
await assertThrows(
async () => await db.insert(schemaName, { name }),
'insert() did not throw without meta values'
);
const updateMap = Object.assign({}, metaValues, { name });
await db.insert(schemaName, updateMap);
rows = await db.knex!(schemaName);
let firstRow = rows?.[0];
t.equal(rows.length, 1, `rows length insert ${rows.length}`);
t.equal(firstRow.name, name, `name check ${firstRow.name}, ${name}`);
t.equal(firstRow.email, null, `email check ${firstRow.email}`);
for (const key in metaValues) {
t.equal(firstRow[key], metaValues[key as BaseMetaKey], `${key} check`);
}
/**
* Update
*/
const email = 'john@thoe.com';
await sleep(1); // required for modified to change
await db.update(schemaName, {
name,
email,
modified: new Date().toISOString(),
});
rows = await db.knex!(schemaName);
firstRow = rows?.[0];
t.equal(rows.length, 1, `rows length update ${rows.length}`);
t.equal(firstRow.name, name, `name check update ${firstRow.name}, ${name}`);
t.equal(firstRow.email, email, `email check update ${firstRow.email}`);
const phone = '8149133530';
await sleep(1);
await db.update(schemaName, {
name,
phone,
modified: new Date().toISOString(),
});
rows = await db.knex!(schemaName);
firstRow = rows?.[0];
t.equal(firstRow.email, email, `email check update ${firstRow.email}`);
t.equal(firstRow.phone, phone, `email check update ${firstRow.phone}`);
for (const key in metaValues) {
const val = firstRow[key];
const expected = metaValues[key as BaseMetaKey];
if (key !== 'modified') {
t.equal(val, expected, `${key} check ${val}, ${expected}`);
} else {
t.notEqual(val, expected, `${key} check ${val}, ${expected}`);
}
}
/**
* Delete
*/
await db.delete(schemaName, name);
rows = await db.knex!(schemaName);
t.equal(rows.length, 0, `rows length delete ${rows.length}`);
/**
* Get
*/
let fvMap = await db.get(schemaName, name);
t.equal(
Object.keys(fvMap).length,
0,
`key count get ${JSON.stringify(fvMap)}`
);
/**
* > 1 entries
*/
const cOne = { name: 'John Whoe', ...getDefaultMetaFieldValueMap() };
const cTwo = { name: 'Jane Whoe', ...getDefaultMetaFieldValueMap() };
// Insert
await db.insert(schemaName, cOne);
t.equal((await db.knex!(schemaName)).length, 1, `rows length minsert`);
await db.insert(schemaName, cTwo);
rows = await db.knex!(schemaName);
t.equal(rows.length, 2, `rows length minsert`);
const cs = [cOne, cTwo];
for (const i in cs) {
for (const k in cs[i]) {
const val = cs[i][k as BaseMetaKey];
t.equal(
rows?.[i]?.[k],
val,
`equality check ${i} ${k} ${val} ${rows?.[i]?.[k]}`
);
}
}
// Update
await db.update(schemaName, { name: cOne.name, email });
const cOneEmail = await db.get(schemaName, cOne.name, 'email');
t.equal(cOneEmail.email, email, `mi update check one ${cOneEmail}`);
const cTwoEmail = await db.get(schemaName, cTwo.name, 'email');
t.equal(cOneEmail.email, email, `mi update check two ${cTwoEmail}`);
// Rename
const newName = 'Johnny Whoe';
await db.rename(schemaName, cOne.name, newName);
fvMap = await db.get(schemaName, cOne.name);
t.equal(
Object.keys(fvMap).length,
0,
`mi rename check old ${JSON.stringify(fvMap)}`
);
fvMap = await db.get(schemaName, newName);
t.equal(fvMap.email, email, `mi rename check new ${JSON.stringify(fvMap)}`);
// Delete
await db.delete(schemaName, newName);
rows = await db.knex!(schemaName);
t.equal(rows.length, 1, `mi delete length ${rows.length}`);
t.equal(rows[0].name, cTwo.name, `mi delete name ${rows[0].name}`);
await db.close();
});
test('CRUD dependent schema', async function (t) {
const db = await getDb();
const Customer = 'Customer';
const SalesInvoice = 'SalesInvoice';
const SalesInvoiceItem = 'SalesInvoiceItem';
const customer: FieldValueMap = {
name: 'John Whoe',
email: 'john@whoe.com',
...getDefaultMetaFieldValueMap(),
};
const invoice: FieldValueMap = {
name: 'SINV-1001',
date: '2022-01-21',
customer: customer.name,
account: 'Debtors',
submitted: false,
cancelled: false,
...getDefaultMetaFieldValueMap(),
};
await assertThrows(
async () => await db.insert(SalesInvoice, invoice),
'foreign key constraint fail failed'
);
await assertDoesNotThrow(async () => {
await db.insert(Customer, customer);
await db.insert(SalesInvoice, invoice);
}, 'insertion failed');
await assertThrows(
async () => await db.delete(Customer, customer.name as string),
'foreign key constraint fail failed'
);
await assertDoesNotThrow(async () => {
await db.delete(SalesInvoice, invoice.name as string);
await db.delete(Customer, customer.name as string);
}, 'deletion failed');
await db.insert(Customer, customer);
await db.insert(SalesInvoice, invoice);
let fvMap = await db.get(SalesInvoice, invoice.name as string);
for (const key in invoice) {
let expected = invoice[key];
if (typeof expected === 'boolean') {
expected = +expected;
}
t.equal(
fvMap[key],
expected,
`equality check ${key}: ${fvMap[key]}, ${invoice[key]}`
);
}
t.equal((fvMap.items as unknown[])?.length, 0, 'empty items check');
const items: FieldValueMap[] = [
{
item: 'Bottle Caps',
quantity: 2,
rate: 100,
amount: 200,
},
];
await assertThrows(
async () => await db.insert(SalesInvoice, { name: invoice.name, items }),
'invoice insertion with ct did not fail'
);
await assertDoesNotThrow(
async () => await db.update(SalesInvoice, { name: invoice.name, items }),
'ct insertion failed'
);
fvMap = await db.get(SalesInvoice, invoice.name as string);
const ct = fvMap.items as FieldValueMap[];
t.equal(ct.length, 1, `ct length ${ct.length}`);
t.equal(ct[0].parent, invoice.name, `ct parent ${ct[0].parent}`);
t.equal(
ct[0].parentFieldname,
'items',
`ct parentFieldname ${ct[0].parentFieldname}`
);
t.equal(
ct[0].parentSchemaName,
SalesInvoice,
`ct parentSchemaName ${ct[0].parentSchemaName}`
);
for (const key in items[0]) {
t.equal(
ct[0][key],
items[0][key],
`ct values ${key}: ${ct[0][key]}, ${items[0][key]}`
);
}
items.push({
item: 'Mentats',
quantity: 4,
rate: 200,
amount: 800,
});
await assertDoesNotThrow(
async () => await db.update(SalesInvoice, { name: invoice.name, items }),
'ct updation failed'
);
let rows = await db.getAll(SalesInvoiceItem, {
fields: ['item', 'quantity', 'rate', 'amount'],
});
t.equal(rows.length, 2, `ct length update ${rows.length}`);
for (const i in rows) {
for (const key in rows[i]) {
t.equal(
rows[i][key],
items[i][key],
`ct values ${i},${key}: ${rows[i][key]}`
);
}
}
invoice.date = '2022-04-01';
invoice.modified = new Date().toISOString();
await db.update('SalesInvoice', {
name: invoice.name,
date: invoice.date,
modified: invoice.modified,
});
rows = await db.knex!(SalesInvoiceItem);
t.equal(rows.length, 2, `postupdate ct empty ${rows.length}`);
await db.delete(SalesInvoice, invoice.name as string);
rows = await db.getAll(SalesInvoiceItem);
t.equal(rows.length, 0, `ct length delete ${rows.length}`);
await db.close();
});
test('db deleteAll', async (t) => {
const db = await getDb();
const emailOne = 'one@temp.com';
const emailTwo = 'two@temp.com';
const emailThree = 'three@temp.com';
const phoneOne = '1';
const phoneTwo = '2';
const customers = [
{ name: 'customer-a', phone: phoneOne, email: emailOne },
{ name: 'customer-b', phone: phoneOne, email: emailOne },
{ name: 'customer-c', phone: phoneOne, email: emailTwo },
{ name: 'customer-d', phone: phoneOne, email: emailTwo },
{ name: 'customer-e', phone: phoneTwo, email: emailTwo },
{ name: 'customer-f', phone: phoneTwo, email: emailThree },
{ name: 'customer-g', phone: phoneTwo, email: emailThree },
];
for (const { name, email, phone } of customers) {
await db.insert('Customer', {
name,
email,
phone,
...getDefaultMetaFieldValueMap(),
});
}
// Get total count
t.equal((await db.getAll('Customer')).length, customers.length);
// Single filter
t.equal(
await db.deleteAll('Customer', { email: emailOne }),
customers.filter((c) => c.email === emailOne).length
);
t.equal(
(await db.getAll('Customer', { filters: { email: emailOne } })).length,
0
);
// Multiple filters
t.equal(
await db.deleteAll('Customer', { email: emailTwo, phone: phoneTwo }),
customers.filter(
({ phone, email }) => email === emailTwo && phone === phoneTwo
).length
);
t.equal(
await db.deleteAll('Customer', { email: emailTwo, phone: phoneTwo }),
0
);
// Includes filters
t.equal(
await db.deleteAll('Customer', { email: ['in', [emailTwo, emailThree]] }),
customers.filter(
({ email, phone }) =>
[emailTwo, emailThree].includes(email) &&
!(phone === phoneTwo && email === emailTwo)
).length
);
t.equal(
(
await db.getAll('Customer', {
filters: { email: ['in', [emailTwo, emailThree]] },
})
).length,
0
);
await db.close();
});