u
This commit is contained in:
80
package-lock.json
generated
80
package-lock.json
generated
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "dashdbschema",
|
||||
"version": "1.0.0",
|
||||
"name": "libdashstore",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dashdbschema",
|
||||
"version": "1.0.0",
|
||||
"name": "libdashstore",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.7.0",
|
||||
"@hpcc-js/wasm-graphviz": "^1.28.0",
|
||||
"chalk": "^6.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"mssql": "^12.7.0"
|
||||
}
|
||||
@@ -256,6 +258,34 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz",
|
||||
"integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-wrap-ansi": "^0.2.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz",
|
||||
"integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "1.4.3",
|
||||
"fast-string-width": "^3.0.2",
|
||||
"fast-wrap-ansi": "^0.2.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.12.0"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -404,6 +434,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz",
|
||||
"integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
@@ -509,6 +551,30 @@
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-string-truncated-width": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
|
||||
"integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-string-width": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
|
||||
"integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-string-truncated-width": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-wrap-ansi": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
|
||||
"integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-string-width": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
@@ -840,6 +906,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"schema-graph": "node generate-schema-graph.js",
|
||||
"init-schema": "node scripts/init-schema.js"
|
||||
"init-schema": "node scripts/init-schema.js",
|
||||
"test:api": "node scripts/test-api.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.7.0",
|
||||
"@hpcc-js/wasm-graphviz": "^1.28.0",
|
||||
"chalk": "^6.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"mssql": "^12.7.0"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ digraph schema {
|
||||
company [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#bee3f8" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD><TD PORT="in1" BGCOLOR="#bee3f8" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD><TD PORT="in2" BGCOLOR="#bee3f8" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="3" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>company</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="default_language_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>default_language_id</B>?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="root_category_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>root_category_id</B>?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="name" COLSPAN="2" BGCOLOR="#f7fafc" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">name</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
numbers [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>numbers</B></FONT></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="company_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>company_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="type" COLSPAN="2" BGCOLOR="#ffffff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">type</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(100)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="prefix" COLSPAN="2" BGCOLOR="#f7fafc" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">prefix</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(50)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="padding" COLSPAN="2" BGCOLOR="#ffffff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">padding</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="next" COLSPAN="2" BGCOLOR="#f7fafc" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">next</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
entity [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>entity</B></FONT></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="customergroup_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>customergroup_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="firstname" COLSPAN="2" BGCOLOR="#ffffff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">firstname</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="lastname" COLSPAN="2" BGCOLOR="#f7fafc" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">lastname</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
companyuser [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>companyuser</B></FONT></TD></TR><TR><TD PORT="company_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>company_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="google_auth_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>google_auth_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
companyuser [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>companyuser</B></FONT></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="company_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>company_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="google_auth_id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>google_auth_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
google_auth [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#a0aec0"><TR><TD COLSPAN="2" BGCOLOR="#bee3f8" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#bee3f8" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#1a365d" POINT-SIZE="12"><B>google_auth</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#ebf8ff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="email" COLSPAN="2" BGCOLOR="#f7fafc" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">email</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">NVARCHAR(320)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="created_at" COLSPAN="2" BGCOLOR="#ffffff" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#2d3748">created_at</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#718096" POINT-SIZE="9">DATETIME2</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
|
||||
}
|
||||
subgraph cluster_i18n {
|
||||
|
||||
@@ -261,26 +261,31 @@
|
||||
<!-- companyuser -->
|
||||
<g id="node6" class="node">
|
||||
<title>companyuser</title>
|
||||
<polygon fill="#bee3f8" stroke="none" points="399.78,-979.8 399.78,-1004.2 550.22,-1004.2 550.22,-979.8 399.78,-979.8"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-979.8 399.78,-1004.2 550.22,-1004.2 550.22,-979.8 399.78,-979.8"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="435.99" y="-989.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#1a365d">companyuser</text>
|
||||
<polygon fill="#ebf8ff" stroke="none" points="399.78,-953 399.78,-979.8 550.22,-979.8 550.22,-953 399.78,-953"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-953 399.78,-979.8 550.22,-979.8 550.22,-953 399.78,-953"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="404.78" y="-963.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#2d3748">company_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="516.23" y="-964.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#718096">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="533.22" y="-964.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#718096">FK</text>
|
||||
<polygon fill="#ebf8ff" stroke="none" points="399.78,-926.2 399.78,-953 550.22,-953 550.22,-926.2 399.78,-926.2"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-926.2 399.78,-953 550.22,-953 550.22,-926.2 399.78,-926.2"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="404.78" y="-936.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#2d3748">google_auth_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="516.23" y="-937.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#718096">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="533.22" y="-937.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#718096">FK</text>
|
||||
<polygon fill="#bee3f8" stroke="none" points="399.78,-993.2 399.78,-1017.6 550.22,-1017.6 550.22,-993.2 399.78,-993.2"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-993.2 399.78,-1017.6 550.22,-1017.6 550.22,-993.2 399.78,-993.2"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="435.99" y="-1002.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#1a365d">companyuser</text>
|
||||
<polygon fill="#ebf8ff" stroke="none" points="399.78,-966.4 399.78,-993.2 550.22,-993.2 550.22,-966.4 399.78,-966.4"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-966.4 399.78,-993.2 550.22,-993.2 550.22,-966.4 399.78,-966.4"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="404.78" y="-976.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#2d3748">id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="515.72" y="-978.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#718096">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="532.72" y="-978.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#718096">PK</text>
|
||||
<polygon fill="#ebf8ff" stroke="none" points="399.78,-939.6 399.78,-966.4 550.22,-966.4 550.22,-939.6 399.78,-939.6"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-939.6 399.78,-966.4 550.22,-966.4 550.22,-939.6 399.78,-939.6"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="404.78" y="-949.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#2d3748">company_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="516.23" y="-951.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#718096">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="533.22" y="-951.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#718096">FK</text>
|
||||
<polygon fill="#ebf8ff" stroke="none" points="399.78,-912.8 399.78,-939.6 550.22,-939.6 550.22,-912.8 399.78,-912.8"/>
|
||||
<polygon fill="none" stroke="#a0aec0" points="399.78,-912.8 399.78,-939.6 550.22,-939.6 550.22,-912.8 399.78,-912.8"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="404.78" y="-923" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#2d3748">google_auth_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="516.23" y="-924.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#718096">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="533.22" y="-924.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#718096">FK</text>
|
||||
</g>
|
||||
<!-- companyuser->company -->
|
||||
<g id="edge39" class="edge">
|
||||
<title>companyuser:sw->company:n</title>
|
||||
<g id="a_edge39"><a xlink:title="FK_companyuser_company">
|
||||
<path fill="none" stroke="#22543d" stroke-width="1.4" d="M398.78,-953C358.66,-912.88 383.86,-883.66 371,-828.4 351.21,-743.39 324.92,-722.56 323.14,-639.82"/>
|
||||
<polygon fill="#22543d" stroke="#22543d" stroke-width="1.4" points="325.59,-639.89 323.07,-632.92 320.69,-639.94 325.59,-639.89"/>
|
||||
<path fill="none" stroke="#22543d" stroke-width="1.4" d="M398.78,-939.6C398.08,-938.9 337.24,-690.01 325.13,-639.63"/>
|
||||
<polygon fill="#22543d" stroke="#22543d" stroke-width="1.4" points="327.53,-639.11 323.53,-632.86 322.76,-640.24 327.53,-639.11"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
@@ -309,8 +314,8 @@
|
||||
<g id="edge38" class="edge">
|
||||
<title>companyuser:se->google_auth:n</title>
|
||||
<g id="a_edge38"><a xlink:title="FK_companyuser_google_auth">
|
||||
<path fill="none" stroke="#742a2a" stroke-width="1.4" d="M550.22,-925.2C597.93,-877.5 488.18,-702.08 479.54,-626.15"/>
|
||||
<polygon fill="#742a2a" stroke="#742a2a" stroke-width="1.4" points="482,-626.35 479.13,-619.51 477.11,-626.65 482,-626.35"/>
|
||||
<path fill="none" stroke="#742a2a" stroke-width="1.4" d="M550.22,-911.8C595.86,-866.16 488.47,-699.21 479.58,-626.23"/>
|
||||
<polygon fill="#742a2a" stroke="#742a2a" stroke-width="1.4" points="482.04,-626.34 479.14,-619.51 477.15,-626.66 482.04,-626.34"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 129 KiB |
25
scripts/init-schema.js
Normal file
25
scripts/init-schema.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dotenv/config';
|
||||
import sql from 'mssql';
|
||||
import { SCHEMA_SCRIPTS } from '../src/schema.js';
|
||||
|
||||
const config = {
|
||||
server: process.env.MSSQL_SERVER,
|
||||
database: process.env.MSSQL_DATABASE,
|
||||
user: process.env.MSSQL_USER,
|
||||
password: process.env.MSSQL_PASSWORD,
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true,
|
||||
},
|
||||
};
|
||||
|
||||
const pool = await sql.connect(config);
|
||||
|
||||
try {
|
||||
for (const script of SCHEMA_SCRIPTS) {
|
||||
await pool.request().query(script);
|
||||
}
|
||||
console.log(`Schema init OK (${SCHEMA_SCRIPTS.length} scripts)`);
|
||||
} finally {
|
||||
await pool.close();
|
||||
}
|
||||
193
scripts/test-api.js
Normal file
193
scripts/test-api.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'dotenv/config';
|
||||
import sql from 'mssql';
|
||||
import chalk from 'chalk';
|
||||
import * as p from '@clack/prompts';
|
||||
import LibDashStore from '../src/index.js';
|
||||
|
||||
const config = {
|
||||
server: process.env.MSSQL_SERVER,
|
||||
database: process.env.MSSQL_DATABASE,
|
||||
user: process.env.MSSQL_USER,
|
||||
password: process.env.MSSQL_PASSWORD,
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true,
|
||||
},
|
||||
};
|
||||
|
||||
function printResult(label, value) {
|
||||
p.log.step(chalk.cyan(label));
|
||||
const text =
|
||||
value === undefined
|
||||
? chalk.dim('undefined')
|
||||
: typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value, null, 2);
|
||||
console.log(chalk.white(text));
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function shutdown(pool, code = 0) {
|
||||
try {
|
||||
await pool.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.clear();
|
||||
p.intro(chalk.bgMagenta.black(' libdashstore ') + chalk.dim(' api tester'));
|
||||
|
||||
const spin = p.spinner();
|
||||
spin.start('Connecting to MSSQL…');
|
||||
|
||||
let pool;
|
||||
try {
|
||||
pool = await sql.connect(config);
|
||||
} catch (err) {
|
||||
spin.stop(chalk.red('Connection failed'));
|
||||
p.log.error(err.message);
|
||||
p.outro(chalk.red('Aborted'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spin.message('Initializing schema…');
|
||||
let client;
|
||||
try {
|
||||
client = await new LibDashStore(pool);
|
||||
spin.stop(chalk.green('Ready'));
|
||||
} catch (err) {
|
||||
spin.stop(chalk.red('Initialize failed'));
|
||||
p.log.error(err.message);
|
||||
await shutdown(pool, 1);
|
||||
}
|
||||
|
||||
p.note(
|
||||
[
|
||||
chalk.dim(`${config.server} / ${config.database}`),
|
||||
chalk.dim('purge ends the session'),
|
||||
].join('\n'),
|
||||
'Connected',
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const action = await p.select({
|
||||
message: 'API method',
|
||||
options: [
|
||||
{
|
||||
value: 'companyuser.getByEmail',
|
||||
label: 'companyuser.getByEmail',
|
||||
hint: 'email → id',
|
||||
},
|
||||
{
|
||||
value: 'company.getByCompanyUserId',
|
||||
label: 'company.getByCompanyUserId',
|
||||
hint: 'id → [id, lang, root, name]',
|
||||
},
|
||||
{
|
||||
value: 'category.getTree',
|
||||
label: 'category.getTree',
|
||||
hint: 'rootId → tree',
|
||||
},
|
||||
{
|
||||
value: 'initialize',
|
||||
label: 'initialize',
|
||||
hint: 'purge + recreate schema',
|
||||
},
|
||||
{
|
||||
value: 'purge',
|
||||
label: chalk.red('purge'),
|
||||
hint: 'drop all · exit',
|
||||
},
|
||||
{ value: 'exit', label: 'exit', hint: 'quit without purge' },
|
||||
],
|
||||
});
|
||||
|
||||
if (p.isCancel(action) || action === 'exit') {
|
||||
p.outro(chalk.dim('Bye'));
|
||||
await shutdown(pool, 0);
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'companyuser.getByEmail') {
|
||||
const email = await p.text({
|
||||
message: 'Email',
|
||||
placeholder: 'user@example.com',
|
||||
validate: (v) => (!v?.trim() ? 'Required' : undefined),
|
||||
});
|
||||
if (p.isCancel(email)) continue;
|
||||
|
||||
const run = p.spinner();
|
||||
run.start('Running…');
|
||||
const id = await client.companyuser.getByEmail(email.trim());
|
||||
run.stop(chalk.green('companyuser.getByEmail'));
|
||||
printResult('id', id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === 'company.getByCompanyUserId') {
|
||||
const raw = await p.text({
|
||||
message: 'Company user id',
|
||||
placeholder: '1',
|
||||
validate: (v) =>
|
||||
!/^\d+$/.test(v?.trim() ?? '') ? 'Integer required' : undefined,
|
||||
});
|
||||
if (p.isCancel(raw)) continue;
|
||||
|
||||
const run = p.spinner();
|
||||
run.start('Running…');
|
||||
const row = await client.company.getByCompanyUserId(Number(raw.trim()));
|
||||
run.stop(chalk.green('company.getByCompanyUserId'));
|
||||
printResult('[id, defaultLangId, rootCategoryId, name]', row);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === 'category.getTree') {
|
||||
const raw = await p.text({
|
||||
message: 'Root category id',
|
||||
placeholder: '1',
|
||||
validate: (v) =>
|
||||
!/^\d+$/.test(v?.trim() ?? '') ? 'Integer required' : undefined,
|
||||
});
|
||||
if (p.isCancel(raw)) continue;
|
||||
|
||||
const run = p.spinner();
|
||||
run.start('Running…');
|
||||
const tree = await client.category.getTree(Number(raw.trim()));
|
||||
run.stop(chalk.green('category.getTree'));
|
||||
printResult('tree', tree);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === 'initialize') {
|
||||
const run = p.spinner();
|
||||
run.start('Initializing…');
|
||||
await client.initialize();
|
||||
run.stop(chalk.green('initialize'));
|
||||
p.log.success('Schema purged and recreated');
|
||||
console.log();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === 'purge') {
|
||||
const run = p.spinner();
|
||||
run.start('Purging…');
|
||||
await client.purge();
|
||||
run.stop(chalk.green('purge'));
|
||||
p.outro(chalk.red('Database purged — exiting'));
|
||||
await shutdown(pool, 0);
|
||||
}
|
||||
} catch (err) {
|
||||
p.log.error(err.message);
|
||||
console.log(chalk.dim(err.stack?.split('\n').slice(1, 4).join('\n') ?? ''));
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
console.error(chalk.red(err.stack ?? err.message));
|
||||
process.exit(1);
|
||||
});
|
||||
30
src/index.js
Normal file
30
src/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import companyuserGetByEmail from './store/companyuser/getByEmail.js';
|
||||
import companyGetByCompanyUserId from './store/company/getByCompanyUserId.js';
|
||||
import categoryGetTree from './store/category/getTree.js';
|
||||
import storePurge from './store/purge.js';
|
||||
import storeInitialize from './store/initialize.js';
|
||||
|
||||
export default class LibDashStore {
|
||||
constructor(pool) {
|
||||
this.pool = pool;
|
||||
|
||||
this.companyuser = {
|
||||
getByEmail: companyuserGetByEmail(pool),
|
||||
};
|
||||
|
||||
this.company = {
|
||||
getByCompanyUserId: companyGetByCompanyUserId(pool),
|
||||
};
|
||||
|
||||
this.category = {
|
||||
getTree: categoryGetTree(pool),
|
||||
};
|
||||
|
||||
this.purge = storePurge(pool);
|
||||
this.initialize = storeInitialize(pool);
|
||||
|
||||
return this.initialize().then(() => this);
|
||||
}
|
||||
}
|
||||
|
||||
export { SCHEMA_SCRIPTS } from './schema.js';
|
||||
@@ -487,9 +487,10 @@ export const INIT_COMPANYUSER = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser')
|
||||
BEGIN
|
||||
CREATE TABLE companyuser (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
google_auth_id INT NOT NULL,
|
||||
company_id INT NOT NULL,
|
||||
CONSTRAINT PK_companyuser PRIMARY KEY (google_auth_id, company_id),
|
||||
CONSTRAINT UQ_companyuser_google_auth_company UNIQUE (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
|
||||
);
|
||||
|
||||
16
src/sql/category.js
Normal file
16
src/sql/category.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const GET_TREE = `
|
||||
WITH tree AS (
|
||||
SELECT id, parent_id, sort
|
||||
FROM category
|
||||
WHERE id = @rootId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id, c.parent_id, c.sort
|
||||
FROM category c
|
||||
INNER JOIN tree t ON c.parent_id = t.id
|
||||
)
|
||||
SELECT id, parent_id, sort
|
||||
FROM tree
|
||||
ORDER BY sort, id
|
||||
`;
|
||||
10
src/sql/company.js
Normal file
10
src/sql/company.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const GET_BY_COMPANY_USER_ID = `
|
||||
SELECT
|
||||
c.id,
|
||||
c.default_language_id,
|
||||
c.root_category_id,
|
||||
c.name
|
||||
FROM company c
|
||||
INNER JOIN companyuser cu ON cu.company_id = c.id
|
||||
WHERE cu.id = @companyUserId
|
||||
`;
|
||||
6
src/sql/companyuser.js
Normal file
6
src/sql/companyuser.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const GET_BY_EMAIL = `
|
||||
SELECT cu.id
|
||||
FROM companyuser cu
|
||||
INNER JOIN google_auth ga ON ga.id = cu.google_auth_id
|
||||
WHERE ga.email = @email
|
||||
`;
|
||||
49
src/sql/purge.js
Normal file
49
src/sql/purge.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/** Drop order = reverse of schema create dependency order. */
|
||||
export const TABLES = [
|
||||
'companyuser',
|
||||
'numbers',
|
||||
'manifestarticle',
|
||||
'manifest',
|
||||
'price',
|
||||
'articletext',
|
||||
'categorytext',
|
||||
'categorypic',
|
||||
'categoryarticle',
|
||||
'optionslistarticle',
|
||||
'articlelistarticle',
|
||||
'brandpic',
|
||||
'articlepic',
|
||||
'articleean',
|
||||
'articlehistory',
|
||||
'article',
|
||||
'brand',
|
||||
'articlelist',
|
||||
'optionslist',
|
||||
'unittext',
|
||||
'unit',
|
||||
'entity',
|
||||
'customergroup',
|
||||
'company',
|
||||
'pic',
|
||||
'category',
|
||||
'tax',
|
||||
'language',
|
||||
'google_auth',
|
||||
'meta',
|
||||
];
|
||||
|
||||
export const PROCEDURES = ['getnextnumberwithprefixandpadding'];
|
||||
|
||||
export function buildPurgeSql() {
|
||||
const drops = [
|
||||
...PROCEDURES.map(
|
||||
(name) =>
|
||||
`IF OBJECT_ID(N'dbo.${name}', N'P') IS NOT NULL DROP PROCEDURE dbo.[${name}];`,
|
||||
),
|
||||
...TABLES.map(
|
||||
(name) =>
|
||||
`IF OBJECT_ID(N'dbo.${name}', N'U') IS NOT NULL DROP TABLE dbo.[${name}];`,
|
||||
),
|
||||
];
|
||||
return drops.join('\n');
|
||||
}
|
||||
38
src/store/category/getTree.js
Normal file
38
src/store/category/getTree.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import sql from 'mssql';
|
||||
import { GET_TREE } from '../../sql/category.js';
|
||||
|
||||
function buildTree(rows, rootId) {
|
||||
const byId = new Map();
|
||||
for (const row of rows) {
|
||||
byId.set(row.id, {
|
||||
id: row.id,
|
||||
parent_id: row.parent_id,
|
||||
sort: row.sort,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
let root = null;
|
||||
for (const node of byId.values()) {
|
||||
if (node.id === rootId) {
|
||||
root = node;
|
||||
continue;
|
||||
}
|
||||
const parent = byId.get(node.parent_id);
|
||||
if (parent) parent.children.push(node);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export default function getTree(pool) {
|
||||
return async (rootId) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('rootId', sql.Int, rootId)
|
||||
.query(GET_TREE);
|
||||
|
||||
if (result.recordset.length === 0) return null;
|
||||
return buildTree(result.recordset, rootId);
|
||||
};
|
||||
}
|
||||
16
src/store/company/getByCompanyUserId.js
Normal file
16
src/store/company/getByCompanyUserId.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import sql from 'mssql';
|
||||
import { GET_BY_COMPANY_USER_ID } from '../../sql/company.js';
|
||||
|
||||
export default function getByCompanyUserId(pool) {
|
||||
return async (companyUserId) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('companyUserId', sql.Int, companyUserId)
|
||||
.query(GET_BY_COMPANY_USER_ID);
|
||||
|
||||
const row = result.recordset[0];
|
||||
if (!row) return null;
|
||||
|
||||
return [row.id, row.default_language_id, row.root_category_id, row.name];
|
||||
};
|
||||
}
|
||||
13
src/store/companyuser/getByEmail.js
Normal file
13
src/store/companyuser/getByEmail.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import sql from 'mssql';
|
||||
import { GET_BY_EMAIL } from '../../sql/companyuser.js';
|
||||
|
||||
export default function getByEmail(pool) {
|
||||
return async (email) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('email', sql.NVarChar(320), email)
|
||||
.query(GET_BY_EMAIL);
|
||||
|
||||
return result.recordset[0]?.id ?? null;
|
||||
};
|
||||
}
|
||||
12
src/store/initialize.js
Normal file
12
src/store/initialize.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SCHEMA_SCRIPTS } from '../schema.js';
|
||||
import purge from './purge.js';
|
||||
|
||||
export default function initialize(pool) {
|
||||
const doPurge = purge(pool);
|
||||
return async () => {
|
||||
await doPurge();
|
||||
for (const script of SCHEMA_SCRIPTS) {
|
||||
await pool.request().query(script);
|
||||
}
|
||||
};
|
||||
}
|
||||
7
src/store/purge.js
Normal file
7
src/store/purge.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { buildPurgeSql } from '../sql/purge.js';
|
||||
|
||||
export default function purge(pool) {
|
||||
return async () => {
|
||||
await pool.request().query(buildPurgeSql());
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user