u
This commit is contained in:
162
README.md
Normal file
162
README.md
Normal file
@@ -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/<table>.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`.
|
||||||
@@ -70,14 +70,10 @@ const MENU = {
|
|||||||
company: {
|
company: {
|
||||||
label: 'company',
|
label: 'company',
|
||||||
items: [
|
items: [
|
||||||
{ value: 'company.create', label: 'create', hint: 'iso639,name,taxrate,reducedtaxrate → id' },
|
{ value: 'company.create', label: 'create', hint: 'iso639,name,taxes → result' },
|
||||||
{ value: 'company.update', label: 'update', hint: 'id + same fields' },
|
{ value: 'company.update', label: 'update', hint: 'id + fields → result' },
|
||||||
{ value: 'company.list', label: 'list', hint: '→ rows' },
|
{ value: 'company.list', label: 'list', hint: '→ result[]' },
|
||||||
{ 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.delete', label: 'delete', hint: 'companyId' },
|
{ value: 'company.delete', label: 'delete', hint: 'companyId' },
|
||||||
{ value: 'company.getByCompanyUserId', label: 'getByCompanyUserId', hint: 'id → tuple' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
lang: {
|
lang: {
|
||||||
@@ -183,12 +179,13 @@ async function runMethod(client, action) {
|
|||||||
const reducedtaxrate = await askNumber('Reduced tax rate', '7');
|
const reducedtaxrate = await askNumber('Reduced tax rate', '7');
|
||||||
if (reducedtaxrate == null) return;
|
if (reducedtaxrate == null) return;
|
||||||
|
|
||||||
const companyId = await client.company.create({
|
const created = await client.company.create({
|
||||||
iso639,
|
iso639,
|
||||||
name,
|
name,
|
||||||
taxrate,
|
taxrate,
|
||||||
reducedtaxrate,
|
reducedtaxrate,
|
||||||
});
|
});
|
||||||
|
const companyId = created.id;
|
||||||
|
|
||||||
const auths = await client.google_auth.list();
|
const auths = await client.google_auth.list();
|
||||||
const existingAuth = auths.find((row) => row.email === email);
|
const existingAuth = auths.find((row) => row.email === email);
|
||||||
@@ -202,7 +199,7 @@ async function runMethod(client, action) {
|
|||||||
return [
|
return [
|
||||||
'created',
|
'created',
|
||||||
{
|
{
|
||||||
companyId,
|
...created,
|
||||||
companyUserId,
|
companyUserId,
|
||||||
authId,
|
authId,
|
||||||
email,
|
email,
|
||||||
@@ -230,7 +227,7 @@ async function runMethod(client, action) {
|
|||||||
const reducedtaxrate = await askNumber('reducedtaxrate', '7');
|
const reducedtaxrate = await askNumber('reducedtaxrate', '7');
|
||||||
if (reducedtaxrate == null) return;
|
if (reducedtaxrate == null) return;
|
||||||
return [
|
return [
|
||||||
'id',
|
'result',
|
||||||
await client.company.create({ iso639, name, taxrate, reducedtaxrate }),
|
await client.company.create({ iso639, name, taxrate, reducedtaxrate }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -251,37 +248,18 @@ async function runMethod(client, action) {
|
|||||||
if (taxrate == null) return;
|
if (taxrate == null) return;
|
||||||
const reducedtaxrate = await askNumber('reducedtaxrate', '7');
|
const reducedtaxrate = await askNumber('reducedtaxrate', '7');
|
||||||
if (reducedtaxrate == null) return;
|
if (reducedtaxrate == null) return;
|
||||||
|
return [
|
||||||
|
'result',
|
||||||
await client.company.update(Number(id), {
|
await client.company.update(Number(id), {
|
||||||
iso639,
|
iso639,
|
||||||
name,
|
name,
|
||||||
taxrate,
|
taxrate,
|
||||||
reducedtaxrate,
|
reducedtaxrate,
|
||||||
});
|
}),
|
||||||
return ['ok', true];
|
];
|
||||||
}
|
}
|
||||||
case 'company.list': {
|
case 'company.list': {
|
||||||
return ['rows', await client.company.list()];
|
return ['result[]', 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];
|
|
||||||
}
|
}
|
||||||
case 'company.delete': {
|
case 'company.delete': {
|
||||||
const companyId = await askInt('Company id');
|
const companyId = await askInt('Company id');
|
||||||
@@ -289,14 +267,6 @@ async function runMethod(client, action) {
|
|||||||
await client.company.delete(Number(companyId));
|
await client.company.delete(Number(companyId));
|
||||||
return ['ok', true];
|
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': {
|
case 'tax.create': {
|
||||||
const rate = await askNumber('rate', '19');
|
const rate = await askNumber('rate', '19');
|
||||||
if (rate == null) return;
|
if (rate == null) return;
|
||||||
|
|||||||
10
src/index.js
10
src/index.js
@@ -1,13 +1,8 @@
|
|||||||
import companyuserGetByEmail from './store/companyuser/getByEmail.js';
|
import companyuserGetByEmail from './store/companyuser/getByEmail.js';
|
||||||
import companyuserAdd from './store/companyuser/add.js';
|
import companyuserAdd from './store/companyuser/add.js';
|
||||||
import companyuserDelete from './store/companyuser/delete.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 companyCreate from './store/company/create.js';
|
||||||
import companyUpdate from './store/company/update.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 companyDelete from './store/company/delete.js';
|
||||||
import companyList from './store/company/list.js';
|
import companyList from './store/company/list.js';
|
||||||
import categoryGetTree from './store/category/getTree.js';
|
import categoryGetTree from './store/category/getTree.js';
|
||||||
@@ -43,13 +38,8 @@ export default class LibDashStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.company = {
|
this.company = {
|
||||||
getByCompanyUserId: companyGetByCompanyUserId(pool),
|
|
||||||
getById: companyGetById(pool),
|
|
||||||
getByName: companyGetByName(pool),
|
|
||||||
create: companyCreate(pool),
|
create: companyCreate(pool),
|
||||||
update: companyUpdate(pool),
|
update: companyUpdate(pool),
|
||||||
setDefaultLang: companySetDefaultLang(pool),
|
|
||||||
setRootCategory: companySetRootCategory(pool),
|
|
||||||
delete: companyDelete(pool),
|
delete: companyDelete(pool),
|
||||||
list: companyList(pool),
|
list: companyList(pool),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = `
|
export const GET_BY_NAME = `
|
||||||
SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate
|
SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate
|
||||||
FROM company
|
FROM company
|
||||||
@@ -52,22 +41,16 @@ WHERE id = @id
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const LIST = `
|
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
|
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 = `
|
export const DELETE = `
|
||||||
DELETE FROM company
|
DELETE FROM company
|
||||||
WHERE id = @companyId
|
WHERE id = @companyId
|
||||||
|
|||||||
@@ -43,7 +43,15 @@ export default function create(pool) {
|
|||||||
.query(CREATE);
|
.query(CREATE);
|
||||||
|
|
||||||
await tx.commit();
|
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) {
|
} catch (err) {
|
||||||
try {
|
try {
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
|
|||||||
@@ -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];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -42,6 +42,15 @@ export default function update(pool) {
|
|||||||
.query(UPDATE);
|
.query(UPDATE);
|
||||||
|
|
||||||
await tx.commit();
|
await tx.commit();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
root_category_id: company.root_category_id,
|
||||||
|
language_id: languageId,
|
||||||
|
taxrate: taxrateId,
|
||||||
|
reducedtaxrate: reducedtaxrateId,
|
||||||
|
taxfreerate: company.taxfreerate,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try {
|
try {
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
|
|||||||
Reference in New Issue
Block a user