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 |
| --- | --- | --- |
| `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
```

View File

@@ -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": {

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: {
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');

View File

@@ -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 = `

View File

@@ -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;

View File

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

View File

@@ -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;

View File

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

View File

@@ -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,

View File

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

View File

@@ -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];

View File

@@ -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];

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

@@ -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;

View File

@@ -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];

View File

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

View File

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

View File

@@ -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;

View File

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

View File

@@ -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];

View File

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

View File

@@ -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;

View File

@@ -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];