feat: Allow dynamically adding CLI parameters in configs

This commit is contained in:
Joachim Van Herwegen 2022-04-13 17:06:46 +02:00
parent e6519992bf
commit bedab907f9
7 changed files with 159 additions and 30 deletions

View File

@ -5,6 +5,9 @@
- The server can be started with a new parameter to automatically generate accounts and pods, - The server can be started with a new parameter to automatically generate accounts and pods,
for more info see [here](guides/seeding-pods.md). for more info see [here](guides/seeding-pods.md).
- A new `RedirectingHttpHandler` class has been added which can be used to redirect certain URLs. - A new `RedirectingHttpHandler` class has been added which can be used to redirect certain URLs.
- A new default configuration `config/https-file-cli.json`
that can set the HTTPS parameters through the CLI has been added.
This is also an example of how to add CLI parameters through a custom configuration.
### Configuration changes ### Configuration changes
You might need to make changes to your v3 configuration if you use a custom config. You might need to make changes to your v3 configuration if you use a custom config.
@ -18,6 +21,8 @@ The following changes pertain to the imports in the default configs:
The following changes are relevant for v3 custom configs that replaced certain features. The following changes are relevant for v3 custom configs that replaced certain features.
- The key/value storage configs in `config/storage/key-value/*` have been changed to reduce config duplication. - The key/value storage configs in `config/storage/key-value/*` have been changed to reduce config duplication.
All storages there that were only relevant for 1 class have been moved to the config of that class. All storages there that were only relevant for 1 class have been moved to the config of that class.
- Due to a parameter rename in `CombinedSettingsResolver`,
`config/app/variables/resolver/resolver.json` has been updated.
### Interface changes ### Interface changes
These changes are relevant if you wrote custom modules for the server that depend on existing interfaces. These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.
@ -27,6 +32,7 @@ These changes are relevant if you wrote custom modules for the server that depen
and has been moved to a separate `ResourceSet` interface. and has been moved to a separate `ResourceSet` interface.
- Several `ModesExtractor`s `PermissionBasedAuthorizer` now take a `ResourceSet` as constructor parameter. - Several `ModesExtractor`s `PermissionBasedAuthorizer` now take a `ResourceSet` as constructor parameter.
- `RepresentationMetadata` no longer accepts strings for predicates in any of its functions. - `RepresentationMetadata` no longer accepts strings for predicates in any of its functions.
- `CombinedSettingsResolver` parameter `computers` has been renamed to `resolvers`.
## v3.0.0 ## v3.0.0
### New features ### New features

View File

@ -5,63 +5,63 @@
"comment": "Converts an input key/value object into an object mapping values to Components.js variables", "comment": "Converts an input key/value object into an object mapping values to Components.js variables",
"@id": "urn:solid-server-app-setup:default:SettingsResolver", "@id": "urn:solid-server-app-setup:default:SettingsResolver",
"@type": "CombinedSettingsResolver", "@type": "CombinedSettingsResolver",
"computers": [ "resolvers": [
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:baseUrl", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:baseUrl",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "BaseUrlExtractor" "@type": "BaseUrlExtractor"
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:loggingLevel", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:loggingLevel",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor", "@type": "KeyExtractor",
"key": "loggingLevel", "key": "loggingLevel",
"defaultValue": "info" "defaultValue": "info"
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:port", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:port",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor", "@type": "KeyExtractor",
"key": "port", "key": "port",
"defaultValue": 3000 "defaultValue": 3000
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:rootFilePath", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:rootFilePath",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "AssetPathExtractor", "@type": "AssetPathExtractor",
"key": "rootFilePath", "key": "rootFilePath",
"defaultPath": "./" "defaultPath": "./"
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:sparqlEndpoint", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:sparqlEndpoint",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor", "@type": "KeyExtractor",
"key": "sparqlEndpoint" "key": "sparqlEndpoint"
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:showStackTrace", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:showStackTrace",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor", "@type": "KeyExtractor",
"key": "showStackTrace", "key": "showStackTrace",
"defaultValue": false "defaultValue": false
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:podConfigJson", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:podConfigJson",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "AssetPathExtractor", "@type": "AssetPathExtractor",
"key": "podConfigJson", "key": "podConfigJson",
"defaultPath": "./pod-config.json" "defaultPath": "./pod-config.json"
} }
}, },
{ {
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:seededPodConfigJson", "CombinedSettingsResolver:_resolvers_key": "urn:solid-server:default:variable:seededPodConfigJson",
"CombinedSettingsResolver:_computers_value": { "CombinedSettingsResolver:_resolvers_value": {
"@type": "AssetPathExtractor", "@type": "AssetPathExtractor",
"key": "seededPodConfigJson" "key": "seededPodConfigJson"
} }

109
config/https-file-cli.json Normal file
View File

@ -0,0 +1,109 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/setup/required.json",
"files-scs:config/app/variables/default.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/static/default.json",
"files-scs:config/identity/access/public.json",
"files-scs:config/identity/email/default.json",
"files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json",
"files-scs:config/identity/pod/static.json",
"files-scs:config/identity/registration/enabled.json",
"files-scs:config/ldp/authentication/dpop-bearer.json",
"files-scs:config/ldp/authorization/webacl.json",
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",
"files-scs:config/util/identifiers/suffix.json",
"files-scs:config/util/index/default.json",
"files-scs:config/util/logging/winston.json",
"files-scs:config/util/representation-conversion/default.json",
"files-scs:config/util/resource-locker/memory.json",
"files-scs:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"Adds CLI options --httpsKey and --httpsCert and uses those to start an HTTPS server.",
"The http/server-factory import above has been omitted since that feature is set below."
]
},
{
"@id": "urn:solid-server-app-setup:default:CliExtractor",
"@type": "YargsCliExtractor",
"extendedParameters": {
"httpsKey": {
"demandOption": true,
"requiresArg": true,
"type": "string",
"describe": "File path to the HTTPS key."
},
"httpsCert": {
"demandOption": true,
"requiresArg": true,
"type": "string",
"describe": "File path to the HTTPS certificate."
}
}
},
{
"comment": "Adds resolvers to assign the CLI values to the Components.js variables.",
"@id": "urn:solid-server-app-setup:default:SettingsResolver",
"@type": "CombinedSettingsResolver",
"resolvers": [
{
"CombinedSettingsResolver:_resolvers_key": "urn:solid-server:custom:variable:httpsKey",
"CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor",
"key": "httpsKey"
}
},
{
"CombinedSettingsResolver:_resolvers_key": "urn:solid-server:custom:variable:httpsCert",
"CombinedSettingsResolver:_resolvers_value": {
"@type": "KeyExtractor",
"key": "httpsCert"
}
}
]
},
{
"comment": [
"Creates an HTTPS server with the settings provided via the command line.",
"Replaces the example import from config/http/server-factory.https-example.json."
],
"@id": "urn:solid-server:default:ServerFactory",
"@type": "WebSocketServerFactory",
"baseServerFactory": {
"@id": "urn:solid-server:default:HttpServerFactory",
"@type": "BaseHttpServerFactory",
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
"options_https": true,
"options_key": {
"@id": "urn:solid-server:custom:variable:httpsKey",
"@type": "Variable"
},
"options_cert": {
"@id": "urn:solid-server:custom:variable:httpsCert",
"@type": "Variable"
}
},
"webSocketHandler": {
"@type": "UnsecureWebSocketsProtocol",
"source": { "@id": "urn:solid-server:default:ResourceStore" }
}
}
]
}

View File

@ -31,10 +31,13 @@ export class YargsCliExtractor extends CliExtractor {
* @param parameters - Parameters that should be parsed from the CLI. @range {json} * @param parameters - Parameters that should be parsed from the CLI. @range {json}
* Format details can be found at https://yargs.js.org/docs/#api-reference-optionskey-opt * Format details can be found at https://yargs.js.org/docs/#api-reference-optionskey-opt
* @param options - Additional options to configure yargs. @range {json} * @param options - Additional options to configure yargs. @range {json}
* @param extendedParameters - The same as @parameters. Separate variable so in Components.js
* we can have both a default set and a user-added version. @range {json}
*/ */
public constructor(parameters: YargsArgOptions = {}, options: CliOptions = {}) { public constructor(parameters: YargsArgOptions = {}, options: CliOptions = {},
extendedParameters: YargsArgOptions = {}) {
super(); super();
this.yargsArgOptions = parameters; this.yargsArgOptions = { ...parameters, ...extendedParameters };
this.yargvOptions = options; this.yargvOptions = options;
} }

View File

@ -6,16 +6,16 @@ import { SettingsResolver } from './SettingsResolver';
* Generates variable values by running a set of {@link SettingsExtractor}s on the input. * Generates variable values by running a set of {@link SettingsExtractor}s on the input.
*/ */
export class CombinedSettingsResolver extends SettingsResolver { export class CombinedSettingsResolver extends SettingsResolver {
public readonly computers: Record<string, SettingsExtractor>; public readonly resolvers: Record<string, SettingsExtractor>;
public constructor(computers: Record<string, SettingsExtractor>) { public constructor(resolvers: Record<string, SettingsExtractor>) {
super(); super();
this.computers = computers; this.resolvers = resolvers;
} }
public async handle(input: Record<string, unknown>): Promise<Record<string, unknown>> { public async handle(input: Record<string, unknown>): Promise<Record<string, unknown>> {
const vars: Record<string, any> = {}; const vars: Record<string, any> = {};
for (const [ name, computer ] of Object.entries(this.computers)) { for (const [ name, computer ] of Object.entries(this.resolvers)) {
try { try {
vars[name] = await computer.handleSafe(input); vars[name] = await computer.handleSafe(input);
} catch (err: unknown) { } catch (err: unknown) {

View File

@ -4,6 +4,7 @@ import { YargsCliExtractor } from '../../../../src/init/cli/YargsCliExtractor';
const error = jest.spyOn(console, 'error').mockImplementation(jest.fn()); const error = jest.spyOn(console, 'error').mockImplementation(jest.fn());
const log = jest.spyOn(console, 'log').mockImplementation(jest.fn()); const log = jest.spyOn(console, 'log').mockImplementation(jest.fn());
const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any); const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any);
describe('A YargsCliExtractor', (): void => { describe('A YargsCliExtractor', (): void => {
const parameters: YargsArgOptions = { const parameters: YargsArgOptions = {
baseUrl: { alias: 'b', requiresArg: true, type: 'string' }, baseUrl: { alias: 'b', requiresArg: true, type: 'string' },
@ -41,6 +42,16 @@ describe('A YargsCliExtractor', (): void => {
await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({})); await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({}));
}); });
it('combines parameters and extra parameters.', async(): Promise<void> => {
extractor = new YargsCliExtractor(parameters, {}, { test: { alias: 't', requiresArg: true, type: 'string' }});
const argv = [ 'node', 'script', '-b', 'http://localhost:3000/', '-p', '3000', '-t', 'test' ];
await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({
baseUrl: 'http://localhost:3000/',
port: 3000,
test: 'test',
}));
});
it('prints usage if defined.', async(): Promise<void> => { it('prints usage if defined.', async(): Promise<void> => {
extractor = new YargsCliExtractor(parameters, { usage: 'node ./bin/server.js [args]' }); extractor = new YargsCliExtractor(parameters, { usage: 'node ./bin/server.js [args]' });
const argv = [ 'node', 'script', '--help' ]; const argv = [ 'node', 'script', '--help' ];

View File

@ -5,22 +5,22 @@ describe('A CombinedSettingsResolver', (): void => {
const values = { test: 'data' }; const values = { test: 'data' };
const varPort = 'urn:solid-server:default:variable:port'; const varPort = 'urn:solid-server:default:variable:port';
const varLog = 'urn:solid-server:default:variable:loggingLevel'; const varLog = 'urn:solid-server:default:variable:loggingLevel';
let computerPort: jest.Mocked<SettingsExtractor>; let resolverPort: jest.Mocked<SettingsExtractor>;
let computerLog: jest.Mocked<SettingsExtractor>; let resolverLog: jest.Mocked<SettingsExtractor>;
let resolver: CombinedSettingsResolver; let resolver: CombinedSettingsResolver;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
computerPort = { resolverPort = {
handleSafe: jest.fn().mockResolvedValue(3000), handleSafe: jest.fn().mockResolvedValue(3000),
} as any; } as any;
computerLog = { resolverLog = {
handleSafe: jest.fn().mockResolvedValue('info'), handleSafe: jest.fn().mockResolvedValue('info'),
} as any; } as any;
resolver = new CombinedSettingsResolver({ resolver = new CombinedSettingsResolver({
[varPort]: computerPort, [varPort]: resolverPort,
[varLog]: computerLog, [varLog]: resolverLog,
}); });
}); });
@ -32,7 +32,7 @@ describe('A CombinedSettingsResolver', (): void => {
}); });
it('rethrows the error if something goes wrong.', async(): Promise<void> => { it('rethrows the error if something goes wrong.', async(): Promise<void> => {
computerPort.handleSafe.mockRejectedValueOnce(new Error('bad data')); resolverPort.handleSafe.mockRejectedValueOnce(new Error('bad data'));
await expect(resolver.handle(values)).rejects.toThrow(`Error in computing value for variable ${varPort}: bad data`); await expect(resolver.handle(values)).rejects.toThrow(`Error in computing value for variable ${varPort}: bad data`);
}); });
}); });