diff --git a/generate-schema-graph.js b/generate-schema-graph.js new file mode 100644 index 0000000..b93c8fb --- /dev/null +++ b/generate-schema-graph.js @@ -0,0 +1,741 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Graphviz } from '@hpcc-js/wasm-graphviz'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const schemaPath = join(__dirname, 'src', 'schema.js'); +const outputPath = join(__dirname, 'schema-graph.svg'); +const dotOutputPath = join(__dirname, 'schema-graph.dot'); + +function parseSchema(source) { + const tables = new Map(); + const edges = []; + + const initRegex = /export const (INIT_\w+)\s*=\s*`([\s\S]*?)`;/g; + let initMatch; + + while ((initMatch = initRegex.exec(source)) !== null) { + const initName = initMatch[1]; + const sqlBlock = initMatch[2]; + + const createRegex = /CREATE TABLE\s+(\w+)\s*\(([\s\S]*?)\);/gi; + let createMatch; + + while ((createMatch = createRegex.exec(sqlBlock)) !== null) { + const tableName = createMatch[1]; + const body = createMatch[2]; + const columns = []; + const constraints = []; + + for (const line of body.split('\n')) { + const trimmed = line.trim().replace(/,$/, ''); + if (!trimmed || trimmed.startsWith('CONSTRAINT')) { + const fkMatch = trimmed.match( + /CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+(\w+)\(([^)]+)\)(?:\s+ON DELETE\s+(.+))?/i + ); + if (fkMatch) { + const constraintName = fkMatch[1]; + const localCols = fkMatch[2] + .split(',') + .map((c) => c.trim().replace(/^\[|\]$/g, '')); + const refTable = fkMatch[3]; + const refCols = fkMatch[4] + .split(',') + .map((c) => c.trim().replace(/^\[|\]$/g, '')); + const onDelete = fkMatch[5]?.trim() ?? null; + + constraints.push({ + name: constraintName, + localCols, + refTable, + refCols, + onDelete, + }); + + edges.push({ + from: tableName, + to: refTable, + label: localCols.join(', '), + localCols, + refCols: refCols.join(', '), + constraint: constraintName, + onDelete, + }); + } + continue; + } + + const colMatch = trimmed.match( + /^(?:\[(\w+)\]|(\w+))\s+([\w]+(?:\([^)]*\))?(?:\s+IDENTITY\([^)]*\))?)/i + ); + if (!colMatch) continue; + + const name = (colMatch[1] ?? colMatch[2]).replace(/^\[|\]$/g, ''); + const type = colMatch[3].replace(/\s+IDENTITY\([^)]*\)/i, ''); + const nullable = + /\bNULL\b/i.test(trimmed) && !/\bNOT NULL\b/i.test(trimmed); + const isPk = /\bPRIMARY KEY\b/i.test(trimmed); + + columns.push({ name, type, nullable, isPk }); + } + + tables.set(tableName, { + name: tableName, + initName, + columns, + constraints, + }); + } + + const alterFkRegex = + /ALTER TABLE\s+(\w+)\s+ADD CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+(\w+)\(([^)]+)\)(?:\s+ON DELETE\s+([\w\s]+?))?\s*;/gi; + let alterMatch; + + while ((alterMatch = alterFkRegex.exec(sqlBlock)) !== null) { + const tableName = alterMatch[1]; + const constraintName = alterMatch[2]; + const localCols = alterMatch[3] + .split(',') + .map((c) => c.trim().replace(/^\[|\]$/g, '')); + const refTable = alterMatch[4]; + const refCols = alterMatch[5] + .split(',') + .map((c) => c.trim().replace(/^\[|\]$/g, '')); + const onDelete = alterMatch[6]?.trim() ?? null; + + const table = tables.get(tableName); + if (table) { + table.constraints.push({ + name: constraintName, + localCols, + refTable, + refCols, + onDelete, + }); + } + + edges.push({ + from: tableName, + to: refTable, + label: localCols.join(', '), + localCols, + refCols: refCols.join(', '), + constraint: constraintName, + onDelete, + }); + } + } + + addInferredEdges(tables, edges); + markForeignKeyColumns(tables, edges); + + return { tables, edges }; +} + +/** + * Some columns follow a `_id` naming convention that implies a + * relationship (e.g. `articlelist_id` -> `articlelist`) but never got an + * explicit FOREIGN KEY constraint in the SQL (often because the referenced + * table is declared later in the file). These are surfaced as dashed + * "inferred" edges so the gap is visible in the diagram instead of the + * column silently looking unrelated. + */ +function addInferredEdges(tables, edges) { + const explicitlyConstrained = new Set(); + for (const edge of edges) { + explicitlyConstrained.add(`${edge.from}.${edge.label}`); + } + + for (const [tableName, table] of tables) { + for (const column of table.columns) { + if (column.isPk) continue; + + const match = column.name.match(/^(.+)_id$/i); + if (!match) continue; + + const candidate = match[1]; + if (!tables.has(candidate) || candidate === tableName) continue; + if (explicitlyConstrained.has(`${tableName}.${column.name}`)) continue; + + edges.push({ + from: tableName, + to: candidate, + label: column.name, + localCols: [column.name], + refCols: 'id', + constraint: null, + onDelete: null, + inferred: true, + }); + } + } +} + +function markForeignKeyColumns(tables, edges) { + for (const edge of edges) { + const table = tables.get(edge.from); + if (!table) continue; + + for (const localCol of edge.localCols ?? []) { + const column = table.columns.find((c) => c.name === localCol); + if (column) column.isFk = true; + } + } +} + +const EDGE_COLORS = [ + '#c53030', + '#2f855a', + '#2b6cb0', + '#b7791f', + '#6b46c1', + '#c05621', + '#319795', + '#97266d', + '#4c51bf', + '#276749', + '#9c4221', + '#0987a0', + '#553c9a', + '#742a2a', + '#22543d', + '#1a365d', + '#702459', + '#744210', + '#285e61', + '#d53f8c', + '#dd6b20', + '#38a169', + '#3182ce', + '#805ad5', +]; + +function edgeKey(edge) { + return `${edge.from}|${edge.to}|${edge.label}|${edge.inferred ? 'inferred' : 'fk'}`; +} + +function assignEdgeColors(edges) { + const colors = new Map(); + let next = 0; + + for (const edge of edges) { + const key = edgeKey(edge); + if (!colors.has(key)) { + colors.set(key, EDGE_COLORS[next % EDGE_COLORS.length]); + next++; + } + } + + return colors; +} + +function escapeHtml(text) { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +/** + * Domain grouping. Graphviz keeps the members of a cluster physically + * together, which is what turns the diagram from one big hairball into + * readable regions. New tables that are not listed here still render, they + * just float outside any cluster - the script logs them so they can be + * assigned. + */ +const CLUSTERS = [ + { + id: 'identity', + label: 'Identity & Company', + tables: [ + 'meta', + 'google_auth', + 'companyuser', + 'company', + 'customergroup', + 'entity', + 'numbers', + ], + }, + { + id: 'i18n', + label: 'Localization', + tables: ['language', 'categorytext', 'articletext', 'unittext'], + }, + { + id: 'catalog', + label: 'Catalog', + tables: [ + 'category', + 'categoryarticle', + 'brand', + 'article', + 'articleean', + 'articlehistory', + 'articlelist', + 'articlelistarticle', + 'optionslist', + 'optionslistarticle', + ], + }, + { + id: 'media', + label: 'Media', + tables: ['pic', 'articlepic', 'categorypic', 'brandpic'], + }, + { + id: 'commerce', + label: 'Pricing & Documents', + tables: ['tax', 'price', 'manifest', 'manifestarticle'], + }, + { id: 'units', label: 'Units', tables: ['unit'] }, +]; + +const COLORS = { + background: '#ebf4ff', + tableBorder: '#a0aec0', + headerBg: '#bee3f8', + headerText: '#1a365d', + rowBg: '#ffffff', + rowBgAlt: '#f7fafc', + keyBg: '#ebf8ff', + text: '#2d3748', + muted: '#718096', +}; + +/** + * Reorder columns so FK ports line up with the horizontal position of their + * targets (barycenter heuristic on column/port order). PK stays on top, + * plain columns stay at the bottom. This is the classic "swap columns to + * reduce edge crossings" trick for port-based ERDs. + */ +function reorderColumnsForCrossingReduction(tables, edges, positions) { + const targetX = (tableName) => positions.get(tableName)?.x ?? 0; + + for (const [tableName, table] of tables) { + const outEdges = edges.filter((e) => e.from === tableName); + const score = new Map(); + + for (const edge of outEdges) { + const x = targetX(edge.to); + for (const col of edge.localCols ?? []) { + if (!score.has(col)) score.set(col, []); + score.get(col).push(x); + } + } + + const median = (values) => { + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1] + sorted[mid]) / 2 + : sorted[mid]; + }; + + const pk = []; + const fk = []; + const rest = []; + + for (const column of table.columns) { + if (column.isPk) pk.push(column); + else if (column.isFk || score.has(column.name)) fk.push(column); + else rest.push(column); + } + + fk.sort((a, b) => { + const aVals = score.get(a.name) ?? [Number.POSITIVE_INFINITY]; + const bVals = score.get(b.name) ?? [Number.POSITIVE_INFINITY]; + return median(aVals) - median(bVals); + }); + + table.columns = [...pk, ...fk, ...rest]; + } +} + +function parsePlainPositions(plain) { + const positions = new Map(); + + for (const line of plain.split('\n')) { + if (!line.startsWith('node ')) continue; + // node name x y width height ... + const match = line.match(/^node\s+(\S+)\s+([-\d.]+)\s+([-\d.]+)\s+/); + if (!match) continue; + positions.set(match[1], { x: Number(match[2]), y: Number(match[3]) }); + } + + return positions; +} + +/** + * Sort cluster members by their first-pass x so Graphviz's crossing + * minimizer starts from a better initial order within each cluster. + */ +function orderClusterMembers(cluster, positions) { + return [...cluster.tables].sort((a, b) => { + const ax = positions.get(a)?.x ?? Number.POSITIVE_INFINITY; + const bx = positions.get(b)?.x ?? Number.POSITIVE_INFINITY; + return ax - bx; + }); +} + +function buildTableLabel(table, inboundCount = 0) { + const titleFont = + `${escapeHtml(table.name)}`; + + let headerInner; + if (inboundCount > 0) { + // Ports sit on the top edge of the title bar so arrows end on the + // header itself instead of crossing through it toward the id row. + const portCells = []; + for (let i = 0; i < inboundCount; i++) { + portCells.push( + `` + ); + } + headerInner = + `
` + + `${portCells.join('')}` + + `` + + `
` + + `${titleFont}
`; + } else { + headerInner = titleFont; + } + + const rows = [ + `` + + `${headerInner}`, + ]; + + for (const [i, column] of table.columns.entries()) { + const marker = column.isPk ? 'PK' : column.isFk ? 'FK' : ''; + const isKey = Boolean(marker); + const bg = isKey ? COLORS.keyBg : i % 2 === 0 ? COLORS.rowBg : COLORS.rowBgAlt; + const nameMarkup = isKey + ? `${escapeHtml(column.name)}` + : escapeHtml(column.name); + const nullMarker = column.nullable ? '?' : ''; + + rows.push( + `` + + `` + + `` + + `` + + `` + + `
` + + `${nameMarkup}${nullMarker}` + + `${escapeHtml(column.type)}` + + (marker ? ` ${marker}` : '') + + `
` + ); + } + + return ( + `<` + + rows.join('') + + `
>` + ); +} + +/** + * Head: unique in* ports on the target title bar, ordered by source x. + * Tail: FK column port, always leaving sideways or diagonally + * (e/w/ne/nw/se/sw) — never straight up/down, which stacks stubs on the + * row edge. + */ +function assignEdgeAnchors(edges, positions) { + const headPort = new Map(); + const tailCompass = new Map(); + const inboundCounts = new Map(); + + const byTarget = new Map(); + const bySourceCol = new Map(); + + for (const edge of edges) { + if (!byTarget.has(edge.to)) byTarget.set(edge.to, []); + byTarget.get(edge.to).push(edge); + + const colKey = `${edge.from}|${edge.localCols?.[0] ?? 'id'}`; + if (!bySourceCol.has(colKey)) bySourceCol.set(colKey, []); + bySourceCol.get(colKey).push(edge); + } + + for (const [target, group] of byTarget) { + group.sort((a, b) => { + const ax = positions?.get(a.from)?.x ?? 0; + const bx = positions?.get(b.from)?.x ?? 0; + if (ax !== bx) return ax - bx; + return a.label.localeCompare(b.label); + }); + inboundCounts.set(target, group.length); + group.forEach((edge, i) => { + headPort.set(edgeKey(edge), `in${i}`); + }); + } + + for (const group of bySourceCol.values()) { + group.sort((a, b) => { + const ax = positions?.get(a.to)?.x ?? 0; + const bx = positions?.get(b.to)?.x ?? 0; + if (ax !== bx) return ax - bx; + return (positions?.get(a.to)?.y ?? 0) - (positions?.get(b.to)?.y ?? 0); + }); + + group.forEach((edge, i) => { + const fromX = positions?.get(edge.from)?.x ?? 0; + const toX = positions?.get(edge.to)?.x ?? fromX; + const fromY = positions?.get(edge.from)?.y ?? 0; + const toY = positions?.get(edge.to)?.y ?? fromY; + const dx = toX - fromX; + const dy = toY - fromY; + const absDx = Math.abs(dx); + const absDy = Math.abs(dy); + + // Graphviz y grows upward; smaller y = visually lower under rankdir=TB. + const goEast = dx > 0 || (dx === 0 && edge.from <= edge.to); + const goDown = dy < 0 || (dy === 0 && goEast); + const fan = + group.length === 1 ? 0 : (i / (group.length - 1)) * 2 - 1; + + let compass; + if (absDx >= absDy) { + // Mostly horizontal — cardinal side, fan to diagonals if shared. + if (group.length === 1) { + compass = goEast ? 'e' : 'w'; + } else { + compass = goEast + ? fan < -0.33 + ? 'ne' + : fan > 0.33 + ? 'se' + : 'e' + : fan < -0.33 + ? 'nw' + : fan > 0.33 + ? 'sw' + : 'w'; + } + } else { + // Mostly vertical — still leave via a diagonal/side, never n/s. + compass = goEast + ? goDown + ? 'se' + : 'ne' + : goDown + ? 'sw' + : 'nw'; + } + + tailCompass.set(edgeKey(edge), compass); + }); + } + + return { headPort, tailCompass, inboundCounts }; +} + +function buildLegendLabel() { + const row = (swatch, text) => + `${swatch}` + + `${text}`; + + return ( + `<` + + `` + + row(`PK`, 'primary key') + + row(`FK`, 'foreign key') + + row(`?`, 'nullable column') + + row( + `—`, + 'FOREIGN KEY constraint' + ) + + row( + `- -`, + 'naming convention only, no constraint' + ) + + `
` + + `Legend
>` + ); +} + +function buildDot(tables, edges, positions = null) { + const edgeColors = assignEdgeColors(edges); + const { headPort, tailCompass, inboundCounts } = assignEdgeAnchors( + edges, + positions + ); + const lines = [ + 'digraph schema {', + // TB = landscape for this schema (many tables per rank spread horizontally). + // mclimit bumps Graphviz's own crossing-minimization iterations. + ` graph [rankdir=TB, splines=spline, overlap=false, bgcolor="${COLORS.background}",`, + ' nodesep=0.55, ranksep=1.0, pad=0.5, fontname="Helvetica",', + ' compound=true, newrank=true, mclimit=4.0];', + ' node [shape=plaintext, fontname="Helvetica"];', + ' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];', + '', + ]; + + const unassigned = new Set(tables.keys()); + + const emitNode = (name, indent) => { + const table = tables.get(name); + const label = buildTableLabel(table, inboundCounts.get(name) ?? 0); + lines.push(`${indent}${name} [label=${label}];`); + }; + + for (const cluster of CLUSTERS) { + const members = ( + positions ? orderClusterMembers(cluster, positions) : cluster.tables + ).filter((name) => tables.has(name)); + if (members.length === 0) continue; + + lines.push(` subgraph cluster_${cluster.id} {`); + lines.push( + ` label="${escapeHtml(cluster.label)}"; labeljust="l"; fontsize=15;`, + ` style="rounded,filled"; fillcolor="#ffffff60"; color="${COLORS.tableBorder}";`, + ` fontcolor="${COLORS.headerText}"; margin=14;` + ); + + for (const name of members) { + emitNode(name, ' '); + unassigned.delete(name); + } + + lines.push(' }'); + } + + const leftover = positions + ? [...unassigned].sort( + (a, b) => (positions.get(a)?.x ?? 0) - (positions.get(b)?.x ?? 0) + ) + : [...unassigned]; + + for (const name of leftover) { + emitNode(name, ' '); + } + + lines.push(''); + lines.push(` __legend [label=${buildLegendLabel()}];`); + lines.push(''); + + for (const edge of edges) { + const key = edgeKey(edge); + const color = edgeColors.get(key); + const sourcePort = edge.localCols?.[0] ?? 'id'; + const targetPort = headPort.get(key) ?? 'in0'; + const tail = tailCompass.get(key) ?? 'e'; + const attrs = [ + `color="${color}"`, + `fontcolor="${color}"`, + `tooltip="${escapeHtml(edge.constraint ?? `${edge.from}.${edge.label} -> ${edge.to}`)}"`, + ]; + + if (edge.inferred) { + attrs.push('style=dashed', 'label="inferred"'); + } + + lines.push( + ` ${edge.from}:${sourcePort}:${tail} -> ${edge.to}:${targetPort}:n [${attrs.join(', ')}];` + ); + } + + lines.push('}'); + return lines.join('\n'); +} + +function countSvgCrossings(svg) { + const paths = [ + ...svg.matchAll(/]*d="([^"]+)"/g), + ].map((m) => m[1]); + const segs = []; + + for (const d of paths) { + const nums = [...d.matchAll(/([-\d.]+)[,\s]+([-\d.]+)/g)].map((m) => [ + Number(m[1]), + Number(m[2]), + ]); + for (let i = 0; i < nums.length - 1; i++) segs.push([nums[i], nums[i + 1]]); + } + + const orient = (a, b, c) => + (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]); + const crosses = (a, b, c, d) => { + const o1 = orient(a, b, c); + const o2 = orient(a, b, d); + const o3 = orient(c, d, a); + const o4 = orient(c, d, b); + return o1 * o2 < 0 && o3 * o4 < 0; + }; + + let n = 0; + for (let i = 0; i < segs.length; i++) { + for (let j = i + 1; j < segs.length; j++) { + const [a, b] = segs[i]; + const [c, d] = segs[j]; + if ( + (a[0] === c[0] && a[1] === c[1]) || + (a[0] === d[0] && a[1] === d[1]) || + (b[0] === c[0] && b[1] === c[1]) || + (b[0] === d[0] && b[1] === d[1]) + ) { + continue; + } + if (crosses(a, b, c, d)) n++; + } + } + + return n; +} + +const schemaSource = readFileSync(schemaPath, 'utf8'); +const { tables, edges } = parseSchema(schemaSource); + +if (tables.size === 0) { + console.error('No tables found in schema.js'); + process.exit(1); +} + +const clustered = new Set(CLUSTERS.flatMap((c) => c.tables)); +const unclustered = [...tables.keys()].filter((name) => !clustered.has(name)); +if (unclustered.length > 0) { + console.warn( + `Not assigned to a cluster (add them to CLUSTERS): ${unclustered.join(', ')}` + ); +} + +const graphviz = await Graphviz.load(); + +// Two layout passes: learn positions, then reorder columns/cluster members +// (barycenter) so FK ports and neighbor tables line up and crossings drop. +let positions = null; +for (let pass = 0; pass < 2; pass++) { + const probeDot = buildDot(tables, edges, positions); + const plain = graphviz.layout(probeDot, 'plain', 'dot'); + positions = parsePlainPositions(plain); + reorderColumnsForCrossingReduction(tables, edges, positions); +} + +const dot = buildDot(tables, edges, positions); +writeFileSync(dotOutputPath, dot, 'utf8'); + +const svg = graphviz.layout(dot, 'svg', 'dot'); +writeFileSync(outputPath, svg, 'utf8'); + +const dim = svg.match(/width="([\d.]+)pt" height="([\d.]+)pt"/); +const crossings = countSvgCrossings(svg); + +console.log(`Parsed ${tables.size} tables and ${edges.length} foreign keys`); +console.log(`Written ${dotOutputPath}`); +console.log(`Written ${outputPath}`); +if (dim) { + console.log( + `Layout ${dim[1]}pt × ${dim[2]}pt · ~${crossings} segment crossings` + ); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 91caf2b..1adc9ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@hpcc-js/wasm-graphviz": "^1.28.0", "dotenv": "^17.4.2", "mssql": "^12.7.0" } @@ -255,6 +256,12 @@ "node": ">=20" } }, + "node_modules/@hpcc-js/wasm-graphviz": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.28.0.tgz", + "integrity": "sha512-KqvPy7ckxPc1xv8Kt7H3bo9cj8U4gfOXxQ8grLZvDZxyQQjnXsKxrweJLLJtEoWWtQqAk4biUB0g2qsjkTiMVA==", + "license": "Apache-2.0" + }, "node_modules/@js-joda/core": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz", diff --git a/package.json b/package.json index 697bd46..9fbbf6c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,20 @@ { - "name": "dashdbschema", - "version": "1.0.0", - "description": "", + "name": "libdashstore", + "version": "0.1.0", + "description": "Dash store library — MSSQL schema and domain APIs", "license": "ISC", - "author": "", "type": "module", - "main": "schema.js", + "main": "./src/index.js", + "exports": { + ".": "./src/index.js", + "./schema": "./src/schema.js" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "schema-graph": "node generate-schema-graph.js", + "init-schema": "node scripts/init-schema.js" }, "dependencies": { + "@hpcc-js/wasm-graphviz": "^1.28.0", "dotenv": "^17.4.2", "mssql": "^12.7.0" } diff --git a/schema-graph.dot b/schema-graph.dot new file mode 100644 index 0000000..34f843e --- /dev/null +++ b/schema-graph.dot @@ -0,0 +1,110 @@ +digraph schema { + graph [rankdir=TB, splines=spline, overlap=false, bgcolor="#ebf4ff", + nodesep=0.55, ranksep=1.0, pad=0.5, fontname="Helvetica", + compound=true, newrank=true, mclimit=4.0]; + node [shape=plaintext, fontname="Helvetica"]; + edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7]; + + subgraph cluster_identity { + label="Identity & Company"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; + customergroup [label=<
customergroup
idINT PK
company_idINT FK
nameNVARCHAR(255)
>]; + company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
nameNVARCHAR(255)
>]; + numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
firstnameNVARCHAR(255)
lastnameNVARCHAR(255)
>]; + companyuser [label=<
companyuser
company_idINT FK
google_auth_idINT FK
>]; + google_auth [label=<
google_auth
idINT PK
emailNVARCHAR(320)
created_atDATETIME2
>]; + } + subgraph cluster_i18n { + label="Localization"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; + language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; + articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + } + subgraph cluster_catalog { + label="Catalog"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; + categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; + articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + article [label=<
article
idINT PK
unit_idINT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + articlelist [label=<
articlelist
idINT PK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; + optionslist [label=<
optionslist
idINT PK
>]; + brand [label=<
brand
idINT PK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; + } + subgraph cluster_media { + label="Media"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; + pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
>]; + } + subgraph cluster_commerce { + label="Pricing & Documents"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + manifest [label=<
manifest
idINT PK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; + price [label=<
price
idINT PK
customergroup_idINT FK
article_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + manifestarticle [label=<
manifestarticle
idINT PK
article_idINT FK
manifest_idINT FK
tax_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; + tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; + } + subgraph cluster_units { + label="Units"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; + fontcolor="#1a365d"; margin=14; + unit [label=<
unit
idINT PK
siNVARCHAR(50)
basesiNVARCHAR(50)
basesifactorFLOAT
>]; + } + + __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; + + category:parent_id:e -> category:in3:n [color="#c53030", fontcolor="#c53030", tooltip="FK_category_parent"]; + company:root_category_id:e -> category:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_company_root_category"]; + company:default_language_id:e -> language:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_default_language"]; + customergroup:company_id:se -> company:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_customergroup_company"]; + entity:customergroup_id:sw -> customergroup:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_entity_customergroup"]; + unittext:unit_id:se -> unit:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_unittext_unit"]; + unittext:language_id:se -> language:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_unittext_language"]; + article:unit_id:w -> unit:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_article_unit"]; + article:brand_id:se -> brand:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_article_brand"]; + article:articlelist_id:se -> articlelist:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_article_articlelist"]; + article:optionslist_id:se -> optionslist:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_article_optionslist"]; + article:fatherarticle_id:e -> article:in3:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_article_fatherarticle"]; + articlehistory:article_id:se -> article:in2:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articlehistory_article"]; + articleean:article_id:sw -> article:in4:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_articleean_article"]; + articlepic:article_id:w -> article:in9:n [color="#22543d", fontcolor="#22543d", tooltip="FK_articlepic_article"]; + articlepic:pic_id:se -> pic:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_articlepic_pic"]; + brandpic:brand_id:w -> brand:in1:n [color="#702459", fontcolor="#702459", tooltip="FK_brandpic_brand"]; + brandpic:pic_id:se -> pic:in2:n [color="#744210", fontcolor="#744210", tooltip="FK_brandpic_pic"]; + articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_articlelistarticle_articlelist"]; + articlelistarticle:article_id:sw -> article:in5:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlelistarticle_article"]; + optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_optionslistarticle_optionslist"]; + optionslistarticle:article_id:sw -> article:in6:n [color="#38a169", fontcolor="#38a169", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in2:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:se -> article:in1:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:w -> category:in4:n [color="#c53030", fontcolor="#c53030", tooltip="FK_categorypic_category"]; + categorypic:pic_id:se -> pic:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_categorypic_pic"]; + categorytext:category_id:se -> category:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_categorytext_language"]; + articletext:article_id:e -> article:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_articletext_article"]; + articletext:language_id:sw -> language:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_articletext_language"]; + price:tax_id:se -> tax:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_price_tax"]; + price:article_id:sw -> article:in7:n [color="#97266d", fontcolor="#97266d", tooltip="FK_price_article"]; + price:customergroup_id:w -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_price_customergroup"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:w -> article:in8:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:se -> tax:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:sw -> company:in1:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:se -> google_auth:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:sw -> company:in2:n [color="#22543d", fontcolor="#22543d", tooltip="FK_companyuser_company"]; +} \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg new file mode 100644 index 0000000..8231a3b --- /dev/null +++ b/schema-graph.svg @@ -0,0 +1,1277 @@ + + + + + + +schema + + +cluster_identity + +Identity & Company + + +cluster_i18n + +Localization + + +cluster_catalog + +Catalog + + +cluster_media + +Media + + +cluster_commerce + +Pricing & Documents + + +cluster_units + +Units + + + +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + + +customergroup + + + + +customergroup + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + + +company + + + + + +company + + +id +INT +PK + + +default_language_id +? +INT +FK + + +root_category_id +? +INT +FK + + +name +NVARCHAR(255) + + + +customergroup:se->company:n + + + + + + + + +language + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) + + + +company:e->language:n + + + + + + + + +category + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT + + + +company:e->category:n + + + + + + + + +numbers + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT + + + +numbers:sw->company:n + + + + + + + + +entity + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +firstname +NVARCHAR(255) + + +lastname +NVARCHAR(255) + + + +entity:sw->customergroup:n + + + + + + + + +companyuser + + +companyuser + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +companyuser:sw->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +email +NVARCHAR(320) + + +created_at +DATETIME2 + + + +companyuser:se->google_auth:n + + + + + + + + +unittext + + +unittext + + +id +INT +PK + + +language_id +INT +FK + + +unit_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) + + + +unittext:se->language:n + + + + + + + + +unit + + + + +unit + + +id +INT +PK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT + + + +unittext:se->unit:n + + + + + + + + +articletext + + +articletext + + +id +INT +PK + + +language_id +INT +FK + + +article_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +articletext:sw->language:n + + + + + + + + +article + + + + + + + + + + + + +article + + +id +INT +PK + + +unit_id +INT +FK + + +fatherarticle_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +brand_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +articletext:e->article:n + + + + + + + + +categorytext + + +categorytext + + +id +INT +PK + + +language_id +INT +FK + + +category_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +categorytext:sw->language:n + + + + + + + + +categorytext:se->category:n + + + + + + + + +category:e->category:n + + + + + + + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK + + + +categoryarticle:sw->category:n + + + + + + + + +categoryarticle:se->article:n + + + + + + + + +articlehistory + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +articlehistory:se->article:n + + + + + + + + +article:e->article:n + + + + + + + + +articlelist + + + + +articlelist + + +id +INT +PK + + + +article:se->articlelist:n + + + + + + + + +optionslist + + + + +optionslist + + +id +INT +PK + + + +article:se->optionslist:n + + + + + + + + +brand + + + + +brand + + +id +INT +PK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +article:se->brand:n + + + + + + + + +article:w->unit:n + + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:sw->article:n + + + + + + + + +articlelistarticle + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT + + + +articlelistarticle:sw->article:n + + + + + + + + +articlelistarticle:sw->articlelist:n + + + + + + + + +optionslistarticle + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT + + + +optionslistarticle:sw->article:n + + + + + + + + +optionslistarticle:sw->optionslist:n + + + + + + + + +categorypic + + +categorypic + + +id +INT +PK + + +category_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +categorypic:w->category:n + + + + + + + + +pic + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + + +categorypic:se->pic:n + + + + + + + + +articlepic + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +articlepic:w->article:n + + + + + + + + +articlepic:se->pic:n + + + + + + + + +brandpic + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +brandpic:w->brand:n + + + + + + + + +brandpic:se->pic:n + + + + + + + + +manifest + + + +manifest + + +id +INT +PK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 + + + +price + + +price + + +id +INT +PK + + +customergroup_id +INT +FK + + +article_id +INT +FK + + +tax_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT + + + +price:w->customergroup:n + + + + + + + + +price:sw->article:n + + + + + + + + +tax + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) + + + +price:se->tax:n + + + + + + + + +manifestarticle + + +manifestarticle + + +id +INT +PK + + +article_id +INT +FK + + +manifest_id +INT +FK + + +tax_id +INT +FK + + +amount +FLOAT + + +net +FLOAT + + +transactedamount +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT + + + +manifestarticle:w->article:n + + + + + + + + +manifestarticle:sw->manifest:n + + + + + + + + +manifestarticle:se->tax:n + + + + + + + + +__legend + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint + + + diff --git a/schema.js b/schema.js deleted file mode 100644 index 0f386c6..0000000 --- a/schema.js +++ /dev/null @@ -1,210 +0,0 @@ -export const INIT_GOOGLE_AUTH = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'google_auth') -BEGIN - CREATE TABLE google_auth ( - id INT IDENTITY(1,1) PRIMARY KEY, - email NVARCHAR(320) NOT NULL, - created_at DATETIME2 NOT NULL CONSTRAINT DF_google_auth_created_at DEFAULT SYSUTCDATETIME(), - CONSTRAINT UQ_google_auth_email UNIQUE (email) - ); -END -`; - -export const INIT_LANGUAGE = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'language') -BEGIN - CREATE TABLE language ( - id INT IDENTITY(1,1) PRIMARY KEY, - iso639 NVARCHAR(10) NOT NULL, - CONSTRAINT UQ_language_iso639 UNIQUE (iso639) - ); -END -`; - -export const INIT_COMPANY = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'company') -BEGIN - CREATE TABLE company ( - id INT IDENTITY(1,1) PRIMARY KEY, - name NVARCHAR(255) NOT NULL, - root_category_id INT NULL, - default_language_id INT NULL, - CONSTRAINT FK_company_default_language FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL - ); -END -`; - -export const INIT_ARTICLE = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'article') -BEGIN - CREATE TABLE article ( - id INT IDENTITY(1,1) PRIMARY KEY, - sku NVARCHAR(100) NULL, - articlelist_id INT NULL, - is_father BIT NOT NULL CONSTRAINT DF_article_is_father DEFAULT 0, - father INT NULL, - fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0, - CONSTRAINT FK_article_father FOREIGN KEY (father) REFERENCES article(id), - CONSTRAINT UQ_article_sku UNIQUE (sku), - CONSTRAINT UQ_article_father_fathersort UNIQUE (father, fathersort) - ); -END -`; - -export const INIT_ARTICLELIST = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlelist') -BEGIN - CREATE TABLE articlelist ( - id INT IDENTITY(1,1) PRIMARY KEY - ); -END -`; - -export const INIT_ARTICLELISTARTICLE = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlelistarticle') -BEGIN - CREATE TABLE articlelistarticle ( - id INT IDENTITY(1,1) PRIMARY KEY, - articlelist_id INT NOT NULL, - article_id INT NOT NULL, - count INT NOT NULL CONSTRAINT DF_articlelistarticle_count DEFAULT 1, - sort INT NOT NULL CONSTRAINT DF_articlelistarticle_sort DEFAULT 0, - CONSTRAINT FK_articlelistarticle_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE CASCADE, - CONSTRAINT FK_articlelistarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, - CONSTRAINT UQ_articlelistarticle_articlelist_article UNIQUE (articlelist_id, article_id), - CONSTRAINT UQ_articlelistarticle_articlelist_sort UNIQUE (articlelist_id, sort) - ); -END -`; - -export const INIT_CATEGORY = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'category') -BEGIN - CREATE TABLE category ( - id INT IDENTITY(1,1) PRIMARY KEY, - parent_id INT NULL, - sort INT NOT NULL CONSTRAINT DF_category_sort DEFAULT 0, - CONSTRAINT FK_category_parent FOREIGN KEY (parent_id) REFERENCES category(id) - ); -END -`; - -export const INIT_CATEGORYARTICLE = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryarticle') -BEGIN - CREATE TABLE categoryarticle ( - id INT IDENTITY(1,1) PRIMARY KEY, - category_id INT NOT NULL, - article_id INT NOT NULL, - CONSTRAINT FK_categoryarticle_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, - CONSTRAINT FK_categoryarticle_article FOREIGN KEY (article_id) REFERENCES article(id), - CONSTRAINT UQ_categoryarticle_category_article UNIQUE (category_id, article_id) - ); -END -`; - -export const INIT_CATEGORYTEXT = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categorytext') -BEGIN - CREATE TABLE categorytext ( - id INT IDENTITY(1,1) PRIMARY KEY, - category_id INT NOT NULL, - language_id INT NOT NULL, - name NVARCHAR(255) NOT NULL, - slug NVARCHAR(255) NOT NULL, - seo NVARCHAR(MAX) NULL, - [desc] NVARCHAR(MAX) NULL, - CONSTRAINT FK_categorytext_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, - CONSTRAINT FK_categorytext_language FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE, - CONSTRAINT UQ_categorytext_category_language UNIQUE (category_id, language_id) - ); -END -`; - -export const INIT_ARTICLENAME = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletext') -BEGIN - CREATE TABLE articletext ( - id INT IDENTITY(1,1) PRIMARY KEY, - article_id INT NOT NULL, - language_id INT NOT NULL, - name NVARCHAR(255) NOT NULL, - slug NVARCHAR(255) NOT NULL, - seo NVARCHAR(MAX) NULL, - [desc] NVARCHAR(MAX) NULL, - created_at DATETIME2 NOT NULL CONSTRAINT DF_articletext_created_at DEFAULT SYSUTCDATETIME(), - updated_at DATETIME2 NOT NULL CONSTRAINT DF_articletext_updated_at DEFAULT SYSUTCDATETIME(), - CONSTRAINT FK_articletext_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, - CONSTRAINT FK_articletext_language FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE, - CONSTRAINT UQ_articletext_article_language UNIQUE (article_id, language_id) - ); -END -`; - -export const INIT_NUMBERS = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'numbers') -BEGIN - CREATE TABLE numbers ( - id INT IDENTITY(1,1) PRIMARY KEY, - [type] NVARCHAR(100) NOT NULL, - prefix NVARCHAR(50) NOT NULL CONSTRAINT DF_numbers_prefix DEFAULT '', - padding INT NOT NULL CONSTRAINT DF_numbers_padding DEFAULT 0, - [next] INT NOT NULL CONSTRAINT DF_numbers_next DEFAULT 1, - CONSTRAINT UQ_numbers_type UNIQUE ([type]) - ); -END - -IF NOT EXISTS (SELECT 1 FROM numbers WHERE [type] = N'articlesku') -BEGIN - INSERT INTO numbers ([type], prefix, padding, [next]) - VALUES (N'articlesku', N'P', 6, 1); -END -`; - -//EXEC dbo.getnextnumberwithprefixandpadding @type = N'articlesku', @value = @sku OUTPUT; -//INSERT INTO article (sku, ...) VALUES (@sku, ...); -export const INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING = ` -IF NOT EXISTS (SELECT 1 FROM sys.procedures WHERE name = N'getnextnumberwithprefixandpadding') -BEGIN - EXEC(' - CREATE PROCEDURE dbo.getnextnumberwithprefixandpadding - @type NVARCHAR(100), - @value NVARCHAR(100) OUTPUT - AS - BEGIN - SET NOCOUNT ON; - - DECLARE @seq TABLE (prefix NVARCHAR(50), padding INT, num INT); - - UPDATE n - SET [next] = [next] + 1 - OUTPUT INSERTED.prefix, INSERTED.padding, DELETED.[next] - INTO @seq(prefix, padding, num) - FROM numbers n WITH (UPDLOCK, ROWLOCK) - WHERE n.[type] = @type; - - IF @@ROWCOUNT = 0 - THROW 50000, ''Number type not found'', 1; - - SELECT @value = prefix + CASE - WHEN padding > 0 THEN RIGHT(REPLICATE(''0'', padding) + CAST(num AS NVARCHAR(20)), padding) - ELSE CAST(num AS NVARCHAR(20)) - END - FROM @seq; - END - '); -END -`; - -export const INIT_COMPANYUSER = ` -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser') -BEGIN - CREATE TABLE companyuser ( - google_auth_id INT NOT NULL, - company_id INT NOT NULL, - CONSTRAINT PK_companyuser PRIMARY KEY (google_auth_id, company_id), - CONSTRAINT FK_companyuser_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE, - CONSTRAINT FK_companyuser_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE - ); -END -`; diff --git a/src/schema.js b/src/schema.js new file mode 100644 index 0000000..fc2382f --- /dev/null +++ b/src/schema.js @@ -0,0 +1,532 @@ +export const INIT_META = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'meta') +BEGIN + CREATE TABLE meta ( + [key] NVARCHAR(255) NOT NULL, + value NVARCHAR(MAX) NOT NULL, + CONSTRAINT PK_meta PRIMARY KEY ([key]) + ); +END + +IF NOT EXISTS (SELECT 1 FROM meta WHERE [key] = N'version') +BEGIN + INSERT INTO meta ([key], value) + VALUES (N'version', N'1'); +END +`; + +export const INIT_GOOGLE_AUTH = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'google_auth') +BEGIN + CREATE TABLE google_auth ( + id INT IDENTITY(1,1) PRIMARY KEY, + email NVARCHAR(320) NOT NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_google_auth_created_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT UQ_google_auth_email UNIQUE (email) + ); +END +`; + +export const INIT_LANGUAGE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'language') +BEGIN + CREATE TABLE language ( + id INT IDENTITY(1,1) PRIMARY KEY, + iso639 NVARCHAR(10) NOT NULL, + CONSTRAINT UQ_language_iso639 UNIQUE (iso639) + ); +END +`; + +export const INIT_TAX = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'tax') +BEGIN + CREATE TABLE tax ( + id INT IDENTITY(1,1) PRIMARY KEY, + rate DECIMAL(18,4) NOT NULL, + CONSTRAINT UQ_tax_rate UNIQUE (rate) + ); +END + +IF NOT EXISTS (SELECT 1 FROM tax WHERE rate = 0) +BEGIN + INSERT INTO tax (rate) VALUES (0); +END + +IF NOT EXISTS (SELECT 1 FROM tax WHERE rate = 7) +BEGIN + INSERT INTO tax (rate) VALUES (7); +END + +IF NOT EXISTS (SELECT 1 FROM tax WHERE rate = 19) +BEGIN + INSERT INTO tax (rate) VALUES (19); +END +`; + +export const INIT_CATEGORY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'category') +BEGIN + CREATE TABLE category ( + id INT IDENTITY(1,1) PRIMARY KEY, + parent_id INT NULL, + sort INT NOT NULL CONSTRAINT DF_category_sort DEFAULT 0, + CONSTRAINT FK_category_parent FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE NO ACTION + ); +END +`; + +export const INIT_PIC = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'pic') +BEGIN + CREATE TABLE pic ( + id INT IDENTITY(1,1) PRIMARY KEY, + w INT NOT NULL, + h INT NOT NULL, + size BIGINT NOT NULL, + hash NVARCHAR(64) NOT NULL, + thumbw INT NOT NULL, + thumbh INT NOT NULL, + thumbhash NVARCHAR(64) NOT NULL, + source NVARCHAR(MAX) NOT NULL + ); +END +`; + +export const INIT_COMPANY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'company') +BEGIN + CREATE TABLE company ( + id INT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(255) NOT NULL, + root_category_id INT NULL, + default_language_id INT NULL, + CONSTRAINT FK_company_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL, + CONSTRAINT FK_company_default_language FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL + ); +END +`; + +export const INIT_CUSTOMERGROUP = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'customergroup') +BEGIN + CREATE TABLE customergroup ( + id INT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(255) NOT NULL, + company_id INT NOT NULL, + CONSTRAINT FK_customergroup_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_ENTITY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entity') +BEGIN + CREATE TABLE entity ( + id INT IDENTITY(1,1) PRIMARY KEY, + customergroup_id INT NOT NULL, + firstname NVARCHAR(255) NOT NULL, + lastname NVARCHAR(255) NOT NULL, + CONSTRAINT FK_entity_customergroup FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_UNIT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'unit') +BEGIN + CREATE TABLE unit ( + id INT IDENTITY(1,1) PRIMARY KEY, + si NVARCHAR(50) NOT NULL, + basesi NVARCHAR(50) NOT NULL, + basesifactor FLOAT NOT NULL, + CONSTRAINT UQ_unit_si UNIQUE (si) + ); +END +`; + +export const INIT_UNITTEXT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'unittext') +BEGIN + CREATE TABLE unittext ( + id INT IDENTITY(1,1) PRIMARY KEY, + unit_id INT NOT NULL, + language_id INT NOT NULL, + symbol NVARCHAR(50) NOT NULL, + name NVARCHAR(255) NOT NULL, + CONSTRAINT FK_unittext_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE CASCADE, + CONSTRAINT FK_unittext_language FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE, + CONSTRAINT UQ_unittext_unit_language UNIQUE (unit_id, language_id) + ); +END +`; + +export const INIT_OPTIONSLIST = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'optionslist') +BEGIN + CREATE TABLE optionslist ( + id INT IDENTITY(1,1) PRIMARY KEY + ); +END +`; + +export const INIT_ARTICLELIST = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlelist') +BEGIN + CREATE TABLE articlelist ( + id INT IDENTITY(1,1) PRIMARY KEY + ); +END +`; + +export const INIT_BRAND = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'brand') +BEGIN + CREATE TABLE brand ( + id INT IDENTITY(1,1) PRIMARY KEY, + name NVARCHAR(255) NOT NULL, + gsprpinfo NVARCHAR(MAX) NULL + ); +END +`; + +export const INIT_ARTICLE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'article') +BEGIN + CREATE TABLE article ( + id INT IDENTITY(1,1) PRIMARY KEY, + sku NVARCHAR(100) NULL, + unit_id INT NOT NULL, + unitamount FLOAT NOT NULL, + weight FLOAT NOT NULL, + brand_id INT NULL, + articlelist_id INT NULL, + optionslist_id INT NULL, + is_father BIT NOT NULL CONSTRAINT DF_article_is_father DEFAULT 0, + fatherarticle_id INT NULL, + fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0, + created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(), + updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT FK_article_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE CASCADE, + CONSTRAINT FK_article_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL, + CONSTRAINT FK_article_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL, + CONSTRAINT FK_article_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL, + CONSTRAINT FK_article_fatherarticle FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION, + CONSTRAINT UQ_article_sku UNIQUE (sku) + ); + + -- Only enforce sibling sort uniqueness among children of the same father. + -- (SQL Server UNIQUE treats NULLs as equal, which would block multiple root articles.) + CREATE UNIQUE INDEX UQ_article_fatherarticle_fathersort + ON article (fatherarticle_id, fathersort) + WHERE fatherarticle_id IS NOT NULL; +END +`; + +export const INIT_ARTICLEHISTORY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlehistory') +BEGIN + CREATE TABLE articlehistory ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + [date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(), + [text] NVARCHAR(MAX) NOT NULL, + CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_ARTICLEEAN = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean') +BEGIN + CREATE TABLE articleean ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + ean NVARCHAR(50) NOT NULL, + CONSTRAINT FK_articleean_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT UQ_articleean_ean UNIQUE (ean) + ); +END +`; + +export const INIT_ARTICLEPIC = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlepic') +BEGIN + CREATE TABLE articlepic ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + pic_id INT NOT NULL, + sort INT NOT NULL CONSTRAINT DF_articlepic_sort DEFAULT 0, + CONSTRAINT FK_articlepic_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_articlepic_pic FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE, + CONSTRAINT UQ_articlepic_article_pic UNIQUE (article_id, pic_id), + CONSTRAINT UQ_articlepic_article_sort UNIQUE (article_id, sort) + ); +END +`; + +export const INIT_BRANDPIC = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'brandpic') +BEGIN + CREATE TABLE brandpic ( + id INT IDENTITY(1,1) PRIMARY KEY, + brand_id INT NOT NULL, + pic_id INT NOT NULL, + sort INT NOT NULL CONSTRAINT DF_brandpic_sort DEFAULT 0, + CONSTRAINT FK_brandpic_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE CASCADE, + CONSTRAINT FK_brandpic_pic FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE, + CONSTRAINT UQ_brandpic_brand_pic UNIQUE (brand_id, pic_id), + CONSTRAINT UQ_brandpic_brand_sort UNIQUE (brand_id, sort) + ); +END +`; + +export const INIT_ARTICLELISTARTICLE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlelistarticle') +BEGIN + CREATE TABLE articlelistarticle ( + id INT IDENTITY(1,1) PRIMARY KEY, + articlelist_id INT NOT NULL, + article_id INT NOT NULL, + count INT NOT NULL CONSTRAINT DF_articlelistarticle_count DEFAULT 1, + sort INT NOT NULL CONSTRAINT DF_articlelistarticle_sort DEFAULT 0, + CONSTRAINT FK_articlelistarticle_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE CASCADE, + CONSTRAINT FK_articlelistarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT UQ_articlelistarticle_articlelist_article UNIQUE (articlelist_id, article_id), + CONSTRAINT UQ_articlelistarticle_articlelist_sort UNIQUE (articlelist_id, sort) + ); +END +`; + +export const INIT_OPTIONSLISTARTICLE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'optionslistarticle') +BEGIN + CREATE TABLE optionslistarticle ( + id INT IDENTITY(1,1) PRIMARY KEY, + optionslist_id INT NOT NULL, + article_id INT NOT NULL, + count INT NOT NULL CONSTRAINT DF_optionslistarticle_count DEFAULT 1, + sort INT NOT NULL CONSTRAINT DF_optionslistarticle_sort DEFAULT 0, + mandated BIT NOT NULL CONSTRAINT DF_optionslistarticle_mandated DEFAULT 0, + CONSTRAINT FK_optionslistarticle_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE CASCADE, + CONSTRAINT FK_optionslistarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT UQ_optionslistarticle_optionslist_article UNIQUE (optionslist_id, article_id), + CONSTRAINT UQ_optionslistarticle_optionslist_sort UNIQUE (optionslist_id, sort) + ); +END +`; + +export const INIT_CATEGORYARTICLE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryarticle') +BEGIN + CREATE TABLE categoryarticle ( + id INT IDENTITY(1,1) PRIMARY KEY, + category_id INT NOT NULL, + article_id INT NOT NULL, + CONSTRAINT FK_categoryarticle_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + CONSTRAINT FK_categoryarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT UQ_categoryarticle_category_article UNIQUE (category_id, article_id) + ); +END +`; + +export const INIT_CATEGORYPIC = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categorypic') +BEGIN + CREATE TABLE categorypic ( + id INT IDENTITY(1,1) PRIMARY KEY, + category_id INT NOT NULL, + pic_id INT NOT NULL, + sort INT NOT NULL CONSTRAINT DF_categorypic_sort DEFAULT 0, + CONSTRAINT FK_categorypic_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + CONSTRAINT FK_categorypic_pic FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE, + CONSTRAINT UQ_categorypic_category_pic UNIQUE (category_id, pic_id), + CONSTRAINT UQ_categorypic_category_sort UNIQUE (category_id, sort) + ); +END +`; + +export const INIT_CATEGORYTEXT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categorytext') +BEGIN + CREATE TABLE categorytext ( + id INT IDENTITY(1,1) PRIMARY KEY, + category_id INT NOT NULL, + language_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + slug NVARCHAR(255) NOT NULL, + seo NVARCHAR(MAX) NULL, + [desc] NVARCHAR(MAX) NULL, + CONSTRAINT FK_categorytext_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + CONSTRAINT FK_categorytext_language FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE, + CONSTRAINT UQ_categorytext_category_language UNIQUE (category_id, language_id) + ); +END +`; + +export const INIT_ARTICLENAME = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletext') +BEGIN + CREATE TABLE articletext ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + language_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + slug NVARCHAR(255) NOT NULL, + seo NVARCHAR(MAX) NULL, + [desc] NVARCHAR(MAX) NULL, + CONSTRAINT FK_articletext_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_articletext_language FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE, + CONSTRAINT UQ_articletext_article_language UNIQUE (article_id, language_id) + ); +END +`; + +export const INIT_PRICE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'price') +BEGIN + CREATE TABLE price ( + id INT IDENTITY(1,1) PRIMARY KEY, + tax_id INT NOT NULL, + article_id INT NOT NULL, + minmultiplier DECIMAL(18,4) NOT NULL CONSTRAINT DF_price_minmultiplier DEFAULT 1, + customergroup_id INT NOT NULL, + net FLOAT NOT NULL, + CONSTRAINT FK_price_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE CASCADE, + CONSTRAINT FK_price_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_price_customergroup FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_MANIFEST = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifest') +BEGIN + CREATE TABLE manifest ( + id INT IDENTITY(1,1) PRIMARY KEY, + [number] NVARCHAR(100) NOT NULL, + [type] NVARCHAR(100) NOT NULL, + [date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME() + ); +END +`; + +export const INIT_MANIFESTARTICLE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifestarticle') +BEGIN + CREATE TABLE manifestarticle ( + id INT IDENTITY(1,1) PRIMARY KEY, + manifest_id INT NOT NULL, + article_id INT NOT NULL, + amount FLOAT NOT NULL, + net FLOAT NOT NULL, + transactedamount FLOAT NOT NULL, + comment NVARCHAR(MAX) NULL, + rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0, + tax_id INT NOT NULL, + CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, + CONSTRAINT FK_manifestarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_manifestarticle_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_NUMBERS = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'numbers') +BEGIN + CREATE TABLE numbers ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + [type] NVARCHAR(100) NOT NULL, + prefix NVARCHAR(50) NOT NULL CONSTRAINT DF_numbers_prefix DEFAULT '', + padding INT NOT NULL CONSTRAINT DF_numbers_padding DEFAULT 0, + [next] INT NOT NULL CONSTRAINT DF_numbers_next DEFAULT 1, + CONSTRAINT FK_numbers_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_numbers_company_type UNIQUE (company_id, [type]) + ); +END +`; + +// EXEC dbo.getnextnumberwithprefixandpadding +// @company_id = 1, @type = N'articlesku', @value = @sku OUTPUT; +export const INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING = ` +IF OBJECT_ID(N'dbo.getnextnumberwithprefixandpadding', N'P') IS NOT NULL + DROP PROCEDURE dbo.getnextnumberwithprefixandpadding; + +EXEC(' +CREATE PROCEDURE dbo.getnextnumberwithprefixandpadding + @company_id INT, + @type NVARCHAR(100), + @value NVARCHAR(100) OUTPUT +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @seq TABLE (prefix NVARCHAR(50), padding INT, num INT); + + UPDATE n + SET [next] = [next] + 1 + OUTPUT INSERTED.prefix, INSERTED.padding, DELETED.[next] + INTO @seq(prefix, padding, num) + FROM numbers n WITH (UPDLOCK, ROWLOCK) + WHERE n.company_id = @company_id AND n.[type] = @type; + + IF @@ROWCOUNT = 0 + THROW 50000, ''Number type not found for company'', 1; + + SELECT @value = prefix + CASE + WHEN padding > 0 THEN RIGHT(REPLICATE(''0'', padding) + CAST(num AS NVARCHAR(20)), padding) + ELSE CAST(num AS NVARCHAR(20)) + END + FROM @seq; +END +'); +`; + +export const INIT_COMPANYUSER = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser') +BEGIN + CREATE TABLE companyuser ( + google_auth_id INT NOT NULL, + company_id INT NOT NULL, + CONSTRAINT PK_companyuser PRIMARY KEY (google_auth_id, company_id), + CONSTRAINT FK_companyuser_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE, + CONSTRAINT FK_companyuser_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE + ); +END +`; + +/** DDL scripts in dependency order for schema init. */ +export const SCHEMA_SCRIPTS = [ + INIT_META, + INIT_GOOGLE_AUTH, + INIT_LANGUAGE, + INIT_TAX, + INIT_CATEGORY, + INIT_PIC, + INIT_COMPANY, + INIT_CUSTOMERGROUP, + INIT_ENTITY, + INIT_UNIT, + INIT_UNITTEXT, + INIT_OPTIONSLIST, + INIT_ARTICLELIST, + INIT_BRAND, + INIT_ARTICLE, + INIT_ARTICLEHISTORY, + INIT_ARTICLEEAN, + INIT_ARTICLEPIC, + INIT_BRANDPIC, + INIT_ARTICLELISTARTICLE, + INIT_OPTIONSLISTARTICLE, + INIT_CATEGORYARTICLE, + INIT_CATEGORYPIC, + INIT_CATEGORYTEXT, + INIT_ARTICLENAME, + INIT_PRICE, + INIT_MANIFEST, + INIT_MANIFESTARTICLE, + INIT_NUMBERS, + INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, + INIT_COMPANYUSER, +];