@env-hopper/backend-core 2.0.1-alpha.2 → 2.0.1-alpha.4
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/index.d.ts +194 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +375 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/db/client.ts +8 -0
- package/src/db/index.ts +8 -4
- package/src/index.ts +67 -52
- package/src/middleware/backendResolver.ts +49 -0
- package/src/middleware/createEhMiddleware.ts +103 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +178 -0
- package/src/middleware/index.ts +43 -0
- package/src/middleware/types.ts +186 -0
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +182 -0
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { stepCountIs, streamText, tool } from "ai";
|
|
|
13
13
|
import multer from "multer";
|
|
14
14
|
import { readFileSync, readdirSync } from "node:fs";
|
|
15
15
|
import { extname, join } from "node:path";
|
|
16
|
+
import express, { Router } from "express";
|
|
17
|
+
import * as trpcExpress from "@trpc/server/adapters/express";
|
|
16
18
|
|
|
17
19
|
//#region src/db/client.ts
|
|
18
20
|
let prismaClient = null;
|
|
@@ -25,6 +27,13 @@ function getDbClient() {
|
|
|
25
27
|
return prismaClient;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
30
|
+
* Sets the internal Prisma client instance.
|
|
31
|
+
* Used by middleware to bridge with existing getDbClient() usage.
|
|
32
|
+
*/
|
|
33
|
+
function setDbClient(client) {
|
|
34
|
+
prismaClient = client;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
28
37
|
* Connects to the database.
|
|
29
38
|
* Call this before performing database operations.
|
|
30
39
|
*/
|
|
@@ -1802,5 +1811,370 @@ async function syncScreenshotsFromDirectory(prisma, screenshotsDir) {
|
|
|
1802
1811
|
}
|
|
1803
1812
|
|
|
1804
1813
|
//#endregion
|
|
1805
|
-
|
|
1814
|
+
//#region src/middleware/database.ts
|
|
1815
|
+
/**
|
|
1816
|
+
* Formats a database connection URL from structured config.
|
|
1817
|
+
*/
|
|
1818
|
+
function formatConnectionUrl(config) {
|
|
1819
|
+
if ("url" in config) return config.url;
|
|
1820
|
+
const { host, port, database, username, password, schema = "public" } = config;
|
|
1821
|
+
return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Internal database manager used by the middleware.
|
|
1825
|
+
* Handles connection URL formatting and lifecycle.
|
|
1826
|
+
*/
|
|
1827
|
+
var EhDatabaseManager = class {
|
|
1828
|
+
constructor(config) {
|
|
1829
|
+
this.client = null;
|
|
1830
|
+
this.config = config;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Get or create the Prisma client instance.
|
|
1834
|
+
* Uses lazy initialization for flexibility.
|
|
1835
|
+
*/
|
|
1836
|
+
getClient() {
|
|
1837
|
+
if (!this.client) {
|
|
1838
|
+
this.client = new PrismaClient({
|
|
1839
|
+
datasourceUrl: formatConnectionUrl(this.config),
|
|
1840
|
+
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["warn", "error"]
|
|
1841
|
+
});
|
|
1842
|
+
setDbClient(this.client);
|
|
1843
|
+
}
|
|
1844
|
+
return this.client;
|
|
1845
|
+
}
|
|
1846
|
+
async connect() {
|
|
1847
|
+
await this.getClient().$connect();
|
|
1848
|
+
}
|
|
1849
|
+
async disconnect() {
|
|
1850
|
+
if (this.client) {
|
|
1851
|
+
await this.client.$disconnect();
|
|
1852
|
+
this.client = null;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region src/middleware/backendResolver.ts
|
|
1859
|
+
/**
|
|
1860
|
+
* Type guard to check if an object implements EhBackendCompanySpecificBackend.
|
|
1861
|
+
*/
|
|
1862
|
+
function isBackendInstance(obj) {
|
|
1863
|
+
return typeof obj === "object" && obj !== null && typeof obj.getBootstrapData === "function" && typeof obj.getAvailabilityMatrix === "function" && typeof obj.getNameMigrations === "function" && typeof obj.getResourceJumps === "function" && typeof obj.getResourceJumpsExtended === "function";
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Normalizes different backend provider types into a consistent async factory function.
|
|
1867
|
+
* Supports:
|
|
1868
|
+
* - Direct object implementing EhBackendCompanySpecificBackend
|
|
1869
|
+
* - Sync factory function that returns the backend
|
|
1870
|
+
* - Async factory function that returns the backend
|
|
1871
|
+
*/
|
|
1872
|
+
function createBackendResolver(provider) {
|
|
1873
|
+
if (isBackendInstance(provider)) return async () => provider;
|
|
1874
|
+
if (typeof provider === "function") return async () => {
|
|
1875
|
+
const result = provider();
|
|
1876
|
+
return result instanceof Promise ? result : result;
|
|
1877
|
+
};
|
|
1878
|
+
throw new Error("Invalid backend provider: must be an object implementing EhBackendCompanySpecificBackend or a factory function");
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
//#endregion
|
|
1882
|
+
//#region src/modules/appCatalogAdmin/catalogBackupController.ts
|
|
1883
|
+
/**
|
|
1884
|
+
* Export the complete app catalog as JSON
|
|
1885
|
+
* Includes all fields from DbAppForCatalog
|
|
1886
|
+
*/
|
|
1887
|
+
async function exportCatalog(_req, res) {
|
|
1888
|
+
try {
|
|
1889
|
+
const apps = await getDbClient().dbAppForCatalog.findMany({ orderBy: { slug: "asc" } });
|
|
1890
|
+
res.json({
|
|
1891
|
+
version: "1.0",
|
|
1892
|
+
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1893
|
+
apps
|
|
1894
|
+
});
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
console.error("Error exporting catalog:", error);
|
|
1897
|
+
res.status(500).json({ error: "Failed to export catalog" });
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Import/restore the complete app catalog from JSON
|
|
1902
|
+
* Overwrites existing data
|
|
1903
|
+
*/
|
|
1904
|
+
async function importCatalog(req, res) {
|
|
1905
|
+
try {
|
|
1906
|
+
const prisma = getDbClient();
|
|
1907
|
+
const { apps } = req.body;
|
|
1908
|
+
if (!Array.isArray(apps)) {
|
|
1909
|
+
res.status(400).json({ error: "Invalid data format: apps must be an array" });
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
await prisma.$transaction(async (tx) => {
|
|
1913
|
+
await tx.dbAppForCatalog.deleteMany({});
|
|
1914
|
+
for (const app of apps) {
|
|
1915
|
+
const { id, createdAt, updatedAt,...appData } = app;
|
|
1916
|
+
await tx.dbAppForCatalog.create({ data: appData });
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
res.json({
|
|
1920
|
+
success: true,
|
|
1921
|
+
imported: apps.length
|
|
1922
|
+
});
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
console.error("Error importing catalog:", error);
|
|
1925
|
+
res.status(500).json({ error: "Failed to import catalog" });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Export an asset (icon or screenshot) by name
|
|
1930
|
+
*/
|
|
1931
|
+
async function exportAsset(req, res) {
|
|
1932
|
+
try {
|
|
1933
|
+
const { name } = req.params;
|
|
1934
|
+
const asset = await getDbClient().dbAsset.findUnique({ where: { name } });
|
|
1935
|
+
if (!asset) {
|
|
1936
|
+
res.status(404).json({ error: "Asset not found" });
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
res.set("Content-Type", asset.mimeType);
|
|
1940
|
+
res.set("Content-Disposition", `attachment; filename="${name}"`);
|
|
1941
|
+
res.send(Buffer.from(asset.content));
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
console.error("Error exporting asset:", error);
|
|
1944
|
+
res.status(500).json({ error: "Failed to export asset" });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* List all assets with metadata
|
|
1949
|
+
*/
|
|
1950
|
+
async function listAssets(_req, res) {
|
|
1951
|
+
try {
|
|
1952
|
+
const assets = await getDbClient().dbAsset.findMany({
|
|
1953
|
+
select: {
|
|
1954
|
+
id: true,
|
|
1955
|
+
name: true,
|
|
1956
|
+
assetType: true,
|
|
1957
|
+
mimeType: true,
|
|
1958
|
+
fileSize: true,
|
|
1959
|
+
width: true,
|
|
1960
|
+
height: true,
|
|
1961
|
+
checksum: true
|
|
1962
|
+
},
|
|
1963
|
+
orderBy: { name: "asc" }
|
|
1964
|
+
});
|
|
1965
|
+
res.json({ assets });
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
console.error("Error listing assets:", error);
|
|
1968
|
+
res.status(500).json({ error: "Failed to list assets" });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Import an asset (icon or screenshot)
|
|
1973
|
+
*/
|
|
1974
|
+
async function importAsset(req, res) {
|
|
1975
|
+
try {
|
|
1976
|
+
const file = req.file;
|
|
1977
|
+
const { name, assetType, mimeType, width, height } = req.body;
|
|
1978
|
+
if (!file) {
|
|
1979
|
+
res.status(400).json({ error: "No file uploaded" });
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const prisma = getDbClient();
|
|
1983
|
+
const checksum = (await import("node:crypto")).createHash("sha256").update(file.buffer).digest("hex");
|
|
1984
|
+
const content = new Uint8Array(file.buffer);
|
|
1985
|
+
await prisma.dbAsset.upsert({
|
|
1986
|
+
where: { name },
|
|
1987
|
+
update: {
|
|
1988
|
+
content,
|
|
1989
|
+
checksum,
|
|
1990
|
+
mimeType: mimeType || file.mimetype,
|
|
1991
|
+
fileSize: file.size,
|
|
1992
|
+
width: width ? parseInt(width) : null,
|
|
1993
|
+
height: height ? parseInt(height) : null,
|
|
1994
|
+
assetType: assetType || "icon"
|
|
1995
|
+
},
|
|
1996
|
+
create: {
|
|
1997
|
+
name,
|
|
1998
|
+
content,
|
|
1999
|
+
checksum,
|
|
2000
|
+
mimeType: mimeType || file.mimetype,
|
|
2001
|
+
fileSize: file.size,
|
|
2002
|
+
width: width ? parseInt(width) : null,
|
|
2003
|
+
height: height ? parseInt(height) : null,
|
|
2004
|
+
assetType: assetType || "icon"
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
res.json({
|
|
2008
|
+
success: true,
|
|
2009
|
+
name,
|
|
2010
|
+
size: file.size
|
|
2011
|
+
});
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
console.error("Error importing asset:", error);
|
|
2014
|
+
res.status(500).json({ error: "Failed to import asset" });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
//#endregion
|
|
2019
|
+
//#region src/middleware/featureRegistry.ts
|
|
2020
|
+
const FEATURES = [
|
|
2021
|
+
{
|
|
2022
|
+
name: "auth",
|
|
2023
|
+
defaultEnabled: true,
|
|
2024
|
+
register: (router$1, options, ctx) => {
|
|
2025
|
+
const basePath = options.basePath;
|
|
2026
|
+
router$1.get(`${basePath}/auth/session`, async (req, res) => {
|
|
2027
|
+
try {
|
|
2028
|
+
const session = await ctx.auth.api.getSession({ headers: req.headers });
|
|
2029
|
+
if (session) res.json(session);
|
|
2030
|
+
else res.status(401).json({ error: "Not authenticated" });
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
console.error("[Auth Session Error]", error);
|
|
2033
|
+
res.status(500).json({ error: "Internal server error" });
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
const authHandler = toNodeHandler(ctx.auth);
|
|
2037
|
+
router$1.all(`${basePath}/auth/{*any}`, authHandler);
|
|
2038
|
+
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
name: "icons",
|
|
2042
|
+
defaultEnabled: true,
|
|
2043
|
+
register: (router$1, options) => {
|
|
2044
|
+
registerIconRestController(router$1, { basePath: `${options.basePath}/icons` });
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
name: "assets",
|
|
2049
|
+
defaultEnabled: true,
|
|
2050
|
+
register: (router$1, options) => {
|
|
2051
|
+
registerAssetRestController(router$1, { basePath: `${options.basePath}/assets` });
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
name: "screenshots",
|
|
2056
|
+
defaultEnabled: true,
|
|
2057
|
+
register: (router$1, options) => {
|
|
2058
|
+
registerScreenshotRestController(router$1, { basePath: `${options.basePath}/screenshots` });
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
name: "adminChat",
|
|
2063
|
+
defaultEnabled: false,
|
|
2064
|
+
register: (router$1, options) => {
|
|
2065
|
+
if (options.adminChat) router$1.post(`${options.basePath}/admin/chat`, createAdminChatHandler(options.adminChat));
|
|
2066
|
+
}
|
|
2067
|
+
},
|
|
2068
|
+
{
|
|
2069
|
+
name: "legacyIconEndpoint",
|
|
2070
|
+
defaultEnabled: false,
|
|
2071
|
+
register: (router$1) => {
|
|
2072
|
+
router$1.get("/static/icon/:icon", async (req, res) => {
|
|
2073
|
+
const { icon } = req.params;
|
|
2074
|
+
if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {
|
|
2075
|
+
res.status(400).send("Invalid icon name");
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
const dbIcon = await getAssetByName(icon);
|
|
2080
|
+
if (!dbIcon) {
|
|
2081
|
+
res.status(404).send("Icon not found");
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
res.setHeader("Content-Type", dbIcon.mimeType);
|
|
2085
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
2086
|
+
res.send(dbIcon.content);
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
console.error("Error fetching icon:", error);
|
|
2089
|
+
res.status(404).send("Icon not found");
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: "catalogBackup",
|
|
2096
|
+
defaultEnabled: true,
|
|
2097
|
+
register: (router$1, options) => {
|
|
2098
|
+
const basePath = options.basePath;
|
|
2099
|
+
const upload$2 = multer({ storage: multer.memoryStorage() });
|
|
2100
|
+
router$1.get(`${basePath}/catalog/backup/export`, exportCatalog);
|
|
2101
|
+
router$1.post(`${basePath}/catalog/backup/import`, importCatalog);
|
|
2102
|
+
router$1.get(`${basePath}/catalog/backup/assets`, listAssets);
|
|
2103
|
+
router$1.get(`${basePath}/catalog/backup/assets/:name`, exportAsset);
|
|
2104
|
+
router$1.post(`${basePath}/catalog/backup/assets`, upload$2.single("file"), importAsset);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
];
|
|
2108
|
+
/**
|
|
2109
|
+
* Registers all enabled features on the router.
|
|
2110
|
+
*/
|
|
2111
|
+
function registerFeatures(router$1, options, context) {
|
|
2112
|
+
const toggles = options.features || {};
|
|
2113
|
+
for (const feature of FEATURES) {
|
|
2114
|
+
const isEnabled = toggles[feature.name] ?? feature.defaultEnabled;
|
|
2115
|
+
if (feature.name === "adminChat" && !options.adminChat) continue;
|
|
2116
|
+
if (isEnabled) feature.register(router$1, options, context);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
//#endregion
|
|
2121
|
+
//#region src/middleware/createEhMiddleware.ts
|
|
2122
|
+
async function createEhMiddleware(options) {
|
|
2123
|
+
var _normalizedOptions$fe, _options$hooks;
|
|
2124
|
+
const basePath = options.basePath ?? "/api";
|
|
2125
|
+
const normalizedOptions = {
|
|
2126
|
+
...options,
|
|
2127
|
+
basePath
|
|
2128
|
+
};
|
|
2129
|
+
const dbManager = new EhDatabaseManager(options.database);
|
|
2130
|
+
dbManager.getClient();
|
|
2131
|
+
const auth = createAuth({
|
|
2132
|
+
appName: options.auth.appName,
|
|
2133
|
+
baseURL: options.auth.baseURL,
|
|
2134
|
+
secret: options.auth.secret,
|
|
2135
|
+
providers: options.auth.providers,
|
|
2136
|
+
plugins: options.auth.plugins,
|
|
2137
|
+
sessionExpiresIn: options.auth.sessionExpiresIn,
|
|
2138
|
+
sessionUpdateAge: options.auth.sessionUpdateAge
|
|
2139
|
+
});
|
|
2140
|
+
const trpcRouter = createTrpcRouter(auth);
|
|
2141
|
+
const resolveBackend = createBackendResolver(options.backend);
|
|
2142
|
+
const createContext = async () => {
|
|
2143
|
+
return createEhTrpcContext({ companySpecificBackend: await resolveBackend() });
|
|
2144
|
+
};
|
|
2145
|
+
const router$1 = Router();
|
|
2146
|
+
router$1.use(express.json());
|
|
2147
|
+
const middlewareContext = {
|
|
2148
|
+
auth,
|
|
2149
|
+
trpcRouter,
|
|
2150
|
+
createContext
|
|
2151
|
+
};
|
|
2152
|
+
if (((_normalizedOptions$fe = normalizedOptions.features) === null || _normalizedOptions$fe === void 0 ? void 0 : _normalizedOptions$fe.trpc) !== false) router$1.use(`${basePath}/trpc`, trpcExpress.createExpressMiddleware({
|
|
2153
|
+
router: trpcRouter,
|
|
2154
|
+
createContext
|
|
2155
|
+
}));
|
|
2156
|
+
registerFeatures(router$1, normalizedOptions, middlewareContext);
|
|
2157
|
+
if ((_options$hooks = options.hooks) === null || _options$hooks === void 0 ? void 0 : _options$hooks.onRoutesRegistered) await options.hooks.onRoutesRegistered(router$1);
|
|
2158
|
+
return {
|
|
2159
|
+
router: router$1,
|
|
2160
|
+
auth,
|
|
2161
|
+
trpcRouter,
|
|
2162
|
+
async connect() {
|
|
2163
|
+
var _options$hooks2;
|
|
2164
|
+
await dbManager.connect();
|
|
2165
|
+
if ((_options$hooks2 = options.hooks) === null || _options$hooks2 === void 0 ? void 0 : _options$hooks2.onDatabaseConnected) await options.hooks.onDatabaseConnected();
|
|
2166
|
+
},
|
|
2167
|
+
async disconnect() {
|
|
2168
|
+
var _options$hooks3;
|
|
2169
|
+
if ((_options$hooks3 = options.hooks) === null || _options$hooks3 === void 0 ? void 0 : _options$hooks3.onDatabaseDisconnecting) await options.hooks.onDatabaseDisconnecting();
|
|
2170
|
+
await dbManager.disconnect();
|
|
2171
|
+
},
|
|
2172
|
+
addRoutes(callback) {
|
|
2173
|
+
callback(router$1);
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
//#endregion
|
|
2179
|
+
export { DEFAULT_ADMIN_SYSTEM_PROMPT, EhDatabaseManager, TABLE_SYNC_MAGAZINE, connectDb, createAdminChatHandler, createAppCatalogAdminRouter, createAuth, createAuthRouter, createDatabaseTools, createEhMiddleware, createEhTrpcContext, createPrismaDatabaseClient, createScreenshotRouter, createTrpcRouter, disconnectDb, getAdminGroupsFromEnv, getAssetByName, getAuthPluginsFromEnv, getAuthProvidersFromEnv, getDbClient, getUserGroups, isAdmin, isMemberOfAllGroups, isMemberOfAnyGroup, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, requireAdmin, requireGroups, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, tool, upsertIcon, upsertIcons, validateAuthConfig };
|
|
1806
2180
|
//# sourceMappingURL=index.js.map
|