@heswell/vuu-portal 0.0.0-stage → 2.0.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.
- package/195.js +448 -0
- package/README.md +105 -2
- package/bin.js +27 -0
- package/index.js +4 -0
- package/main.js +9 -0
- package/package.json +54 -4
- package/src/ModuleRegistry.d.ts +8 -0
- package/src/PortalApplication.d.ts +21 -0
- package/src/bin.d.ts +2 -0
- package/src/index.d.ts +5 -0
- package/src/main.d.ts +8 -0
- package/src/modules/ModuleDiscovery/ModuleAdminService.d.ts +16 -0
- package/src/modules/ModuleDiscovery/ModuleDiscoveryModule.d.ts +8 -0
- package/src/modules/ModuleDiscovery/ModuleDiscoveryProvider.d.ts +7 -0
- package/src/modules/ModuleDiscovery/ModuleDiscoveryService.d.ts +1 -0
- package/src/modules/ModuleDiscovery/ModuleDiscoveryTableDefs.d.ts +3 -0
- package/src/modules/ModuleDiscovery/ModuleStore.d.ts +66 -0
package/195.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { ConfigFactory, EditSessionRpcHandler, ModuleFactory, Provider, TableDef, VUU_DEFAULT_COLUMNS, ViewPortDef, createConfiguredAuthProviders, createVuuServerApplication } from "@heswell/vuu-server";
|
|
2
|
+
import { MODULE_ADMIN_RPC, executeModuleAdminRpc, managedModuleColumnValues, managedModulePermissionValues, validateModuleConfig } from "@heswell/module-admin";
|
|
3
|
+
import node_fs from "node:fs";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
function moduleClientIdentifier(name) {
|
|
6
|
+
const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
7
|
+
return kebab.startsWith("vuu-") ? kebab : `vuu-${kebab}`;
|
|
8
|
+
}
|
|
9
|
+
function createModuleRegistry(tableContainer, user) {
|
|
10
|
+
return {
|
|
11
|
+
modules: selectModules(readModules(tableContainer.getTable("modules")), readModulePermissions(tableContainer.getTable("modulePermissions")), user.authorizations)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function readModules(table) {
|
|
15
|
+
return table.rows.map((row)=>{
|
|
16
|
+
const name = stringValue(table, row, "name");
|
|
17
|
+
const parentModuleId = numberValue(table, row, "parentModuleId");
|
|
18
|
+
return {
|
|
19
|
+
clientIdentifier: moduleClientIdentifier(name),
|
|
20
|
+
id: numberValue(table, row, "id"),
|
|
21
|
+
parentModuleId,
|
|
22
|
+
accessRole: "",
|
|
23
|
+
name,
|
|
24
|
+
title: stringValue(table, row, "title"),
|
|
25
|
+
description: stringValue(table, row, "description"),
|
|
26
|
+
version: numberValue(table, row, "version"),
|
|
27
|
+
enabled: booleanValue(table, row, "enabled"),
|
|
28
|
+
navLocation: 0 === parentModuleId ? stringValue(table, row, "location") : "",
|
|
29
|
+
path: stringValue(table, row, "path"),
|
|
30
|
+
mfComponent: stringValue(table, row, "mfComponent"),
|
|
31
|
+
mfScope: stringValue(table, row, "mfScope"),
|
|
32
|
+
mfUrl: stringValue(table, row, "mfUrl"),
|
|
33
|
+
...navIcon(table, row)
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function navIcon(table, row) {
|
|
38
|
+
const navIconUrl = row[columnIndex(table, "navIconUrl")];
|
|
39
|
+
return "string" == typeof navIconUrl && navIconUrl ? {
|
|
40
|
+
navIconUrl
|
|
41
|
+
} : {};
|
|
42
|
+
}
|
|
43
|
+
function readModulePermissions(table) {
|
|
44
|
+
return table.rows.map((row)=>({
|
|
45
|
+
moduleId: numberValue(table, row, "module_id"),
|
|
46
|
+
role: stringValue(table, row, "role")
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
function selectModules(modules, modulePermissions, authorizations) {
|
|
50
|
+
const moduleRoles = new Map();
|
|
51
|
+
const permittedModuleRoles = new Map();
|
|
52
|
+
const roles = new Set(authorizations);
|
|
53
|
+
modulePermissions.forEach(({ moduleId, role })=>{
|
|
54
|
+
moduleRoles.set(moduleId, role);
|
|
55
|
+
if (roles.has(role)) permittedModuleRoles.set(moduleId, role);
|
|
56
|
+
});
|
|
57
|
+
const latestByName = new Map();
|
|
58
|
+
modules.forEach((module)=>{
|
|
59
|
+
const accessRole = accessRoleForModule(module, moduleRoles, permittedModuleRoles);
|
|
60
|
+
if (!module.enabled || !accessRole) return;
|
|
61
|
+
const moduleWithAccess = {
|
|
62
|
+
...module,
|
|
63
|
+
accessRole
|
|
64
|
+
};
|
|
65
|
+
const current = latestByName.get(module.name);
|
|
66
|
+
if (!current || module.version > current.version || module.version === current.version && module.id > current.id) latestByName.set(module.name, moduleWithAccess);
|
|
67
|
+
});
|
|
68
|
+
const permittedModules = [
|
|
69
|
+
...latestByName.values()
|
|
70
|
+
];
|
|
71
|
+
const permittedIds = new Set(permittedModules.map(({ id })=>id));
|
|
72
|
+
return permittedModules.filter(({ parentModuleId })=>0 === parentModuleId || permittedIds.has(parentModuleId)).map(({ parentModuleId: _parentModuleId, ...module })=>module).sort((left, right)=>left.name.localeCompare(right.name));
|
|
73
|
+
}
|
|
74
|
+
function accessRoleForModule(module, moduleRoles, permittedModuleRoles) {
|
|
75
|
+
const ownRole = permittedModuleRoles.get(module.id);
|
|
76
|
+
if (ownRole) return ownRole;
|
|
77
|
+
if (0 !== module.parentModuleId && !moduleRoles.has(module.id)) return permittedModuleRoles.get(module.parentModuleId) ?? "";
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
function stringValue(table, row, column) {
|
|
81
|
+
const value = row[columnIndex(table, column)];
|
|
82
|
+
if ("string" != typeof value) throw new Error(`Expected ${table.name}.${column} to be a string`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function numberValue(table, row, column) {
|
|
86
|
+
const value = row[columnIndex(table, column)];
|
|
87
|
+
if ("number" != typeof value) throw new Error(`Expected ${table.name}.${column} to be a number`);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function booleanValue(table, row, column) {
|
|
91
|
+
const value = row[columnIndex(table, column)];
|
|
92
|
+
if ("boolean" != typeof value) throw new Error(`Expected ${table.name}.${column} to be a boolean`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function columnIndex(table, column) {
|
|
96
|
+
const index = table.tableDef.columns.findIndex(({ name })=>name === column);
|
|
97
|
+
if (-1 === index) throw new Error(`Table ${table.name} does not contain ${column}`);
|
|
98
|
+
return index;
|
|
99
|
+
}
|
|
100
|
+
const modulesTable = TableDef({
|
|
101
|
+
columns: [
|
|
102
|
+
{
|
|
103
|
+
name: "id",
|
|
104
|
+
dataType: "int"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "parentModuleId",
|
|
108
|
+
dataType: "int"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "name",
|
|
112
|
+
dataType: "string"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "title",
|
|
116
|
+
dataType: "string"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "description",
|
|
120
|
+
dataType: "string"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "version",
|
|
124
|
+
dataType: "int"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "enabled",
|
|
128
|
+
dataType: "boolean"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "location",
|
|
132
|
+
dataType: "string"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "path",
|
|
136
|
+
dataType: "string"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "mfComponent",
|
|
140
|
+
dataType: "string"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "mfScope",
|
|
144
|
+
dataType: "string"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "mfUrl",
|
|
148
|
+
dataType: "string"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "navIconUrl",
|
|
152
|
+
dataType: "string"
|
|
153
|
+
},
|
|
154
|
+
...VUU_DEFAULT_COLUMNS
|
|
155
|
+
],
|
|
156
|
+
keyField: "id",
|
|
157
|
+
name: "modules"
|
|
158
|
+
});
|
|
159
|
+
const modulePermissionsTable = TableDef({
|
|
160
|
+
columns: [
|
|
161
|
+
{
|
|
162
|
+
name: "id",
|
|
163
|
+
dataType: "int"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "module_id",
|
|
167
|
+
dataType: "int"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "role",
|
|
171
|
+
dataType: "string"
|
|
172
|
+
},
|
|
173
|
+
...VUU_DEFAULT_COLUMNS
|
|
174
|
+
],
|
|
175
|
+
keyField: "id",
|
|
176
|
+
name: "modulePermissions"
|
|
177
|
+
});
|
|
178
|
+
class ModuleDiscoveryProvider extends Provider {
|
|
179
|
+
rows;
|
|
180
|
+
constructor(table, rows){
|
|
181
|
+
super(table), this.rows = rows;
|
|
182
|
+
}
|
|
183
|
+
async load() {
|
|
184
|
+
for (const row of this.rows)this.table.insert(row, false);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const success = (data)=>({
|
|
188
|
+
type: "SUCCESS_RESULT",
|
|
189
|
+
data
|
|
190
|
+
});
|
|
191
|
+
const failure = (errorMessage)=>({
|
|
192
|
+
type: "ERROR_RESULT",
|
|
193
|
+
errorMessage
|
|
194
|
+
});
|
|
195
|
+
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
196
|
+
const DEFAULT_MODULE_ADMIN_ROLE = "module-admin-access";
|
|
197
|
+
class ModuleAdminService extends EditSessionRpcHandler {
|
|
198
|
+
state;
|
|
199
|
+
adminRole;
|
|
200
|
+
now;
|
|
201
|
+
constructor(tableContainer, state, { adminRole = DEFAULT_MODULE_ADMIN_ROLE, now = Date.now } = {}){
|
|
202
|
+
super(tableContainer), this.state = state;
|
|
203
|
+
this.adminRole = adminRole;
|
|
204
|
+
this.now = now;
|
|
205
|
+
this.registerRpc(MODULE_ADMIN_RPC.createModule, this.handleModuleAdminRpc("createModule"));
|
|
206
|
+
this.registerRpc(MODULE_ADMIN_RPC.updateModule, this.handleModuleAdminRpc("updateModule"));
|
|
207
|
+
this.registerRpc(MODULE_ADMIN_RPC.setModuleEnabled, this.handleModuleAdminRpc("setModuleEnabled"));
|
|
208
|
+
this.registerRpc(MODULE_ADMIN_RPC.deleteModule, this.handleModuleAdminRpc("deleteModule"));
|
|
209
|
+
}
|
|
210
|
+
handleModuleAdminRpc(rpcName) {
|
|
211
|
+
return async ({ ctx, namedParams })=>{
|
|
212
|
+
if (!ctx?.user?.authorizations?.includes(this.adminRole)) return failure(`Administering modules requires the ${this.adminRole} role`);
|
|
213
|
+
try {
|
|
214
|
+
const { modules, result } = executeModuleAdminRpc(this.state.modules, rpcName, namedParams, this.now);
|
|
215
|
+
this.state.replaceAndSave(modules);
|
|
216
|
+
this.syncTables(modules);
|
|
217
|
+
return success(result);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return failure(toErrorMessage(error));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
syncTables(modules) {
|
|
224
|
+
syncTable(this.tableContainer.getTable("modules"), modules.map(managedModuleColumnValues));
|
|
225
|
+
syncTable(this.tableContainer.getTable("modulePermissions"), managedModulePermissionValues(modules));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function syncTable(table, values) {
|
|
229
|
+
const expectedKeys = new Set(values.map((row)=>String(row[table.schema.key])));
|
|
230
|
+
for (const row of [
|
|
231
|
+
...table.rows
|
|
232
|
+
]){
|
|
233
|
+
const key = String(row[table.indexOfKeyField]);
|
|
234
|
+
if (!expectedKeys.has(key)) table.delete(key);
|
|
235
|
+
}
|
|
236
|
+
for (const rowValues of values){
|
|
237
|
+
const row = table.tableDef.columns.map(({ name })=>rowValues[name] ?? "");
|
|
238
|
+
const key = String(row[table.indexOfKeyField]);
|
|
239
|
+
const existing = table.getRowAtKey(key, false);
|
|
240
|
+
if (existing) {
|
|
241
|
+
row[table.columnMap.vuuCreatedTimestamp] = existing[table.columnMap.vuuCreatedTimestamp];
|
|
242
|
+
table.update(table.rowIndexAtKey(key), row);
|
|
243
|
+
} else table.insert(row);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const yaml = ()=>{
|
|
247
|
+
if ("u" < typeof Bun || !Bun.YAML) throw new Error("@heswell/vuu-portal module files require the Bun runtime");
|
|
248
|
+
return Bun.YAML;
|
|
249
|
+
};
|
|
250
|
+
class InMemoryModuleStore {
|
|
251
|
+
modules;
|
|
252
|
+
constructor(modules = []){
|
|
253
|
+
this.modules = modules;
|
|
254
|
+
}
|
|
255
|
+
load() {
|
|
256
|
+
return cloneModules(this.modules);
|
|
257
|
+
}
|
|
258
|
+
save(modules) {
|
|
259
|
+
this.modules = cloneModules(modules);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
class ModuleState {
|
|
263
|
+
store;
|
|
264
|
+
#modules;
|
|
265
|
+
constructor(store, modules){
|
|
266
|
+
this.store = store;
|
|
267
|
+
this.#modules = cloneModules(modules);
|
|
268
|
+
}
|
|
269
|
+
get modules() {
|
|
270
|
+
return cloneModules(this.#modules);
|
|
271
|
+
}
|
|
272
|
+
replaceAndSave(modules) {
|
|
273
|
+
const next = cloneModules(modules);
|
|
274
|
+
this.store.save(next);
|
|
275
|
+
this.#modules = next;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
class YamlModuleStore {
|
|
279
|
+
filePath;
|
|
280
|
+
seedModules;
|
|
281
|
+
constructor(filePath, seedModules){
|
|
282
|
+
this.filePath = filePath;
|
|
283
|
+
this.seedModules = seedModules;
|
|
284
|
+
}
|
|
285
|
+
load() {
|
|
286
|
+
if (!node_fs.existsSync(this.filePath)) {
|
|
287
|
+
const seeded = cloneModules(this.seedModules());
|
|
288
|
+
assertValidManagedModules(seeded, this.filePath);
|
|
289
|
+
this.save(seeded);
|
|
290
|
+
return seeded;
|
|
291
|
+
}
|
|
292
|
+
const modules = readModulesFile(this.filePath).map((value, index)=>parseManagedModule(value, index));
|
|
293
|
+
assertValidManagedModules(modules, this.filePath);
|
|
294
|
+
return modules;
|
|
295
|
+
}
|
|
296
|
+
save(modules) {
|
|
297
|
+
const directory = node_path.dirname(this.filePath);
|
|
298
|
+
node_fs.mkdirSync(directory, {
|
|
299
|
+
recursive: true
|
|
300
|
+
});
|
|
301
|
+
const tempPath = node_path.join(directory, `.${node_path.basename(this.filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
302
|
+
const contents = yaml().stringify({
|
|
303
|
+
modules: cloneModules(modules)
|
|
304
|
+
}, null, 2);
|
|
305
|
+
try {
|
|
306
|
+
node_fs.writeFileSync(tempPath, contents, "utf8");
|
|
307
|
+
node_fs.renameSync(tempPath, this.filePath);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
try {
|
|
310
|
+
if (node_fs.existsSync(tempPath)) node_fs.unlinkSync(tempPath);
|
|
311
|
+
} catch {}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function createModuleState(store) {
|
|
317
|
+
return new ModuleState(store, store.load());
|
|
318
|
+
}
|
|
319
|
+
function loadDefaultModules(filePath, timestamp) {
|
|
320
|
+
const modules = readModulesFile(filePath).map((value, index)=>parseManagedModule(isRecord(value) ? {
|
|
321
|
+
...value,
|
|
322
|
+
created: timestamp,
|
|
323
|
+
updated: timestamp
|
|
324
|
+
} : value, index));
|
|
325
|
+
assertValidManagedModules(modules, filePath);
|
|
326
|
+
return modules;
|
|
327
|
+
}
|
|
328
|
+
function readModulesFile(filePath) {
|
|
329
|
+
const parsed = yaml().parse(node_fs.readFileSync(filePath, "utf8"));
|
|
330
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.modules)) throw new Error(`Modules file '${filePath}' must contain a 'modules' list`);
|
|
331
|
+
return parsed.modules;
|
|
332
|
+
}
|
|
333
|
+
function cloneModules(modules) {
|
|
334
|
+
return modules.map((module)=>({
|
|
335
|
+
...module
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
const managedModuleKeys = [
|
|
339
|
+
"id",
|
|
340
|
+
"version",
|
|
341
|
+
"created",
|
|
342
|
+
"updated",
|
|
343
|
+
"parentModuleId",
|
|
344
|
+
"name",
|
|
345
|
+
"title",
|
|
346
|
+
"description",
|
|
347
|
+
"enabled",
|
|
348
|
+
"location",
|
|
349
|
+
"path",
|
|
350
|
+
"mfComponent",
|
|
351
|
+
"mfScope",
|
|
352
|
+
"mfUrl",
|
|
353
|
+
"navIconUrl",
|
|
354
|
+
"accessRole"
|
|
355
|
+
];
|
|
356
|
+
const retiredModuleKeys = [
|
|
357
|
+
"vuuConnectionId",
|
|
358
|
+
"vuuWebsocketUrl",
|
|
359
|
+
"vuuRestUrl"
|
|
360
|
+
];
|
|
361
|
+
function parseManagedModule(value, index) {
|
|
362
|
+
if (!isRecord(value)) throw new Error(`Modules file entry ${index} must be an object`);
|
|
363
|
+
for (const key of Object.keys(value))if (!managedModuleKeys.includes(key) && !retiredModuleKeys.includes(key)) throw new Error(`Modules file entry ${index} contains unknown field '${key}'`);
|
|
364
|
+
const module = {};
|
|
365
|
+
for (const key of managedModuleKeys){
|
|
366
|
+
const fieldValue = value[key];
|
|
367
|
+
if (void 0 === fieldValue) throw new Error(`Modules file entry ${index} is missing '${key}'`);
|
|
368
|
+
const expected = [
|
|
369
|
+
"id",
|
|
370
|
+
"version",
|
|
371
|
+
"created",
|
|
372
|
+
"updated",
|
|
373
|
+
"parentModuleId"
|
|
374
|
+
].includes(key) ? "number" : "enabled" === key ? "boolean" : "string";
|
|
375
|
+
if (typeof fieldValue !== expected) throw new Error(`Modules file entry ${index} field '${key}' must be a ${expected}`);
|
|
376
|
+
if ("number" === expected && !Number.isInteger(fieldValue)) throw new Error(`Modules file entry ${index} field '${key}' must be an integer`);
|
|
377
|
+
module[key] = fieldValue;
|
|
378
|
+
}
|
|
379
|
+
return module;
|
|
380
|
+
}
|
|
381
|
+
function assertValidManagedModules(modules, filePath) {
|
|
382
|
+
const ids = new Set();
|
|
383
|
+
modules.forEach((module, index)=>{
|
|
384
|
+
if (ids.has(module.id)) throw new Error(`Modules file '${filePath}' contains duplicate id ${module.id}`);
|
|
385
|
+
ids.add(module.id);
|
|
386
|
+
const errors = validateModuleConfig(module, modules, module.id);
|
|
387
|
+
const message = formatValidationErrors(errors);
|
|
388
|
+
if (message) throw new Error(`Modules file '${filePath}' entry ${index} is invalid: ${message}`);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
function formatValidationErrors(errors) {
|
|
392
|
+
return Object.entries(errors).filter(([, message])=>message).map(([field, message])=>`${field}: ${message}`).join("; ");
|
|
393
|
+
}
|
|
394
|
+
function isRecord(value) {
|
|
395
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
396
|
+
}
|
|
397
|
+
function loadModuleState(config) {
|
|
398
|
+
const filePath = config.getPath("vuu.portal.modulesFile", "modules.yaml");
|
|
399
|
+
const defaultsPath = config.getPath("vuu.portal.defaultModulesFile", "default-modules.yaml");
|
|
400
|
+
return createModuleState(new YamlModuleStore(filePath, ()=>loadDefaultModules(defaultsPath, Date.now())));
|
|
401
|
+
}
|
|
402
|
+
function loadModuleAdminRole(config) {
|
|
403
|
+
const role = config.getString("vuu.portal.moduleAdminRole", DEFAULT_MODULE_ADMIN_ROLE).trim();
|
|
404
|
+
if (!role) throw new Error("vuu.portal.moduleAdminRole must not be empty");
|
|
405
|
+
return role;
|
|
406
|
+
}
|
|
407
|
+
const ModuleDiscoveryModule = (state, { adminRole = DEFAULT_MODULE_ADMIN_ROLE } = {})=>{
|
|
408
|
+
const modules = state.modules;
|
|
409
|
+
const moduleRows = modules.map((module)=>valuesToRow(modulesTable, managedModuleColumnValues(module)));
|
|
410
|
+
const permissionRows = managedModulePermissionValues(modules).map((values)=>valuesToRow(modulePermissionsTable, values));
|
|
411
|
+
let service;
|
|
412
|
+
const getService = (tableContainer)=>service ??= new ModuleAdminService(tableContainer, state, {
|
|
413
|
+
adminRole
|
|
414
|
+
});
|
|
415
|
+
return ModuleFactory.withNameSpace("MODULE_DISCOVERY").addTable(modulesTable, (table)=>new ModuleDiscoveryProvider(table, moduleRows), (table, _provider, _providerContainer, tableContainer)=>ViewPortDef(columnsFor(table), getService(tableContainer))).addTable(modulePermissionsTable, (table)=>new ModuleDiscoveryProvider(table, permissionRows), (table, _provider, _providerContainer, tableContainer)=>ViewPortDef(columnsFor(table), getService(tableContainer))).asModule();
|
|
416
|
+
};
|
|
417
|
+
function columnsFor(table) {
|
|
418
|
+
return table.schema.columns.map(({ name, serverDataType: dataType }, index)=>({
|
|
419
|
+
name,
|
|
420
|
+
dataType,
|
|
421
|
+
index
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
function valuesToRow(table, values) {
|
|
425
|
+
return table.columns.map(({ name })=>values[name] ?? "");
|
|
426
|
+
}
|
|
427
|
+
const PORTAL_DEFAULT_HTTPS_PORT = 8443;
|
|
428
|
+
const PORTAL_DEFAULT_WEBSOCKET_PATH = "/websocket-portal";
|
|
429
|
+
const PORTAL_DEFAULT_WEBSOCKET_PORT = 8091;
|
|
430
|
+
function createPortalApplication({ config, configFile } = {}) {
|
|
431
|
+
const portalConfig = config ?? ConfigFactory.load(configFile);
|
|
432
|
+
return createVuuServerApplication({
|
|
433
|
+
authProviders: createConfiguredAuthProviders(portalConfig),
|
|
434
|
+
config: portalConfig,
|
|
435
|
+
defaultHttpsPort: PORTAL_DEFAULT_HTTPS_PORT,
|
|
436
|
+
defaultWebSocketPath: PORTAL_DEFAULT_WEBSOCKET_PATH,
|
|
437
|
+
defaultWebSocketPort: PORTAL_DEFAULT_WEBSOCKET_PORT,
|
|
438
|
+
loginSuccessProvider: (user, tableContainer)=>({
|
|
439
|
+
moduleRegistry: createModuleRegistry(tableContainer, user)
|
|
440
|
+
}),
|
|
441
|
+
modules: [
|
|
442
|
+
ModuleDiscoveryModule(loadModuleState(portalConfig), {
|
|
443
|
+
adminRole: loadModuleAdminRole(portalConfig)
|
|
444
|
+
})
|
|
445
|
+
]
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
export { DEFAULT_MODULE_ADMIN_ROLE, InMemoryModuleStore, ModuleDiscoveryModule, ModuleState, PORTAL_DEFAULT_WEBSOCKET_PATH, YamlModuleStore, createModuleRegistry, createModuleState, createPortalApplication, loadDefaultModules, loadModuleAdminRole, loadModuleState, moduleClientIdentifier };
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @heswell/vuu-portal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vuu portal server: a [`@heswell/vuu-server`](../vuu-server) application that
|
|
4
|
+
hosts the `MODULE_DISCOVERY` module and returns each user's module registry in
|
|
5
|
+
the login response. Requires the [Bun](https://bun.sh) runtime to run the
|
|
6
|
+
server. The library exports can be imported under Node.js too.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @heswell/vuu-portal
|
|
10
|
+
# TypeScript consumers: the vuu-server declarations reference Bun types
|
|
11
|
+
npm install -D @types/bun
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Entry points
|
|
15
|
+
|
|
16
|
+
| Import / command | Purpose |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `@heswell/vuu-portal` | Library API. Importing it has no side effects. |
|
|
19
|
+
| `@heswell/vuu-portal/main` | `main(options?)`: creates and starts the portal. Importing it has no side effects. |
|
|
20
|
+
| `vuu-portal` bin | Starts the portal from a shell. |
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
createModuleRegistry,
|
|
25
|
+
createPortalApplication,
|
|
26
|
+
loadDefaultModules,
|
|
27
|
+
loadModuleAdminRole,
|
|
28
|
+
loadModuleState,
|
|
29
|
+
ModuleDiscoveryModule,
|
|
30
|
+
} from "@heswell/vuu-portal";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Root exports:
|
|
34
|
+
|
|
35
|
+
- `createPortalApplication({ config?, configFile? })` creates the portal
|
|
36
|
+
application without starting it.
|
|
37
|
+
- `createModuleRegistry` and `moduleClientIdentifier`.
|
|
38
|
+
- `ModuleDiscoveryModule`, `loadModuleState`, `loadModuleAdminRole`.
|
|
39
|
+
- Module catalogue stores: `ModuleState`, `createModuleState`,
|
|
40
|
+
`YamlModuleStore`, `InMemoryModuleStore`, `loadDefaultModules` and the
|
|
41
|
+
`ModuleStore` type.
|
|
42
|
+
- `DEFAULT_MODULE_ADMIN_ROLE`, plus the `PORTAL_DEFAULT_*` fallback ports and
|
|
43
|
+
path.
|
|
44
|
+
|
|
45
|
+
## Starting the portal
|
|
46
|
+
|
|
47
|
+
Everything is configured in `application.conf`. Point the portal at your own
|
|
48
|
+
conf and run the stock entry unchanged:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
# bin
|
|
52
|
+
bunx vuu-portal --config ./path/to/application.conf
|
|
53
|
+
VUU_CONFIG_FILE=./path/to/application.conf bunx vuu-portal
|
|
54
|
+
|
|
55
|
+
# from code
|
|
56
|
+
import { main } from "@heswell/vuu-portal/main";
|
|
57
|
+
await main({ configFile: "./path/to/application.conf" });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The conf file is located in this order:
|
|
61
|
+
|
|
62
|
+
1. `--config <path>` (bin) or `main({ configFile })`. Relative paths resolve
|
|
63
|
+
against the current directory.
|
|
64
|
+
2. `$VUU_CONFIG_FILE`.
|
|
65
|
+
3. `$VUU_APP`, which selects `packages/$VUU_APP/application.conf` under the
|
|
66
|
+
current directory.
|
|
67
|
+
4. `./application.conf`.
|
|
68
|
+
|
|
69
|
+
In this repository, `npm run start:portal` starts it with
|
|
70
|
+
`VUU_APP=vuu-portal`, which uses [`application.conf`](./application.conf).
|
|
71
|
+
|
|
72
|
+
### Settings
|
|
73
|
+
|
|
74
|
+
Any setting can be overridden from the environment with a HOCON-style
|
|
75
|
+
`${?VAR}` line in the conf, and the portal's own
|
|
76
|
+
[`application.conf`](./application.conf) already does this for the ports,
|
|
77
|
+
websocket path, auth mode, CORS origin, Keycloak URL and realm, and module
|
|
78
|
+
files (`VUU_HTTPS_PORT`, `VUU_WEBSOCKET_PORT`, `VUU_WEBSOCKET_PATH`,
|
|
79
|
+
`VUU_AUTH_MODE`, `VUU_CORS_ALLOWED_ORIGIN`, `VUU_KEYCLOAK_URL`,
|
|
80
|
+
`VUU_KEYCLOAK_REALM`, `VUU_PORTAL_MODULES_FILE`,
|
|
81
|
+
`VUU_PORTAL_DEFAULT_MODULES_FILE`). See the
|
|
82
|
+
[`@heswell/service-utils` README](../service-utils/README.md#environment-variable-substitution)
|
|
83
|
+
for the syntax.
|
|
84
|
+
|
|
85
|
+
| Key | Fallback when not set |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `vuu.https.port` | `8443` |
|
|
88
|
+
| `vuu.websocket.port` | `8091` |
|
|
89
|
+
| `vuu.websocket.path` | `/websocket-portal` |
|
|
90
|
+
| `vuu.ssl`, `vuu.certPath`, `vuu.keyPath` | SSL disabled |
|
|
91
|
+
| `vuu.auth.*`, `vuu.keycloak.*` | See [`@heswell/vuu-server`](../vuu-server/README.md) |
|
|
92
|
+
| `vuu.portal.defaultModulesFile` | `default-modules.yaml` |
|
|
93
|
+
| `vuu.portal.modulesFile` | `modules.yaml` |
|
|
94
|
+
| `vuu.portal.moduleAdminRole` | `module-admin-access` |
|
|
95
|
+
|
|
96
|
+
### Module catalogue
|
|
97
|
+
|
|
98
|
+
The portal's modules are defined in YAML, not in code. Relative file paths
|
|
99
|
+
are resolved against the directory that contains `application.conf`;
|
|
100
|
+
absolute paths and `~/` paths are also accepted.
|
|
101
|
+
|
|
102
|
+
- `vuu.portal.modulesFile` is the runtime state, edited through the module
|
|
103
|
+
admin. If it does not exist, it is created from the default modules file on
|
|
104
|
+
start.
|
|
105
|
+
- `vuu.portal.defaultModulesFile` is the seed catalogue. See
|
|
106
|
+
[`default-modules.yaml`](./default-modules.yaml) for the format.
|
package/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { main } from "./main.js";
|
|
3
|
+
const USAGE = `Usage: vuu-portal [--config <path/to/application.conf>]
|
|
4
|
+
|
|
5
|
+
Starts the Vuu portal server. All settings (ports, websocket path, auth,
|
|
6
|
+
module catalogue files, module admin role) come from application.conf.
|
|
7
|
+
|
|
8
|
+
Without --config the conf is located via $VUU_CONFIG_FILE, then $VUU_APP
|
|
9
|
+
(packages/<app>/application.conf), then ./application.conf.`;
|
|
10
|
+
const parseConfigFile = (args)=>{
|
|
11
|
+
let configFile;
|
|
12
|
+
for(let index = 0; index < args.length; index++){
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
if ("--help" === arg || "-h" === arg) {
|
|
15
|
+
console.log(USAGE);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
} else if ("--config" === arg || "-c" === arg) {
|
|
18
|
+
configFile = args[++index];
|
|
19
|
+
if (!configFile) throw new Error(`${arg} requires a path\n\n${USAGE}`);
|
|
20
|
+
} else if (arg.startsWith("--config=")) configFile = arg.slice(9);
|
|
21
|
+
else throw new Error(`Unknown argument '${arg}'\n\n${USAGE}`);
|
|
22
|
+
}
|
|
23
|
+
return configFile;
|
|
24
|
+
};
|
|
25
|
+
await main({
|
|
26
|
+
configFile: parseConfigFile(process.argv.slice(2))
|
|
27
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var src_PORTAL_DEFAULT_HTTPS_PORT = 8443;
|
|
2
|
+
var src_PORTAL_DEFAULT_WEBSOCKET_PORT = 8091;
|
|
3
|
+
export { DEFAULT_MODULE_ADMIN_ROLE, InMemoryModuleStore, ModuleDiscoveryModule, ModuleState, PORTAL_DEFAULT_WEBSOCKET_PATH, YamlModuleStore, createModuleRegistry, createModuleState, createPortalApplication, loadDefaultModules, loadModuleAdminRole, loadModuleState, moduleClientIdentifier } from "./195.js";
|
|
4
|
+
export { src_PORTAL_DEFAULT_HTTPS_PORT as PORTAL_DEFAULT_HTTPS_PORT, src_PORTAL_DEFAULT_WEBSOCKET_PORT as PORTAL_DEFAULT_WEBSOCKET_PORT };
|
package/main.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createPortalApplication } from "./195.js";
|
|
2
|
+
async function main(options) {
|
|
3
|
+
const application = createPortalApplication(options);
|
|
4
|
+
await application.start();
|
|
5
|
+
return application;
|
|
6
|
+
}
|
|
7
|
+
const src_main = main;
|
|
8
|
+
export default src_main;
|
|
9
|
+
export { main };
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heswell/vuu-portal",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "VUU portal server with remote module discovery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./main": {
|
|
15
|
+
"types": "./src/main.d.ts",
|
|
16
|
+
"import": "./main.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./bin.js"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"author": "heswell",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@heswell/module-admin": "2.0.1",
|
|
30
|
+
"@heswell/vuu-server": "3.1.0",
|
|
31
|
+
"@heswell/vuu-table": "1.0.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@vuu-ui/vuu-protocol-types": "4.0.0-alpha.20"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/heswell/vuu-websocket.git",
|
|
39
|
+
"directory": "packages/vuu-portal"
|
|
40
|
+
},
|
|
41
|
+
"typesVersions": {
|
|
42
|
+
"*": {
|
|
43
|
+
"main": [
|
|
44
|
+
"./src/main.d.ts"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"bin": {
|
|
49
|
+
"vuu-portal": "bin.js"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"*.js",
|
|
53
|
+
"src/**/*.d.ts",
|
|
54
|
+
"README.md"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModuleRegistry, TableContainer, VuuUser } from "@heswell/vuu-server";
|
|
2
|
+
/**
|
|
3
|
+
* Derives a module's own client identifier from its name, e.g.
|
|
4
|
+
* `userAdmin` -> `vuu-user-admin`. The UI keys each module's saved state by
|
|
5
|
+
* this value, so it must be unique per module.
|
|
6
|
+
*/
|
|
7
|
+
export declare function moduleClientIdentifier(name: string): string;
|
|
8
|
+
export declare function createModuleRegistry(tableContainer: TableContainer, user: VuuUser): ModuleRegistry;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Config, type VuuServerApplication } from "@heswell/vuu-server";
|
|
2
|
+
export declare const PORTAL_DEFAULT_HTTPS_PORT = 8443;
|
|
3
|
+
export declare const PORTAL_DEFAULT_WEBSOCKET_PATH = "/websocket-portal";
|
|
4
|
+
export declare const PORTAL_DEFAULT_WEBSOCKET_PORT = 8091;
|
|
5
|
+
export type PortalApplicationOptions = {
|
|
6
|
+
/** An already loaded config. Takes precedence over `configFile`. */
|
|
7
|
+
config?: Config;
|
|
8
|
+
/**
|
|
9
|
+
* Path to the portal's application.conf, resolved against the current
|
|
10
|
+
* directory. Defaults to ConfigFactory's lookup: $VUU_CONFIG_FILE, then
|
|
11
|
+
* $VUU_APP, then ./application.conf.
|
|
12
|
+
*/
|
|
13
|
+
configFile?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates (but does not start) the portal server, configured entirely by
|
|
17
|
+
* application.conf: ports, websocket path, auth, the module catalogue files
|
|
18
|
+
* (vuu.portal.modulesFile / vuu.portal.defaultModulesFile, resolved relative
|
|
19
|
+
* to the conf file) and the module admin role (vuu.portal.moduleAdminRole).
|
|
20
|
+
*/
|
|
21
|
+
export declare function createPortalApplication({ config, configFile, }?: PortalApplicationOptions): VuuServerApplication;
|
package/src/bin.d.ts
ADDED
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createModuleRegistry, moduleClientIdentifier, } from "./ModuleRegistry.js";
|
|
2
|
+
export { loadModuleAdminRole, loadModuleState, ModuleDiscoveryModule, ModuleState, } from "./modules/ModuleDiscovery/ModuleDiscoveryModule.js";
|
|
3
|
+
export { createModuleState, InMemoryModuleStore, loadDefaultModules, type ModuleStore, YamlModuleStore, } from "./modules/ModuleDiscovery/ModuleStore.js";
|
|
4
|
+
export { DEFAULT_MODULE_ADMIN_ROLE } from "./modules/ModuleDiscovery/ModuleAdminService.js";
|
|
5
|
+
export { createPortalApplication, PORTAL_DEFAULT_HTTPS_PORT, PORTAL_DEFAULT_WEBSOCKET_PATH, PORTAL_DEFAULT_WEBSOCKET_PORT, type PortalApplicationOptions, } from "./PortalApplication.js";
|
package/src/main.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PortalApplicationOptions } from "./PortalApplication.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates and starts the portal application from application.conf (see
|
|
4
|
+
* createPortalApplication). Importing this module has no side effects; use
|
|
5
|
+
* the `vuu-portal` bin to start the portal from a shell.
|
|
6
|
+
*/
|
|
7
|
+
export declare function main(options?: PortalApplicationOptions): Promise<import("@heswell/vuu-server").VuuServerApplication>;
|
|
8
|
+
export default main;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EditSessionRpcHandler, type TableContainer } from "@heswell/vuu-server";
|
|
2
|
+
import type { ModuleState } from "./ModuleStore.js";
|
|
3
|
+
export declare const DEFAULT_MODULE_ADMIN_ROLE = "module-admin-access";
|
|
4
|
+
export interface ModuleAdminServiceOptions {
|
|
5
|
+
/** Role a user must hold to create, update, enable/disable or delete modules. */
|
|
6
|
+
adminRole?: string;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
export declare class ModuleAdminService extends EditSessionRpcHandler {
|
|
10
|
+
private readonly state;
|
|
11
|
+
private readonly adminRole;
|
|
12
|
+
private readonly now;
|
|
13
|
+
constructor(tableContainer: TableContainer, state: ModuleState, { adminRole, now }?: ModuleAdminServiceOptions);
|
|
14
|
+
private handleModuleAdminRpc;
|
|
15
|
+
private syncTables;
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Config } from "@heswell/vuu-server";
|
|
2
|
+
import { ModuleState } from "./ModuleStore.js";
|
|
3
|
+
export { InMemoryModuleStore, ModuleState, YamlModuleStore, createModuleState, loadDefaultModules, } from "./ModuleStore.js";
|
|
4
|
+
export declare function loadModuleState(config: Pick<Config, "getPath">): ModuleState;
|
|
5
|
+
export declare function loadModuleAdminRole(config: Pick<Config, "getString">): string;
|
|
6
|
+
export declare const ModuleDiscoveryModule: (state: ModuleState, { adminRole }?: {
|
|
7
|
+
adminRole?: string;
|
|
8
|
+
}) => import("@heswell/vuu-server").Module;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Table } from "@heswell/vuu-table";
|
|
2
|
+
import { Provider } from "@heswell/vuu-server";
|
|
3
|
+
export declare class ModuleDiscoveryProvider extends Provider {
|
|
4
|
+
private readonly rows;
|
|
5
|
+
constructor(table: Table, rows: Array<Array<string | number | boolean>>);
|
|
6
|
+
load(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ModuleAdminService as ModuleDiscoveryService } from "./ModuleAdminService.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ManagedModule } from "@heswell/module-admin";
|
|
2
|
+
export interface ModuleStore {
|
|
3
|
+
load(): ManagedModule[];
|
|
4
|
+
save(modules: readonly ManagedModule[]): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class InMemoryModuleStore implements ModuleStore {
|
|
7
|
+
private modules;
|
|
8
|
+
constructor(modules?: readonly ManagedModule[]);
|
|
9
|
+
load(): {
|
|
10
|
+
parentModuleId: number;
|
|
11
|
+
name: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
location: string;
|
|
16
|
+
path: string;
|
|
17
|
+
mfComponent: string;
|
|
18
|
+
mfScope: string;
|
|
19
|
+
mfUrl: string;
|
|
20
|
+
navIconUrl: string;
|
|
21
|
+
accessRole: string;
|
|
22
|
+
id: number;
|
|
23
|
+
version: number;
|
|
24
|
+
created: number;
|
|
25
|
+
updated: number;
|
|
26
|
+
}[];
|
|
27
|
+
save(modules: readonly ManagedModule[]): void;
|
|
28
|
+
}
|
|
29
|
+
export declare class ModuleState {
|
|
30
|
+
#private;
|
|
31
|
+
private readonly store;
|
|
32
|
+
constructor(store: ModuleStore, modules: readonly ManagedModule[]);
|
|
33
|
+
get modules(): {
|
|
34
|
+
parentModuleId: number;
|
|
35
|
+
name: string;
|
|
36
|
+
title: string;
|
|
37
|
+
description: string;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
location: string;
|
|
40
|
+
path: string;
|
|
41
|
+
mfComponent: string;
|
|
42
|
+
mfScope: string;
|
|
43
|
+
mfUrl: string;
|
|
44
|
+
navIconUrl: string;
|
|
45
|
+
accessRole: string;
|
|
46
|
+
id: number;
|
|
47
|
+
version: number;
|
|
48
|
+
created: number;
|
|
49
|
+
updated: number;
|
|
50
|
+
}[];
|
|
51
|
+
replaceAndSave(modules: readonly ManagedModule[]): void;
|
|
52
|
+
}
|
|
53
|
+
export declare class YamlModuleStore implements ModuleStore {
|
|
54
|
+
private readonly filePath;
|
|
55
|
+
private readonly seedModules;
|
|
56
|
+
constructor(filePath: string, seedModules: () => readonly ManagedModule[]);
|
|
57
|
+
load(): ManagedModule[];
|
|
58
|
+
save(modules: readonly ManagedModule[]): void;
|
|
59
|
+
}
|
|
60
|
+
export declare function createModuleState(store: ModuleStore): ModuleState;
|
|
61
|
+
/**
|
|
62
|
+
* Reads the default module catalog used to seed `modules.yaml`. Entries have
|
|
63
|
+
* the same fields as `modules.yaml`, except `created` and `updated`, which are
|
|
64
|
+
* set to `timestamp`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function loadDefaultModules(filePath: string, timestamp: number): ManagedModule[];
|