diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5ead61 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# libdashstore + +MSSQL schema + domain API for Dash. One class, methods mirror the file path under `src/store/`. + +```js +import sql from 'mssql'; +import LibDashStore from 'libdashstore'; + +const pool = await sql.connect({ /* … */ }); +const client = await new LibDashStore(pool); // runs initialize() (ensure schema, no purge) +``` + +SQL lives in `src/sql/.js`. Schema DDL: `src/schema.js` / `import 'libdashstore/schema'`. + +--- + +## Schema + +| Method | Description | +| --- | --- | +| `initialize()` | Runs all `SCHEMA_SCRIPTS` (`IF NOT EXISTS`). Does **not** purge. | +| `purge()` | Drops all known tables + number procedure. | + +--- + +## `company` + +| Method | Signature | Returns | +| --- | --- | --- | +| `create` | `({ iso639, name, taxrate, reducedtaxrate })` | result | +| `update` | `(id, { iso639, name, taxrate, reducedtaxrate })` | result | +| `delete` | `(companyId)` | `void` | +| `list` | `()` | `result[]` | + +### result (`create` / `update` / `list` items) + +```ts +{ + id: number // company id + root_category_id: number + language_id: number // language.id for iso639 + taxrate: number // tax.id + reducedtaxrate: number // tax.id + taxfreerate: number // tax.id (rate 0; unchanged on update) +} +``` + +### `create` (transaction) + +For `POST /api/company`: + +1. Fails if `name` is taken (`Company name not available`) +2. Ensures language by `iso639` (get or create) +3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerate` +4. Creates root category (`parent_id = null`) +5. Inserts company with all FKs set + +### `update` (transaction) + +For `PATCH /api/company/:id`: + +1. Fails if company missing / name taken by another company +2. Ensures lang + tax rates +3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate` + (`taxfreerate` / root category unchanged) + +--- + +## `google_auth` + +| Method | Signature | Returns | +| --- | --- | --- | +| `add` | `(email)` | `number` (auth id) | +| `getById` | `(id)` | `row \| undefined` | +| `getByEmail` | `(email)` | `row \| undefined` | +| `setSelectedCompany` | `(authId, companyId)` | `void` | +| `list` | `()` | `row[]` | +| `del` | `(id)` | `void` | + +### Row shape + +```ts +{ + id: number + email: string + created_at: Date + selected_company: number | null +} +``` + +Backend mapping: + +- Login: `getByEmail(email)` ← `getAllowedEmail` +- Auth middleware: `getById(id)` ← `getAllowedEmailById` +- `PUT /api/auth/selected-company`: `setSelectedCompany(authId, companyId)` + +--- + +## `companyuser` + +| Method | Signature | Returns | +| --- | --- | --- | +| `add` | `(companyId, authId)` | `number` (companyuser id) | +| `getByEmail` | `(email)` | `number[]` (companyuser ids) | +| `delete` | `(id)` | `void` | + +--- + +## `lang` + +| Method | Signature | Returns | +| --- | --- | --- | +| `create` | `(iso639)` | `number` | +| `getByIso639` | `(iso639)` | `{ id, iso639 } \| undefined` | +| `ensure` | `(iso639, tx?)` | `number` (existing or new id) | +| `list` | `()` | `{ id, iso639 }[]` | +| `delete` | `(id)` | `void` | + +--- + +## `tax` + +| Method | Signature | Returns | +| --- | --- | --- | +| `create` | `(rate)` | `number` | +| `getByRate` | `(rate)` | `{ id, rate } \| undefined` | +| `ensure` | `(rate, tx?)` | `number` (existing or new id) | +| `list` | `()` | `{ id, rate }[]` | + +--- + +## `category` + +| Method | Signature | Returns | +| --- | --- | --- | +| `create` | `({ parent_id, sort })` | `number` | +| `updateParentId` | `(id, parentId)` | `void` | +| `delete` | `(id)` | `void` | +| `getTree` | `(rootId)` | `{ id, parent_id, sort }[]` (flat CTE result) | + +--- + +## Backend route cheatsheet + +| Route | Store call | +| --- | --- | +| `POST /api/company` | `company.create({ iso639, name, taxrate, reducedtaxrate })` | +| `PATCH /api/company/:id` | `company.update(id, { iso639, name, taxrate, reducedtaxrate })` | +| `DELETE /api/company/:id` | `company.delete(id)` | +| `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(authId, companyId)` | + +--- + +## Scripts + +```bash +npm run init-schema # apply SCHEMA_SCRIPTS via .env +npm run test:api # interactive API tester +npm run schema-graph # regenerate schema-graph.svg +``` + +Env: `MSSQL_SERVER`, `MSSQL_DATABASE`, `MSSQL_USER`, `MSSQL_PASSWORD`. diff --git a/scripts/test-api.js b/scripts/test-api.js index 06d70fd..074d407 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -70,14 +70,10 @@ const MENU = { company: { label: 'company', items: [ - { value: 'company.create', label: 'create', hint: 'iso639,name,taxrate,reducedtaxrate → id' }, - { value: 'company.update', label: 'update', hint: 'id + same fields' }, - { value: 'company.list', label: 'list', hint: '→ rows' }, - { value: 'company.getById', label: 'getById', hint: 'id → row' }, - { value: 'company.setDefaultLang', label: 'setDefaultLang', hint: 'companyId, langId' }, - { value: 'company.setRootCategory', label: 'setRootCategory', hint: 'companyId, categoryId' }, + { value: 'company.create', label: 'create', hint: 'iso639,name,taxes → result' }, + { value: 'company.update', label: 'update', hint: 'id + fields → result' }, + { value: 'company.list', label: 'list', hint: '→ result[]' }, { value: 'company.delete', label: 'delete', hint: 'companyId' }, - { value: 'company.getByCompanyUserId', label: 'getByCompanyUserId', hint: 'id → tuple' }, ], }, lang: { @@ -183,12 +179,13 @@ async function runMethod(client, action) { const reducedtaxrate = await askNumber('Reduced tax rate', '7'); if (reducedtaxrate == null) return; - const companyId = await client.company.create({ + const created = await client.company.create({ iso639, name, taxrate, reducedtaxrate, }); + const companyId = created.id; const auths = await client.google_auth.list(); const existingAuth = auths.find((row) => row.email === email); @@ -202,7 +199,7 @@ async function runMethod(client, action) { return [ 'created', { - companyId, + ...created, companyUserId, authId, email, @@ -230,7 +227,7 @@ async function runMethod(client, action) { const reducedtaxrate = await askNumber('reducedtaxrate', '7'); if (reducedtaxrate == null) return; return [ - 'id', + 'result', await client.company.create({ iso639, name, taxrate, reducedtaxrate }), ]; } @@ -251,37 +248,18 @@ async function runMethod(client, action) { if (taxrate == null) return; const reducedtaxrate = await askNumber('reducedtaxrate', '7'); if (reducedtaxrate == null) return; - await client.company.update(Number(id), { - iso639, - name, - taxrate, - reducedtaxrate, - }); - return ['ok', true]; + return [ + 'result', + await client.company.update(Number(id), { + iso639, + name, + taxrate, + reducedtaxrate, + }), + ]; } case 'company.list': { - return ['rows', await client.company.list()]; - } - case 'company.getById': { - const id = await askInt('Company id'); - if (id == null) return; - return ['row', await client.company.getById(Number(id))]; - } - 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]; + return ['result[]', await client.company.list()]; } case 'company.delete': { const companyId = await askInt('Company id'); @@ -289,14 +267,6 @@ async function runMethod(client, action) { 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 'tax.create': { const rate = await askNumber('rate', '19'); if (rate == null) return; diff --git a/src/index.js b/src/index.js index 7424ede..7b9a10b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,8 @@ 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 companyGetById from './store/company/getById.js'; -import companyGetByName from './store/company/getByName.js'; import companyCreate from './store/company/create.js'; import companyUpdate from './store/company/update.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'; @@ -43,13 +38,8 @@ export default class LibDashStore { }; this.company = { - getByCompanyUserId: companyGetByCompanyUserId(pool), - getById: companyGetById(pool), - getByName: companyGetByName(pool), create: companyCreate(pool), update: companyUpdate(pool), - setDefaultLang: companySetDefaultLang(pool), - setRootCategory: companySetRootCategory(pool), delete: companyDelete(pool), list: companyList(pool), }; diff --git a/src/sql/company.js b/src/sql/company.js index d228180..daa1fc4 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -1,14 +1,3 @@ -export const GET_BY_COMPANY_USER_ID = ` -SELECT - c.id, - c.default_language_id, - c.root_category_id, - c.name -FROM company c -INNER JOIN companyuser cu ON cu.company_id = c.id -WHERE cu.id = @companyUserId -`; - export const GET_BY_NAME = ` SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate FROM company @@ -52,22 +41,16 @@ WHERE id = @id `; export const LIST = ` -SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate +SELECT + id, + root_category_id, + default_language_id AS language_id, + taxrate, + reducedtaxrate, + taxfreerate 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/store/company/create.js b/src/store/company/create.js index e0bc3ea..ce90c96 100644 --- a/src/store/company/create.js +++ b/src/store/company/create.js @@ -43,7 +43,15 @@ export default function create(pool) { .query(CREATE); await tx.commit(); - return result.recordset[0].id; + + return { + id: result.recordset[0].id, + root_category_id: rootCategoryId, + language_id: languageId, + taxrate: taxrateId, + reducedtaxrate: reducedtaxrateId, + taxfreerate: taxfreerateId, + }; } catch (err) { try { await tx.rollback(); diff --git a/src/store/company/getByCompanyUserId.js b/src/store/company/getByCompanyUserId.js deleted file mode 100644 index 996933e..0000000 --- a/src/store/company/getByCompanyUserId.js +++ /dev/null @@ -1,16 +0,0 @@ -import sql from 'mssql'; -import { GET_BY_COMPANY_USER_ID } from '../../sql/company.js'; - -export default function getByCompanyUserId(pool) { - return async (companyUserId) => { - const result = await pool - .request() - .input('companyUserId', sql.Int, companyUserId) - .query(GET_BY_COMPANY_USER_ID); - - const row = result.recordset[0]; - if (!row) return; - - return [row.id, row.default_language_id, row.root_category_id, row.name]; - }; -} diff --git a/src/store/company/setDefaultLang.js b/src/store/company/setDefaultLang.js deleted file mode 100644 index 2ad3598..0000000 --- a/src/store/company/setDefaultLang.js +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 5be9e59..0000000 --- a/src/store/company/setRootCategory.js +++ /dev/null @@ -1,12 +0,0 @@ -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/company/update.js b/src/store/company/update.js index 09e2db3..e3bd9c8 100644 --- a/src/store/company/update.js +++ b/src/store/company/update.js @@ -42,6 +42,15 @@ export default function update(pool) { .query(UPDATE); await tx.commit(); + + return { + id, + root_category_id: company.root_category_id, + language_id: languageId, + taxrate: taxrateId, + reducedtaxrate: reducedtaxrateId, + taxfreerate: company.taxfreerate, + }; } catch (err) { try { await tx.rollback();