diff --git a/scripts/test-api.js b/scripts/test-api.js index db94393..eb481ed 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -50,6 +50,11 @@ async function shutdown(pool, code = 0) { } const METHODS = [ + { + value: 'setup.company', + label: chalk.cyan('setup company'), + hint: 'email + name + lang', + }, { 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' }, @@ -60,8 +65,11 @@ const METHODS = [ { 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.updateParentId', label: 'category.updateParentId', hint: 'id, parentId' }, + { value: 'category.delete', label: 'category.delete', hint: 'id' }, { value: 'category.getTree', label: 'category.getTree', hint: 'rootId → rows' }, { value: 'companyuser.add', label: 'companyuser.add', hint: 'companyId, authId → id' }, + { value: 'companyuser.delete', label: 'companyuser.delete', hint: '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' }, @@ -73,6 +81,55 @@ const METHODS = [ async function runMethod(client, action) { switch (action) { + case 'setup.company': { + const email = await askText('Email', { + placeholder: 'user@example.com', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (email == null) return; + const name = await askText('Company name', { + placeholder: 'Acme', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + const iso639 = await askText('Default language (iso639)', { + placeholder: 'de', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (iso639 == null) return; + + const langs = await client.lang.list(); + const existingLang = langs.find((row) => row.iso639 === iso639); + const langId = existingLang + ? existingLang.id + : await client.lang.create(iso639); + + const companyId = await client.company.create({ name }); + await client.company.setDefaultLang(companyId, langId); + + const auths = await client.google_auth.list(); + const existingAuth = auths.find((row) => row.email === email); + const authId = existingAuth + ? existingAuth.id + : await client.google_auth.add(email); + + const companyUserId = await client.companyuser.add(companyId, authId); + + return [ + 'created', + { + companyId, + companyUserId, + authId, + langId, + email, + name, + iso639, + langCreated: !existingLang, + authCreated: !existingAuth, + }, + ]; + } case 'company.create': { const name = await askText('Name', { placeholder: 'Acme', @@ -143,6 +200,25 @@ async function runMethod(client, action) { const parent_id = parentRaw === '' ? null : Number(parentRaw); return ['id', await client.category.create({ parent_id, sort: Number(sortRaw) })]; } + case 'category.updateParentId': { + const id = await askInt('Category id'); + if (id == null) return; + 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 parentId = parentRaw === '' ? null : Number(parentRaw); + await client.category.updateParentId(Number(id), parentId); + return ['ok', true]; + } + case 'category.delete': { + const id = await askInt('Category id'); + if (id == null) return; + await client.category.delete(Number(id)); + return ['ok', true]; + } case 'category.getTree': { const rootId = await askInt('Root category id'); if (rootId == null) return; @@ -155,6 +231,12 @@ async function runMethod(client, action) { if (authId == null) return; return ['id', await client.companyuser.add(Number(companyId), Number(authId))]; } + case 'companyuser.delete': { + const id = await askInt('Company user id'); + if (id == null) return; + await client.companyuser.delete(Number(id)); + return ['ok', true]; + } case 'companyuser.getByEmail': { const email = await askText('Email', { placeholder: 'user@example.com', @@ -237,6 +319,24 @@ async function main() { } if (action === 'purge') { + const confirm1 = await p.confirm({ + message: chalk.red('Drop ALL tables?'), + initialValue: false, + }); + if (p.isCancel(confirm1) || !confirm1) { + p.log.warn('Purge cancelled'); + continue; + } + + const confirm2 = await p.confirm({ + message: chalk.red('Really purge? This cannot be undone.'), + initialValue: false, + }); + if (p.isCancel(confirm2) || !confirm2) { + p.log.warn('Purge cancelled'); + continue; + } + const run = p.spinner(); run.start('Purging…'); await client.purge(); diff --git a/src/index.js b/src/index.js index b0ff30e..7a7a8c8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import companyuserGetByEmail from './store/companyuser/getByEmail.js'; import companyuserAdd from './store/companyuser/add.js'; +import companyuserDelete from './store/companyuser/delete.js'; import companyGetByCompanyUserId from './store/company/getByCompanyUserId.js'; import companyCreate from './store/company/create.js'; import companySetDefaultLang from './store/company/setDefaultLang.js'; @@ -8,6 +9,8 @@ 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 categoryUpdateParentId from './store/category/updateParentId.js'; +import categoryDelete from './store/category/delete.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -24,6 +27,7 @@ export default class LibDashStore { this.companyuser = { getByEmail: companyuserGetByEmail(pool), add: companyuserAdd(pool), + delete: companyuserDelete(pool), }; this.company = { @@ -38,6 +42,8 @@ export default class LibDashStore { this.category = { getTree: categoryGetTree(pool), create: categoryCreate(pool), + updateParentId: categoryUpdateParentId(pool), + delete: categoryDelete(pool), }; this.lang = { diff --git a/src/schema.js b/src/schema.js index 2504a9a..c40dd66 100644 --- a/src/schema.js +++ b/src/schema.js @@ -101,6 +101,7 @@ BEGIN name NVARCHAR(255) NOT NULL, root_category_id INT NULL, default_language_id INT NULL, + CONSTRAINT UQ_company_name UNIQUE (name), CONSTRAINT FK_company_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL, CONSTRAINT FK_company_default_language FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL ); diff --git a/src/sql/category.js b/src/sql/category.js index 11313d8..57f6b97 100644 --- a/src/sql/category.js +++ b/src/sql/category.js @@ -20,3 +20,14 @@ INSERT INTO category (parent_id, sort) OUTPUT INSERTED.id VALUES (@parentId, @sort) `; + +export const UPDATE_PARENT_ID = ` +UPDATE category +SET parent_id = @parentId +WHERE id = @id +`; + +export const DELETE = ` +DELETE FROM category +WHERE id = @id +`; diff --git a/src/sql/company.js b/src/sql/company.js index d41b4aa..bd5f22c 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -10,9 +10,9 @@ WHERE cu.id = @companyUserId `; export const CREATE = ` -INSERT INTO company (name) +INSERT INTO company (name, root_category_id) OUTPUT INSERTED.id -VALUES (@name) +VALUES (@name, @rootCategoryId) `; export const LIST = ` diff --git a/src/sql/companyuser.js b/src/sql/companyuser.js index dfd615d..79b5e42 100644 --- a/src/sql/companyuser.js +++ b/src/sql/companyuser.js @@ -10,3 +10,8 @@ INSERT INTO companyuser (google_auth_id, company_id) OUTPUT INSERTED.id VALUES (@authId, @companyId) `; + +export const DELETE = ` +DELETE FROM companyuser +WHERE id = @id +`; diff --git a/src/store/category/delete.js b/src/store/category/delete.js new file mode 100644 index 0000000..c2d3e89 --- /dev/null +++ b/src/store/category/delete.js @@ -0,0 +1,11 @@ +import sql from 'mssql'; +import { DELETE } from '../../sql/category.js'; + +export default function del(pool) { + return async (id) => { + await pool + .request() + .input('id', sql.Int, id) + .query(DELETE); + }; +} diff --git a/src/store/category/updateParentId.js b/src/store/category/updateParentId.js new file mode 100644 index 0000000..8029b9d --- /dev/null +++ b/src/store/category/updateParentId.js @@ -0,0 +1,12 @@ +import sql from 'mssql'; +import { UPDATE_PARENT_ID } from '../../sql/category.js'; + +export default function updateParentId(pool) { + return async (id, parentId) => { + await pool + .request() + .input('id', sql.Int, id) + .input('parentId', sql.Int, parentId) + .query(UPDATE_PARENT_ID); + }; +} diff --git a/src/store/company/create.js b/src/store/company/create.js index 813b52d..eacd1bc 100644 --- a/src/store/company/create.js +++ b/src/store/company/create.js @@ -1,11 +1,21 @@ import sql from 'mssql'; import { CREATE } from '../../sql/company.js'; +import { CREATE as CREATE_CATEGORY } from '../../sql/category.js'; export default function create(pool) { return async ({ name }) => { + const category = await pool + .request() + .input('parentId', sql.Int, null) + .input('sort', sql.Int, 0) + .query(CREATE_CATEGORY); + + const rootCategoryId = category.recordset[0].id; + const result = await pool .request() .input('name', sql.NVarChar(255), name) + .input('rootCategoryId', sql.Int, rootCategoryId) .query(CREATE); return result.recordset[0].id; diff --git a/src/store/companyuser/delete.js b/src/store/companyuser/delete.js new file mode 100644 index 0000000..3b66e2a --- /dev/null +++ b/src/store/companyuser/delete.js @@ -0,0 +1,11 @@ +import sql from 'mssql'; +import { DELETE } from '../../sql/companyuser.js'; + +export default function del(pool) { + return async (id) => { + await pool + .request() + .input('id', sql.Int, id) + .query(DELETE); + }; +}