From d8004397a4ee29852522b6afa9da02af688989fd Mon Sep 17 00:00:00 2001 From: Simon MacDonald Date: Wed, 7 Jan 2026 15:26:27 -0500 Subject: [PATCH 01/10] feat: add timezone property to scheduled function Signed-off-by: Simon MacDonald --- .../pragmas/populate-lambda/_scheduled.js | 13 +- src/config/pragmas/populate-lambda/index.js | 2 +- src/config/pragmas/validate/_scheduled.js | 19 ++- .../unit/src/config/pragmas/scheduled-test.js | 139 +++++++++++++++++- 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/src/config/pragmas/populate-lambda/_scheduled.js b/src/config/pragmas/populate-lambda/_scheduled.js index 0211d7a4..94ccbe2e 100644 --- a/src/config/pragmas/populate-lambda/_scheduled.js +++ b/src/config/pragmas/populate-lambda/_scheduled.js @@ -33,12 +33,14 @@ module.exports = function populateScheduled (params) { let { item, errors, plugin } = params let rate = null let cron = null + let timezone = null if (plugin) { let { name, src } = item if (name && src && (item.rate || item.cron)) { if (item.rate) rate = get.rate(item.rate) if (item.cron) cron = get.cron(item.cron) - return { ...item, rate, cron, ...getLambdaDirs(params, { plugin }) } + if (item.timezone) timezone = item.timezone + return { ...item, rate, cron, timezone, ...getLambdaDirs(params, { plugin }) } } errors.push(`Invalid plugin-generated @scheduled item: name: ${name}, rate: ${item.rate}, cron: ${item.cron}, src: ${src}`) return @@ -57,12 +59,12 @@ module.exports = function populateScheduled (params) { if (isCron) cron = get.cron(clean(isCron)) let dirs = getLambdaDirs(params, { name }) - return { name, rate, cron, ...dirs } + return { name, rate, cron, timezone, ...dirs } } else if (is.object(item)) { let name = Object.keys(item)[0] - // Handle rate + cron props + // Handle rate + cron + timezone props if (item[name].rate) { let itemRate = item[name].rate let exp = is.array(itemRate) ? itemRate.join(' ') : itemRate @@ -73,9 +75,12 @@ module.exports = function populateScheduled (params) { let exp = is.array(itemCron) ? itemCron.join(' ') : itemCron cron = get.cron(exp) } + if (item[name].timezone) { + timezone = item[name].timezone + } let dirs = getLambdaDirs(params, { name, customSrc: item[name].src }) - return { name, rate, cron, ...dirs } + return { name, rate, cron, timezone, ...dirs } } errors.push(`Invalid @scheduled item: ${item}`) } diff --git a/src/config/pragmas/populate-lambda/index.js b/src/config/pragmas/populate-lambda/index.js index 702b63ad..f35c3318 100644 --- a/src/config/pragmas/populate-lambda/index.js +++ b/src/config/pragmas/populate-lambda/index.js @@ -170,7 +170,7 @@ function populate (type, pragma, inventory, errors, plugin) { let normalize = path => path.replace(/[\\\/]/g, sep) // Lambda setter plugins can technically return anything, so this ensures everything is tidy -let lambdaProps = [ 'cron', 'method', 'path', 'plugin', 'rate', 'route', 'table', 'type' ] +let lambdaProps = [ 'cron', 'method', 'path', 'plugin', 'rate', 'route', 'table', 'timezone', 'type' ] let configProps = [ ...Object.keys(defaultFunctionConfig()), 'fifo', 'views' ] let getKnownProps = (knownProps, raw = {}) => { let props = knownProps.flatMap(prop => is.defined(raw[prop]) ? [ [ prop, raw[prop] ] ] : []) diff --git a/src/config/pragmas/validate/_scheduled.js b/src/config/pragmas/validate/_scheduled.js index f1ab0c98..4790f586 100644 --- a/src/config/pragmas/validate/_scheduled.js +++ b/src/config/pragmas/validate/_scheduled.js @@ -14,7 +14,7 @@ module.exports = function validateScheduled (scheduled, errors) { unique(scheduled, '@scheduled', errors) scheduled.forEach(schedule => { - let { name, rate, cron } = schedule + let { name, rate, cron, timezone } = schedule regex(name, 'veryLooseName', '@scheduled', errors) // Assume 14 chars are taken up by resource naming in arc/package @@ -22,6 +22,7 @@ module.exports = function validateScheduled (scheduled, errors) { if (cron) validateCron(schedule, errors) if (rate) validateRate(schedule, errors) + if (timezone) validateTimezone(schedule, errors) if (!cron && !rate) errors.push(`Invalid @scheduled item (no cron or rate expression found): ${name}`) if (cron && rate) errors.push(`Invalid @scheduled item (use either cron or rate, not both): ${name}`) @@ -53,6 +54,22 @@ function validateCron (schedule, errors) { if (!year.toString().match(minHrYr)) expErr('year', year) } +function validateTimezone (schedule, errors) { + let { name, timezone } = schedule + if (!is.string(timezone) || !timezone.length) { + errors.push(`Invalid @scheduled item (timezone must be a non-empty string): ${name}`) + } + else { + // AWS EventBridge accepts IANA timezone identifiers (e.g., America/New_York, Europe/London) + try { + Intl.DateTimeFormat(undefined, { timeZone: timezone }) + } + catch { + errors.push(`Invalid @scheduled item (timezone must be a valid IANA timzone identifier): ${name}`) + } + } +} + let singular = [ 'minute', 'hour', 'day' ] let plural = [ 'minutes', 'hours', 'days' ] function validateRate (schedule, errors) { diff --git a/test/unit/src/config/pragmas/scheduled-test.js b/test/unit/src/config/pragmas/scheduled-test.js index 3579bdea..51521024 100644 --- a/test/unit/src/config/pragmas/scheduled-test.js +++ b/test/unit/src/config/pragmas/scheduled-test.js @@ -129,6 +129,45 @@ ${complexValues.join('\n')} }) }) +test('@scheduled population: complex format with timezone', t => { + t.plan(13) + + let tz = 'America/New_York' + let complexValues = [ + `${names[0]} + rate ${rate.expression} + src ${names[0]}/path + timezone ${tz}`, + `${names[1]} + cron ${cron.expression} + src ${names[1]}/path + timezone ${tz}`, + ] + let arc = parse(` +@scheduled +${complexValues.join('\n')} +`) + let scheduled = populateScheduled({ arc, inventory }) + t.assert.equal(scheduled.length, complexValues.length, 'Got correct number of scheduled events back') + names.forEach(name => { + t.assert.ok(scheduled.some(sched => sched.name === name), `Got scheduled event: ${name}`) + }) + scheduled.forEach(sched => { + t.assert.equal(sched.src, join(cwd, `${sched.name}/path`), `Scheduled event configured with correct source dir: ${sched.name}/path`) + t.assert.ok(sched.handlerFile.startsWith(join(cwd, `${sched.name}/path`)), `Handler file is in the correct source dir`) + t.assert.equal(sched.timezone, tz, `Got back correct timezone: ${tz}`) + if (sched.rate) { + t.assert.equal(str(rate), str(sched.rate), `Got back correct rate object: ${str(rate)}`) + t.assert.equal(sched.cron, null, `Got back null cron param`) + } + else if (sched.cron) { + t.assert.equal(str(cron), str(sched.cron), `Got back correct cron object: ${str(cron)}`) + t.assert.equal(sched.rate, null, `Got back null rate param`) + } + else t.assert.fail('Could not find rate or cron expression') + }) +}) + test('@scheduled population: complex format (JSON)', t => { t.plan(11) @@ -165,6 +204,46 @@ test('@scheduled population: complex format (JSON)', t => { }) }) +test('@scheduled population: complex format with timezone (JSON)', t => { + t.plan(13) + + let tz = 'America/New_York' + let json = { + 'scheduled': { + [names[0]]: { + rate: rate.expression, + src: `${names[0]}/path`, + timezone: tz, + }, + [names[1]]: { + cron: cron.expression, + src: `${names[1]}/path`, + timezone: tz, + }, + }, + } + let arc = parse.json(str(json)) + let scheduled = populateScheduled({ arc, inventory }) + t.assert.equal(scheduled.length, Object.keys(json.scheduled).length, 'Got correct number of scheduled events back') + names.forEach(name => { + t.assert.ok(scheduled.some(sched => sched.name === name), `Got scheduled event: ${name}`) + }) + scheduled.forEach(sched => { + t.assert.equal(sched.src, join(cwd, `${sched.name}/path`), `Scheduled event configured with correct source dir: ${sched.name}/path`) + t.assert.ok(sched.handlerFile.startsWith(join(cwd, `${sched.name}/path`)), `Handler file is in the correct source dir`) + t.assert.equal(sched.timezone, tz, `Got back correct timezone: ${tz}`) + if (sched.rate) { + t.assert.equal(str(rate), str(sched.rate), `Got back correct rate object: ${str(rate)}`) + t.assert.equal(sched.cron, null, `Got back null cron param`) + } + else if (sched.cron) { + t.assert.equal(str(cron), str(sched.cron), `Got back correct cron object: ${str(cron)}`) + t.assert.equal(sched.rate, null, `Got back null rate param`) + } + else t.assert.fail('Could not find rate or cron expression') + }) +}) + test('@scheduled population: complex format + fallback to default paths', t => { t.plan(11) @@ -230,8 +309,40 @@ test('@scheduled population: plugin setter', t => { }) }) +test('@scheduled population: plugin setter with timezone', t => { + t.plan(13) + + let tz = 'America/New_York' + let inventory = inventoryDefaults() + let setter = () => [ + { name: names[0], rate: rate.expression, src: join(scheduledDir, names[0]), timezone: tz }, + { name: names[1], cron: cron.expression, src: join(scheduledDir, names[1]), timezone: tz }, + ] + inventory.plugins = setterPluginSetup(setter) + + let scheduled = populateScheduled({ arc: {}, inventory }) + t.assert.equal(scheduled.length, values.length, 'Got correct number of scheduled events back') + names.forEach(name => { + t.assert.ok(scheduled.some(sched => sched.name === name), `Got scheduled event: ${name}`) + }) + scheduled.forEach(sched => { + t.assert.equal(sched.src, join(scheduledDir, sched.name), `Scheduled event configured with correct source dir: ${sched.src}`) + t.assert.ok(sched.handlerFile.startsWith(sched.src), `Handler file is in the correct source dir`) + t.assert.equal(sched.timezone, tz, `Got back correct timezone: ${tz}`) + if (sched.rate) { + t.assert.equal(str(rate), str(sched.rate), `Got back correct rate object: ${str(rate)}`) + t.assert.equal(sched.cron, null, `Got back null cron param`) + } + else if (sched.cron) { + t.assert.equal(str(cron), str(sched.cron), `Got back correct cron object: ${str(cron)}`) + t.assert.equal(sched.rate, null, `Got back null rate param`) + } + else t.assert.fail('Could not find rate or cron expression') + }) +}) + test('@scheduled population: validation errors', t => { - t.plan(27) + t.plan(30) let errors = [] function run (str) { let arc = parse(`@scheduled\n${str}`) @@ -267,6 +378,16 @@ test('@scheduled population: validation errors', t => { run(`hi cron(/ / / / * /)`) run(`hi cron(* * L * L *)`) run(`hi cron(* * W * "#" *)`) + // Valid timezone + run(`hi + rate 1 day + timezone America/New_York`) + run(`hi + rate 1 day + timezone Europe/London`) + run(`hi + rate 1 day + timezone UTC`) t.assert.equal(errors.length, 0, `Valid scheduled did not error`) // Errors @@ -350,6 +471,22 @@ test('@scheduled population: validation errors', t => { run(`hi`) check() + + // Invalid timezone errors + run(`hi + rate 1 day + timezone Gallifrey/Capitol`) + check('Invalid timezone errored') + + run(`hi + rate 1 day + timezone NotATimezone`) + check('Invalid timezone errored') + + run(`hi + rate 1 day + timezone 123`) + check('Invalid timezone errored') }) test('@scheduled population: plugin errors', t => { From cbdb8aff9ee8e34c2ccec4409555af6c22e75139 Mon Sep 17 00:00:00 2001 From: Simon MacDonald Date: Wed, 7 Jan 2026 15:33:20 -0500 Subject: [PATCH 02/10] doc: update changelog Signed-off-by: Simon MacDonald --- changelog.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 52a43861..2413cdc7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,24 @@ # Architect Inventory changelog +--- +## [6.1.0] 2026-01-07 + +- Add support for `timezone` property for scheduled functions + +### Changed + +- Update to latest `@architect/utils` + +--- +## [6.0.0] 2025-11-27 + +### Changed + +- Update to latest `@architect/utils` + --- ## [5.0.0] 2025-09-24 - + ### Changed - Breaking change: dropped node 16, 18 support @@ -15,7 +31,7 @@ ### Fixed -- Error out if imported plugin has no discernable plugin API implementation; added in [#83](https://github.com/architect/inventory/pull/83) by @andybee, thanks! +- Error out if imported plugin has no discernable plugin API implementation; added in [#83](https://github.com/architect/inventory/pull/83) by @andybee, thanks! --- From 29a4e7fae5d43725c0759f1ee116ed9c92b742cc Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 7 Jan 2026 15:46:57 -0800 Subject: [PATCH 03/10] Fix: tests, reestablish code coverage thresholds Chore: tidy up coverage ignore comments --- package.json | 4 ++-- src/config/pragmas/plugins.js | 4 ++-- src/config/pragmas/populate-lambda/get-handler.js | 1 - src/config/pragmas/populate-lambda/get-lambda.js | 1 - src/config/pragmas/sort/http.js | 3 ++- src/config/project/index.js | 2 -- src/config/project/prefs/dotenv.js | 2 +- src/config/project/prefs/index.js | 13 ++++++++----- src/env/index.js | 2 +- src/index.js | 2 -- src/lib/asap-src.js | 2 +- 11 files changed, 17 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index e3d88400..5c9bcd70 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "lint": "eslint . --fix", "test": "npm run lint && npm run test:integration && npm run test:coverage", "test:integration": "node --test --test-reporter=spec test/integration/*-test.js", - "test:coverage": "node --test --test-reporter=spec --experimental-test-coverage test/unit/**/*-test.js", - "test:unit": "node --test --test-reporter=spec test/unit/**/*-test.js", + "test:coverage": "node --test --test-reporter=spec --experimental-test-coverage --test-coverage-lines=100 --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js'", + "test:unit": "node --test --test-reporter=spec --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js'", "rc": "npm version prerelease --preid RC", "vendor": "scripts/vendor" }, diff --git a/src/config/pragmas/plugins.js b/src/config/pragmas/plugins.js index 0819d56b..68f3b094 100644 --- a/src/config/pragmas/plugins.js +++ b/src/config/pragmas/plugins.js @@ -53,7 +53,6 @@ module.exports = async function getPluginModules ({ arc, inventory, errors }) { if (pluginPath) { try { - /* istanbul ignore next: idk why but for some reason nyc isn't picking up the catches; all cases are covered in tests, though! */ if (type === 'plugin') { try { plugins[name] = require(pluginPath) @@ -167,7 +166,8 @@ async function resolve (path, cwd) { catch { return } - /* istanbul ignore next: idk why but for some reason nyc isn't picking up the catches; all cases are covered in tests, though! */ + // idk why but for some reason we aren't picking up the catches; all cases are covered in tests, though! + /* node:coverage ignore next 2 */ if (gotSomething) return mjsPath else return } diff --git a/src/config/pragmas/populate-lambda/get-handler.js b/src/config/pragmas/populate-lambda/get-handler.js index fc8c7c65..6f8114e4 100644 --- a/src/config/pragmas/populate-lambda/get-handler.js +++ b/src/config/pragmas/populate-lambda/get-handler.js @@ -22,7 +22,6 @@ module.exports = function getHandler ({ config, src, build, errors }) { } // Compiled to a binary else if (customRuntimeType === 'compiled') { - /* istanbul ignore next */ let bootstrap = `bootstrap${isWin ? '.exe' : ''}` handlerFile = join(build, runtimeConfig.buildSubpath || '', runtimeConfig.handlerFile || bootstrap) handlerMethod = null diff --git a/src/config/pragmas/populate-lambda/get-lambda.js b/src/config/pragmas/populate-lambda/get-lambda.js index 21edd396..5c2ca5db 100644 --- a/src/config/pragmas/populate-lambda/get-lambda.js +++ b/src/config/pragmas/populate-lambda/get-lambda.js @@ -18,6 +18,5 @@ module.exports = function getLambda (params) { if (type === 'scheduled') return getScheduled(params) if (type === ts) return getTablesStreams(params) if (type === 'tables') return getTablesStreams(params) // Shortcut for creating streams - /* istanbul ignore else: clearer to be explicit here */ if (type === 'ws') return getWS(params) } diff --git a/src/config/pragmas/sort/http.js b/src/config/pragmas/sort/http.js index 2baeec06..969c9265 100644 --- a/src/config/pragmas/sort/http.js +++ b/src/config/pragmas/sort/http.js @@ -31,7 +31,8 @@ module.exports = function sortHTTP (http) { let sorted = [] httpMethods.forEach(method => { if (!tree[method]) return - /* istanbul ignore next: random test shuffles may not trigger all paths */ + // random test shuffles may not trigger all paths + /* node:coverage ignore next */ tree[method] // Sort by depth .sort((a, b) => b.depth - a.depth) diff --git a/src/config/project/index.js b/src/config/project/index.js index e970e243..29f5c6ac 100644 --- a/src/config/project/index.js +++ b/src/config/project/index.js @@ -33,7 +33,6 @@ module.exports = function getProjectConfig (params) { _project[`${scope}PreferencesFile`] = p.preferencesFile // Build out the final preferences - /* istanbul ignore else: jic */ if (!_project.preferences) _project.preferences = {} Object.keys(p.preferences).forEach(pragma => { // Ignore the raw data @@ -44,7 +43,6 @@ module.exports = function getProjectConfig (params) { return } // Traverse and merge individual settings - /* istanbul ignore else: jic */ if (!_project.preferences[pragma]) _project.preferences[pragma] = {} Object.entries(p.preferences[pragma]).forEach(([ setting, value ]) => { _project.preferences[pragma][setting] = value diff --git a/src/config/project/prefs/dotenv.js b/src/config/project/prefs/dotenv.js index 1c5686ed..d31a45e1 100644 --- a/src/config/project/prefs/dotenv.js +++ b/src/config/project/prefs/dotenv.js @@ -1,7 +1,7 @@ // Copyright (c) 2015, Scott Motte // All rights reserved. -/* istanbul ignore file */ +/* node:coverage disable */ /* eslint-disable */ // node_modules/dotenv/lib/main.js var fs = require("fs"); diff --git a/src/config/project/prefs/index.js b/src/config/project/prefs/index.js index 85141869..e4fecd28 100644 --- a/src/config/project/prefs/index.js +++ b/src/config/project/prefs/index.js @@ -25,13 +25,14 @@ module.exports = function getPrefs ({ scope, inventory, errors }) { // Arc outputs an object of nested arrays // Basically, construct a pared-down intermediate prefs obj for consumers Object.entries(prefs.arc).forEach(([ key, val ]) => { - /* istanbul ignore else: Parser should get this, but jic */ + // Parser should get this, but jic ignore the else - except node test doesn't do ignore else if (!preferences[key]) preferences[key] = {} - /* istanbul ignore else: Parser should only produce arrays, but jic */ + // Parser should only produce arrays, but jic + /* node:coverage ignore next */ if (is.array(val)) { val.forEach(v => { if (is.array(v)) { - /* istanbul ignore if: Single vals should be strings, but jic */ + // Single vals should be strings, but jic - except node test doesn't do ignore if if (v.length === 1) preferences[key] = v[0] if (v.length === 2) preferences[key][v[0]] = v[1] if (v.length > 2) preferences[key][v[0]] = [ ...v.slice(1) ] @@ -46,7 +47,8 @@ module.exports = function getPrefs ({ scope, inventory, errors }) { // Turn env vars with spaces into strings if (key === 'env') { [ 'testing', 'staging', 'production' ].forEach(e => { - /* istanbul ignore else: Yet another jic */ + // Yet another jic + /* node:coverage ignore next */ if (preferences.env[e]) { Object.entries(preferences.env[e]).forEach(([ key, val ]) => { if (!valid.envVar.test(key)) { @@ -62,7 +64,8 @@ module.exports = function getPrefs ({ scope, inventory, errors }) { if (key === 'sandbox-start' || key === 'sandbox-startup') { preferences[key] = val.map(v => { if (is.string(v)) return v - /* istanbul ignore else: Yet another jic */ + // Yet another jic + /* node:coverage ignore next */ if (is.array(v)) return v.join(' ') }) } diff --git a/src/env/index.js b/src/env/index.js index 25e6f6b3..e9ac64d7 100644 --- a/src/env/index.js +++ b/src/env/index.js @@ -13,7 +13,7 @@ module.exports = function env (params, inventory, callback) { let { profile, region } = inventory.aws let result = [] let awsLite = require('@aws-lite/client') - /* istanbul ignore next */ + /* node:coverage ignore next */ awsLite({ profile, region, plugins: [ import('@aws-lite/ssm') ] }).then(_aws => { aws = _aws diff --git a/src/index.js b/src/index.js index c3c0e1f9..5e64ac3a 100644 --- a/src/index.js +++ b/src/index.js @@ -67,7 +67,6 @@ module.exports = function architectInventory (params = {}, callback) { // @plugins come first, as they register hooks all around the project plugins(project, (err, result) => { - /* istanbul ignore next: yeah we know what happens here */ if (err) callback(err) else { inventory.plugins = result @@ -102,7 +101,6 @@ module.exports = function architectInventory (params = {}, callback) { // Maybe get env vars getEnv(params, inventory, function done (err) { - /* istanbul ignore next: yeah we know what happens here */ if (err) callback(err) else { callback(null, { diff --git a/src/lib/asap-src.js b/src/lib/asap-src.js index 009c9ec9..223f9588 100644 --- a/src/lib/asap-src.js +++ b/src/lib/asap-src.js @@ -19,8 +19,8 @@ module.exports = function asapSrc (params = {}) { try { return require.resolve('@architect/asap') } + /* node:coverage ignore next 3 */ catch { - /* istanbul ignore next */ throw Error('Cannot find ASAP module!') } } From f90affbb4145dfa46cd2327b4f77ada57bfe2564 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 7 Jan 2026 15:51:41 -0800 Subject: [PATCH 04/10] Chore: Update default function runtime to Node.js 22.x --- src/defaults/function-config.js | 2 +- test/unit/src/config/pragmas/populate-lambda/index-test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/defaults/function-config.js b/src/defaults/function-config.js index 769bdd56..c8ecb8bb 100644 --- a/src/defaults/function-config.js +++ b/src/defaults/function-config.js @@ -5,7 +5,7 @@ module.exports = function createDefaultFunctionConfig () { return { timeout: 5, memory: 1152, - runtime: 'nodejs20.x', + runtime: 'nodejs22.x', architecture: 'arm64', handler: 'index.handler', state: 'n/a', diff --git a/test/unit/src/config/pragmas/populate-lambda/index-test.js b/test/unit/src/config/pragmas/populate-lambda/index-test.js index 9e6df31d..e1c5d1e4 100644 --- a/test/unit/src/config/pragmas/populate-lambda/index-test.js +++ b/test/unit/src/config/pragmas/populate-lambda/index-test.js @@ -298,7 +298,7 @@ custom setting modified = { timeout: 10, memory: 128, - runtime: 'nodejs20.x', + runtime: 'nodejs22.x', handler: 'index.handler', custom: 'setting', } @@ -323,7 +323,7 @@ custom setting modified = { timeout: 10, memory: 128, - runtime: 'nodejs20.x', + runtime: 'nodejs22.x', handler: 'lambda.handler', custom: 'setting', } From db0b882bac556a38179129f36e7b8dd5cafe68a3 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Wed, 7 Jan 2026 21:09:21 -0800 Subject: [PATCH 05/10] Feat: Add `batchSize`, `batchWindow` support to `@queues` Ensure `fifo`, batchSize`, `batchWindow` are top-level `@queues` semantics properties --- src/config/pragmas/populate-lambda/_queues.js | 32 ++++++++++ .../pragmas/populate-lambda/get-lambda.js | 3 +- src/config/pragmas/populate-lambda/index.js | 10 +++- src/config/pragmas/validate/_events.js | 14 +++++ test/unit/src/config/pragmas/queues-test.js | 58 ++++++++++++++++++- 5 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/config/pragmas/populate-lambda/_queues.js diff --git a/src/config/pragmas/populate-lambda/_queues.js b/src/config/pragmas/populate-lambda/_queues.js new file mode 100644 index 00000000..40ce8b1c --- /dev/null +++ b/src/config/pragmas/populate-lambda/_queues.js @@ -0,0 +1,32 @@ +let { is, getLambdaDirs } = require('../../../lib') + +function getQueueBehaviorProps (item, name) { + let fifo = is.defined(item?.[name]?.fifo) ? item[name].fifo : null + let batchSize = is.defined(item?.[name]?.batchSize) ? item[name].batchSize : null + let batchWindow = is.defined(item?.[name]?.batchWindow) ? item[name].batchWindow : null + return { fifo, batchSize, batchWindow } +} + +module.exports = function populateQueues (params) { + let { type, item, errors, plugin } = params + + if (plugin) { + let { name, src } = item + if (name && src) { + return { ...item, ...getQueueBehaviorProps(item, name), ...getLambdaDirs(params, { plugin }) } + } + errors.push(`Invalid plugin-generated @${type} item: name: ${name}, src: ${src}`) + return + } + else if (is.string(item)) { + let name = item + let dirs = getLambdaDirs(params, { name }) + return { name, ...getQueueBehaviorProps(item, name), ...dirs } + } + else if (is.object(item)) { + let name = Object.keys(item)[0] + let dirs = getLambdaDirs(params, { name, customSrc: item[name].src }) + return { name, ...getQueueBehaviorProps(item, name), ...dirs } + } + errors.push(`Invalid @${type} item: ${item}`) +} diff --git a/src/config/pragmas/populate-lambda/get-lambda.js b/src/config/pragmas/populate-lambda/get-lambda.js index 5c2ca5db..6d2a9063 100644 --- a/src/config/pragmas/populate-lambda/get-lambda.js +++ b/src/config/pragmas/populate-lambda/get-lambda.js @@ -2,6 +2,7 @@ let getHTTP = require('./_http') let getEvents = require('./_events') let getCustomLambdas = require('./_custom-lambdas') +let getQueues = require('./_queues') let getScheduled = require('./_scheduled') let getWS = require('./_ws') let getTablesStreams = require('./_tables-streams') @@ -14,7 +15,7 @@ module.exports = function getLambda (params) { if (type === 'http') return getHTTP(params) if (type === 'events') return getEvents(params) if (type === cl) return getCustomLambdas(params) - if (type === 'queues') return getEvents(params) // Effectively the same as events + if (type === 'queues') return getQueues(params) if (type === 'scheduled') return getScheduled(params) if (type === ts) return getTablesStreams(params) if (type === 'tables') return getTablesStreams(params) // Shortcut for creating streams diff --git a/src/config/pragmas/populate-lambda/index.js b/src/config/pragmas/populate-lambda/index.js index 702b63ad..4cf0fac3 100644 --- a/src/config/pragmas/populate-lambda/index.js +++ b/src/config/pragmas/populate-lambda/index.js @@ -90,9 +90,13 @@ function populate (type, pragma, inventory, errors, plugin) { let config = defaultProjectConfig() config = { ...config, ...getKnownProps(configProps, result.config) } - // Knock out any pragma-specific early + // Knock out any pragma-specific bits early if (type === 'queues') { - config.fifo = config.fifo === undefined ? true : config.fifo + // Queues lifted up FIFO out of config and into top-level function semantics + // config.fifo remains for backward compat (until we want to make a breaking change), while also allowing queue functions to respect @aws global overrides + if (!is.nullish(result.fifo)) config.fifo = result.fifo + else if ((!is.nullish(config.fifo))) result.fifo = config.fifo + else config.fifo = result.fifo = true } if (type === 'http') { if (name.startsWith('get ') || name.startsWith('any ')) { @@ -170,7 +174,7 @@ function populate (type, pragma, inventory, errors, plugin) { let normalize = path => path.replace(/[\\\/]/g, sep) // Lambda setter plugins can technically return anything, so this ensures everything is tidy -let lambdaProps = [ 'cron', 'method', 'path', 'plugin', 'rate', 'route', 'table', 'type' ] +let lambdaProps = [ 'cron', 'batchSize', 'batchWindow', 'fifo', 'method', 'path', 'plugin', 'rate', 'route', 'table', 'type' ] let configProps = [ ...Object.keys(defaultFunctionConfig()), 'fifo', 'views' ] let getKnownProps = (knownProps, raw = {}) => { let props = knownProps.flatMap(prop => is.defined(raw[prop]) ? [ [ prop, raw[prop] ] ] : []) diff --git a/src/config/pragmas/validate/_events.js b/src/config/pragmas/validate/_events.js index 462dfd5f..cfdb5032 100644 --- a/src/config/pragmas/validate/_events.js +++ b/src/config/pragmas/validate/_events.js @@ -1,3 +1,4 @@ +let { is } = require('../../../lib') let { regex, size, unique } = require('./_lib') /** @@ -38,6 +39,19 @@ module.exports = function validateEventsAndQueues (pragma, pragmaName, errors) { if (n.startsWith('aws') || n.startsWith('amazon')) { errors.push(`Invalid ${pragmaName} item (cannot start with 'AWS' or 'Amazon'): ${name}`) } + + if (event.pragma === 'queues') { + let { fifo, batchSize, batchWindow } = event + if (!is.nullish(fifo) && !is.bool(fifo)) { + errors.push(`Invalid ${pragmaName} item (fifo must be a boolean): ${name}`) + } + if (!is.nullish(batchSize) && !is.number(batchSize)) { + errors.push(`Invalid ${pragmaName} item (batchSize must be a number): ${name}`) + } + if (!is.nullish(batchWindow) && !is.number(batchWindow)) { + errors.push(`Invalid ${pragmaName} item (batchWindow must be a number): ${name}`) + } + } }) } } diff --git a/test/unit/src/config/pragmas/queues-test.js b/test/unit/src/config/pragmas/queues-test.js index 22b43801..e15d0257 100644 --- a/test/unit/src/config/pragmas/queues-test.js +++ b/test/unit/src/config/pragmas/queues-test.js @@ -1,13 +1,15 @@ let { join } = require('node:path') let parse = require('@architect/parser') let { test } = require('node:test') -let cwd = process.cwd() let inventoryDefaults = require('../../../../../src/defaults') let populateQueues = require('../../../../../src/config/pragmas/queues') +let testLib = require('../../../../lib') +let cwd = process.cwd() let inventory = inventoryDefaults() let queuesDir = join(cwd, 'src', 'queues') let values = [ 'foo', 'bar' ] +let setterPluginSetup = testLib.setterPluginSetup.bind({}, 'queues') test('Set up env', t => { t.plan(1) @@ -53,6 +55,27 @@ ${values[0]} }) }) +test('@queues: fifo, batchSize, batchWindow', t => { + t.plan(3) + let arc + let queues + + arc = parse(` +@queues +${values[0]} + fifo false + batchSize 1 + batchWindow 2 +`) + queues = populateQueues({ arc, inventory }) + queues.forEach(queue => { + let { fifo, batchSize, batchWindow } = queue + t.assert.equal(fifo, false, `Queue fifo is correct`) + t.assert.equal(batchSize, 1, `Queue batchSize is correct`) + t.assert.equal(batchWindow, 2, `Queue batchWindow is correct`) + }) +}) + test('@queues population: simple format', t => { t.plan(7) @@ -122,8 +145,30 @@ ${complexValues.join('\n')} }) }) -test('@queues population: validation errors', t => { +test('@queues population: plugin setter', t => { t.plan(13) + + let inventory = inventoryDefaults() + let setter = () => values.map(v => ({ name: v, src: join(queuesDir, v) })) + inventory.plugins = setterPluginSetup(setter) + + let queues = populateQueues({ arc: {}, inventory }) + t.assert.equal(queues.length, values.length, 'Got correct number of queues back') + values.forEach(val => { + t.assert.ok(queues.some(queue => queue.name === val), `Got queue: ${val}`) + }) + queues.forEach(queue => { + let { handlerFile, name, src, fifo, batchSize, batchWindow } = queue + t.assert.equal(src, join(queuesDir, name), `Queue configured with correct source dir: ${src}`) + t.assert.ok(handlerFile.startsWith(src), `Handler file is in the correct source dir`) + t.assert.equal(fifo, true, `Queue defaults to correct fifo`) + t.assert.equal(batchSize, null, `Queue defaults to correct batchSize`) + t.assert.equal(batchWindow, null, `Queue defaults to correct batchWindow`) + }) +}) + +test('@queues population: validation errors', t => { + t.plan(16) // Test assumes complex format is outputting the same data as simple, so we're only testing errors in the simple format let errors = [] function run (str) { @@ -185,4 +230,13 @@ hi run(`Amazon.hi-there`) check() + + run(`a-queue\n fifo hi`) + check() + + run(`a-queue\n batchSize hi`) + check() + + run(`a-queue\n batchWindow hi`) + check() }) From faf531fc40e43cd58b29124194cea8d6ed24d3d7 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 8 Jan 2026 13:20:31 -0800 Subject: [PATCH 06/10] Update changelog --- changelog.md | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 52a43861..f2dcc48d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,31 @@ # Architect Inventory changelog --- + +## [6.1.0] 2026-01-08 + +### Added + +- Added `batchSize`, `batchWindow` support to `@queues` + - Ensure `fifo`, `batchSize`, `batchWindow` are top-level `@queues` semantics properties + +--- + +## [6.0.0] 2026-01-08 + +### Changed + +- Breaking change: dropped Node.js 20 support + +--- + ## [5.0.0] 2025-09-24 - + ### Changed -- Breaking change: dropped node 16, 18 support +- Breaking change: dropped Node.js 16, 18 support - Updated deps -- Moved to node native test runner +- Moved to Node.js native test runner --- @@ -15,7 +33,7 @@ ### Fixed -- Error out if imported plugin has no discernable plugin API implementation; added in [#83](https://github.com/architect/inventory/pull/83) by @andybee, thanks! +- Error out if imported plugin has no discernable plugin API implementation; added in [#83](https://github.com/architect/inventory/pull/83) by @andybee, thanks! --- From f1bad746264bbaf993bbfcb871e7a7a3f468d2ff Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 8 Jan 2026 13:21:36 -0800 Subject: [PATCH 07/10] 6.1.0-RC.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c9bcd70..28c2be59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/inventory", - "version": "6.0.0", + "version": "6.1.0-RC.0", "description": "Architect project resource enumeration utility", "main": "src/index.js", "scripts": { From b9314203a7284339fb403883d4e564fe33b27373 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 8 Jan 2026 14:28:09 -0800 Subject: [PATCH 08/10] Minor bits --- package.json | 2 +- src/config/pragmas/validate/_events.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 28c2be59..58ce7a9a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "npm run lint && npm run test:integration && npm run test:coverage", "test:integration": "node --test --test-reporter=spec test/integration/*-test.js", "test:coverage": "node --test --test-reporter=spec --experimental-test-coverage --test-coverage-lines=100 --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js'", - "test:unit": "node --test --test-reporter=spec --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js'", + "test:unit": "node --test --test-reporter=spec 'test/unit/**/*-test.js'", "rc": "npm version prerelease --preid RC", "vendor": "scripts/vendor" }, diff --git a/src/config/pragmas/validate/_events.js b/src/config/pragmas/validate/_events.js index cfdb5032..4edf3b40 100644 --- a/src/config/pragmas/validate/_events.js +++ b/src/config/pragmas/validate/_events.js @@ -43,13 +43,13 @@ module.exports = function validateEventsAndQueues (pragma, pragmaName, errors) { if (event.pragma === 'queues') { let { fifo, batchSize, batchWindow } = event if (!is.nullish(fifo) && !is.bool(fifo)) { - errors.push(`Invalid ${pragmaName} item (fifo must be a boolean): ${name}`) + errors.push(`Invalid ${pragmaName} item (fifo must be a boolean): ${name}`) } if (!is.nullish(batchSize) && !is.number(batchSize)) { - errors.push(`Invalid ${pragmaName} item (batchSize must be a number): ${name}`) + errors.push(`Invalid ${pragmaName} item (batchSize must be a number): ${name}`) } if (!is.nullish(batchWindow) && !is.number(batchWindow)) { - errors.push(`Invalid ${pragmaName} item (batchWindow must be a number): ${name}`) + errors.push(`Invalid ${pragmaName} item (batchWindow must be a number): ${name}`) } } }) From 3ea15275f6c2209904fc5ac8c3a3cf5930e9b3e4 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 8 Jan 2026 15:13:42 -0800 Subject: [PATCH 09/10] 6.1.0-RC.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58ce7a9a..39d4697b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@architect/inventory", - "version": "6.1.0-RC.0", + "version": "6.1.0-RC.1", "description": "Architect project resource enumeration utility", "main": "src/index.js", "scripts": { From 7b9dacaa66996348e8f23c812d183f73a8f4e893 Mon Sep 17 00:00:00 2001 From: Ryan Block Date: Thu, 8 Jan 2026 15:28:36 -0800 Subject: [PATCH 10/10] Chore: Restore HTML code coverage from lcov output, thanks @filmaj! --- .gitignore | 6 ++++-- package.json | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4b3dcffa..0f67a96d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -scratch -node_modules/ !test/mock/**/node_modules/ +coverage/ +lcov.info +node_modules/ +scratch diff --git a/package.json b/package.json index 39d4697b..8fa114c0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "eslint . --fix", "test": "npm run lint && npm run test:integration && npm run test:coverage", "test:integration": "node --test --test-reporter=spec test/integration/*-test.js", - "test:coverage": "node --test --test-reporter=spec --experimental-test-coverage --test-coverage-lines=100 --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js'", + "test:coverage": "node --test --test-reporter=lcov --test-reporter-destination=lcov.info --experimental-test-coverage --test-coverage-lines=100 --test-coverage-exclude='test/**/*' 'test/unit/**/*-test.js' && npx lcoview lcov.info -q -s . -d coverage", "test:unit": "node --test --test-reporter=spec 'test/unit/**/*-test.js'", "rc": "npm version prerelease --preid RC", "vendor": "scripts/vendor" @@ -31,6 +31,7 @@ "@architect/eslint-config": "~3.0.0", "dotenv": "~17.2.2", "eslint": "~9.36.0", + "lcoview": "~1.1.1", "mock-tmp": "~0.0.4" } }