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 { 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[] = 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(); });