@vario-software/vario-app-framework-backend 2025.37.0

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.
@@ -0,0 +1,83 @@
1
+ const Eav = class
2
+ {
3
+ constructor(ApiAdapter)
4
+ {
5
+ this.ApiAdapter = ApiAdapter;
6
+ }
7
+
8
+ getGroup = async function(groupKey)
9
+ {
10
+ const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`, {
11
+ method: 'GET',
12
+ });
13
+
14
+ return eavGroup;
15
+ };
16
+
17
+ setGroup = async function(eavGroup)
18
+ {
19
+ const existingGroup = await this.getGroupIdByKey(eavGroup.key);
20
+
21
+ if (existingGroup)
22
+ {
23
+ eavGroup.id = existingGroup;
24
+
25
+ return eavGroup;
26
+ }
27
+
28
+ eavGroup = await this.ApiAdapter.fetch('/cmn/eav-groups', {
29
+ method: 'POST',
30
+ body: JSON.stringify(eavGroup),
31
+ });
32
+
33
+ return eavGroup.data;
34
+ };
35
+
36
+ changeGroup = async function (groupKey, callback)
37
+ {
38
+ let { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`, {
39
+ method: 'GET',
40
+ });
41
+
42
+ eavGroup = callback(eavGroup);
43
+
44
+ eavGroup = await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}`, {
45
+ method: 'PUT',
46
+ body: JSON.stringify(eavGroup),
47
+ });
48
+
49
+ return eavGroup.data;
50
+ };
51
+
52
+ deleteGroup = async function (groupKey)
53
+ {
54
+ const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`);
55
+
56
+ await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}/remove-data`, {
57
+ method: 'POST',
58
+ body: JSON.stringify({ entities: eavGroup.entities }),
59
+ });
60
+
61
+ await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}`, {
62
+ method: 'DELETE',
63
+ });
64
+
65
+ return true;
66
+ };
67
+
68
+ getGroupIdByKey = async function(groupKey)
69
+ {
70
+ try
71
+ {
72
+ const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`);
73
+
74
+ return eavGroup.id;
75
+ }
76
+ catch
77
+ {
78
+ return null;
79
+ }
80
+ };
81
+ };
82
+
83
+ module.exports = Eav;
@@ -0,0 +1,95 @@
1
+ const { getApp } = require('#backend/utils/context.js');
2
+
3
+ const Migration = class
4
+ {
5
+ constructor(ApiAdapter)
6
+ {
7
+ this.ApiAdapter = ApiAdapter;
8
+ }
9
+
10
+ get = async function(identifier)
11
+ {
12
+ const { data } = await this.getAll({ limit: 1, identifier });
13
+
14
+ return data[0];
15
+ };
16
+
17
+ getAll = async function({ offset = 0, limit = 10, identifier })
18
+ {
19
+ const app = getApp();
20
+
21
+ const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
22
+ statement: `
23
+ SELECT id,
24
+ identifier
25
+ FROM system.queryAppMigrations
26
+ WHERE id NOTNULL
27
+ AND appIdentifier = '${app.client.appIdentifier}'
28
+ ${identifier ? `AND identifier = '${identifier}'` : ''}
29
+ `,
30
+ offset,
31
+ limit,
32
+ });
33
+
34
+ return {
35
+ data,
36
+ moreElements,
37
+ nextOffset,
38
+ };
39
+ };
40
+
41
+ async getNote(key)
42
+ {
43
+ const app = getApp();
44
+
45
+ const { data } = await this.ApiAdapter.vql({
46
+ statement: `
47
+ SELECT note
48
+ FROM system.queryAppMigrations
49
+ WHERE appIdentifier = '${app.client.appIdentifier}'
50
+ AND identifier = '${key}'
51
+ `,
52
+ });
53
+
54
+ let note;
55
+
56
+ try
57
+ {
58
+ note = JSON.parse(data[0]?.note);
59
+ }
60
+ catch (error)
61
+ {
62
+ note = null;
63
+ }
64
+
65
+ return note;
66
+ }
67
+
68
+ set = async function(identifier, note)
69
+ {
70
+ const app = getApp();
71
+
72
+ const { data: response } = await this.ApiAdapter.fetch('/cmn/system/app-migration', {
73
+ body: {
74
+ appIdentifier: app.client.appIdentifier,
75
+ identifier,
76
+ executedAt: new Date().toISOString(),
77
+ note,
78
+ },
79
+ method: 'post',
80
+ });
81
+
82
+ return response;
83
+ };
84
+
85
+ delete = async function(id)
86
+ {
87
+ const { data: response } = await this.ApiAdapter.fetch(`/cmn/system/app-migration/${id}`, {
88
+ method: 'delete',
89
+ });
90
+
91
+ return response;
92
+ };
93
+ };
94
+
95
+ module.exports = Migration;
@@ -0,0 +1,149 @@
1
+ const TextEnum = class
2
+ {
3
+ constructor(ApiAdapter)
4
+ {
5
+ this.ApiAdapter = ApiAdapter;
6
+ }
7
+
8
+ get = async function(key)
9
+ {
10
+ const { data } = await this.getAll({ limit: 1, key });
11
+
12
+ return data[0];
13
+ };
14
+
15
+ getAll = async function({ offset = 0, limit = 10, key })
16
+ {
17
+ const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
18
+ statement: `
19
+ SELECT
20
+ id,
21
+ label,
22
+ key,
23
+ textEnums.id,
24
+ textEnums.label
25
+ FROM masterdata.query-textenum-group
26
+ ${key ? `WHERE key = '${key}'` : ''}
27
+ `,
28
+ offset,
29
+ limit,
30
+ });
31
+
32
+ return {
33
+ data,
34
+ moreElements,
35
+ nextOffset,
36
+ };
37
+ };
38
+
39
+ setGroup = async function(textEnumGroupTemplate)
40
+ {
41
+ let textEnumGroup = await this.getEnumGroup(textEnumGroupTemplate.key);
42
+
43
+ if (textEnumGroup)
44
+ {
45
+ await this.removeEnums(textEnumGroup);
46
+ }
47
+ else
48
+ {
49
+ textEnumGroup = await this.createGroup(textEnumGroupTemplate);
50
+ }
51
+
52
+ return textEnumGroup;
53
+ };
54
+
55
+ setEnums = async function(textEnumGroupKey, textEnums)
56
+ {
57
+ await this.createEnums(
58
+ textEnumGroupKey,
59
+ textEnums,
60
+ );
61
+ };
62
+
63
+ createGroup = async function(textEnumGroup)
64
+ {
65
+ const response = await this.ApiAdapter.fetch(
66
+ '/cmn/masterdata/text-enum-groups',
67
+ {
68
+ body: JSON.stringify(textEnumGroup),
69
+ method: 'POST',
70
+ });
71
+
72
+ return response.data;
73
+ };
74
+
75
+ getAllEnums = async function({ offset = 0, limit = 10, groupId })
76
+ {
77
+ const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
78
+ statement: `
79
+ SELECT
80
+ id,
81
+ entry
82
+ FROM masterdata.query-textenum
83
+ ${groupId ? `WHERE group.id = '${groupId}'` : ''}
84
+ `,
85
+ offset,
86
+ limit,
87
+ });
88
+
89
+ return {
90
+ data,
91
+ moreElements,
92
+ nextOffset,
93
+ };
94
+ };
95
+
96
+ removeEnums = async function(textEnumGroup)
97
+ {
98
+ const textEnums = await this.getEnumsByGroup(textEnumGroup);
99
+
100
+ if (!textEnums)
101
+ {
102
+ return true;
103
+ }
104
+
105
+ await textEnums.reduce(async (previousPromise, textEnum) =>
106
+ {
107
+ await previousPromise;
108
+
109
+ return this.removeEnum(textEnum);
110
+ }, Promise.resolve());
111
+
112
+ return true;
113
+ };
114
+
115
+ removeEnum = async function(textEnum)
116
+ {
117
+ return this.ApiAdapter.fetch(
118
+ `/cmn/masterdata/text-enums/${textEnum.id}`,
119
+ {
120
+ method: 'DELETE',
121
+ },
122
+ );
123
+ };
124
+
125
+ createEnums = async function(customGroupKey, textEnums)
126
+ {
127
+ await textEnums.reduce(async (previousPromise, textEnum) =>
128
+ {
129
+ await previousPromise;
130
+
131
+ return this.createEnum(textEnum);
132
+ }, Promise.resolve());
133
+
134
+ return true;
135
+ };
136
+
137
+ createEnum = async function(textEnum)
138
+ {
139
+ await this.ApiAdapter.fetch(
140
+ '/cmn/masterdata/text-enums',
141
+ {
142
+ body: JSON.stringify(textEnum),
143
+ method: 'POST',
144
+ },
145
+ );
146
+ };
147
+ };
148
+
149
+ module.exports = TextEnum;
@@ -0,0 +1,44 @@
1
+ const { getRequest } = require('#backend/utils/context.js');
2
+ const { getApp } = require('#backend/utils/context.js');
3
+
4
+ const TextEnum = class
5
+ {
6
+ constructor(ApiAdapter)
7
+ {
8
+ this.ApiAdapter = ApiAdapter;
9
+ }
10
+
11
+ register = async function(destinationQueue, url)
12
+ {
13
+ const apiUrl = `${process.env.WEBHOOK_HOST ?? `https://${getRequest().get('host')}`}`;
14
+
15
+ const app = getApp();
16
+
17
+ await this.ApiAdapter.fetch('/cmn/system/app-message-webhook/register', {
18
+ method: 'POST',
19
+ body: JSON.stringify({
20
+ url: `${apiUrl}${url}`,
21
+ destinationQueue,
22
+ appIdentifier: app.client.appIdentifier,
23
+ }),
24
+ });
25
+ };
26
+
27
+ deregister = async function(destinationQueue, url)
28
+ {
29
+ const apiUrl = `${process.env.WEBHOOK_HOST ?? `https://${getRequest().get('host')}`}`;
30
+
31
+ const app = getApp();
32
+
33
+ await this.ApiAdapter.fetch('/cmn/system/app-message-webhook/deregister', {
34
+ method: 'POST',
35
+ body: JSON.stringify({
36
+ url: `${apiUrl}${url}`,
37
+ destinationQueue,
38
+ appIdentifier: app.client.appIdentifier,
39
+ }),
40
+ });
41
+ };
42
+ };
43
+
44
+ module.exports = TextEnum;
package/app.js ADDED
@@ -0,0 +1,115 @@
1
+ const express = require('express');
2
+ const bodyParser = require('body-parser');
3
+ const cors = require('cors');
4
+ const { createProxyMiddleware } = require('http-proxy-middleware');
5
+ const path = require('path');
6
+ const ErpApi = require('#backend/api/ErpApi.js');
7
+ const appAuthentication = require('#backend/setup/appAuthentication.js');
8
+ const setupContext = require('#backend/setup/context.js');
9
+ const setupException = require('#backend/setup/exception.js');
10
+ const { log } = require('#backend/utils/logger.js');
11
+ const OfflineToken = require('#backend/modules/offlineToken.js');
12
+ const AccessToken = require('#backend/modules/accessToken.js');
13
+ const BaseUrlCache = require('#backend/modules/baseUrlCache.js');
14
+
15
+ const VarioCloudApp = class
16
+ {
17
+ constructor(client, options = {})
18
+ {
19
+ this.express = express();
20
+ this.port = '8080';
21
+ this.uiPath = null;
22
+ this.uiPrefix = '/ui';
23
+
24
+ this.version = 'latest';
25
+
26
+ this.client = client;
27
+
28
+ this.log = options.log ?? log;
29
+ this.offlineToken = options.offlineToken ?? new OfflineToken(this);
30
+ this.accessToken = options.accessToken ?? new AccessToken(this);
31
+ this.baseUrlCache = options.baseUrlCache ?? new BaseUrlCache(this);
32
+
33
+ this.erp = ErpApi;
34
+
35
+ this.express.disable('x-powered-by');
36
+
37
+ this.express.use(cors());
38
+
39
+ this.express.use(bodyParser.json(options.bodyParser));
40
+ this.express.use(bodyParser.raw({ type: 'application/octet-stream', limit: 100 * 1024 * 1024 }));
41
+
42
+ this.apiServer = express.Router();
43
+
44
+ this.apiServer.use(setupContext(this));
45
+ this.apiServer.use(appAuthentication);
46
+
47
+ this.express.use(options.apiPrefix ?? '/api', this.apiServer);
48
+ }
49
+
50
+ start()
51
+ {
52
+ validateClient(this.client);
53
+
54
+ if (this.uiPath)
55
+ {
56
+ this.uiServer = express.Router();
57
+
58
+ if (this.uiPath.startsWith('http'))
59
+ {
60
+ this.uiServer.use(
61
+ createProxyMiddleware({
62
+ target: `${this.uiPath}/ui`,
63
+ changeOrigin: true,
64
+ }),
65
+ );
66
+ }
67
+ else
68
+ {
69
+ this.uiServer.use(express.static(this.uiPath));
70
+
71
+ // For SPA-Routing
72
+ this.uiServer.get('/*', (req, res) =>
73
+ {
74
+ res.sendFile(path.resolve(this.uiPath, 'index.html'));
75
+ });
76
+ }
77
+
78
+ this.express.use(this.uiPrefix, this.uiServer);
79
+ }
80
+
81
+ this.express.use(setupException(this));
82
+
83
+ return new Promise((resolve, reject) =>
84
+ {
85
+ this.express.listen(this.port, error =>
86
+ {
87
+ if (error)
88
+ {
89
+ reject(error);
90
+ return;
91
+ }
92
+
93
+ resolve(this);
94
+ });
95
+ });
96
+ }
97
+ };
98
+
99
+ function validateClient(client)
100
+ {
101
+ if (!client)
102
+ {
103
+ throw new Error('client config missing');
104
+ }
105
+
106
+ const missingProps = ['clientId', 'clientSecret', 'appIdentifier']
107
+ .filter(prop => !client[prop]);
108
+
109
+ if (missingProps.length)
110
+ {
111
+ throw new Error(`client config is missing: ${missingProps.join()}`);
112
+ }
113
+ }
114
+
115
+ module.exports = VarioCloudApp;
@@ -0,0 +1,43 @@
1
+ class AccessToken
2
+ {
3
+ #cache = {};
4
+
5
+ async init()
6
+ {
7
+ return Promise.resolve();
8
+ }
9
+
10
+ async get(tenant)
11
+ {
12
+ const tokenData = this.#cache[tenant];
13
+
14
+ if (!tokenData)
15
+ {
16
+ return null;
17
+ }
18
+
19
+ if (Date.now() >= tokenData.expiresAt)
20
+ {
21
+ await this.delete(tenant);
22
+
23
+ return null;
24
+ }
25
+
26
+ return tokenData.accessToken;
27
+ }
28
+
29
+ async set(tenant, accessToken, expiresAt)
30
+ {
31
+ this.#cache[tenant] = {
32
+ accessToken,
33
+ expiresAt,
34
+ };
35
+ }
36
+
37
+ async delete(tenant)
38
+ {
39
+ delete this.#cache[tenant];
40
+ }
41
+ }
42
+
43
+ module.exports = AccessToken;
@@ -0,0 +1,53 @@
1
+ const { getTenant } = require('#backend/utils/context.js');
2
+ const { validateOfflineToken } = require('#backend/utils/token.js');
3
+
4
+ class BaseUrlCache
5
+ {
6
+ #cache = {};
7
+
8
+ constructor(app)
9
+ {
10
+ this.app = app;
11
+ }
12
+
13
+ async init()
14
+ {
15
+ return Promise.resolve();
16
+ }
17
+
18
+ async get()
19
+ {
20
+ const tenant = getTenant();
21
+
22
+ if (this.#cache[tenant])
23
+ {
24
+ return this.#cache[tenant];
25
+ }
26
+
27
+ const offlineToken = await this.app.offlineToken.get(tenant);
28
+ const { iss } = await validateOfflineToken(offlineToken);
29
+
30
+ const domain = iss.replace('https://sso.', '').split('/')[0];
31
+ const baseUrl = `https://${tenant}.${domain}`;
32
+
33
+ await this.set(baseUrl);
34
+
35
+ return baseUrl;
36
+ }
37
+
38
+ async set(value)
39
+ {
40
+ const tenant = getTenant();
41
+
42
+ this.#cache[tenant] = value;
43
+ }
44
+
45
+ async delete()
46
+ {
47
+ const tenant = getTenant();
48
+
49
+ delete this.#cache[tenant];
50
+ }
51
+ }
52
+
53
+ module.exports = BaseUrlCache;
@@ -0,0 +1,44 @@
1
+ class OfflineToken
2
+ {
3
+ constructor(app, filename = 'offlineToken.db')
4
+ {
5
+ this.app = app;
6
+ this.filename = filename;
7
+ this.database = {};
8
+ }
9
+
10
+ async init()
11
+ {
12
+ // eslint-disable-next-line v-custom-rules/no-await-on-import
13
+ const { JSONFilePreset } = await import('lowdb/node');
14
+
15
+ this.database = await JSONFilePreset(this.filename, {});
16
+ }
17
+
18
+ async get(tenant)
19
+ {
20
+ return this.database.data[tenant];
21
+ }
22
+
23
+ async set(tenant, offlineToken)
24
+ {
25
+ this.app.accessToken.delete(tenant);
26
+
27
+ return this.database.update(tokens =>
28
+ {
29
+ tokens[tenant] = offlineToken;
30
+ });
31
+ }
32
+
33
+ async delete(tenant)
34
+ {
35
+ this.app.accessToken.delete(tenant);
36
+
37
+ return this.database.update(tokens =>
38
+ {
39
+ delete tokens[tenant];
40
+ });
41
+ }
42
+ }
43
+
44
+ module.exports = OfflineToken;
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@vario-software/vario-app-framework-backend",
3
+ "version": "2025.37.0",
4
+ "repository": "https://github.com/vario-software/vario-app-framework",
5
+ "author": "VARIO Software AG",
6
+ "homepage": "https://www.vario.ag",
7
+ "description": "The VARIO App Framework makes it easy and quick to set up a VARIO Cloud App in Node.js by handling API communication, authentication and providing useful built-in utilities.",
8
+ "license": "MIT",
9
+ "publishConfig": { "access": "public" },
10
+ "dependencies": {
11
+ "body-parser": "2.2.0",
12
+ "cors": "^2.8.5",
13
+ "express": "4.21.2",
14
+ "follow-redirects": "1.15.11",
15
+ "http-proxy-middleware": "3.0.5",
16
+ "jose": "6.1.0",
17
+ "lodash": "4.17.21",
18
+ "lowdb": "^7.0.1"
19
+ },
20
+ "imports": {
21
+ "#backend/*": "./*"
22
+ }
23
+ }
@@ -0,0 +1,45 @@
1
+ const { getContext, getRequestId } = require('#backend/utils/context.js');
2
+ const { validateAppToken } = require('#backend/utils/token.js');
3
+
4
+ function appAuthentication(req, res, next)
5
+ {
6
+ const authorizationHeader = req.get('Authorization');
7
+
8
+ // Read appToken from Authorization-Header
9
+ let token = authorizationHeader?.replace('Bearer ', '');
10
+
11
+ if (['/api/install', '/api/uninstall'].includes(req.path))
12
+ {
13
+ /* if the app has no ui we need to
14
+ extract the appToken from the query */
15
+ if (req.method === 'GET')
16
+ {
17
+ token = req.query.appToken;
18
+ }
19
+ }
20
+
21
+ validateAppToken(token)
22
+ .then(accessToken =>
23
+ {
24
+ const context = getContext();
25
+
26
+ context.appToken = token;
27
+ context.accessToken = accessToken;
28
+
29
+ next();
30
+ })
31
+ .catch(error =>
32
+ {
33
+ console.log({
34
+ message: 'Auth failed',
35
+ requestId: getRequestId(),
36
+ token,
37
+ error,
38
+ requestPath: req.path,
39
+ });
40
+
41
+ res.status(401).end();
42
+ });
43
+ }
44
+
45
+ module.exports = appAuthentication;