This commit is contained in:
seb
2026-07-31 07:22:34 +02:00
parent 02bca8e236
commit bbd69e8c7c
29 changed files with 144 additions and 87 deletions

View File

@@ -29,10 +29,10 @@ ID fields in the JS API use camelCase `…Id` (DB columns stay snake_case).
| Method | Signature | Returns | | 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 }` | | `update` | `(id, { iso639, name, taxrate, reducedtaxrate })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` |
| `delete` | `(companyId)` | `void` | | `delete` | `(companyId)` | `void` |
| `list` | `()` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }[]` | | `list` | `(authId)` | `{ id, name, rootCategoryId, languageId, taxrate, reducedtaxrate, taxfreerate }[]` |
### `create` (transaction) ### `create` (transaction)
@@ -43,6 +43,11 @@ For `POST /api/company`:
3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId` 3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId`
4. Creates root category (`parentId = null`) 4. Creates root category (`parentId = null`)
5. Inserts company with all FKs set 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) ### `update` (transaction)
@@ -121,7 +126,8 @@ Backend mapping:
| Route | Store call | | 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 })` | | `PATCH /api/company/:id` | `company.update(id, { iso639, name, taxrate, reducedtaxrate })` |
| `DELETE /api/company/:id` | `company.delete(id)` | | `DELETE /api/company/:id` | `company.delete(id)` |
| `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(authId, companyId)` | | `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(authId, companyId)` |
@@ -131,7 +137,9 @@ Backend mapping:
## Scripts ## Scripts
```bash ```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 test:api # interactive API tester
npm run schema-graph # regenerate schema-graph.svg npm run schema-graph # regenerate schema-graph.svg
``` ```

View File

@@ -12,6 +12,8 @@
"scripts": { "scripts": {
"schema-graph": "node generate-schema-graph.js", "schema-graph": "node generate-schema-graph.js",
"init-schema": "node scripts/init-schema.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" "test:api": "node scripts/test-api.js"
}, },
"dependencies": { "dependencies": {

21
scripts/purge-db.js Normal file
View File

@@ -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();
}

25
scripts/reset-db.js Normal file
View File

@@ -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();
}

View File

@@ -70,9 +70,9 @@ const MENU = {
company: { company: {
label: 'company', label: 'company',
items: [ 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.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' }, { value: 'company.delete', label: 'delete', hint: 'companyId' },
], ],
}, },
@@ -178,25 +178,25 @@ 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 existing = await client.google_auth.getByEmail(email);
const authId = existing
? existing.id
: await client.google_auth.add(email);
const created = await client.company.create({ const created = await client.company.create({
iso639, iso639,
name, name,
taxrate, taxrate,
reducedtaxrate, reducedtaxrate,
authId,
}); });
const companyId = created.id; 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); await client.google_auth.setSelectedCompany(authId, companyId);
return [ return [
'created', 'created',
{ {
...created, ...created,
companyUserId,
authId, authId,
email, email,
name, name,
@@ -207,6 +207,8 @@ async function runMethod(client, action) {
]; ];
} }
case 'company.create': { case 'company.create': {
const authId = await askInt('google_auth id');
if (authId == null) return;
const name = await askText('Name', { const name = await askText('Name', {
placeholder: 'Acme', placeholder: 'Acme',
validate: (v) => (!v?.trim() ? 'Required' : undefined), validate: (v) => (!v?.trim() ? 'Required' : undefined),
@@ -223,7 +225,13 @@ async function runMethod(client, action) {
if (reducedtaxrate == null) return; if (reducedtaxrate == null) return;
return [ return [
'result', 'result',
await client.company.create({ iso639, name, taxrate, reducedtaxrate }), await client.company.create({
iso639,
name,
taxrate,
reducedtaxrate,
authId: Number(authId),
}),
]; ];
} }
case 'company.update': { case 'company.update': {
@@ -254,7 +262,9 @@ async function runMethod(client, action) {
]; ];
} }
case 'company.list': { 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': { case 'company.delete': {
const companyId = await askInt('Company id'); const companyId = await askInt('Company id');

View File

@@ -42,13 +42,16 @@ WHERE id = @id
export const LIST = ` export const LIST = `
SELECT SELECT
id, c.id,
root_category_id AS rootCategoryId, c.name,
default_language_id AS languageId, c.root_category_id AS rootCategoryId,
taxrate AS taxrateId, c.default_language_id AS languageId,
reducedtaxrate AS reducedtaxrateId, c.taxrate,
taxfreerate AS taxfreerateId c.reducedtaxrate,
FROM company c.taxfreerate
FROM company c
INNER JOIN companyuser cu ON cu.company_id = c.id
WHERE cu.google_auth_id = @authId
`; `;
export const DELETE = ` export const DELETE = `

View File

@@ -1,12 +1,11 @@
import sql from 'mssql';
import { CREATE } from '../../sql/category.js'; import { CREATE } from '../../sql/category.js';
export default function create(pool) { export default function create(pool) {
return async ({ parentId, sort }) => { return async ({ parentId, sort }) => {
const result = await pool const result = await pool
.request() .request()
.input('parentId', sql.Int, parentId) .input('parentId', parentId)
.input('sort', sql.Int, sort) .input('sort', sort)
.query(CREATE); .query(CREATE);
return result.recordset[0].id; return result.recordset[0].id;

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { DELETE } from '../../sql/category.js'; import { DELETE } from '../../sql/category.js';
export default function del(pool) { export default function del(pool) {
return async (id) => { return async (id) => {
await pool await pool
.request() .request()
.input('id', sql.Int, id) .input('id', id)
.query(DELETE); .query(DELETE);
}; };
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_TREE } from '../../sql/category.js'; import { GET_TREE } from '../../sql/category.js';
export default function getTree(pool) { export default function getTree(pool) {
return async (rootId) => { return async (rootId) => {
const result = await pool const result = await pool
.request() .request()
.input('rootId', sql.Int, rootId) .input('rootId', rootId)
.query(GET_TREE); .query(GET_TREE);
return result.recordset; return result.recordset;

View File

@@ -1,12 +1,11 @@
import sql from 'mssql';
import { UPDATE_PARENT_ID } from '../../sql/category.js'; import { UPDATE_PARENT_ID } from '../../sql/category.js';
export default function updateParentId(pool) { export default function updateParentId(pool) {
return async (id, parentId) => { return async (id, parentId) => {
await pool await pool
.request() .request()
.input('id', sql.Int, id) .input('id', id)
.input('parentId', sql.Int, parentId) .input('parentId', parentId)
.query(UPDATE_PARENT_ID); .query(UPDATE_PARENT_ID);
}; };
} }

View File

@@ -1,6 +1,6 @@
import sql from 'mssql';
import { CREATE } from '../../sql/company.js'; import { CREATE } from '../../sql/company.js';
import { CREATE as CREATE_CATEGORY } from '../../sql/category.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 getByName from './getByName.js';
import langEnsure from '../lang/ensure.js'; import langEnsure from '../lang/ensure.js';
import taxEnsure from '../tax/ensure.js'; import taxEnsure from '../tax/ensure.js';
@@ -11,8 +11,8 @@ export default function create(pool) {
const ensureLang = langEnsure(pool); const ensureLang = langEnsure(pool);
const ensureTax = taxEnsure(pool); const ensureTax = taxEnsure(pool);
return async ({ iso639, name, taxrate, reducedtaxrate }) => { return async ({ iso639, name, taxrate, reducedtaxrate, authId }) => {
const tx = new sql.Transaction(pool); const tx = pool.transaction();
await tx.begin(); await tx.begin();
try { try {
@@ -27,25 +27,32 @@ export default function create(pool) {
const taxfreerateId = await ensureTax(0, tx); const taxfreerateId = await ensureTax(0, tx);
const category = await request(pool, tx) const category = await request(pool, tx)
.input('parentId', sql.Int, null) .input('parentId', null)
.input('sort', sql.Int, 0) .input('sort', 0)
.query(CREATE_CATEGORY); .query(CREATE_CATEGORY);
const rootCategoryId = category.recordset[0].id; const rootCategoryId = category.recordset[0].id;
const result = await request(pool, tx) const result = await request(pool, tx)
.input('name', sql.NVarChar(255), name) .input('name', name)
.input('rootCategoryId', sql.Int, rootCategoryId) .input('rootCategoryId', rootCategoryId)
.input('languageId', sql.Int, languageId) .input('languageId', languageId)
.input('taxrate', sql.Int, taxrateId) .input('taxrate', taxrateId)
.input('reducedtaxrate', sql.Int, reducedtaxrateId) .input('reducedtaxrate', reducedtaxrateId)
.input('taxfreerate', sql.Int, taxfreerateId) .input('taxfreerate', taxfreerateId)
.query(CREATE); .query(CREATE);
const companyId = result.recordset[0].id;
await request(pool, tx)
.input('companyId', companyId)
.input('authId', authId)
.query(ADD_COMPANYUSER);
await tx.commit(); await tx.commit();
return { return {
id: result.recordset[0].id, id: companyId,
rootCategoryId, rootCategoryId,
languageId, languageId,
taxrateId, taxrateId,

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { DELETE } from '../../sql/company.js'; import { DELETE } from '../../sql/company.js';
export default function del(pool) { export default function del(pool) {
return async (companyId) => { return async (companyId) => {
await pool await pool
.request() .request()
.input('companyId', sql.Int, companyId) .input('companyId', companyId)
.query(DELETE); .query(DELETE);
}; };
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_ID } from '../../sql/company.js'; import { GET_BY_ID } from '../../sql/company.js';
import request from '../request.js'; import request from '../request.js';
export default function getById(pool) { export default function getById(pool) {
return async (id, tx) => { return async (id, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('id', sql.Int, id) .input('id', id)
.query(GET_BY_ID); .query(GET_BY_ID);
return result.recordset[0]; return result.recordset[0];

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_NAME } from '../../sql/company.js'; import { GET_BY_NAME } from '../../sql/company.js';
import request from '../request.js'; import request from '../request.js';
export default function getByName(pool) { export default function getByName(pool) {
return async (name, tx) => { return async (name, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('name', sql.NVarChar(255), name) .input('name', name)
.query(GET_BY_NAME); .query(GET_BY_NAME);
return result.recordset[0]; return result.recordset[0];

View File

@@ -1,8 +1,11 @@
import { LIST } from '../../sql/company.js'; import { LIST } from '../../sql/company.js';
export default function list(pool) { export default function list(pool) {
return async () => { return async (authId) => {
const result = await pool.request().query(LIST); const result = await pool
.request()
.input('authId', authId)
.query(LIST);
return result.recordset; return result.recordset;
}; };
} }

View File

@@ -1,4 +1,3 @@
import sql from 'mssql';
import { UPDATE } from '../../sql/company.js'; import { UPDATE } from '../../sql/company.js';
import getById from './getById.js'; import getById from './getById.js';
import getByName from './getByName.js'; import getByName from './getByName.js';
@@ -13,7 +12,7 @@ export default function update(pool) {
const ensureTax = taxEnsure(pool); const ensureTax = taxEnsure(pool);
return async (id, { iso639, name, taxrate, reducedtaxrate }) => { return async (id, { iso639, name, taxrate, reducedtaxrate }) => {
const tx = new sql.Transaction(pool); const tx = pool.transaction();
await tx.begin(); await tx.begin();
try { try {
@@ -34,11 +33,11 @@ export default function update(pool) {
const reducedtaxrateId = await ensureTax(reducedtaxrate, tx); const reducedtaxrateId = await ensureTax(reducedtaxrate, tx);
await request(pool, tx) await request(pool, tx)
.input('id', sql.Int, id) .input('id', id)
.input('name', sql.NVarChar(255), name) .input('name', name)
.input('languageId', sql.Int, languageId) .input('languageId', languageId)
.input('taxrate', sql.Int, taxrateId) .input('taxrate', taxrateId)
.input('reducedtaxrate', sql.Int, reducedtaxrateId) .input('reducedtaxrate', reducedtaxrateId)
.query(UPDATE); .query(UPDATE);
await tx.commit(); await tx.commit();

View File

@@ -1,12 +1,11 @@
import sql from 'mssql';
import { ADD } from '../../sql/companyuser.js'; import { ADD } from '../../sql/companyuser.js';
export default function add(pool) { export default function add(pool) {
return async (companyId, authId) => { return async (companyId, authId) => {
const result = await pool const result = await pool
.request() .request()
.input('companyId', sql.Int, companyId) .input('companyId', companyId)
.input('authId', sql.Int, authId) .input('authId', authId)
.query(ADD); .query(ADD);
return result.recordset[0].id; return result.recordset[0].id;

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { DELETE } from '../../sql/companyuser.js'; import { DELETE } from '../../sql/companyuser.js';
export default function del(pool) { export default function del(pool) {
return async (id) => { return async (id) => {
await pool await pool
.request() .request()
.input('id', sql.Int, id) .input('id', id)
.query(DELETE); .query(DELETE);
}; };
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_EMAIL } from '../../sql/companyuser.js'; import { GET_BY_EMAIL } from '../../sql/companyuser.js';
export default function getByEmail(pool) { export default function getByEmail(pool) {
return async (email) => { return async (email) => {
const result = await pool const result = await pool
.request() .request()
.input('email', sql.NVarChar(320), email) .input('email', email)
.query(GET_BY_EMAIL); .query(GET_BY_EMAIL);
return result.recordset.map((row) => row.id); return result.recordset.map((row) => row.id);

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { ADD } from '../../sql/google_auth.js'; import { ADD } from '../../sql/google_auth.js';
export default function add(pool) { export default function add(pool) {
return async (email) => { return async (email) => {
const result = await pool const result = await pool
.request() .request()
.input('email', sql.NVarChar(320), email) .input('email', email)
.query(ADD); .query(ADD);
return result.recordset[0].id; return result.recordset[0].id;

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_EMAIL } from '../../sql/google_auth.js'; import { GET_BY_EMAIL } from '../../sql/google_auth.js';
export default function getByEmail(pool) { export default function getByEmail(pool) {
return async (email) => { return async (email) => {
const result = await pool const result = await pool
.request() .request()
.input('email', sql.NVarChar(320), email) .input('email', email)
.query(GET_BY_EMAIL); .query(GET_BY_EMAIL);
return result.recordset[0]; return result.recordset[0];

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { REMOVE_ALLOWED_EMAIL } from '../../sql/google_auth.js'; import { REMOVE_ALLOWED_EMAIL } from '../../sql/google_auth.js';
export default function removeAllowedEmail(pool) { export default function removeAllowedEmail(pool) {
return async (email) => { return async (email) => {
await pool await pool
.request() .request()
.input('email', sql.NVarChar(320), email) .input('email', email)
.query(REMOVE_ALLOWED_EMAIL); .query(REMOVE_ALLOWED_EMAIL);
}; };
} }

View File

@@ -1,12 +1,11 @@
import sql from 'mssql';
import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js'; import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js';
export default function setSelectedCompany(pool) { export default function setSelectedCompany(pool) {
return async (id, companyId) => { return async (id, companyId) => {
await pool await pool
.request() .request()
.input('id', sql.Int, id) .input('id', id)
.input('companyId', sql.Int, companyId) .input('companyId', companyId)
.query(SET_SELECTED_COMPANY); .query(SET_SELECTED_COMPANY);
}; };
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { CREATE } from '../../sql/language.js'; import { CREATE } from '../../sql/language.js';
import request from '../request.js'; import request from '../request.js';
export default function create(pool) { export default function create(pool) {
return async (iso639, tx) => { return async (iso639, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('iso639', sql.NVarChar(10), iso639) .input('iso639', iso639)
.query(CREATE); .query(CREATE);
return result.recordset[0].id; return result.recordset[0].id;

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { DELETE } from '../../sql/language.js'; import { DELETE } from '../../sql/language.js';
export default function del(pool) { export default function del(pool) {
return async (id) => { return async (id) => {
await pool await pool
.request() .request()
.input('id', sql.Int, id) .input('id', id)
.query(DELETE); .query(DELETE);
}; };
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_ISO639 } from '../../sql/language.js'; import { GET_BY_ISO639 } from '../../sql/language.js';
import request from '../request.js'; import request from '../request.js';
export default function getByIso639(pool) { export default function getByIso639(pool) {
return async (iso639, tx) => { return async (iso639, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('iso639', sql.NVarChar(10), iso639) .input('iso639', iso639)
.query(GET_BY_ISO639); .query(GET_BY_ISO639);
return result.recordset[0]; return result.recordset[0];

View File

@@ -1,5 +1,3 @@
import sql from 'mssql';
export default function request(pool, tx) { export default function request(pool, tx) {
return tx ? new sql.Request(tx) : pool.request(); return (tx ?? pool).request();
} }

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { CREATE } from '../../sql/tax.js'; import { CREATE } from '../../sql/tax.js';
import request from '../request.js'; import request from '../request.js';
export default function create(pool) { export default function create(pool) {
return async (rate, tx) => { return async (rate, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('rate', sql.Decimal(18, 4), rate) .input('rate', rate)
.query(CREATE); .query(CREATE);
return result.recordset[0].id; return result.recordset[0].id;

View File

@@ -1,11 +1,10 @@
import sql from 'mssql';
import { GET_BY_RATE } from '../../sql/tax.js'; import { GET_BY_RATE } from '../../sql/tax.js';
import request from '../request.js'; import request from '../request.js';
export default function getByRate(pool) { export default function getByRate(pool) {
return async (rate, tx) => { return async (rate, tx) => {
const result = await request(pool, tx) const result = await request(pool, tx)
.input('rate', sql.Decimal(18, 4), rate) .input('rate', rate)
.query(GET_BY_RATE); .query(GET_BY_RATE);
return result.recordset[0]; return result.recordset[0];