Skip to content

Commit 95a51ad

Browse files
committed
[config] refactor config.options into core and file-based configuration types
- permits omission of optional fields when using the library programmatically - forces more constraints on config validation when using the UI
1 parent 4cb2a37 commit 95a51ad

File tree

27 files changed

+391
-153
lines changed

27 files changed

+391
-153
lines changed

examples/example_algorithm/_generic_hasher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { joinFieldsForHash, cleanValueList, extractAlgoColumnsFromObject, BaseHa
1818
import type { Config, Validator, makeHasherFunction } from 'common-identifier-algorithm-shared';
1919

2020
class GenericHasher extends BaseHasher {
21-
constructor(config: Config.Options["algorithm"]) {
21+
constructor(config: Config.CoreConfiguration["algorithm"]) {
2222
super(config);
2323
}
2424

@@ -38,7 +38,7 @@ class GenericHasher extends BaseHasher {
3838
}
3939

4040
export const REGION = "ANY";
41-
export const makeHasher: makeHasherFunction = (config: Config.Options["algorithm"]) => {
41+
export const makeHasher: makeHasherFunction = (config: Config.CoreConfiguration["algorithm"]) => {
4242
switch (config.hash.strategy.toLowerCase()) {
4343
case 'sha256':
4444
return new GenericHasher(config);

examples/programmatic_usage.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// REPLACE ALL REFERENCES TO "_generic_hasher" WITH THE DESIRED ALGORITHM IN THE ALGORITHMS DIRECTORY.
2-
import { generateHashesForDocument, validateDocument, type CidDocument, type Config } from '../src/index';
2+
import { generateHashesForDocument, validateConfigCore, validateDocument, type CidDocument, type Config } from '../src/index';
33
import { SUPPORTED_VALIDATORS } from '../src/validation/Validation';
44
import { makeHasher } from './example_algorithm/_generic_hasher';
55

@@ -10,10 +10,10 @@ import { makeHasher } from './example_algorithm/_generic_hasher';
1010
1111
see ../docs/configuration-files.md for more detail on the relevant config fields.
1212
*/
13-
const config: Config.Options = {
13+
const config: Config.CoreConfiguration = {
1414
meta: {
1515
// this must match the shortCode of the algorithm being used
16-
region: "UNKONWN"
16+
region: "UNKNOWN"
1717
},
1818
// the schema information for the source data
1919
source: {
@@ -28,7 +28,6 @@ const config: Config.Options = {
2828
id: [
2929
{ op: SUPPORTED_VALIDATORS.FIELD_TYPE, value: 'string' },
3030
{ op: SUPPORTED_VALIDATORS.MAX_FIELD_LENGTH, value: 11 }
31-
// { op: SUPPORTED_VALIDATORS.LINKED_FIELD, target: "col2" }
3231
]
3332
},
3433
algorithm: {
@@ -39,13 +38,7 @@ const config: Config.Options = {
3938
reference: [],
4039
static: [ "id" ]
4140
},
42-
},
43-
// schema for main output file, skipping for brevity
44-
destination: { columns: [] },
45-
// schema for mapping output file, skipping for brevity
46-
destination_map: { columns: [] },
47-
// schema for error files, skipping for brevity
48-
destination_errors: { columns: [] }
41+
}
4942
}
5043

5144
/*
@@ -61,8 +54,14 @@ const doc: CidDocument = {
6154
}
6255

6356
function main() {
57+
const configValidationResult = validateConfigCore(config, "UNKNOWN");
58+
if (!!configValidationResult) {
59+
console.log(`ERROR: ${configValidationResult}`);
60+
throw new Error("Runtime validation of config failed, check config input");
61+
}
62+
6463
// validate the input data against all configured validation rules.
65-
const validationResult = validateDocument(config, doc, false);
64+
const validationResult = validateDocument({ config: config, decoded: doc, isMapping: false });
6665
if (!validationResult.ok) {
6766
console.dir(validationResult.results, { depth: 5 });
6867
throw new Error("Data contains validation errors, check input");

scratch.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use strict";
2+
// Common Identifier Application
3+
// Copyright (C) 2024 World Food Programme
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
Object.defineProperty(exports, "__esModule", { value: true });
9+
exports.Config = void 0;
10+
var Config;
11+
(function (Config) {
12+
;
13+
})(Config || (exports.Config = Config = {}));

src/config/Config.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export namespace Config {
3030
static: string[];
3131
reference: string[];
3232
}
33-
export type StringBasedSalt = { source: 'STRING'; value: string };
34-
export type FileBasedSalt = {
33+
type StringBasedSalt = { source: 'STRING'; value: string };
34+
type FileBasedSalt = {
3535
source: 'FILE';
3636
validator_regex?: string;
3737
value: {
@@ -40,18 +40,8 @@ export namespace Config {
4040
linux?: string;
4141
};
4242
};
43-
export interface Options {
44-
isBackup?: boolean;
45-
meta: {
46-
region: string;
47-
version?: string;
48-
signature?: string;
49-
};
50-
messages?: {
51-
terms_and_conditions: string;
52-
error_in_config: string;
53-
error_in_salt: string;
54-
};
43+
export interface CoreConfiguration {
44+
meta: { region: string }
5545
source: ColumnMap;
5646
validations?: { [key: string]: ValidationRule[] };
5747
algorithm: {
@@ -61,6 +51,21 @@ export namespace Config {
6151
};
6252
salt: StringBasedSalt | FileBasedSalt;
6353
};
54+
}
55+
export interface FileConfiguration extends CoreConfiguration {
56+
isBackup?: boolean;
57+
meta: {
58+
region: string;
59+
version: string;
60+
signature: string;
61+
}
62+
// messages relevant for UI only so defining as optional here.
63+
// TODO: messages are not relevant for file-based usage without the UI, factor them out to a UIConfig type.
64+
messages?: {
65+
terms_and_conditions: string;
66+
error_in_config: string;
67+
error_in_salt: string;
68+
};
6469
destination: ColumnMap;
6570
destination_map: ColumnMap;
6671
destination_errors: ColumnMap;

src/config/configStore.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,35 +36,38 @@ function ensureAppDirectoryExists(appDir: string) {
3636
// This function does not do any validations on the data
3737
// (that should have happened after loading the config)
3838
// NOTE: the config is saved as JSON (TOML serialization can be weird)
39-
function saveConfig(configData: Config.Options, outputPath: string) {
39+
function saveConfig(configData: Config.FileConfiguration, outputPath: string) {
4040
// update the config hash on import to account for the
4141
const outputData = JSON.stringify(configData, null, ' ');
4242
fs.writeFileSync(outputPath, outputData, CONFIG_FILE_ENCODING);
4343
log('Written config data to ', outputPath);
4444
}
4545

4646
interface ConfigStorePaths {
47-
configFilePath: string;
48-
appConfigFilePath: string;
49-
backupConfigFilePath: string;
50-
region: string;
47+
config: string;
48+
appConfig: string;
49+
backupConfig: string;
5150
}
5251

5352
export class ConfigStore {
54-
data?: Config.Options = undefined;
53+
data?: Config.FileConfiguration = undefined;
5554
validationResult = {};
5655
hasConfigLoaded: boolean = false;
5756
isValid: boolean = false;
5857
isUsingBackupConfig: boolean = false;
5958
lastUpdated: Date;
6059
loadError: string | undefined;
60+
usingUI: boolean;
6161

6262
appConfig: AppConfigData = DEFAULT_APP_CONFIG;
63-
configPaths: ConfigStorePaths;
63+
filePaths: ConfigStorePaths;
64+
region: string;
6465

65-
constructor(configPaths: ConfigStorePaths) {
66+
constructor(filePaths: ConfigStorePaths, region: string, usingUI: boolean = false) {
6667
this.lastUpdated = new Date();
67-
this.configPaths = configPaths;
68+
this.filePaths = filePaths;
69+
this.region = region;
70+
this.usingUI = usingUI;
6871
}
6972

7073
getConfig() {
@@ -73,22 +76,22 @@ export class ConfigStore {
7376

7477
// Returns the region of the store
7578
getRegion() {
76-
return this.configPaths.region;
79+
return this.region;
7780
}
7881

7982
// Returns the path of the user config file
8083
getConfigFilePath() {
81-
return this.configPaths.configFilePath;
84+
return this.filePaths.config;
8285
}
8386

8487
// Returns the path of the backup config file
8588
getBackupConfigFilePath() {
86-
return this.configPaths.backupConfigFilePath;
89+
return this.filePaths.backupConfig;
8790
}
8891

8992
// Returns the path of the application config file
9093
getAppConfigFilePath() {
91-
return this.configPaths.appConfigFilePath;
94+
return this.filePaths.appConfig;
9295
}
9396

9497
// Returns true if the current config is a backup configuration
@@ -213,14 +216,14 @@ export class ConfigStore {
213216
}
214217

215218
// use a backup config and store its 'backupness'
216-
useBackupConfig(configData: Config.Options) {
219+
useBackupConfig(configData: Config.FileConfiguration) {
217220
this.isUsingBackupConfig = true;
218221
this.lastUpdated = new Date();
219222
this._useConfig(configData);
220223
}
221224

222225
// use an actual user-provided config and store its 'user-providedness'
223-
useUserConfig(configData: Config.Options, lastUpdateDate: Date) {
226+
useUserConfig(configData: Config.FileConfiguration, lastUpdateDate: Date) {
224227
this.isUsingBackupConfig = false;
225228
this.lastUpdated = lastUpdateDate;
226229
this._useConfig(configData);
@@ -234,7 +237,7 @@ export class ConfigStore {
234237
}
235238

236239
// use an already validated config as the app config
237-
_useConfig(configData: Config.Options) {
240+
_useConfig(configData: Config.FileConfiguration) {
238241
this.data = configData;
239242
this.hasConfigLoaded = true;
240243
this.isValid = true;
@@ -248,7 +251,7 @@ export class ConfigStore {
248251
}
249252

250253
// Overwrites the existing configuration file with the new data
251-
saveNewConfigData(configData: Config.Options) {
254+
saveNewConfigData(configData: Config.FileConfiguration) {
252255
// before saving ensure that we can save the configuration
253256
ensureAppDirectoryExists(dirname(this.getConfigFilePath()));
254257
// TODO: maybe do a rename w/ timestamp for backup
@@ -292,7 +295,7 @@ export class ConfigStore {
292295
}
293296
}
294297

295-
export function makeConfigStore(storeConfig: ConfigStorePaths) {
296-
if (!storeConfig) throw new Error(`ConfigStore params MUST be provided.`);
297-
return new ConfigStore(storeConfig);
298+
export function makeConfigStore({filePaths, region, usingUI}: {filePaths: ConfigStorePaths, region: string, usingUI: boolean}) {
299+
if (!filePaths || !region) throw new Error(`ConfigStore params MUST be provided.`);
300+
return new ConfigStore(filePaths, region, usingUI);
298301
}

src/config/generateConfigHash.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@ type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> };
2525

2626
// Takes a config, removes the "signature" and salt keys from it, generates
2727
// a stable JSON representation and hashes it using the provided algorithm
28-
export function generateConfigHash(config: Config.Options, hashType = DEFAULT_HASH_TYPE) {
28+
export function generateConfigHash<T extends Config.CoreConfiguration>(config: T, hashType = DEFAULT_HASH_TYPE) {
2929
// create a nested copy of the object
30-
const configCopy = { ...(JSON.parse(JSON.stringify(config)) as RecursivePartial<Config.Options>) };
30+
const configCopy = { ...(JSON.parse(JSON.stringify(config)) as RecursivePartial<T>) };
3131

3232
// remove the "signature" key
33-
delete configCopy.meta!.signature;
33+
if (configCopy.meta && "signature" in configCopy.meta) {
34+
delete configCopy.meta.signature;
35+
}
3436

3537
// remove the "messages" key
3638
// TODO: messages should go in a separate locales file to future proof translations
37-
delete configCopy.messages;
39+
if ("messages" in configCopy) {
40+
delete configCopy.messages;
41+
}
3842

3943
// remove the "algorithm.salt" part as it may have injected keys
4044
// TODO: this enables messing with the salt file path pre-injection without signature validations, but is required for compatibility w/ the injection workflow

src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { loadConfig } from './loadConfig';
2-
export { validateConfig } from './validateConfig';
2+
export { validateConfigCore, validateConfigFile } from './validateConfig';
33
export { generateConfigHash } from './generateConfigHash';
44
export { makeConfigStore, ConfigStore } from './configStore';
55
export { appDataLocation, attemptToReadTOMLData } from './utils';

src/config/loadConfig.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import fs from 'node:fs';
1818

19-
import { validateConfig } from './validateConfig';
19+
import { validateConfigFile } from './validateConfig';
2020
import { loadSaltFile } from './loadSaltFile';
2121
import { generateConfigHash } from './generateConfigHash';
2222

@@ -29,18 +29,9 @@ const log = Debug('CID:loadConfig');
2929
export const CONFIG_FILE_ENCODING: fs.EncodingOption = 'utf-8';
3030

3131
type LoadConfigResult =
32-
| { success: true; lastUpdated: Date; config: Config.Options }
33-
| {
34-
success: false;
35-
error: string;
36-
isSaltFileError: false;
37-
}
38-
| {
39-
success: false;
40-
error: string;
41-
isSaltFileError: true;
42-
config: Config.Options;
43-
};
32+
| { success: true; lastUpdated: Date; config: Config.FileConfiguration; }
33+
| { success: false; error: string; isSaltFileError: false; }
34+
| { success: false; error: string; isSaltFileError: true; config: Config.FileConfiguration; };
4435

4536

4637
// Main entry point for loading a config file.
@@ -49,11 +40,11 @@ type LoadConfigResult =
4940
// - { success: false, error: "string" } if there are errors
5041
// - { success: false, isSaltFileError: true, error: "string"}
5142
// if there is something wrong with the salt file
52-
export function loadConfig(configPath: string, region: string): LoadConfigResult {
43+
export function loadConfig(configPath: string, region: string, usingUI: boolean=false): LoadConfigResult {
5344
log('Loading config from', configPath);
5445

5546
// attempt to read the file
56-
const configData = attemptToReadTOMLData<Config.Options>(configPath, CONFIG_FILE_ENCODING);
47+
const configData = attemptToReadTOMLData<Config.FileConfiguration>(configPath, CONFIG_FILE_ENCODING);
5748

5849
// if cannot be read, we have an error
5950
if (!configData) {
@@ -68,7 +59,7 @@ export function loadConfig(configPath: string, region: string): LoadConfigResult
6859
const lastUpdateDate = new Date(fs.statSync(configPath).mtime);
6960

7061
// validate the config
71-
const validationResult = validateConfig(configData, region);
62+
const validationResult = validateConfigFile(configData, region, usingUI);
7263

7364
// if the config is not valid return false
7465
if (validationResult) {
@@ -132,7 +123,7 @@ export function loadConfig(configPath: string, region: string): LoadConfigResult
132123
}
133124

134125
// replace the "FILE" with "STRING" amd embed the salt data
135-
configData.algorithm.salt = configData.algorithm.salt as unknown as Config.StringBasedSalt;
126+
configData.algorithm.salt = configData.algorithm.salt as unknown as Config.CoreConfiguration["algorithm"]["salt"];
136127
configData.algorithm.salt.source = 'STRING';
137128
configData.algorithm.salt.value = saltData;
138129

src/config/loadSaltFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const DEFAULT_VALIDATOR_REGEXP: RegExp =
3030

3131
// Attempts to load and clean up the salt file data
3232
export function loadSaltFile(
33-
saltFilePath: Config.Options['algorithm']['salt']['value'],
33+
saltFilePath: Config.FileConfiguration['algorithm']['salt']['value'],
3434
validatorRegexp = DEFAULT_VALIDATOR_REGEXP,
3535
) {
3636
// resolve the salt file path from the config & platform

src/config/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function attemptToReadTOMLData<T>(filePath: string, encoding: fs.Encoding
5454

5555
// takes into consideration the platform and the type of value provided by the config
5656
// to return an actual, absolute salt file path
57-
export function getSaltFilePath(saltValueConfig: Config.Options['algorithm']['salt']['value']) {
57+
export function getSaltFilePath(saltValueConfig: Config.CoreConfiguration['algorithm']['salt']['value']) {
5858
// if the value is a string always use it
5959
if (typeof saltValueConfig === 'string') return saltValueConfig;
6060

0 commit comments

Comments
 (0)