From bbd69e8c7cb3a4efee82be5aabe373f68ca08172 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 07:22:34 +0200 Subject: [PATCH] u --- README.md | 16 ++++++++--- package.json | 2 ++ scripts/purge-db.js | 21 ++++++++++++++ scripts/reset-db.js | 25 +++++++++++++++++ scripts/test-api.js | 30 +++++++++++++------- src/sql/company.js | 17 ++++++----- src/store/category/create.js | 5 ++-- src/store/category/delete.js | 3 +- src/store/category/getTree.js | 3 +- src/store/category/updateParentId.js | 5 ++-- src/store/company/create.js | 31 +++++++++++++-------- src/store/company/delete.js | 3 +- src/store/company/getById.js | 3 +- src/store/company/getByName.js | 3 +- src/store/company/list.js | 7 +++-- src/store/company/update.js | 13 ++++----- src/store/companyuser/add.js | 5 ++-- src/store/companyuser/delete.js | 3 +- src/store/companyuser/getByEmail.js | 3 +- src/store/google_auth/add.js | 3 +- src/store/google_auth/getByEmail.js | 3 +- src/store/google_auth/removeAllowedEmail.js | 3 +- src/store/google_auth/setSelectedCompany.js | 5 ++-- src/store/lang/create.js | 3 +- src/store/lang/delete.js | 3 +- src/store/lang/getByIso639.js | 3 +- src/store/request.js | 4 +-- src/store/tax/create.js | 3 +- src/store/tax/getByRate.js | 3 +- 29 files changed, 144 insertions(+), 87 deletions(-) create mode 100644 scripts/purge-db.js create mode 100644 scripts/reset-db.js diff --git a/README.md b/README.md index ba1baa0..c12d723 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ ID fields in the JS API use camelCase `…Id` (DB columns stay snake_case). | Method | Signature | Returns | | --- | --- | --- | -| `create` | `({ iso639, name, taxrate, reducedtaxrate })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` | +| `create` | `({ iso639, name, taxrate, reducedtaxrate, authId })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` | | `update` | `(id, { iso639, name, taxrate, reducedtaxrate })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` | | `delete` | `(companyId)` | `void` | -| `list` | `()` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }[]` | +| `list` | `(authId)` | `{ id, name, rootCategoryId, languageId, taxrate, reducedtaxrate, taxfreerate }[]` | ### `create` (transaction) @@ -43,6 +43,11 @@ For `POST /api/company`: 3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId` 4. Creates root category (`parentId = null`) 5. Inserts company with all FKs set +6. Links creator as `companyuser` (`authId` → `company_id`) + +### `list` + +Returns only companies where `authId` has a `companyuser` row. ### `update` (transaction) @@ -121,7 +126,8 @@ Backend mapping: | Route | Store call | | --- | --- | -| `POST /api/company` | `company.create({ iso639, name, taxrate, reducedtaxrate })` | +| `POST /api/company` | `company.create({ iso639, name, taxrate, reducedtaxrate, authId })` | +| `GET /api/company` | `company.list(authId)` | | `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)` | @@ -131,7 +137,9 @@ Backend mapping: ## Scripts ```bash -npm run init-schema # apply SCHEMA_SCRIPTS via .env +npm run purge-db # drop all tables + procedure (wipe data) +npm run reset-db # purge + init-schema (empty DB, fresh schema) +npm run init-schema # ensure schema only (does NOT delete data) npm run test:api # interactive API tester npm run schema-graph # regenerate schema-graph.svg ``` diff --git a/package.json b/package.json index 274fc37..981f9d7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "scripts": { "schema-graph": "node generate-schema-graph.js", "init-schema": "node scripts/init-schema.js", + "purge-db": "node scripts/purge-db.js", + "reset-db": "node scripts/reset-db.js", "test:api": "node scripts/test-api.js" }, "dependencies": { diff --git a/scripts/purge-db.js b/scripts/purge-db.js new file mode 100644 index 0000000..5b9195e --- /dev/null +++ b/scripts/purge-db.js @@ -0,0 +1,21 @@ +import 'dotenv/config'; +import sql from 'mssql'; +import { buildPurgeSql } from '../src/sql/purge.js'; + +const pool = await sql.connect({ + server: process.env.MSSQL_SERVER, + database: process.env.MSSQL_DATABASE, + user: process.env.MSSQL_USER, + password: process.env.MSSQL_PASSWORD, + options: { + encrypt: false, + trustServerCertificate: true, + }, +}); + +try { + await pool.request().query(buildPurgeSql()); + console.log('Database purged'); +} finally { + await pool.close(); +} diff --git a/scripts/reset-db.js b/scripts/reset-db.js new file mode 100644 index 0000000..5d44b35 --- /dev/null +++ b/scripts/reset-db.js @@ -0,0 +1,25 @@ +import 'dotenv/config'; +import sql from 'mssql'; +import { buildPurgeSql } from '../src/sql/purge.js'; +import { SCHEMA_SCRIPTS } from '../src/schema.js'; + +const pool = await sql.connect({ + server: process.env.MSSQL_SERVER, + database: process.env.MSSQL_DATABASE, + user: process.env.MSSQL_USER, + password: process.env.MSSQL_PASSWORD, + options: { + encrypt: false, + trustServerCertificate: true, + }, +}); + +try { + await pool.request().query(buildPurgeSql()); + for (const script of SCHEMA_SCRIPTS) { + await pool.request().query(script); + } + console.log(`Database reset (${SCHEMA_SCRIPTS.length} schema scripts applied)`); +} finally { + await pool.close(); +} diff --git a/scripts/test-api.js b/scripts/test-api.js index 9ed89d9..90820e8 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -70,9 +70,9 @@ const MENU = { company: { label: 'company', items: [ - { value: 'company.create', label: 'create', hint: 'iso639,name,taxes → result' }, + { value: 'company.create', label: 'create', hint: 'authId, iso639, name, taxes → result' }, { value: 'company.update', label: 'update', hint: 'id + fields → result' }, - { value: 'company.list', label: 'list', hint: '→ result[]' }, + { value: 'company.list', label: 'list', hint: 'authId → result[]' }, { value: 'company.delete', label: 'delete', hint: 'companyId' }, ], }, @@ -178,25 +178,25 @@ async function runMethod(client, action) { const reducedtaxrate = await askNumber('Reduced tax rate', '7'); if (reducedtaxrate == null) return; + const existing = await client.google_auth.getByEmail(email); + const authId = existing + ? existing.id + : await client.google_auth.add(email); + const created = await client.company.create({ iso639, name, taxrate, reducedtaxrate, + authId, }); const companyId = created.id; - const existing = await client.google_auth.getByEmail(email); - const authId = existing - ? existing.id - : await client.google_auth.add(email); - const companyUserId = await client.companyuser.add(companyId, authId); await client.google_auth.setSelectedCompany(authId, companyId); return [ 'created', { ...created, - companyUserId, authId, email, name, @@ -207,6 +207,8 @@ async function runMethod(client, action) { ]; } case 'company.create': { + const authId = await askInt('google_auth id'); + if (authId == null) return; const name = await askText('Name', { placeholder: 'Acme', validate: (v) => (!v?.trim() ? 'Required' : undefined), @@ -223,7 +225,13 @@ async function runMethod(client, action) { if (reducedtaxrate == null) return; return [ 'result', - await client.company.create({ iso639, name, taxrate, reducedtaxrate }), + await client.company.create({ + iso639, + name, + taxrate, + reducedtaxrate, + authId: Number(authId), + }), ]; } case 'company.update': { @@ -254,7 +262,9 @@ async function runMethod(client, action) { ]; } case 'company.list': { - return ['result[]', await client.company.list()]; + const authId = await askInt('google_auth id'); + if (authId == null) return; + return ['result[]', await client.company.list(Number(authId))]; } case 'company.delete': { const companyId = await askInt('Company id'); diff --git a/src/sql/company.js b/src/sql/company.js index 61c9537..84064e0 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -42,13 +42,16 @@ WHERE id = @id export const LIST = ` SELECT - id, - root_category_id AS rootCategoryId, - default_language_id AS languageId, - taxrate AS taxrateId, - reducedtaxrate AS reducedtaxrateId, - taxfreerate AS taxfreerateId -FROM company + c.id, + c.name, + c.root_category_id AS rootCategoryId, + c.default_language_id AS languageId, + c.taxrate, + c.reducedtaxrate, + c.taxfreerate +FROM company c +INNER JOIN companyuser cu ON cu.company_id = c.id +WHERE cu.google_auth_id = @authId `; export const DELETE = ` diff --git a/src/store/category/create.js b/src/store/category/create.js index 64794ec..a236859 100644 --- a/src/store/category/create.js +++ b/src/store/category/create.js @@ -1,12 +1,11 @@ -import sql from 'mssql'; import { CREATE } from '../../sql/category.js'; export default function create(pool) { return async ({ parentId, sort }) => { const result = await pool .request() - .input('parentId', sql.Int, parentId) - .input('sort', sql.Int, sort) + .input('parentId', parentId) + .input('sort', sort) .query(CREATE); return result.recordset[0].id; diff --git a/src/store/category/delete.js b/src/store/category/delete.js index c2d3e89..a33c788 100644 --- a/src/store/category/delete.js +++ b/src/store/category/delete.js @@ -1,11 +1,10 @@ -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) + .input('id', id) .query(DELETE); }; } diff --git a/src/store/category/getTree.js b/src/store/category/getTree.js index 8ae28ac..1acc2de 100644 --- a/src/store/category/getTree.js +++ b/src/store/category/getTree.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_TREE } from '../../sql/category.js'; export default function getTree(pool) { return async (rootId) => { const result = await pool .request() - .input('rootId', sql.Int, rootId) + .input('rootId', rootId) .query(GET_TREE); return result.recordset; diff --git a/src/store/category/updateParentId.js b/src/store/category/updateParentId.js index 8029b9d..25e720a 100644 --- a/src/store/category/updateParentId.js +++ b/src/store/category/updateParentId.js @@ -1,12 +1,11 @@ -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) + .input('id', id) + .input('parentId', parentId) .query(UPDATE_PARENT_ID); }; } diff --git a/src/store/company/create.js b/src/store/company/create.js index b7063bb..488475a 100644 --- a/src/store/company/create.js +++ b/src/store/company/create.js @@ -1,6 +1,6 @@ -import sql from 'mssql'; import { CREATE } from '../../sql/company.js'; import { CREATE as CREATE_CATEGORY } from '../../sql/category.js'; +import { ADD as ADD_COMPANYUSER } from '../../sql/companyuser.js'; import getByName from './getByName.js'; import langEnsure from '../lang/ensure.js'; import taxEnsure from '../tax/ensure.js'; @@ -11,8 +11,8 @@ export default function create(pool) { const ensureLang = langEnsure(pool); const ensureTax = taxEnsure(pool); - return async ({ iso639, name, taxrate, reducedtaxrate }) => { - const tx = new sql.Transaction(pool); + return async ({ iso639, name, taxrate, reducedtaxrate, authId }) => { + const tx = pool.transaction(); await tx.begin(); try { @@ -27,25 +27,32 @@ export default function create(pool) { const taxfreerateId = await ensureTax(0, tx); const category = await request(pool, tx) - .input('parentId', sql.Int, null) - .input('sort', sql.Int, 0) + .input('parentId', null) + .input('sort', 0) .query(CREATE_CATEGORY); const rootCategoryId = category.recordset[0].id; const result = await request(pool, tx) - .input('name', sql.NVarChar(255), name) - .input('rootCategoryId', sql.Int, rootCategoryId) - .input('languageId', sql.Int, languageId) - .input('taxrate', sql.Int, taxrateId) - .input('reducedtaxrate', sql.Int, reducedtaxrateId) - .input('taxfreerate', sql.Int, taxfreerateId) + .input('name', name) + .input('rootCategoryId', rootCategoryId) + .input('languageId', languageId) + .input('taxrate', taxrateId) + .input('reducedtaxrate', reducedtaxrateId) + .input('taxfreerate', taxfreerateId) .query(CREATE); + const companyId = result.recordset[0].id; + + await request(pool, tx) + .input('companyId', companyId) + .input('authId', authId) + .query(ADD_COMPANYUSER); + await tx.commit(); return { - id: result.recordset[0].id, + id: companyId, rootCategoryId, languageId, taxrateId, diff --git a/src/store/company/delete.js b/src/store/company/delete.js index 6e3c9d2..34e46f3 100644 --- a/src/store/company/delete.js +++ b/src/store/company/delete.js @@ -1,11 +1,10 @@ -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) + .input('companyId', companyId) .query(DELETE); }; } diff --git a/src/store/company/getById.js b/src/store/company/getById.js index 43ed5a1..24e9821 100644 --- a/src/store/company/getById.js +++ b/src/store/company/getById.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_ID } from '../../sql/company.js'; import request from '../request.js'; export default function getById(pool) { return async (id, tx) => { const result = await request(pool, tx) - .input('id', sql.Int, id) + .input('id', id) .query(GET_BY_ID); return result.recordset[0]; diff --git a/src/store/company/getByName.js b/src/store/company/getByName.js index cd6d0bf..8b6ae14 100644 --- a/src/store/company/getByName.js +++ b/src/store/company/getByName.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_NAME } from '../../sql/company.js'; import request from '../request.js'; export default function getByName(pool) { return async (name, tx) => { const result = await request(pool, tx) - .input('name', sql.NVarChar(255), name) + .input('name', name) .query(GET_BY_NAME); return result.recordset[0]; diff --git a/src/store/company/list.js b/src/store/company/list.js index 9305e1a..9551b63 100644 --- a/src/store/company/list.js +++ b/src/store/company/list.js @@ -1,8 +1,11 @@ import { LIST } from '../../sql/company.js'; export default function list(pool) { - return async () => { - const result = await pool.request().query(LIST); + return async (authId) => { + const result = await pool + .request() + .input('authId', authId) + .query(LIST); return result.recordset; }; } diff --git a/src/store/company/update.js b/src/store/company/update.js index 5d3fee0..a710be7 100644 --- a/src/store/company/update.js +++ b/src/store/company/update.js @@ -1,4 +1,3 @@ -import sql from 'mssql'; import { UPDATE } from '../../sql/company.js'; import getById from './getById.js'; import getByName from './getByName.js'; @@ -13,7 +12,7 @@ export default function update(pool) { const ensureTax = taxEnsure(pool); return async (id, { iso639, name, taxrate, reducedtaxrate }) => { - const tx = new sql.Transaction(pool); + const tx = pool.transaction(); await tx.begin(); try { @@ -34,11 +33,11 @@ export default function update(pool) { const reducedtaxrateId = await ensureTax(reducedtaxrate, tx); await request(pool, tx) - .input('id', sql.Int, id) - .input('name', sql.NVarChar(255), name) - .input('languageId', sql.Int, languageId) - .input('taxrate', sql.Int, taxrateId) - .input('reducedtaxrate', sql.Int, reducedtaxrateId) + .input('id', id) + .input('name', name) + .input('languageId', languageId) + .input('taxrate', taxrateId) + .input('reducedtaxrate', reducedtaxrateId) .query(UPDATE); await tx.commit(); diff --git a/src/store/companyuser/add.js b/src/store/companyuser/add.js index a5b9499..525d2b8 100644 --- a/src/store/companyuser/add.js +++ b/src/store/companyuser/add.js @@ -1,12 +1,11 @@ -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) + .input('companyId', companyId) + .input('authId', authId) .query(ADD); return result.recordset[0].id; diff --git a/src/store/companyuser/delete.js b/src/store/companyuser/delete.js index 3b66e2a..3fe631e 100644 --- a/src/store/companyuser/delete.js +++ b/src/store/companyuser/delete.js @@ -1,11 +1,10 @@ -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) + .input('id', id) .query(DELETE); }; } diff --git a/src/store/companyuser/getByEmail.js b/src/store/companyuser/getByEmail.js index 4bfe520..07f09f3 100644 --- a/src/store/companyuser/getByEmail.js +++ b/src/store/companyuser/getByEmail.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_EMAIL } from '../../sql/companyuser.js'; export default function getByEmail(pool) { return async (email) => { const result = await pool .request() - .input('email', sql.NVarChar(320), email) + .input('email', email) .query(GET_BY_EMAIL); return result.recordset.map((row) => row.id); diff --git a/src/store/google_auth/add.js b/src/store/google_auth/add.js index 3360df2..81c0232 100644 --- a/src/store/google_auth/add.js +++ b/src/store/google_auth/add.js @@ -1,11 +1,10 @@ -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) + .input('email', email) .query(ADD); return result.recordset[0].id; diff --git a/src/store/google_auth/getByEmail.js b/src/store/google_auth/getByEmail.js index 3481251..c47eb69 100644 --- a/src/store/google_auth/getByEmail.js +++ b/src/store/google_auth/getByEmail.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_EMAIL } from '../../sql/google_auth.js'; export default function getByEmail(pool) { return async (email) => { const result = await pool .request() - .input('email', sql.NVarChar(320), email) + .input('email', email) .query(GET_BY_EMAIL); return result.recordset[0]; diff --git a/src/store/google_auth/removeAllowedEmail.js b/src/store/google_auth/removeAllowedEmail.js index f40baf7..76b04dd 100644 --- a/src/store/google_auth/removeAllowedEmail.js +++ b/src/store/google_auth/removeAllowedEmail.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { REMOVE_ALLOWED_EMAIL } from '../../sql/google_auth.js'; export default function removeAllowedEmail(pool) { return async (email) => { await pool .request() - .input('email', sql.NVarChar(320), email) + .input('email', email) .query(REMOVE_ALLOWED_EMAIL); }; } diff --git a/src/store/google_auth/setSelectedCompany.js b/src/store/google_auth/setSelectedCompany.js index 5f7d66c..699f133 100644 --- a/src/store/google_auth/setSelectedCompany.js +++ b/src/store/google_auth/setSelectedCompany.js @@ -1,12 +1,11 @@ -import sql from 'mssql'; import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js'; export default function setSelectedCompany(pool) { return async (id, companyId) => { await pool .request() - .input('id', sql.Int, id) - .input('companyId', sql.Int, companyId) + .input('id', id) + .input('companyId', companyId) .query(SET_SELECTED_COMPANY); }; } diff --git a/src/store/lang/create.js b/src/store/lang/create.js index 7c73439..a60f7ad 100644 --- a/src/store/lang/create.js +++ b/src/store/lang/create.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { CREATE } from '../../sql/language.js'; import request from '../request.js'; export default function create(pool) { return async (iso639, tx) => { const result = await request(pool, tx) - .input('iso639', sql.NVarChar(10), iso639) + .input('iso639', iso639) .query(CREATE); return result.recordset[0].id; diff --git a/src/store/lang/delete.js b/src/store/lang/delete.js index b863ae0..5420238 100644 --- a/src/store/lang/delete.js +++ b/src/store/lang/delete.js @@ -1,11 +1,10 @@ -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) + .input('id', id) .query(DELETE); }; } diff --git a/src/store/lang/getByIso639.js b/src/store/lang/getByIso639.js index 44551fc..9412178 100644 --- a/src/store/lang/getByIso639.js +++ b/src/store/lang/getByIso639.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_ISO639 } from '../../sql/language.js'; import request from '../request.js'; export default function getByIso639(pool) { return async (iso639, tx) => { const result = await request(pool, tx) - .input('iso639', sql.NVarChar(10), iso639) + .input('iso639', iso639) .query(GET_BY_ISO639); return result.recordset[0]; diff --git a/src/store/request.js b/src/store/request.js index 1985667..d020696 100644 --- a/src/store/request.js +++ b/src/store/request.js @@ -1,5 +1,3 @@ -import sql from 'mssql'; - export default function request(pool, tx) { - return tx ? new sql.Request(tx) : pool.request(); + return (tx ?? pool).request(); } diff --git a/src/store/tax/create.js b/src/store/tax/create.js index 701fb1c..ed0a916 100644 --- a/src/store/tax/create.js +++ b/src/store/tax/create.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { CREATE } from '../../sql/tax.js'; import request from '../request.js'; export default function create(pool) { return async (rate, tx) => { const result = await request(pool, tx) - .input('rate', sql.Decimal(18, 4), rate) + .input('rate', rate) .query(CREATE); return result.recordset[0].id; diff --git a/src/store/tax/getByRate.js b/src/store/tax/getByRate.js index 4c3892d..fe565a0 100644 --- a/src/store/tax/getByRate.js +++ b/src/store/tax/getByRate.js @@ -1,11 +1,10 @@ -import sql from 'mssql'; import { GET_BY_RATE } from '../../sql/tax.js'; import request from '../request.js'; export default function getByRate(pool) { return async (rate, tx) => { const result = await request(pool, tx) - .input('rate', sql.Decimal(18, 4), rate) + .input('rate', rate) .query(GET_BY_RATE); return result.recordset[0];