implement support for string form human durations in config
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config': minor
|
||||
---
|
||||
|
||||
Make `readDurationFromConfig` support both ISO and ms formats as well, to make it easier to enter time as an end user
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch
|
||||
'@backstage/plugin-notifications-backend-module-email': patch
|
||||
'@backstage/plugin-events-backend-module-aws-sqs': patch
|
||||
'@backstage/backend-plugin-api': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
'@backstage/backend-defaults': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Support ISO and ms string forms of durations in config too
|
||||
+5
-5
@@ -474,8 +474,8 @@ export interface Config {
|
||||
cache?:
|
||||
| {
|
||||
store: 'memory';
|
||||
/** An optional default TTL (in milliseconds). */
|
||||
defaultTtl?: number | HumanDuration;
|
||||
/** An optional default TTL (in milliseconds, if given as a number). */
|
||||
defaultTtl?: number | HumanDuration | string;
|
||||
}
|
||||
| {
|
||||
store: 'redis';
|
||||
@@ -484,8 +484,8 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
connection: string;
|
||||
/** An optional default TTL (in milliseconds). */
|
||||
defaultTtl?: number | HumanDuration;
|
||||
/** An optional default TTL (in milliseconds, if given as a number). */
|
||||
defaultTtl?: number | HumanDuration | string;
|
||||
/**
|
||||
* Whether or not [useRedisSets](https://github.com/jaredwray/keyv/tree/main/packages/redis#useredissets) should be configured to this redis cache.
|
||||
* Defaults to true if unspecified.
|
||||
@@ -500,7 +500,7 @@ export interface Config {
|
||||
*/
|
||||
connection: string;
|
||||
/** An optional default TTL (in milliseconds). */
|
||||
defaultTtl?: number | HumanDuration;
|
||||
defaultTtl?: number | HumanDuration | string;
|
||||
};
|
||||
|
||||
cors?: {
|
||||
|
||||
@@ -185,6 +185,6 @@ describe('CacheManager integration', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/Invalid configuration backend.cache.defaultTtl/);
|
||||
).toThrow(/Invalid duration 'hello' in config/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import Keyv from 'keyv';
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import { CacheManagerOptions, ttlToMilliseconds } from './types';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import { readDurationFromConfig } from '@backstage/config';
|
||||
|
||||
type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;
|
||||
|
||||
@@ -75,17 +76,12 @@ export class CacheManager {
|
||||
});
|
||||
|
||||
let defaultTtl: number | undefined;
|
||||
if (defaultTtlConfig !== undefined && defaultTtlConfig !== null) {
|
||||
if (defaultTtlConfig !== undefined) {
|
||||
if (typeof defaultTtlConfig === 'number') {
|
||||
defaultTtl = defaultTtlConfig;
|
||||
} else if (
|
||||
typeof defaultTtlConfig === 'object' &&
|
||||
!Array.isArray(defaultTtlConfig)
|
||||
) {
|
||||
defaultTtl = durationToMilliseconds(defaultTtlConfig);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid configuration backend.cache.defaultTtl: ${defaultTtlConfig}, expected milliseconds number or HumanDuration object`,
|
||||
defaultTtl = durationToMilliseconds(
|
||||
readDurationFromConfig(config, { key: 'backend.cache.defaultTtl' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,19 +354,6 @@ export interface SchedulerService {
|
||||
getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]>;
|
||||
}
|
||||
|
||||
function readDuration(config: Config, key: string): HumanDuration {
|
||||
if (typeof config.get(key) === 'string') {
|
||||
const value = config.getString(key);
|
||||
const duration = Duration.fromISO(value);
|
||||
if (!duration.isValid) {
|
||||
throw new Error(`Invalid duration: ${value}`);
|
||||
}
|
||||
return duration.toObject();
|
||||
}
|
||||
|
||||
return readDurationFromConfig(config, { key });
|
||||
}
|
||||
|
||||
function readFrequency(
|
||||
config: Config,
|
||||
key: string,
|
||||
@@ -382,7 +369,7 @@ function readFrequency(
|
||||
return { trigger: 'manual' };
|
||||
}
|
||||
|
||||
return readDuration(config, key);
|
||||
return readDurationFromConfig(config, { key });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,10 +383,10 @@ export function readSchedulerServiceTaskScheduleDefinitionFromConfig(
|
||||
config: Config,
|
||||
): SchedulerServiceTaskScheduleDefinition {
|
||||
const frequency = readFrequency(config, 'frequency');
|
||||
const timeout = readDuration(config, 'timeout');
|
||||
const timeout = readDurationFromConfig(config, { key: 'timeout' });
|
||||
|
||||
const initialDelay = config.has('initialDelay')
|
||||
? readDuration(config, 'initialDelay')
|
||||
? readDurationFromConfig(config, { key: 'initialDelay' })
|
||||
: undefined;
|
||||
|
||||
const scope = config.getOptionalString('scope');
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/types": "workspace:^",
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -15,114 +15,254 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
readDurationFromConfig,
|
||||
propsOfHumanDuration,
|
||||
readDurationFromConfig,
|
||||
} from './readDurationFromConfig';
|
||||
import { ConfigReader } from './reader';
|
||||
|
||||
describe('readDurationFromConfig', () => {
|
||||
it('reads all known keys', () => {
|
||||
const config = new ConfigReader({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
describe('ISO form', () => {
|
||||
it('parses the known forms', () => {
|
||||
const config = new ConfigReader({
|
||||
d1: 'P2DT6H',
|
||||
d2: 'PT0.5S',
|
||||
d3: 'PT3.1S',
|
||||
d4: 'P1Y2M3W4DT5H6M7.8S',
|
||||
});
|
||||
|
||||
expect(readDurationFromConfig(config, { key: 'd1' })).toEqual({
|
||||
days: 2,
|
||||
hours: 6,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd2' })).toEqual({
|
||||
milliseconds: 500,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd3' })).toEqual({
|
||||
seconds: 3,
|
||||
milliseconds: 100,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd4' })).toEqual({
|
||||
years: 1,
|
||||
months: 2,
|
||||
weeks: 3,
|
||||
days: 4,
|
||||
hours: 5,
|
||||
minutes: 6,
|
||||
seconds: 7,
|
||||
milliseconds: 800,
|
||||
});
|
||||
});
|
||||
expect(readDurationFromConfig(config)).toEqual({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
|
||||
it('throws on errors', () => {
|
||||
const config = new ConfigReader({
|
||||
d1: 'P 1Y',
|
||||
d2: 'P1L',
|
||||
d3: 'P',
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd1' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid duration 'P 1Y' in config at 'd1', Error: Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)"`,
|
||||
);
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd2' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid duration 'P1L' in config at 'd2', Error: Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)"`,
|
||||
);
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd3' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid duration 'P' in config at 'd3', Error: Invalid ISO format, no values given"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all known keys, for a subkey', () => {
|
||||
const config = new ConfigReader({
|
||||
sub: {
|
||||
key: {
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
describe('ms form', () => {
|
||||
it('parses the known units', () => {
|
||||
// this is not exhaustive, but tests all supported units to ensure that
|
||||
// our conversion to HumanDuration form works
|
||||
const config = new ConfigReader({
|
||||
d1: '1y',
|
||||
d2: '2 years',
|
||||
d3: '4w',
|
||||
d4: '5h',
|
||||
d5: '6 hrs',
|
||||
d6: '7min',
|
||||
d7: '9 minutes',
|
||||
d8: '3.5 seconds',
|
||||
d9: '25 ms',
|
||||
d10: '1850ms',
|
||||
});
|
||||
|
||||
expect(readDurationFromConfig(config, { key: 'd1' })).toEqual({
|
||||
years: 1,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd2' })).toEqual({
|
||||
years: 2,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd3' })).toEqual({
|
||||
weeks: 4,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd4' })).toEqual({
|
||||
hours: 5,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd5' })).toEqual({
|
||||
hours: 6,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd6' })).toEqual({
|
||||
minutes: 7,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd7' })).toEqual({
|
||||
minutes: 9,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd8' })).toEqual({
|
||||
seconds: 3,
|
||||
milliseconds: 500,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd9' })).toEqual({
|
||||
milliseconds: 25,
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'd10' })).toEqual({
|
||||
seconds: 1,
|
||||
milliseconds: 850,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on errors', () => {
|
||||
const config = new ConfigReader({
|
||||
d1: '1m 3s',
|
||||
d2: '-3s',
|
||||
d3: '',
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd1' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid duration '1m 3s' in config at 'd1', Error: Not a valid duration string, try a number followed by a unit such as '1d' or '2 seconds'"`,
|
||||
);
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd2' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid duration '-3s' in config at 'd2', Error: Negative durations are not allowed"`,
|
||||
);
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'd3' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid type in config for key 'd3' in 'mock-config', got empty-string, wanted string"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('object form', () => {
|
||||
it('reads all known keys', () => {
|
||||
const config = new ConfigReader({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
});
|
||||
expect(readDurationFromConfig(config)).toEqual({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all known keys, for a subkey', () => {
|
||||
const config = new ConfigReader({
|
||||
sub: {
|
||||
key: {
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'sub.key' })).toEqual({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects wrong type of target, for a subkey', () => {
|
||||
const config = new ConfigReader({
|
||||
sub: { key: 7 },
|
||||
});
|
||||
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
|
||||
"Failed to read duration from config, TypeError: Invalid type in config for key 'sub.key' in 'mock-config', got number, wanted object",
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects no keys', () => {
|
||||
const config = new ConfigReader({});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects no keys, for a subkey', () => {
|
||||
const config = new ConfigReader({ sub: { key: {} } });
|
||||
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
|
||||
`Failed to read duration from config at 'sub.key', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unknown keys', () => {
|
||||
const config = new ConfigReader({
|
||||
minutes: 3,
|
||||
invalid: 'value',
|
||||
});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(propsOfHumanDuration)('rejects non-number %p', prop => {
|
||||
const config = new ConfigReader({
|
||||
[prop]: 'value',
|
||||
});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Unable to convert config value for key '${prop}' in 'mock-config' to a number`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(propsOfHumanDuration)(
|
||||
'rejects non-number %p, for a subkey',
|
||||
prop => {
|
||||
const config = new ConfigReader({
|
||||
sub: {
|
||||
key: {
|
||||
[prop]: 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
readDurationFromConfig(config, { key: 'sub.key' }),
|
||||
).toThrow(
|
||||
`Failed to read duration from config, Error: Unable to convert config value for key 'sub.key.${prop}' in 'mock-config' to a number`,
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(readDurationFromConfig(config, { key: 'sub.key' })).toEqual({
|
||||
milliseconds: 1,
|
||||
seconds: 2,
|
||||
minutes: 3,
|
||||
hours: 4,
|
||||
days: 5,
|
||||
weeks: 6,
|
||||
months: 7,
|
||||
years: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects wrong type of target, for a subkey', () => {
|
||||
const config = new ConfigReader({
|
||||
sub: { key: 7 },
|
||||
});
|
||||
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
|
||||
"Failed to read duration from config, TypeError: Invalid type in config for key 'sub.key' in 'mock-config', got number, wanted object",
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects no keys', () => {
|
||||
const config = new ConfigReader({});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects no keys, for a subkey', () => {
|
||||
const config = new ConfigReader({ sub: { key: {} } });
|
||||
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
|
||||
`Failed to read duration from config at 'sub.key', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unknown keys', () => {
|
||||
const config = new ConfigReader({
|
||||
minutes: 3,
|
||||
invalid: 'value',
|
||||
});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(propsOfHumanDuration)('rejects non-number %p', prop => {
|
||||
const config = new ConfigReader({
|
||||
[prop]: 'value',
|
||||
});
|
||||
expect(() => readDurationFromConfig(config)).toThrow(
|
||||
`Failed to read duration from config, Error: Unable to convert config value for key '${prop}' in 'mock-config' to a number`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(propsOfHumanDuration)('rejects non-number %p, for a subkey', prop => {
|
||||
const config = new ConfigReader({
|
||||
sub: {
|
||||
key: {
|
||||
[prop]: 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
|
||||
`Failed to read duration from config, Error: Unable to convert config value for key 'sub.key.${prop}' in 'mock-config' to a number`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { InputError, stringifyError } from '@backstage/errors';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import ms from 'ms';
|
||||
|
||||
export const propsOfHumanDuration = [
|
||||
'years',
|
||||
@@ -30,18 +31,29 @@ export const propsOfHumanDuration = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Reads a duration from a config object.
|
||||
* Reads a duration from config.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* The supported formats are:
|
||||
*
|
||||
* - A string in the format of '1d', '2 seconds' etc. as supported by the `ms`
|
||||
* library.
|
||||
* - A standard ISO formatted duration string, e.g. 'P2DT6H' or 'PT1M'.
|
||||
* - An object with individual units (in plural) as keys, e.g. `{ days: 2, hours: 6 }`.
|
||||
*
|
||||
* The string forms are naturally only supported if the `options.key` argument
|
||||
* is passed, since a `Config` argument always represents an object by its
|
||||
* nature.
|
||||
*
|
||||
* This does not support optionality; if you want to support optional durations,
|
||||
* you need to first check the presence of the target with `config.has(...)` and
|
||||
* then call this function.
|
||||
*
|
||||
* @param config - A configuration object
|
||||
* @param key - If specified, read the duration from the given subkey
|
||||
* under the config object
|
||||
* @param key - If specified, read the duration from the given subkey under the
|
||||
* config object
|
||||
* @returns A duration object
|
||||
*/
|
||||
export function readDurationFromConfig(
|
||||
@@ -49,6 +61,33 @@ export function readDurationFromConfig(
|
||||
options?: {
|
||||
key?: string;
|
||||
},
|
||||
): HumanDuration {
|
||||
if (options?.key && typeof config.getOptional(options.key) === 'string') {
|
||||
const value = config.getString(options.key).trim();
|
||||
try {
|
||||
return value.startsWith('P')
|
||||
? parseIsoDuration(value)
|
||||
: parseMsDuration(value);
|
||||
} catch (error) {
|
||||
throw new InputError(
|
||||
`Invalid duration '${value}' in config at '${
|
||||
options.key
|
||||
}', ${stringifyError(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return parseObjectDuration(config, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the object form of durations.
|
||||
*/
|
||||
export function parseObjectDuration(
|
||||
config: Config,
|
||||
options?: {
|
||||
key?: string;
|
||||
},
|
||||
): HumanDuration {
|
||||
let root: Config;
|
||||
let found = false;
|
||||
@@ -94,8 +133,178 @@ export function readDurationFromConfig(
|
||||
if (options?.key) {
|
||||
prefix += ` at '${options.key}'`;
|
||||
}
|
||||
throw new InputError(`${prefix}, ${error}`);
|
||||
throw new Error(`${prefix}, ${error}`);
|
||||
}
|
||||
|
||||
return result as HumanDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses friendly string durations like '1d', '2 seconds' etc using the ms
|
||||
* library.
|
||||
*/
|
||||
export function parseMsDuration(input: string): HumanDuration {
|
||||
if (/^\d+$/.exec(input)) {
|
||||
// We explicitly disallow the only-digits form of the ms library, because
|
||||
// from a configuration perspective it's just confusing to even be able to
|
||||
// specify that
|
||||
throw new Error(
|
||||
`The value cannot be a plain number; try adding a unit like 'ms' or 'seconds'`,
|
||||
);
|
||||
}
|
||||
|
||||
let milliseconds = ms(input);
|
||||
if (!Number.isFinite(milliseconds)) {
|
||||
throw new Error(
|
||||
`Not a valid duration string, try a number followed by a unit such as '1d' or '2 seconds'`,
|
||||
);
|
||||
} else if (milliseconds < 0) {
|
||||
throw new Error('Negative durations are not allowed');
|
||||
} else if (milliseconds === 0) {
|
||||
return { milliseconds: 0 };
|
||||
}
|
||||
|
||||
// As used by the ms library
|
||||
const s = 1000;
|
||||
const m = s * 60;
|
||||
const h = m * 60;
|
||||
const d = h * 24;
|
||||
const w = d * 7;
|
||||
const y = d * 365.25;
|
||||
|
||||
const result: HumanDuration = {};
|
||||
|
||||
if (milliseconds >= y) {
|
||||
const years = Math.floor(milliseconds / y);
|
||||
milliseconds -= years * y;
|
||||
result.years = years;
|
||||
}
|
||||
|
||||
if (milliseconds >= w) {
|
||||
const weeks = Math.floor(milliseconds / w);
|
||||
milliseconds -= weeks * w;
|
||||
result.weeks = weeks;
|
||||
}
|
||||
|
||||
if (milliseconds >= d) {
|
||||
const days = Math.floor(milliseconds / d);
|
||||
milliseconds -= days * d;
|
||||
result.days = days;
|
||||
}
|
||||
|
||||
if (milliseconds >= h) {
|
||||
const hours = Math.floor(milliseconds / h);
|
||||
milliseconds -= hours * h;
|
||||
result.hours = hours;
|
||||
}
|
||||
|
||||
if (milliseconds >= m) {
|
||||
const minutes = Math.floor(milliseconds / m);
|
||||
milliseconds -= minutes * m;
|
||||
result.minutes = minutes;
|
||||
}
|
||||
|
||||
if (milliseconds >= s) {
|
||||
const seconds = Math.floor(milliseconds / s);
|
||||
milliseconds -= seconds * s;
|
||||
result.seconds = seconds;
|
||||
}
|
||||
|
||||
if (milliseconds > 0) {
|
||||
result.milliseconds = milliseconds;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ISO formatted duration string.
|
||||
*
|
||||
* Implementation taken from luxon's Duration.fromISO to not force that
|
||||
* dependency on everyone.
|
||||
*/
|
||||
export function parseIsoDuration(input: string): HumanDuration {
|
||||
const match =
|
||||
/^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/.exec(
|
||||
input,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)`,
|
||||
);
|
||||
}
|
||||
|
||||
const [
|
||||
s,
|
||||
yearStr,
|
||||
monthStr,
|
||||
weekStr,
|
||||
dayStr,
|
||||
hourStr,
|
||||
minuteStr,
|
||||
secondStr,
|
||||
millisecondsStr,
|
||||
] = match;
|
||||
|
||||
const hasNegativePrefix = s[0] === '-';
|
||||
const negativeSeconds = !!secondStr && secondStr[0] === '-';
|
||||
|
||||
const maybeNegate = (num: number | undefined, force = false) =>
|
||||
num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num;
|
||||
|
||||
const parseFloating = (value: string) => {
|
||||
if (typeof value === 'undefined' || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
return parseFloat(value);
|
||||
};
|
||||
|
||||
const parseMillis = (fraction: string | undefined) => {
|
||||
// Return undefined (instead of 0) in these cases, where fraction is not set
|
||||
if (
|
||||
typeof fraction === 'undefined' ||
|
||||
fraction === null ||
|
||||
fraction === ''
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const f = parseFloat(`0.${fraction}`) * 1000;
|
||||
return Math.floor(f);
|
||||
};
|
||||
|
||||
const years = maybeNegate(parseFloating(yearStr));
|
||||
const months = maybeNegate(parseFloating(monthStr));
|
||||
const weeks = maybeNegate(parseFloating(weekStr));
|
||||
const days = maybeNegate(parseFloating(dayStr));
|
||||
const hours = maybeNegate(parseFloating(hourStr));
|
||||
const minutes = maybeNegate(parseFloating(minuteStr));
|
||||
const seconds = maybeNegate(parseFloating(secondStr), secondStr === '-0');
|
||||
const milliseconds = maybeNegate(
|
||||
parseMillis(millisecondsStr),
|
||||
negativeSeconds,
|
||||
);
|
||||
|
||||
if (
|
||||
years === undefined &&
|
||||
months === undefined &&
|
||||
weeks === undefined &&
|
||||
days === undefined &&
|
||||
hours === undefined &&
|
||||
minutes === undefined &&
|
||||
seconds === undefined &&
|
||||
milliseconds === undefined
|
||||
) {
|
||||
throw new Error('Invalid ISO format, no values given');
|
||||
}
|
||||
|
||||
return {
|
||||
...(years ? { years } : {}),
|
||||
...(months ? { months } : {}),
|
||||
...(weeks ? { weeks } : {}),
|
||||
...(days ? { days } : {}),
|
||||
...(hours ? { hours } : {}),
|
||||
...(minutes ? { minutes } : {}),
|
||||
...(seconds ? { seconds } : {}),
|
||||
...(milliseconds ? { milliseconds } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface Config {
|
||||
/**
|
||||
* The backstage token expiration.
|
||||
*/
|
||||
backstageTokenExpiration?: HumanDuration;
|
||||
backstageTokenExpiration?: HumanDuration | string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -143,7 +143,7 @@ export interface Config {
|
||||
/**
|
||||
* The backstage token expiration.
|
||||
*/
|
||||
backstageTokenExpiration?: HumanDuration;
|
||||
backstageTokenExpiration?: HumanDuration | string;
|
||||
};
|
||||
/**
|
||||
* Additional app origins to allow for authenticating
|
||||
|
||||
Vendored
+2
-2
@@ -157,9 +157,9 @@ export interface Config {
|
||||
/** Defer stitching to be performed asynchronously */
|
||||
mode: 'deferred';
|
||||
/** Polling interval for tasks in seconds */
|
||||
pollingInterval?: HumanDuration;
|
||||
pollingInterval?: HumanDuration | string;
|
||||
/** How long to wait for a stitch to complete before giving up in seconds */
|
||||
stitchTimeout?: HumanDuration;
|
||||
stitchTimeout?: HumanDuration | string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,15 +70,12 @@ export function stitchingStrategyFromConfig(config: Config): StitchingStrategy {
|
||||
const stitchTimeoutKey = 'catalog.stitchingStrategy.stitchTimeout';
|
||||
|
||||
const pollingInterval = config.has(pollingIntervalKey)
|
||||
? readDurationFromConfig(config, {
|
||||
key: pollingIntervalKey,
|
||||
})
|
||||
? readDurationFromConfig(config, { key: pollingIntervalKey })
|
||||
: { seconds: 1 };
|
||||
const stitchTimeout = config.has(stitchTimeoutKey)
|
||||
? readDurationFromConfig(config, {
|
||||
key: stitchTimeoutKey,
|
||||
})
|
||||
? readDurationFromConfig(config, { key: stitchTimeoutKey })
|
||||
: { seconds: 60 };
|
||||
|
||||
return {
|
||||
mode: 'deferred',
|
||||
pollingInterval: pollingInterval,
|
||||
|
||||
+4
-4
@@ -48,12 +48,12 @@ export interface Config {
|
||||
/**
|
||||
* (Optional) Visibility timeout for messages in flight.
|
||||
*/
|
||||
visibilityTimeout: HumanDuration;
|
||||
visibilityTimeout: HumanDuration | string;
|
||||
/**
|
||||
* (Optional) Wait time when polling for available messages.
|
||||
* Default: 20 seconds.
|
||||
*/
|
||||
waitTime: HumanDuration;
|
||||
waitTime: HumanDuration | string;
|
||||
};
|
||||
/**
|
||||
* (Optional) Timeout for the task execution which includes polling for messages
|
||||
@@ -62,12 +62,12 @@ export interface Config {
|
||||
*
|
||||
* Must be greater than `queue.waitTime` + `waitTimeAfterEmptyReceive`.
|
||||
*/
|
||||
timeout: HumanDuration;
|
||||
timeout: HumanDuration | string;
|
||||
/**
|
||||
* (Optional) Wait time before polling again if no message was received.
|
||||
* Default: 1 minute.
|
||||
*/
|
||||
waitTimeAfterEmptyReceive: HumanDuration;
|
||||
waitTimeAfterEmptyReceive: HumanDuration | string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { HumanDuration, JsonObject } from '@backstage/types';
|
||||
import { Config, readDurationFromConfig } from '@backstage/config';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const CONFIG_PREFIX_MODULE = 'events.modules.awsSqs.';
|
||||
@@ -39,7 +39,7 @@ function readOptionalHumanDuration(
|
||||
config: Config,
|
||||
key: string,
|
||||
): HumanDuration | undefined {
|
||||
return config.getOptional<JsonObject>(key) as HumanDuration;
|
||||
return config.has(key) ? readDurationFromConfig(config, { key }) : undefined;
|
||||
}
|
||||
|
||||
function readOptionalDuration(
|
||||
|
||||
+2
-2
@@ -99,7 +99,7 @@ export interface Config {
|
||||
/**
|
||||
* Throttle duration between email sending, defaults to 100ms
|
||||
*/
|
||||
throttleInterval?: HumanDuration;
|
||||
throttleInterval?: HumanDuration | string;
|
||||
/**
|
||||
* Configuration for broadcast notifications
|
||||
*/
|
||||
@@ -120,7 +120,7 @@ export interface Config {
|
||||
/**
|
||||
* Email cache TTL, defaults to 1 hour
|
||||
*/
|
||||
ttl?: HumanDuration;
|
||||
ttl?: HumanDuration | string;
|
||||
};
|
||||
filter?: {
|
||||
/**
|
||||
|
||||
+11
-7
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
NotificationProcessor,
|
||||
NotificationSendOptions,
|
||||
@@ -75,14 +76,17 @@ export class NotificationsEmailProcessor implements NotificationProcessor {
|
||||
this.replyTo = emailProcessorConfig.getOptionalString('replyTo');
|
||||
this.concurrencyLimit =
|
||||
emailProcessorConfig.getOptionalNumber('concurrencyLimit') ?? 2;
|
||||
const throttleConfig =
|
||||
emailProcessorConfig.getOptionalConfig('throttleInterval');
|
||||
this.throttleInterval = throttleConfig
|
||||
? durationToMilliseconds(readDurationFromConfig(throttleConfig))
|
||||
this.throttleInterval = emailProcessorConfig.has('throttleInterval')
|
||||
? durationToMilliseconds(
|
||||
readDurationFromConfig(emailProcessorConfig, {
|
||||
key: 'throttleInterval',
|
||||
}),
|
||||
)
|
||||
: 100;
|
||||
const cacheConfig = emailProcessorConfig.getOptionalConfig('cache.ttl');
|
||||
this.cacheTtl = cacheConfig
|
||||
? durationToMilliseconds(readDurationFromConfig(cacheConfig))
|
||||
this.cacheTtl = emailProcessorConfig.has('cache.ttl')
|
||||
? durationToMilliseconds(
|
||||
readDurationFromConfig(emailProcessorConfig, { key: 'cache.ttl' }),
|
||||
)
|
||||
: 3_600_000;
|
||||
this.frontendBaseUrl = config.getString('app.baseUrl');
|
||||
this.allowlistEmailAddresses = emailProcessorConfig.getOptionalStringArray(
|
||||
|
||||
+4
-4
@@ -26,6 +26,7 @@ export interface Config {
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The commit message used when new components are created.
|
||||
*/
|
||||
@@ -64,16 +65,15 @@ export interface Config {
|
||||
* be attempted to recover.
|
||||
*
|
||||
* If not specified, the default value is 5 seconds.
|
||||
*
|
||||
*/
|
||||
EXPERIMENTAL_recoverTasksTimeout?: HumanDuration;
|
||||
EXPERIMENTAL_recoverTasksTimeout?: HumanDuration | string;
|
||||
|
||||
/**
|
||||
* Makes sure to auto-expire and clean up things that time out or for other reasons should not be left lingering.
|
||||
*
|
||||
* By default, the frequency is every 5 minutes.
|
||||
*/
|
||||
taskTimeoutJanitorFrequency?: HumanDuration;
|
||||
taskTimeoutJanitorFrequency?: HumanDuration | string;
|
||||
|
||||
/**
|
||||
* Sets the task's heartbeat timeout, when to consider a task to be staled.
|
||||
@@ -82,6 +82,6 @@ export interface Config {
|
||||
*
|
||||
* Default value is 24 hours.
|
||||
*/
|
||||
taskTimeout?: HumanDuration;
|
||||
taskTimeout?: HumanDuration | string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user