import { readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const storeDir = join(__dirname, 'src', 'store'); const outputPath = join(__dirname, 'API.md'); // Modules that are exposed on the LibDashStore instance (from src/index.js). // Order here controls the order of sections in the generated document. const MODULE_ORDER = [ 'companyuser', 'company', 'category', 'article', 'lang', 'tax', 'unit', 'companysetting', 'brand', 'customergroup', 'entity', 'entityaddress', 'entitycontact', 'supplier', 'articlesupplier', 'delivery', 'quote', 'order', 'shipment', 'shipping', 'manifestattachment', 'ledger', 'warehouse', 'stock', 'posterminal', 'printer', 'google_auth', 'pic', 'numbers', 'propertyname', 'articleproperty', 'attributename', 'attributevalue', 'articleattributevalue', 'labeltemplate', ]; // Files that are internal helpers and should not appear as public API entries. const INTERNAL_FILES = new Set([ 'request.js', 'slugify.js', 'initialize.js', 'purge.js', 'helpers.js', 'writeLines.js', 'shapeSetting.js', 'shapeUnit.js', 'ensureDefault.js', 'ensureLieferantenGroup.js', 'resolveCompanyAccess.js', 'resolveSelectedCompanyAccess.js', 'resolveCategoryAccess.js', 'resolveCustomerGroupAccess.js', 'resolveEntityAccess.js', 'resolvePicAccess.js', 'resolveArticleBySku.js', 'resolveManifestAccess.js', 'queryBySku.js', 'queryCategoryArticles.js', 'getCategoryPosterminalIds.js', 'nextSku.js', 'nextNumber.js', 'peekNumber.js', 'shape.js', 'createStore.js', 'manifestLines.js', 'runner.js', 'index.js', ]); /** * Strip the outer braces from a JSDoc type (e.g. `{number}` -> `number`), * preserving nested braces inside generics. * @param {string} type * @returns {string} */ function stripBraces(type) { if (type.startsWith('{') && type.endsWith('}')) { return type.slice(1, -1); } return type; } /** * Extract the leading JSDoc block from a source string. * @param {string} source * @returns {{ description: string, example: string, params: Array, returns: string|null, throws: string|null } | null} */ function extractJsDoc(source) { const match = source.match(/\/\*\*([\s\S]*?)\*\//); if (!match) return null; const block = match[1]; const lines = block .split('\n') .map((l) => l.replace(/^\s*\*\s?/, '').replace(/^\s*\*/, '')); const description = []; const example = []; const params = []; let returns = null; let throws = null; let inExample = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed === '@example') { inExample = true; continue; } if (trimmed.startsWith('@')) { inExample = false; const paramMatch = trimmed.match(/^@param\s+(\{.*\})\s+(\S+)\s*[-–]?\s*(.*)$/); if (paramMatch) { params.push({ type: stripBraces(paramMatch[1]), name: paramMatch[2], description: paramMatch[3].trim(), }); continue; } const returnsMatch = trimmed.match(/^@returns\s+(\{.*\})\s*[-–]?\s*(.*)$/); if (returnsMatch) { returns = { type: stripBraces(returnsMatch[1]), description: returnsMatch[2].trim() }; continue; } const throwsMatch = trimmed.match(/^@throws\s+(\{.*\})\s*[-–]?\s*(.*)$/); if (throwsMatch) { throws = { type: stripBraces(throwsMatch[1]), description: throwsMatch[2].trim() }; continue; } continue; } if (inExample) { example.push(line); } else if (trimmed) { description.push(trimmed); } } return { description: description.join(' ').trim(), example: example.join('\n').trim(), params, returns, throws, }; } /** * Recursively collect all .js files under a directory. * @param {string} dir * @returns {string[]} absolute file paths */ function collectFiles(dir) { const out = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { out.push(...collectFiles(full)); } else if (entry.endsWith('.js')) { out.push(full); } } return out; } /** * Build a map of module name -> array of { name, file, description, example }. * @returns {Map>} */ function buildModules() { const modules = new Map(); for (const file of collectFiles(storeDir)) { const rel = file.slice(storeDir.length + 1).split('/'); const moduleName = rel[0]; const fileName = rel[rel.length - 1]; if (INTERNAL_FILES.has(fileName)) continue; const source = readFileSync(file, 'utf8'); const doc = extractJsDoc(source); if (!doc) continue; const fnName = fileName.replace(/\.js$/, ''); if (!modules.has(moduleName)) { modules.set(moduleName, []); } modules.get(moduleName).push({ name: fnName, file: rel.join('/'), description: doc.description, example: doc.example, params: doc.params, returns: doc.returns, throws: doc.throws, }); } for (const entries of modules.values()) { entries.sort((a, b) => a.name.localeCompare(b.name)); } return modules; } function renderSection(moduleName, entries) { const lines = []; lines.push(`# ${moduleName}`); lines.push(''); lines.push( 'This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.', ); lines.push(''); for (const entry of entries) { lines.push(`## \`${moduleName}.${entry.name}\``); lines.push(''); lines.push(`*Source: \`src/store/${entry.file}\`*`); lines.push(''); if (entry.description) { lines.push(entry.description); lines.push(''); } if (entry.params && entry.params.length) { lines.push('**Parameters**'); lines.push(''); lines.push('| Name | Type | Description |'); lines.push('| --- | --- | --- |'); for (const p of entry.params) { lines.push(`| \`${p.name}\` | \`${p.type}\` | ${p.description} |`); } lines.push(''); } if (entry.returns) { lines.push('**Returns**'); lines.push(''); lines.push(`\`${entry.returns.type}\`${entry.returns.description ? ` — ${entry.returns.description}` : ''}`); lines.push(''); } if (entry.throws) { lines.push('**Throws**'); lines.push(''); lines.push(`\`${entry.throws.type}\`${entry.throws.description ? ` — ${entry.throws.description}` : ''}`); lines.push(''); } if (entry.example) { lines.push('**Example**'); lines.push(''); lines.push('```js'); lines.push(entry.example); lines.push('```'); lines.push(''); } } return lines.join('\n'); } function formatParamType(param, allParams) { const cleanName = param.name.replace(/^\[|\]$/g, '').split('=')[0].trim(); const prefix = cleanName + '.'; const childParams = allParams.filter((p) => { const pClean = p.name.replace(/^\[|\]$/g, '').split('=')[0].trim(); return pClean.startsWith(prefix) && !pClean.slice(prefix.length).includes('.'); }); if (childParams.length > 0) { const props = childParams.map((cp) => { const isOpt = cp.name.startsWith('['); const propName = cp.name.replace(/^\[|\]$/g, '').split('=')[0].trim().slice(prefix.length); const propType = formatParamType(cp, allParams); return `${propName}${isOpt ? '?' : ''}: ${propType}`; }); return `{${props.join(', ')}}`; } return param.type || 'any'; } function formatMethodSignature(entry) { const topLevelParams = (entry.params || []).filter((p) => !p.name.includes('.')); const paramsStr = topLevelParams .map((p) => { const isOptional = p.name.startsWith('['); const cleanName = p.name.replace(/^\[|\]$/g, '').split('=')[0].trim(); const typeStr = formatParamType(p, entry.params || []); return `${cleanName}${isOptional ? '?' : ''}: ${typeStr}`; }) .join(', '); const returnType = entry.returns?.type ? `: ${entry.returns.type}` : ': void'; return `(${paramsStr})${returnType}`; } function render() { const modules = buildModules(); const ordered = MODULE_ORDER.filter((m) => modules.has(m)); const extra = [...modules.keys()].filter((m) => !MODULE_ORDER.includes(m)).sort(); const moduleNames = [...ordered, ...extra]; // Write one file per section: API.
.md const written = []; for (const moduleName of moduleNames) { const sectionPath = join(__dirname, `API.${moduleName}.md`); const markdown = renderSection(moduleName, modules.get(moduleName)); writeFileSync(sectionPath, markdown, 'utf8'); written.push({ moduleName, sectionPath, bytes: markdown.length }); } const SCHEMA_TABLES = [ ['account', 'id PK, company_id->company, type:s(100), name:s(255)'], ['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'], ['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'], ['articleean', 'id PK, article_id->article, ean:s(50) UQ'], ['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt, meta?:txt'], ['articlelist', 'id PK'], ['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'], ['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'], ['articleproperty', 'id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0'], ['articlesupplier', 'id PK, article_id->article, entity_id->entity, net?:f, tax_id?->tax, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)'], ['articletext', 'id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)'], ['articletombstone', 'id PK, company_id->company, article_id?:i, sku?:s(100), name?:s(255), taxrate?:d(18,4), net?:f, deleted_at:dt'], ['attributename', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], ['attributevalue', 'id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)'], ['attributevaluepic', 'id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)'], ['brand', 'id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)'], ['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'], ['category', 'id PK, parent_id?->category, sort:i=0'], ['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'], ['categoryattributevalue', 'id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)'], ['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'], ['categoryproperty', 'id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0'], ['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'], ['company', 'id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'], ['companyhistory', 'id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt'], ['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'], ['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'], ['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'], ['deliverysummary', 'id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0'], ['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'], ['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'], ['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'], ['entityhistory', 'id PK, entity_id->entity, companyuser_id?->companyuser, date:dt, text:txt'], ['file', 'id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt'], ['google_auth', 'id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company'], ['labeltemplate', 'id PK, company_id->company, name:s(255), type:s(50), width:f, height:f, data:txt, sort:i=0'], ['language', 'id PK, iso639:s(10) UQ'], ['ledger', 'id PK, manifestarticle_id->manifestarticle, warehouse_id?->warehouse, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]'], ['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'], ['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'], ['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'], ['mcp_auth_code', 'code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt'], ['mcp_client', 'id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt'], ['mcp_token', 'id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt'], ['meta', 'key:s(255) PK, value:txt'], ['modelconversation', 'id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)=\'idle\', started_by?:i'], ['modelmessage', 'id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt'], ['modelvfs', 'id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)'], ['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'], ['optionslist', 'id PK'], ['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'], ['ordersummary', 'id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0'], ['package', 'id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt'], ['payment', 'id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f'], ['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin'], ['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'], ['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'], ['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'], ['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'], ['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], ['quotesummary', 'id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0'], ['shipmentsummary', 'id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)=\'new\''], ['shipping', 'id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)'], ['subagentschedule', 'id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt'], ['tax', 'id PK, rate:d(18,4) UQ'], ['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'], ['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'], ['warehouse', 'id PK, company_id->company, name:s(255), sort:i=0, UQ(company_id, name)'], ]; // Write the index API.md linking to each section file. const lines = []; lines.push('# LibDashStore API'); lines.push(''); lines.push( 'This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.', ); lines.push(''); lines.push( 'The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each method is an async function that takes a `pool` (mssql connection pool) at construction time and returns a callable function. Methods are grouped by the namespace they are exposed under.', ); lines.push(''); lines.push('## Database Schema'); lines.push(''); lines.push( '> **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default', ); lines.push(''); for (const [table, cols] of SCHEMA_TABLES) { lines.push(cols ? `- **${table}**: \`${cols}\`` : `- **${table}**`); } lines.push(''); lines.push('## Table of Contents'); lines.push(''); for (const { moduleName } of written) { lines.push(`- [${moduleName}](API.${moduleName}.md)`); const entries = modules.get(moduleName) || []; for (const entry of entries) { const sig = formatMethodSignature(entry); lines.push(` - \`${entry.name}${sig}\``); } } lines.push(''); return { index: lines.join('\n'), written }; } const { index, written } = render(); writeFileSync(outputPath, index, 'utf8'); console.log(`Wrote ${outputPath} (${index.length} bytes)`); for (const { moduleName, sectionPath, bytes } of written) { console.log(`Wrote ${sectionPath} (${bytes} bytes)`); }