gufi-cli 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/docs.js +1 -5
- package/dist/lib/docs-resolver.d.ts +8 -0
- package/dist/lib/docs-resolver.js +27 -0
- package/dist/lib/security.js +5 -0
- package/dist/mcp.js +56 -43
- package/docs/dev-guide/1-01-architecture.md +358 -0
- package/docs/dev-guide/1-02-multi-tenant.md +415 -0
- package/docs/dev-guide/1-03-column-types.md +594 -0
- package/docs/dev-guide/1-04-json-config.md +442 -0
- package/docs/dev-guide/1-05-authentication.md +427 -0
- package/docs/dev-guide/2-01-api-reference.md +564 -0
- package/docs/dev-guide/2-02-automations.md +508 -0
- package/docs/dev-guide/2-03-gufi-cli.md +568 -0
- package/docs/dev-guide/2-04-realtime.md +401 -0
- package/docs/dev-guide/2-05-permissions.md +497 -0
- package/docs/dev-guide/2-06-integrations-overview.md +104 -0
- package/docs/dev-guide/2-07-stripe.md +173 -0
- package/docs/dev-guide/2-08-nayax.md +297 -0
- package/docs/dev-guide/2-09-ourvend.md +226 -0
- package/docs/dev-guide/2-10-tns.md +177 -0
- package/docs/dev-guide/2-11-custom-http.md +268 -0
- package/docs/dev-guide/3-01-custom-views.md +555 -0
- package/docs/dev-guide/3-02-webhooks-api.md +446 -0
- package/docs/mcp/00-overview.md +329 -0
- package/docs/mcp/01-architecture.md +226 -0
- package/docs/mcp/02-modules.md +285 -0
- package/docs/mcp/03-fields.md +357 -0
- package/docs/mcp/04-views.md +613 -0
- package/docs/mcp/05-automations.md +461 -0
- package/docs/mcp/06-api.md +531 -0
- package/docs/mcp/07-packages.md +246 -0
- package/docs/mcp/08-common-errors.md +284 -0
- package/docs/mcp/09-examples.md +453 -0
- package/docs/mcp/README.md +71 -0
- package/docs/mcp/tool-descriptions.json +64 -0
- package/package.json +3 -2
package/dist/commands/docs.js
CHANGED
|
@@ -9,12 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
|
-
import { fileURLToPath } from "url";
|
|
13
12
|
import chalk from "chalk";
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
|
-
// docs/mcp path relative to CLI
|
|
17
|
-
const DOCS_MCP_PATH = path.resolve(__dirname, "../../../../docs/mcp");
|
|
13
|
+
import { DOCS_MCP_PATH } from "../lib/docs-resolver.js";
|
|
18
14
|
// Topic to file mapping
|
|
19
15
|
const TOPIC_FILES = {
|
|
20
16
|
overview: { file: "00-overview.md", description: "Overview and workflow" },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves documentation paths for both development (monorepo) and global install.
|
|
3
|
+
*
|
|
4
|
+
* - Global install: docs are bundled at <pkg>/docs/
|
|
5
|
+
* - Development: docs live at <monorepo>/docs/
|
|
6
|
+
*/
|
|
7
|
+
export declare const DOCS_MCP_PATH: string;
|
|
8
|
+
export declare const DOCS_DEV_GUIDE_PATH: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves documentation paths for both development (monorepo) and global install.
|
|
3
|
+
*
|
|
4
|
+
* - Global install: docs are bundled at <pkg>/docs/
|
|
5
|
+
* - Development: docs live at <monorepo>/docs/
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
// Package root: from dist/lib/ go up 2 levels
|
|
13
|
+
const PKG_ROOT = path.resolve(__dirname, "../..");
|
|
14
|
+
function resolveDocsPath(subdir) {
|
|
15
|
+
// 1. Package-local (global install): <pkg>/docs/<subdir>
|
|
16
|
+
const localPath = path.join(PKG_ROOT, "docs", subdir);
|
|
17
|
+
if (fs.existsSync(localPath))
|
|
18
|
+
return localPath;
|
|
19
|
+
// 2. Monorepo (development): <pkg>/../../docs/<subdir>
|
|
20
|
+
const monorepoPath = path.resolve(PKG_ROOT, "../../docs", subdir);
|
|
21
|
+
if (fs.existsSync(monorepoPath))
|
|
22
|
+
return monorepoPath;
|
|
23
|
+
// Fallback to local path (will fail gracefully downstream)
|
|
24
|
+
return localPath;
|
|
25
|
+
}
|
|
26
|
+
export const DOCS_MCP_PATH = resolveDocsPath("mcp");
|
|
27
|
+
export const DOCS_DEV_GUIDE_PATH = resolveDocsPath("dev-guide");
|
package/dist/lib/security.js
CHANGED
|
@@ -114,6 +114,11 @@ const ALLOWED_PATTERNS = [
|
|
|
114
114
|
// CSRF token reading from cookies - legitimate security practice
|
|
115
115
|
/csrf-token/i,
|
|
116
116
|
/getCsrfToken/,
|
|
117
|
+
// Auth cookie management - legitimate for ecommerce auth
|
|
118
|
+
/strong_store_auth/,
|
|
119
|
+
/setAuthCookie/,
|
|
120
|
+
/clearAuthCookie/,
|
|
121
|
+
/document\.cookie/,
|
|
117
122
|
];
|
|
118
123
|
/**
|
|
119
124
|
* Scan a file for suspicious patterns
|
package/dist/mcp.js
CHANGED
|
@@ -48,10 +48,8 @@ const COLUMN_TYPE_NAMES = [
|
|
|
48
48
|
// For ES modules __dirname equivalent
|
|
49
49
|
const __filename = fileURLToPath(import.meta.url);
|
|
50
50
|
const __dirname = path.dirname(__filename);
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
// docs/dev-guide path for integration docs
|
|
54
|
-
const DOCS_DEV_GUIDE_PATH = path.resolve(__dirname, "../../../docs/dev-guide");
|
|
51
|
+
// Docs paths - resolved for both global install and monorepo dev
|
|
52
|
+
import { DOCS_MCP_PATH, DOCS_DEV_GUIDE_PATH } from "./lib/docs-resolver.js";
|
|
55
53
|
/**
|
|
56
54
|
* Parse frontmatter from markdown content
|
|
57
55
|
*/
|
|
@@ -502,22 +500,24 @@ const SCHEMA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
502
500
|
const PHYSICAL_TABLE_RE = /^m\d+_t\d+$/;
|
|
503
501
|
/**
|
|
504
502
|
* Get company schema (cached)
|
|
503
|
+
* Returns { schema, error } - error is set if fetch failed
|
|
505
504
|
*/
|
|
506
505
|
async function getCompanySchema(companyId, env) {
|
|
507
506
|
// Include env in cache key to avoid mixing schemas from different environments
|
|
508
507
|
const cacheKey = `${companyId}:${env || 'prod'}`;
|
|
509
508
|
const cached = schemaCache.get(cacheKey);
|
|
510
509
|
if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL) {
|
|
511
|
-
return cached.schema;
|
|
510
|
+
return { schema: cached.schema };
|
|
512
511
|
}
|
|
513
512
|
try {
|
|
514
513
|
const data = await apiRequest("/api/cli/schema", {}, companyId, true, env);
|
|
515
514
|
schemaCache.set(cacheKey, { schema: data, timestamp: Date.now() });
|
|
516
|
-
return data;
|
|
515
|
+
return { schema: data };
|
|
517
516
|
}
|
|
518
517
|
catch (err) {
|
|
519
|
-
|
|
520
|
-
|
|
518
|
+
const errorMsg = err.message;
|
|
519
|
+
console.error(`[MCP] Failed to get schema for company ${companyId}:`, errorMsg);
|
|
520
|
+
return { schema: null, error: errorMsg };
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
523
|
/**
|
|
@@ -538,19 +538,20 @@ async function resolveTableName(tableName, companyId, env) {
|
|
|
538
538
|
throw new Error(`Invalid table name: "${tableName}". Use physical (m123_t456) or logical (module.entity) format`);
|
|
539
539
|
}
|
|
540
540
|
const [moduleName, entityName] = parts;
|
|
541
|
-
const schema = await getCompanySchema(companyId, env);
|
|
541
|
+
const { schema, error } = await getCompanySchema(companyId, env);
|
|
542
542
|
if (!schema?.modules) {
|
|
543
|
-
|
|
543
|
+
const reason = error || "unknown error";
|
|
544
|
+
throw new Error(`Cannot resolve logical table name "${tableName}": ${reason}`);
|
|
544
545
|
}
|
|
545
546
|
// Find module and entity
|
|
546
547
|
for (const mod of schema.modules) {
|
|
547
548
|
const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
|
|
548
|
-
mod.
|
|
549
|
+
mod.display_name?.toLowerCase() === moduleName.toLowerCase();
|
|
549
550
|
if (!modMatch)
|
|
550
551
|
continue;
|
|
551
552
|
for (const ent of mod.entities || []) {
|
|
552
553
|
const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
|
|
553
|
-
ent.
|
|
554
|
+
ent.display_name?.toLowerCase() === entityName.toLowerCase();
|
|
554
555
|
if (entMatch) {
|
|
555
556
|
// Found! Build physical name from entity ID
|
|
556
557
|
const moduleId = mod.id;
|
|
@@ -856,20 +857,20 @@ Solo Admin y Consultant pueden gestionar endpoints.`,
|
|
|
856
857
|
name: "gufi_view_pull",
|
|
857
858
|
description: `📥 Download view files to local directory for editing.
|
|
858
859
|
|
|
859
|
-
Downloads to: ~/gufi-dev/view_<id>/
|
|
860
|
+
Downloads to: ~/gufi-dev/company_<id>/view_<id>/
|
|
860
861
|
|
|
861
862
|
After pulling, use Read/Edit tools to work with local files.
|
|
862
863
|
Then use gufi_view_push to upload changes.
|
|
863
864
|
|
|
864
|
-
Example: gufi_view_pull({ view_id: 13 })`,
|
|
865
|
+
Example: gufi_view_pull({ view_id: 13, company_id: '150' })`,
|
|
865
866
|
inputSchema: {
|
|
866
867
|
type: "object",
|
|
867
868
|
properties: {
|
|
868
869
|
view_id: { type: "number", description: "View ID to pull" },
|
|
869
|
-
company_id: { type: "string", description: "Company ID (required
|
|
870
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
870
871
|
env: ENV_PARAM,
|
|
871
872
|
},
|
|
872
|
-
required: ["view_id"],
|
|
873
|
+
required: ["view_id", "company_id"],
|
|
873
874
|
},
|
|
874
875
|
},
|
|
875
876
|
// 📤 Push local changes to draft
|
|
@@ -1453,9 +1454,9 @@ const toolHandlers = {
|
|
|
1453
1454
|
let tableName = null;
|
|
1454
1455
|
// Resolve entity if provided (optional for standalone scripts)
|
|
1455
1456
|
if (entity) {
|
|
1456
|
-
const schema = await getCompanySchema(company_id, env);
|
|
1457
|
+
const { schema, error } = await getCompanySchema(company_id, env);
|
|
1457
1458
|
if (!schema?.modules) {
|
|
1458
|
-
throw new Error(
|
|
1459
|
+
throw new Error(`Cannot get schema: ${error || "unknown error"}`);
|
|
1459
1460
|
}
|
|
1460
1461
|
// Parse entity (module.entity format)
|
|
1461
1462
|
const parts = entity.split(".");
|
|
@@ -1466,13 +1467,13 @@ const toolHandlers = {
|
|
|
1466
1467
|
// Find module and entity IDs
|
|
1467
1468
|
for (const mod of schema.modules) {
|
|
1468
1469
|
const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
|
|
1469
|
-
mod.
|
|
1470
|
+
mod.display_name?.toLowerCase() === moduleName.toLowerCase();
|
|
1470
1471
|
if (!modMatch)
|
|
1471
1472
|
continue;
|
|
1472
1473
|
moduleId = mod.id;
|
|
1473
1474
|
for (const ent of mod.entities || []) {
|
|
1474
1475
|
const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
|
|
1475
|
-
ent.
|
|
1476
|
+
ent.display_name?.toLowerCase() === entityName.toLowerCase();
|
|
1476
1477
|
if (entMatch) {
|
|
1477
1478
|
tableId = ent.id;
|
|
1478
1479
|
tableName = `m${moduleId}_t${tableId}`;
|
|
@@ -1785,7 +1786,9 @@ const toolHandlers = {
|
|
|
1785
1786
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1786
1787
|
async gufi_view_pull(params) {
|
|
1787
1788
|
const viewId = params.view_id;
|
|
1788
|
-
const companyId = params.company_id;
|
|
1789
|
+
const companyId = params.company_id != null ? String(params.company_id) : undefined;
|
|
1790
|
+
if (!companyId)
|
|
1791
|
+
throw new Error("company_id is required for view pull");
|
|
1789
1792
|
const env = params.env;
|
|
1790
1793
|
const useLocal = canWriteLocal();
|
|
1791
1794
|
// Get view info
|
|
@@ -1798,8 +1801,8 @@ const toolHandlers = {
|
|
|
1798
1801
|
const filesResponse = await apiRequest(`/api/marketplace/views/${viewId}/files`, {}, companyId, true, env);
|
|
1799
1802
|
const files = Array.isArray(filesResponse) ? filesResponse : (filesResponse.data || []);
|
|
1800
1803
|
if (useLocal) {
|
|
1801
|
-
// 💜 CLI: Save to ~/gufi-dev/view_<id>/
|
|
1802
|
-
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
1804
|
+
// 💜 CLI: Save to ~/gufi-dev/company_<id>/view_<id>/
|
|
1805
|
+
const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
|
|
1803
1806
|
fs.mkdirSync(viewDir, { recursive: true });
|
|
1804
1807
|
// 💜 Get latest snapshot for version tracking
|
|
1805
1808
|
let latestSnapshot;
|
|
@@ -1823,9 +1826,10 @@ const toolHandlers = {
|
|
|
1823
1826
|
const meta = {
|
|
1824
1827
|
viewId,
|
|
1825
1828
|
viewName: `view_${viewId}`,
|
|
1829
|
+
company_id: Number(companyId),
|
|
1826
1830
|
packageId: view.package_id || 0,
|
|
1827
1831
|
lastSync: new Date().toISOString(),
|
|
1828
|
-
lastPulledSnapshot: latestSnapshot,
|
|
1832
|
+
lastPulledSnapshot: latestSnapshot,
|
|
1829
1833
|
files: fileMeta,
|
|
1830
1834
|
};
|
|
1831
1835
|
fs.writeFileSync(path.join(viewDir, ".gufi-view.json"), JSON.stringify(meta, null, 2));
|
|
@@ -1837,7 +1841,7 @@ const toolHandlers = {
|
|
|
1837
1841
|
local_path: viewDir,
|
|
1838
1842
|
storage: "local",
|
|
1839
1843
|
files_count: files.length,
|
|
1840
|
-
_hint: `📥 View downloaded to ${viewDir}/. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId},
|
|
1844
|
+
_hint: `📥 View downloaded to ${viewDir}/. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId}, company_id: '${companyId}' }) to upload.`,
|
|
1841
1845
|
};
|
|
1842
1846
|
}
|
|
1843
1847
|
else {
|
|
@@ -1871,33 +1875,42 @@ const toolHandlers = {
|
|
|
1871
1875
|
},
|
|
1872
1876
|
async gufi_view_push(params) {
|
|
1873
1877
|
const viewId = params.view_id;
|
|
1874
|
-
let companyId = params.company_id;
|
|
1878
|
+
let companyId = params.company_id != null ? String(params.company_id) : undefined;
|
|
1875
1879
|
const env = params.env;
|
|
1876
1880
|
const useLocal = canWriteLocal();
|
|
1881
|
+
if (!viewId) {
|
|
1882
|
+
throw new Error("view_id is required");
|
|
1883
|
+
}
|
|
1877
1884
|
// 💜 Try to get company_id from view metadata if not provided
|
|
1878
|
-
if (!companyId &&
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
+
if (!companyId && useLocal) {
|
|
1886
|
+
const companyDirs = fs.existsSync(LOCAL_VIEWS_DIR)
|
|
1887
|
+
? fs.readdirSync(LOCAL_VIEWS_DIR).filter(d => d.startsWith("company_"))
|
|
1888
|
+
: [];
|
|
1889
|
+
for (const cd of companyDirs) {
|
|
1890
|
+
const metaPath = path.join(LOCAL_VIEWS_DIR, cd, `view_${viewId}`, ".gufi-view.json");
|
|
1891
|
+
if (fs.existsSync(metaPath)) {
|
|
1892
|
+
try {
|
|
1893
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
1894
|
+
if (meta.company_id)
|
|
1895
|
+
companyId = String(meta.company_id);
|
|
1896
|
+
}
|
|
1897
|
+
catch { /* ignore */ }
|
|
1898
|
+
break;
|
|
1885
1899
|
}
|
|
1886
|
-
catch { /* ignore */ }
|
|
1887
1900
|
}
|
|
1888
1901
|
}
|
|
1889
|
-
if (!
|
|
1890
|
-
throw new Error("
|
|
1902
|
+
if (!companyId) {
|
|
1903
|
+
throw new Error("company_id is required. Pass it as parameter or pull the view first.");
|
|
1891
1904
|
}
|
|
1892
1905
|
let files = [];
|
|
1893
|
-
let lastPulledSnapshot;
|
|
1906
|
+
let lastPulledSnapshot;
|
|
1894
1907
|
if (useLocal) {
|
|
1895
|
-
// 💜 CLI: Read from ~/gufi-dev/view_<id>/
|
|
1896
|
-
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
1908
|
+
// 💜 CLI: Read from ~/gufi-dev/company_<id>/view_<id>/
|
|
1909
|
+
const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
|
|
1897
1910
|
if (!fs.existsSync(viewDir)) {
|
|
1898
|
-
throw new Error(`View directory not found: ${
|
|
1911
|
+
throw new Error(`View directory not found. Run gufi_view_pull({ view_id: ${viewId}, company_id: '${companyId}' }) first.`);
|
|
1899
1912
|
}
|
|
1900
|
-
// 💜 Read metadata for version tracking
|
|
1913
|
+
// 💜 Read metadata for version tracking
|
|
1901
1914
|
const metaPath = path.join(viewDir, ".gufi-view.json");
|
|
1902
1915
|
if (fs.existsSync(metaPath)) {
|
|
1903
1916
|
try {
|
|
@@ -2018,7 +2031,7 @@ const toolHandlers = {
|
|
|
2018
2031
|
}
|
|
2019
2032
|
// 💜 Update local metadata with new snapshot number (for version tracking)
|
|
2020
2033
|
if (useLocal && result.snapshot) {
|
|
2021
|
-
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
2034
|
+
const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
|
|
2022
2035
|
const metaPath = path.join(viewDir, ".gufi-view.json");
|
|
2023
2036
|
if (fs.existsSync(metaPath)) {
|
|
2024
2037
|
try {
|
|
@@ -2556,7 +2569,7 @@ function findTableInModulesMcp(modules, tableName) {
|
|
|
2556
2569
|
fields: (ent.fields || []).map((f) => ({
|
|
2557
2570
|
name: f.name,
|
|
2558
2571
|
type: f.type,
|
|
2559
|
-
|
|
2572
|
+
display_name: f.display_name,
|
|
2560
2573
|
required: f.required || false,
|
|
2561
2574
|
options: f.options?.map((o) => o.value || o),
|
|
2562
2575
|
})),
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: architecture
|
|
3
|
+
title: "Architecture Overview"
|
|
4
|
+
description: "How Gufi is built and why"
|
|
5
|
+
icon: Layers
|
|
6
|
+
category: dev
|
|
7
|
+
part: 1
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Architecture Overview
|
|
11
|
+
|
|
12
|
+
How Gufi is built and why
|
|
13
|
+
|
|
14
|
+
## System Architecture
|
|
15
|
+
|
|
16
|
+
Gufi follows a modern microservices architecture optimized for multi-tenancy, real-time updates, and developer experience.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ CLIENTS │
|
|
21
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
22
|
+
│ │ Web App │ │ Mobile │ │ CLI │ │ API │ │
|
|
23
|
+
│ │ (React) │ │ (PWA) │ │ (Node) │ │ Clients │ │
|
|
24
|
+
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
|
25
|
+
│ │ │ │ │ │
|
|
26
|
+
└───────┼─────────────┼─────────────┼─────────────┼───────────────┘
|
|
27
|
+
│ │ │ │
|
|
28
|
+
▼ ▼ ▼ ▼
|
|
29
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ LOAD BALANCER │
|
|
31
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
32
|
+
│
|
|
33
|
+
┌───────────────────┼───────────────────┐
|
|
34
|
+
│ │ │
|
|
35
|
+
▼ ▼ ▼
|
|
36
|
+
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
37
|
+
│ Backend ERP │ │ Marketplace │ │ WebSocket │
|
|
38
|
+
│ (Express) │ │ Backend │ │ Server │
|
|
39
|
+
│ :3000 │ │ :3003 │ │ :4000 │
|
|
40
|
+
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
|
41
|
+
│ │ │
|
|
42
|
+
└──────────────────┼───────────────────┘
|
|
43
|
+
│
|
|
44
|
+
┌──────────────────┴──────────────────┐
|
|
45
|
+
│ │
|
|
46
|
+
▼ ▼
|
|
47
|
+
┌───────────────┐ ┌───────────────┐
|
|
48
|
+
│ PostgreSQL │ │ Redis │
|
|
49
|
+
│ (Primary DB) │ │ (Cache) │
|
|
50
|
+
└───────────────┘ └───────────────┘
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌───────────────┐
|
|
54
|
+
│ Worker │
|
|
55
|
+
│ (pg-boss) │
|
|
56
|
+
└───────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Core Services
|
|
60
|
+
|
|
61
|
+
### Backend ERP (Port 3000)
|
|
62
|
+
|
|
63
|
+
The main API server handling:
|
|
64
|
+
|
|
65
|
+
- Authentication and authorization
|
|
66
|
+
- CRUD operations for all entities
|
|
67
|
+
- Module and schema management
|
|
68
|
+
- File uploads and storage
|
|
69
|
+
- Business logic and validations
|
|
70
|
+
|
|
71
|
+
**Tech Stack:**
|
|
72
|
+
- Node.js with Express
|
|
73
|
+
- JWT + HttpOnly cookie authentication
|
|
74
|
+
- Zod for validation
|
|
75
|
+
- pg (node-postgres) for database
|
|
76
|
+
|
|
77
|
+
### Marketplace Backend (Port 3003)
|
|
78
|
+
|
|
79
|
+
Handles marketplace-specific operations:
|
|
80
|
+
|
|
81
|
+
- Package publishing and versioning
|
|
82
|
+
- View code storage and retrieval
|
|
83
|
+
- Developer accounts and billing
|
|
84
|
+
- Cross-company module installations
|
|
85
|
+
|
|
86
|
+
**Key Endpoints:**
|
|
87
|
+
- `/packages` - Package CRUD
|
|
88
|
+
- `/views` - Custom view management
|
|
89
|
+
- `/developer` - Developer portal API
|
|
90
|
+
|
|
91
|
+
### WebSocket Server (Port 4000)
|
|
92
|
+
|
|
93
|
+
Real-time communication:
|
|
94
|
+
|
|
95
|
+
- LISTEN/NOTIFY from PostgreSQL
|
|
96
|
+
- Broadcasts changes to connected clients
|
|
97
|
+
- Room-based subscriptions per company/table
|
|
98
|
+
- Presence tracking
|
|
99
|
+
|
|
100
|
+
**Protocol:**
|
|
101
|
+
- Native WebSocket (not Socket.IO)
|
|
102
|
+
- JSON message format
|
|
103
|
+
- Heartbeat for connection health
|
|
104
|
+
|
|
105
|
+
### Worker (pg-boss)
|
|
106
|
+
|
|
107
|
+
Background job processing:
|
|
108
|
+
|
|
109
|
+
- Scheduled automations
|
|
110
|
+
- Email sending
|
|
111
|
+
- Heavy computations
|
|
112
|
+
- Data synchronization
|
|
113
|
+
- External API calls
|
|
114
|
+
|
|
115
|
+
**Features:**
|
|
116
|
+
- Persistent job queue in PostgreSQL
|
|
117
|
+
- Retry with exponential backoff
|
|
118
|
+
- Dead letter queue for failures
|
|
119
|
+
- Monitoring and alerting
|
|
120
|
+
|
|
121
|
+
## Frontend Architecture
|
|
122
|
+
|
|
123
|
+
### React Application
|
|
124
|
+
|
|
125
|
+
Built with modern React patterns:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
frontend/src/
|
|
129
|
+
├── features/ # Feature-based organization
|
|
130
|
+
│ ├── auth/ # Login, registration
|
|
131
|
+
│ ├── tables/ # Table views and editing
|
|
132
|
+
│ ├── modules/ # Module management
|
|
133
|
+
│ └── ...
|
|
134
|
+
├── shared/ # Shared components
|
|
135
|
+
│ ├── ui/ # Base UI components
|
|
136
|
+
│ ├── views/ # View system components
|
|
137
|
+
│ ├── dialogs/ # Dialog components
|
|
138
|
+
│ └── hooks/ # Custom hooks
|
|
139
|
+
├── stores/ # Zustand state stores
|
|
140
|
+
├── contexts/ # React contexts
|
|
141
|
+
└── sdk/ # Gufi SDK for views
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### State Management
|
|
145
|
+
|
|
146
|
+
| Store | Purpose |
|
|
147
|
+
|---|---|
|
|
148
|
+
| **authStore** | User session, tokens |
|
|
149
|
+
| **schemaStore** | Company schema cache |
|
|
150
|
+
| **permissionsStore** | Permission cache |
|
|
151
|
+
| **uiStore** | UI state (sidebar, dialogs) |
|
|
152
|
+
|
|
153
|
+
Using Zustand for simple, type-safe state management.
|
|
154
|
+
|
|
155
|
+
### Data Fetching
|
|
156
|
+
|
|
157
|
+
Using Refine framework:
|
|
158
|
+
|
|
159
|
+
- Automatic CRUD hooks
|
|
160
|
+
- Caching and invalidation
|
|
161
|
+
- Optimistic updates
|
|
162
|
+
- Pagination helpers
|
|
163
|
+
|
|
164
|
+
## Database Architecture
|
|
165
|
+
|
|
166
|
+
### PostgreSQL as Core
|
|
167
|
+
|
|
168
|
+
Why PostgreSQL:
|
|
169
|
+
|
|
170
|
+
- JSONB for flexible schemas
|
|
171
|
+
- Row-level security
|
|
172
|
+
- LISTEN/NOTIFY for real-time
|
|
173
|
+
- Excellent performance
|
|
174
|
+
- Strong ACID compliance
|
|
175
|
+
|
|
176
|
+
### Multi-Tenant Schema
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
PostgreSQL Instance
|
|
180
|
+
├── core (schema) # Platform data
|
|
181
|
+
│ ├── users # All users
|
|
182
|
+
│ ├── companies # All companies
|
|
183
|
+
│ ├── modules # Module definitions
|
|
184
|
+
│ ├── entities # Entity definitions
|
|
185
|
+
│ └── automation_scripts # Shared scripts
|
|
186
|
+
│
|
|
187
|
+
├── company_1 (schema) # Company 1 data
|
|
188
|
+
│ ├── m1_t1 # Module 1, Entity 1
|
|
189
|
+
│ ├── m1_t2 # Module 1, Entity 2
|
|
190
|
+
│ ├── __audit_log__ # Audit trail
|
|
191
|
+
│ └── ...
|
|
192
|
+
│
|
|
193
|
+
├── company_2 (schema) # Company 2 data
|
|
194
|
+
│ └── ...
|
|
195
|
+
└── ...
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Table Naming Convention
|
|
199
|
+
|
|
200
|
+
Physical tables use IDs:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
m{moduleId}_t{entityId}
|
|
204
|
+
|
|
205
|
+
Example: m360_t4589
|
|
206
|
+
└── Module 360, Entity 4589
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Logical names (products, customers) map to physical names via schema.
|
|
210
|
+
|
|
211
|
+
## API Design
|
|
212
|
+
|
|
213
|
+
### RESTful Endpoints
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
GET /api/tables/:tableId # List records
|
|
217
|
+
GET /api/tables/:tableId/:rowId # Get record
|
|
218
|
+
POST /api/tables/:tableId # Create record
|
|
219
|
+
PUT /api/tables/:tableId/:rowId # Update record
|
|
220
|
+
DELETE /api/tables/:tableId/:rowId # Delete record
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Authentication Flow
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
1. Login → POST /api/auth/login
|
|
227
|
+
← Access token (15min) + Refresh cookie (7days)
|
|
228
|
+
|
|
229
|
+
2. API Request → GET /api/... + Authorization: Bearer {token}
|
|
230
|
+
← Data response
|
|
231
|
+
|
|
232
|
+
3. Token Expiring → POST /api/auth/refresh
|
|
233
|
+
← New access token
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Response Format
|
|
237
|
+
|
|
238
|
+
Success:
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"data": { ... },
|
|
242
|
+
"meta": {
|
|
243
|
+
"total": 100,
|
|
244
|
+
"page": 1,
|
|
245
|
+
"pageSize": 20
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Error:
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"error": {
|
|
254
|
+
"code": "VALIDATION_ERROR",
|
|
255
|
+
"message": "Price must be positive",
|
|
256
|
+
"details": { "field": "price" }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Security Model
|
|
262
|
+
|
|
263
|
+
### Layers of Security
|
|
264
|
+
|
|
265
|
+
1. **Network**: HTTPS, WAF, Rate limiting
|
|
266
|
+
2. **Authentication**: JWT, refresh tokens, 2FA
|
|
267
|
+
3. **Authorization**: Role-based, entity-level
|
|
268
|
+
4. **Data**: Row-level security, tenant isolation
|
|
269
|
+
5. **Audit**: Complete change tracking
|
|
270
|
+
|
|
271
|
+
### Tenant Isolation
|
|
272
|
+
|
|
273
|
+
Each request:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
1. Extract company_id from JWT
|
|
277
|
+
2. SET search_path TO company_{id}
|
|
278
|
+
3. Execute query (sees only company data)
|
|
279
|
+
4. RESET search_path
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
No cross-tenant queries possible at database level.
|
|
283
|
+
|
|
284
|
+
## Caching Strategy
|
|
285
|
+
|
|
286
|
+
### Redis Cache Layers
|
|
287
|
+
|
|
288
|
+
| Layer | TTL | Purpose |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| **Schema** | 5 min | Company schemas |
|
|
291
|
+
| **Permissions** | 1 min | User permissions |
|
|
292
|
+
| **Queries** | 30 sec | Expensive queries |
|
|
293
|
+
| **Sessions** | 7 days | User sessions |
|
|
294
|
+
|
|
295
|
+
### Cache Invalidation
|
|
296
|
+
|
|
297
|
+
Event-driven invalidation:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
Record Changed → Invalidate related caches
|
|
301
|
+
→ Notify WebSocket
|
|
302
|
+
→ Clients refetch
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Deployment
|
|
306
|
+
|
|
307
|
+
### Container Architecture
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
services:
|
|
311
|
+
frontend:
|
|
312
|
+
image: gufi/frontend
|
|
313
|
+
ports: [5173, 8080]
|
|
314
|
+
|
|
315
|
+
backend:
|
|
316
|
+
image: gufi/backend
|
|
317
|
+
ports: [3000]
|
|
318
|
+
depends_on: [db, redis]
|
|
319
|
+
|
|
320
|
+
websocket:
|
|
321
|
+
image: gufi/websocket
|
|
322
|
+
ports: [4000]
|
|
323
|
+
depends_on: [db, redis]
|
|
324
|
+
|
|
325
|
+
worker:
|
|
326
|
+
image: gufi/worker
|
|
327
|
+
depends_on: [db, redis]
|
|
328
|
+
|
|
329
|
+
db:
|
|
330
|
+
image: postgres:15
|
|
331
|
+
volumes: [db_data]
|
|
332
|
+
|
|
333
|
+
redis:
|
|
334
|
+
image: redis:7
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Scaling
|
|
338
|
+
|
|
339
|
+
| Component | Scale Strategy |
|
|
340
|
+
|---|---|
|
|
341
|
+
| Frontend | Horizontal (CDN + replicas) |
|
|
342
|
+
| Backend | Horizontal (load balanced) |
|
|
343
|
+
| WebSocket | Horizontal (Redis pub/sub) |
|
|
344
|
+
| Worker | Horizontal (pg-boss handles) |
|
|
345
|
+
| PostgreSQL | Vertical + Read replicas |
|
|
346
|
+
| Redis | Cluster mode |
|
|
347
|
+
|
|
348
|
+
## Performance Targets
|
|
349
|
+
|
|
350
|
+
| Metric | Target |
|
|
351
|
+
|---|---|
|
|
352
|
+
| API Response (P95) | < 100ms |
|
|
353
|
+
| Page Load | < 2s |
|
|
354
|
+
| Time to Interactive | < 3s |
|
|
355
|
+
| WebSocket Latency | < 50ms |
|
|
356
|
+
| Database Query | < 50ms |
|
|
357
|
+
|
|
358
|
+
Monitored continuously with alerting on degradation.
|