u
This commit is contained in:
16
README.md
16
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
|
||||
```
|
||||
|
||||
@@ -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
21
scripts/purge-db.js
Normal 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
25
scripts/reset-db.js
Normal 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();
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user