u
This commit is contained in:
741
generate-schema-graph.js
Normal file
741
generate-schema-graph.js
Normal file
@@ -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 `<table>_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, '>')
|
||||||
|
.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 =
|
||||||
|
`<FONT COLOR="${COLORS.headerText}" POINT-SIZE="12"><B>${escapeHtml(table.name)}</B></FONT>`;
|
||||||
|
|
||||||
|
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(
|
||||||
|
`<TD PORT="in${i}" BGCOLOR="${COLORS.headerBg}" BORDER="0" ` +
|
||||||
|
`WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
headerInner =
|
||||||
|
`<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE">` +
|
||||||
|
`<TR>${portCells.join('')}</TR>` +
|
||||||
|
`<TR><TD COLSPAN="${inboundCount}" ALIGN="CENTER" CELLPADDING="3">` +
|
||||||
|
`${titleFont}</TD></TR>` +
|
||||||
|
`</TABLE>`;
|
||||||
|
} else {
|
||||||
|
headerInner = titleFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
`<TR><TD COLSPAN="2" BGCOLOR="${COLORS.headerBg}" ALIGN="CENTER" ` +
|
||||||
|
`CELLPADDING="${inboundCount > 0 ? 1 : 4}" BORDER="1">` +
|
||||||
|
`${headerInner}</TD></TR>`,
|
||||||
|
];
|
||||||
|
|
||||||
|
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
|
||||||
|
? `<B>${escapeHtml(column.name)}</B>`
|
||||||
|
: escapeHtml(column.name);
|
||||||
|
const nullMarker = column.nullable ? '?' : '';
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
`<TR><TD PORT="${escapeHtml(column.name)}" COLSPAN="2" BGCOLOR="${bg}" ` +
|
||||||
|
`BORDER="1" CELLPADDING="0">` +
|
||||||
|
`<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE">` +
|
||||||
|
`<TR>` +
|
||||||
|
`<TD ALIGN="LEFT" CELLPADDING="4">` +
|
||||||
|
`<FONT COLOR="${COLORS.text}">${nameMarkup}${nullMarker}</FONT></TD>` +
|
||||||
|
`<TD ALIGN="RIGHT" CELLPADDING="4">` +
|
||||||
|
`<FONT COLOR="${COLORS.muted}" POINT-SIZE="9">${escapeHtml(column.type)}` +
|
||||||
|
(marker ? ` <B>${marker}</B>` : '') +
|
||||||
|
`</FONT></TD>` +
|
||||||
|
`</TR></TABLE></TD></TR>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
`<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" ` +
|
||||||
|
`COLOR="${COLORS.tableBorder}">` +
|
||||||
|
rows.join('') +
|
||||||
|
`</TABLE>>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) =>
|
||||||
|
`<TR><TD ALIGN="LEFT">${swatch}</TD><TD ALIGN="LEFT">` +
|
||||||
|
`<FONT COLOR="${COLORS.text}" POINT-SIZE="9">${text}</FONT></TD></TR>`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
`<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" ` +
|
||||||
|
`COLOR="${COLORS.tableBorder}" BGCOLOR="${COLORS.rowBg}">` +
|
||||||
|
`<TR><TD COLSPAN="2" BGCOLOR="${COLORS.headerBg}">` +
|
||||||
|
`<FONT COLOR="${COLORS.headerText}" POINT-SIZE="11"><B>Legend</B></FONT></TD></TR>` +
|
||||||
|
row(`<FONT COLOR="${COLORS.text}"><B>PK</B></FONT>`, 'primary key') +
|
||||||
|
row(`<FONT COLOR="${COLORS.text}"><B>FK</B></FONT>`, 'foreign key') +
|
||||||
|
row(`<FONT COLOR="${COLORS.text}"><B>?</B></FONT>`, 'nullable column') +
|
||||||
|
row(
|
||||||
|
`<FONT COLOR="${COLORS.muted}">—</FONT>`,
|
||||||
|
'FOREIGN KEY constraint'
|
||||||
|
) +
|
||||||
|
row(
|
||||||
|
`<FONT COLOR="${COLORS.muted}">- -</FONT>`,
|
||||||
|
'naming convention only, no constraint'
|
||||||
|
) +
|
||||||
|
`</TABLE>>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(/<g id="edge\d+"[\s\S]*?<path[^>]*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`
|
||||||
|
);
|
||||||
|
}
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hpcc-js/wasm-graphviz": "^1.28.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"mssql": "^12.7.0"
|
"mssql": "^12.7.0"
|
||||||
}
|
}
|
||||||
@@ -255,6 +256,12 @@
|
|||||||
"node": ">=20"
|
"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": {
|
"node_modules/@js-joda/core": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz",
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,15 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "dashdbschema",
|
"name": "libdashstore",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "Dash store library — MSSQL schema and domain APIs",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"author": "",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "schema.js",
|
"main": "./src/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.js",
|
||||||
|
"./schema": "./src/schema.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"schema-graph": "node generate-schema-graph.js",
|
||||||
|
"init-schema": "node scripts/init-schema.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hpcc-js/wasm-graphviz": "^1.28.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"mssql": "^12.7.0"
|
"mssql": "^12.7.0"
|
||||||
}
|
}
|
||||||
|
|||||||
110
schema-graph.dot
Normal file
110
schema-graph.dot
Normal file
File diff suppressed because one or more lines are too long
1277
schema-graph.svg
Normal file
1277
schema-graph.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 128 KiB |
210
schema.js
210
schema.js
@@ -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
|
|
||||||
`;
|
|
||||||
532
src/schema.js
Normal file
532
src/schema.js
Normal file
@@ -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,
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user