diff --git a/scripts/test-api.js b/scripts/test-api.js index 64b4a6d..45e20aa 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -27,6 +27,19 @@ function printResult(label, value) { console.log(); } +async function askText(message, { placeholder, validate } = {}) { + const value = await p.text({ message, placeholder, validate }); + if (p.isCancel(value)) return null; + return value.trim(); +} + +async function askInt(message, placeholder = '1') { + return askText(message, { + placeholder, + validate: (v) => (!/^\d+$/.test(v?.trim() ?? '') ? 'Integer required' : undefined), + }); +} + async function shutdown(pool, code = 0) { try { await pool.close(); @@ -36,6 +49,146 @@ async function shutdown(pool, code = 0) { process.exit(code); } +const METHODS = [ + { value: 'company.create', label: 'company.create', hint: '{name} → id' }, + { value: 'company.list', label: 'company.list', hint: '→ rows' }, + { value: 'company.setDefaultLang', label: 'company.setDefaultLang', hint: 'companyId, langId' }, + { value: 'company.setRootCategory', label: 'company.setRootCategory', hint: 'companyId, categoryId' }, + { value: 'company.delete', label: 'company.delete', hint: 'companyId' }, + { value: 'company.getByCompanyUserId', label: 'company.getByCompanyUserId', hint: 'id → tuple' }, + { value: 'lang.create', label: 'lang.create', hint: 'iso639 → id' }, + { value: 'lang.list', label: 'lang.list', hint: '→ rows' }, + { value: 'lang.delete', label: 'lang.delete', hint: 'id' }, + { value: 'category.create', label: 'category.create', hint: '{parent_id, sort} → id' }, + { value: 'category.getTree', label: 'category.getTree', hint: 'rootId → rows' }, + { value: 'companyuser.add', label: 'companyuser.add', hint: 'companyId, authId → id' }, + { value: 'companyuser.getByEmail', label: 'companyuser.getByEmail', hint: 'email → ids' }, + { value: 'google_auth.add', label: 'google_auth.add', hint: 'email → authId' }, + { value: 'google_auth.list', label: 'google_auth.list', hint: '→ rows' }, + { value: 'google_auth.del', label: 'google_auth.del', hint: 'id' }, + { value: 'initialize', label: 'initialize', hint: 'purge + recreate' }, + { value: 'purge', label: chalk.red('purge'), hint: 'drop all · exit' }, + { value: 'exit', label: 'exit', hint: 'quit without purge' }, +]; + +async function runMethod(client, action) { + switch (action) { + case 'company.create': { + const name = await askText('Name', { + placeholder: 'Acme', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return ['id', await client.company.create({ name })]; + } + case 'company.list': { + return ['rows', await client.company.list()]; + } + case 'company.setDefaultLang': { + const companyId = await askInt('Company id'); + if (companyId == null) return; + const languageId = await askInt('Language id'); + if (languageId == null) return; + await client.company.setDefaultLang(Number(companyId), Number(languageId)); + return ['ok', true]; + } + case 'company.setRootCategory': { + const companyId = await askInt('Company id'); + if (companyId == null) return; + const categoryId = await askInt('Category id'); + if (categoryId == null) return; + await client.company.setRootCategory(Number(companyId), Number(categoryId)); + return ['ok', true]; + } + case 'company.delete': { + const companyId = await askInt('Company id'); + if (companyId == null) return; + await client.company.delete(Number(companyId)); + return ['ok', true]; + } + case 'company.getByCompanyUserId': { + const id = await askInt('Company user id'); + if (id == null) return; + return [ + '[id, defaultLangId, rootCategoryId, name]', + await client.company.getByCompanyUserId(Number(id)), + ]; + } + case 'lang.create': { + const iso639 = await askText('ISO 639', { + placeholder: 'de', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (iso639 == null) return; + return ['id', await client.lang.create(iso639)]; + } + case 'lang.list': { + return ['rows', await client.lang.list()]; + } + case 'lang.delete': { + const id = await askInt('Language id'); + if (id == null) return; + await client.lang.delete(Number(id)); + return ['ok', true]; + } + case 'category.create': { + const parentRaw = await askText('parent_id (null as empty)', { + placeholder: '', + validate: (v) => + v?.trim() && !/^\d+$/.test(v.trim()) ? 'Integer or empty' : undefined, + }); + if (parentRaw == null) return; + const sortRaw = await askInt('sort', '0'); + if (sortRaw == null) return; + const parent_id = parentRaw === '' ? null : Number(parentRaw); + return ['id', await client.category.create({ parent_id, sort: Number(sortRaw) })]; + } + case 'category.getTree': { + const rootId = await askInt('Root category id'); + if (rootId == null) return; + return ['rows', await client.category.getTree(Number(rootId))]; + } + case 'companyuser.add': { + const companyId = await askInt('Company id'); + if (companyId == null) return; + const authId = await askInt('Auth id'); + if (authId == null) return; + return ['id', await client.companyuser.add(Number(companyId), Number(authId))]; + } + case 'companyuser.getByEmail': { + const email = await askText('Email', { + placeholder: 'user@example.com', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (email == null) return; + return ['ids', await client.companyuser.getByEmail(email)]; + } + case 'google_auth.add': { + const email = await askText('Email', { + placeholder: 'user@example.com', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (email == null) return; + return ['authId', await client.google_auth.add(email)]; + } + case 'google_auth.list': { + return ['rows', await client.google_auth.list()]; + } + case 'google_auth.del': { + const id = await askInt('Auth id'); + if (id == null) return; + await client.google_auth.del(Number(id)); + return ['ok', true]; + } + case 'initialize': { + await client.initialize(); + return ['ok', true]; + } + default: + return null; + } +} + async function main() { console.clear(); p.intro(chalk.bgMagenta.black(' libdashstore ') + chalk.dim(' api tester')); @@ -75,34 +228,7 @@ async function main() { while (true) { const action = await p.select({ message: 'API method', - options: [ - { - value: 'companyuser.getByEmail', - label: 'companyuser.getByEmail', - hint: 'email → id', - }, - { - value: 'company.getByCompanyUserId', - label: 'company.getByCompanyUserId', - hint: 'id → [id, lang, root, name]', - }, - { - value: 'category.getTree', - label: 'category.getTree', - hint: 'rootId → tree', - }, - { - value: 'initialize', - label: 'initialize', - hint: 'purge + recreate schema', - }, - { - value: 'purge', - label: chalk.red('purge'), - hint: 'drop all · exit', - }, - { value: 'exit', label: 'exit', hint: 'quit without purge' }, - ], + options: METHODS, }); if (p.isCancel(action) || action === 'exit') { @@ -110,74 +236,20 @@ async function main() { await shutdown(pool, 0); } + if (action === 'purge') { + const run = p.spinner(); + run.start('Purging…'); + await client.purge(); + run.stop(chalk.green('purge')); + p.outro(chalk.red('Database purged — exiting')); + await shutdown(pool, 0); + } + try { - if (action === 'companyuser.getByEmail') { - const email = await p.text({ - message: 'Email', - placeholder: 'user@example.com', - validate: (v) => (!v?.trim() ? 'Required' : undefined), - }); - if (p.isCancel(email)) continue; - - const run = p.spinner(); - run.start('Running…'); - const id = await client.companyuser.getByEmail(email.trim()); - run.stop(chalk.green('companyuser.getByEmail')); - printResult('id', id); - continue; - } - - if (action === 'company.getByCompanyUserId') { - const raw = await p.text({ - message: 'Company user id', - placeholder: '1', - validate: (v) => - !/^\d+$/.test(v?.trim() ?? '') ? 'Integer required' : undefined, - }); - if (p.isCancel(raw)) continue; - - const run = p.spinner(); - run.start('Running…'); - const row = await client.company.getByCompanyUserId(Number(raw.trim())); - run.stop(chalk.green('company.getByCompanyUserId')); - printResult('[id, defaultLangId, rootCategoryId, name]', row); - continue; - } - - if (action === 'category.getTree') { - const raw = await p.text({ - message: 'Root category id', - placeholder: '1', - validate: (v) => - !/^\d+$/.test(v?.trim() ?? '') ? 'Integer required' : undefined, - }); - if (p.isCancel(raw)) continue; - - const run = p.spinner(); - run.start('Running…'); - const tree = await client.category.getTree(Number(raw.trim())); - run.stop(chalk.green('category.getTree')); - printResult('tree', tree); - continue; - } - - if (action === 'initialize') { - const run = p.spinner(); - run.start('Initializing…'); - await client.initialize(); - run.stop(chalk.green('initialize')); - p.log.success('Schema purged and recreated'); - console.log(); - continue; - } - - if (action === 'purge') { - const run = p.spinner(); - run.start('Purging…'); - await client.purge(); - run.stop(chalk.green('purge')); - p.outro(chalk.red('Database purged — exiting')); - await shutdown(pool, 0); + const result = await runMethod(client, action); + if (result) { + p.log.success(action); + printResult(result[0], result[1]); } } catch (err) { p.log.error(err.message); diff --git a/src/index.js b/src/index.js index e7a6c91..b0ff30e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,19 @@ import companyuserGetByEmail from './store/companyuser/getByEmail.js'; +import companyuserAdd from './store/companyuser/add.js'; import companyGetByCompanyUserId from './store/company/getByCompanyUserId.js'; +import companyCreate from './store/company/create.js'; +import companySetDefaultLang from './store/company/setDefaultLang.js'; +import companySetRootCategory from './store/company/setRootCategory.js'; +import companyDelete from './store/company/delete.js'; +import companyList from './store/company/list.js'; import categoryGetTree from './store/category/getTree.js'; +import categoryCreate from './store/category/create.js'; +import langCreate from './store/lang/create.js'; +import langDelete from './store/lang/delete.js'; +import langList from './store/lang/list.js'; +import googleAuthAdd from './store/google_auth/add.js'; +import googleAuthList from './store/google_auth/list.js'; +import googleAuthDel from './store/google_auth/del.js'; import storePurge from './store/purge.js'; import storeInitialize from './store/initialize.js'; @@ -10,14 +23,33 @@ export default class LibDashStore { this.companyuser = { getByEmail: companyuserGetByEmail(pool), + add: companyuserAdd(pool), }; this.company = { getByCompanyUserId: companyGetByCompanyUserId(pool), + create: companyCreate(pool), + setDefaultLang: companySetDefaultLang(pool), + setRootCategory: companySetRootCategory(pool), + delete: companyDelete(pool), + list: companyList(pool), }; this.category = { getTree: categoryGetTree(pool), + create: categoryCreate(pool), + }; + + this.lang = { + create: langCreate(pool), + delete: langDelete(pool), + list: langList(pool), + }; + + this.google_auth = { + add: googleAuthAdd(pool), + list: googleAuthList(pool), + del: googleAuthDel(pool), }; this.purge = storePurge(pool); diff --git a/src/sql/category.js b/src/sql/category.js index b602e38..11313d8 100644 --- a/src/sql/category.js +++ b/src/sql/category.js @@ -14,3 +14,9 @@ SELECT id, parent_id, sort FROM tree ORDER BY sort, id `; + +export const CREATE = ` +INSERT INTO category (parent_id, sort) +OUTPUT INSERTED.id +VALUES (@parentId, @sort) +`; diff --git a/src/sql/company.js b/src/sql/company.js index 0acdc63..d41b4aa 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -8,3 +8,31 @@ FROM company c INNER JOIN companyuser cu ON cu.company_id = c.id WHERE cu.id = @companyUserId `; + +export const CREATE = ` +INSERT INTO company (name) +OUTPUT INSERTED.id +VALUES (@name) +`; + +export const LIST = ` +SELECT id, name, root_category_id, default_language_id +FROM company +`; + +export const SET_DEFAULT_LANG = ` +UPDATE company +SET default_language_id = @languageId +WHERE id = @companyId +`; + +export const SET_ROOT_CATEGORY = ` +UPDATE company +SET root_category_id = @categoryId +WHERE id = @companyId +`; + +export const DELETE = ` +DELETE FROM company +WHERE id = @companyId +`; diff --git a/src/sql/companyuser.js b/src/sql/companyuser.js index 7e5ca08..dfd615d 100644 --- a/src/sql/companyuser.js +++ b/src/sql/companyuser.js @@ -4,3 +4,9 @@ FROM companyuser cu INNER JOIN google_auth ga ON ga.id = cu.google_auth_id WHERE ga.email = @email `; + +export const ADD = ` +INSERT INTO companyuser (google_auth_id, company_id) +OUTPUT INSERTED.id +VALUES (@authId, @companyId) +`; diff --git a/src/sql/google_auth.js b/src/sql/google_auth.js new file mode 100644 index 0000000..cfb8842 --- /dev/null +++ b/src/sql/google_auth.js @@ -0,0 +1,16 @@ +export const ADD = ` +INSERT INTO google_auth (email) +OUTPUT INSERTED.id +VALUES (@email) +`; + +export const LIST = ` +SELECT id, email, created_at +FROM google_auth +ORDER BY id +`; + +export const DEL = ` +DELETE FROM google_auth +WHERE id = @id +`; diff --git a/src/sql/language.js b/src/sql/language.js new file mode 100644 index 0000000..683ec2a --- /dev/null +++ b/src/sql/language.js @@ -0,0 +1,15 @@ +export const CREATE = ` +INSERT INTO language (iso639) +OUTPUT INSERTED.id +VALUES (@iso639) +`; + +export const LIST = ` +SELECT id, iso639 +FROM language +`; + +export const DELETE = ` +DELETE FROM language +WHERE id = @id +`; diff --git a/src/store/category/create.js b/src/store/category/create.js new file mode 100644 index 0000000..2a0c748 --- /dev/null +++ b/src/store/category/create.js @@ -0,0 +1,14 @@ +import sql from 'mssql'; +import { CREATE } from '../../sql/category.js'; + +export default function create(pool) { + return async ({ parent_id, sort }) => { + const result = await pool + .request() + .input('parentId', sql.Int, parent_id) + .input('sort', sql.Int, sort) + .query(CREATE); + + return result.recordset[0].id; + }; +} diff --git a/src/store/category/getTree.js b/src/store/category/getTree.js index caafcb6..8ae28ac 100644 --- a/src/store/category/getTree.js +++ b/src/store/category/getTree.js @@ -1,30 +1,6 @@ import sql from 'mssql'; import { GET_TREE } from '../../sql/category.js'; -function buildTree(rows, rootId) { - const byId = new Map(); - for (const row of rows) { - byId.set(row.id, { - id: row.id, - parent_id: row.parent_id, - sort: row.sort, - children: [], - }); - } - - let root = null; - for (const node of byId.values()) { - if (node.id === rootId) { - root = node; - continue; - } - const parent = byId.get(node.parent_id); - if (parent) parent.children.push(node); - } - - return root; -} - export default function getTree(pool) { return async (rootId) => { const result = await pool @@ -32,7 +8,6 @@ export default function getTree(pool) { .input('rootId', sql.Int, rootId) .query(GET_TREE); - if (result.recordset.length === 0) return null; - return buildTree(result.recordset, rootId); + return result.recordset; }; } diff --git a/src/store/company/create.js b/src/store/company/create.js new file mode 100644 index 0000000..813b52d --- /dev/null +++ b/src/store/company/create.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { CREATE } from '../../sql/company.js'; + +export default function create(pool) { + return async ({ name }) => { + const result = await pool + .request() + .input('name', sql.NVarChar(255), name) + .query(CREATE); + + return result.recordset[0].id; + }; +} diff --git a/src/store/company/delete.js b/src/store/company/delete.js new file mode 100644 index 0000000..6e3c9d2 --- /dev/null +++ b/src/store/company/delete.js @@ -0,0 +1,11 @@ +import sql from 'mssql'; +import { DELETE } from '../../sql/company.js'; + +export default function del(pool) { + return async (companyId) => { + await pool + .request() + .input('companyId', sql.Int, companyId) + .query(DELETE); + }; +} diff --git a/src/store/company/getByCompanyUserId.js b/src/store/company/getByCompanyUserId.js index a430f8d..996933e 100644 --- a/src/store/company/getByCompanyUserId.js +++ b/src/store/company/getByCompanyUserId.js @@ -9,7 +9,7 @@ export default function getByCompanyUserId(pool) { .query(GET_BY_COMPANY_USER_ID); const row = result.recordset[0]; - if (!row) return null; + if (!row) return; return [row.id, row.default_language_id, row.root_category_id, row.name]; }; diff --git a/src/store/company/list.js b/src/store/company/list.js new file mode 100644 index 0000000..9305e1a --- /dev/null +++ b/src/store/company/list.js @@ -0,0 +1,8 @@ +import { LIST } from '../../sql/company.js'; + +export default function list(pool) { + return async () => { + const result = await pool.request().query(LIST); + return result.recordset; + }; +} diff --git a/src/store/company/setDefaultLang.js b/src/store/company/setDefaultLang.js new file mode 100644 index 0000000..2ad3598 --- /dev/null +++ b/src/store/company/setDefaultLang.js @@ -0,0 +1,12 @@ +import sql from 'mssql'; +import { SET_DEFAULT_LANG } from '../../sql/company.js'; + +export default function setDefaultLang(pool) { + return async (companyId, languageId) => { + await pool + .request() + .input('companyId', sql.Int, companyId) + .input('languageId', sql.Int, languageId) + .query(SET_DEFAULT_LANG); + }; +} diff --git a/src/store/company/setRootCategory.js b/src/store/company/setRootCategory.js new file mode 100644 index 0000000..5be9e59 --- /dev/null +++ b/src/store/company/setRootCategory.js @@ -0,0 +1,12 @@ +import sql from 'mssql'; +import { SET_ROOT_CATEGORY } from '../../sql/company.js'; + +export default function setRootCategory(pool) { + return async (companyId, categoryId) => { + await pool + .request() + .input('companyId', sql.Int, companyId) + .input('categoryId', sql.Int, categoryId) + .query(SET_ROOT_CATEGORY); + }; +} diff --git a/src/store/companyuser/add.js b/src/store/companyuser/add.js new file mode 100644 index 0000000..a5b9499 --- /dev/null +++ b/src/store/companyuser/add.js @@ -0,0 +1,14 @@ +import sql from 'mssql'; +import { ADD } from '../../sql/companyuser.js'; + +export default function add(pool) { + return async (companyId, authId) => { + const result = await pool + .request() + .input('companyId', sql.Int, companyId) + .input('authId', sql.Int, authId) + .query(ADD); + + return result.recordset[0].id; + }; +} diff --git a/src/store/companyuser/getByEmail.js b/src/store/companyuser/getByEmail.js index 171ff88..4bfe520 100644 --- a/src/store/companyuser/getByEmail.js +++ b/src/store/companyuser/getByEmail.js @@ -8,6 +8,6 @@ export default function getByEmail(pool) { .input('email', sql.NVarChar(320), email) .query(GET_BY_EMAIL); - return result.recordset[0]?.id ?? null; + return result.recordset.map((row) => row.id); }; } diff --git a/src/store/google_auth/add.js b/src/store/google_auth/add.js new file mode 100644 index 0000000..3360df2 --- /dev/null +++ b/src/store/google_auth/add.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { ADD } from '../../sql/google_auth.js'; + +export default function add(pool) { + return async (email) => { + const result = await pool + .request() + .input('email', sql.NVarChar(320), email) + .query(ADD); + + return result.recordset[0].id; + }; +} diff --git a/src/store/google_auth/del.js b/src/store/google_auth/del.js new file mode 100644 index 0000000..9bf3f79 --- /dev/null +++ b/src/store/google_auth/del.js @@ -0,0 +1,11 @@ +import sql from 'mssql'; +import { DEL } from '../../sql/google_auth.js'; + +export default function del(pool) { + return async (id) => { + await pool + .request() + .input('id', sql.Int, id) + .query(DEL); + }; +} diff --git a/src/store/google_auth/list.js b/src/store/google_auth/list.js new file mode 100644 index 0000000..7f9e765 --- /dev/null +++ b/src/store/google_auth/list.js @@ -0,0 +1,8 @@ +import { LIST } from '../../sql/google_auth.js'; + +export default function list(pool) { + return async () => { + const result = await pool.request().query(LIST); + return result.recordset; + }; +} diff --git a/src/store/lang/create.js b/src/store/lang/create.js new file mode 100644 index 0000000..1337305 --- /dev/null +++ b/src/store/lang/create.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { CREATE } from '../../sql/language.js'; + +export default function create(pool) { + return async (iso639) => { + const result = await pool + .request() + .input('iso639', sql.NVarChar(10), iso639) + .query(CREATE); + + return result.recordset[0].id; + }; +} diff --git a/src/store/lang/delete.js b/src/store/lang/delete.js new file mode 100644 index 0000000..b863ae0 --- /dev/null +++ b/src/store/lang/delete.js @@ -0,0 +1,11 @@ +import sql from 'mssql'; +import { DELETE } from '../../sql/language.js'; + +export default function del(pool) { + return async (id) => { + await pool + .request() + .input('id', sql.Int, id) + .query(DELETE); + }; +} diff --git a/src/store/lang/list.js b/src/store/lang/list.js new file mode 100644 index 0000000..fc259f3 --- /dev/null +++ b/src/store/lang/list.js @@ -0,0 +1,8 @@ +import { LIST } from '../../sql/language.js'; + +export default function list(pool) { + return async () => { + const result = await pool.request().query(LIST); + return result.recordset; + }; +}