Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
scratch
node_modules/
!test/mock/**/node_modules/
coverage/
lcov.info
node_modules/
scratch
28 changes: 24 additions & 4 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
# 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
- Add support for `timezone` property for scheduled functions

---

## [6.0.0] 2025-11-27

### Changed

- Breaking change: dropped Node.js 20 support
- Update to latest `@architect/utils`

---

## [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

---

## [4.0.9] 2025-04-26

### 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!

---

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@architect/inventory",
"version": "6.0.0",
"version": "6.1.0-RC.1",
"description": "Architect project resource enumeration utility",
"main": "src/index.js",
"scripts": {
"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=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"
},
Expand All @@ -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"
}
}
4 changes: 2 additions & 2 deletions src/config/pragmas/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
32 changes: 32 additions & 0 deletions src/config/pragmas/populate-lambda/_queues.js
Original file line number Diff line number Diff line change
@@ -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}`)
}
13 changes: 9 additions & 4 deletions src/config/pragmas/populate-lambda/_scheduled.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}`)
}
1 change: 0 additions & 1 deletion src/config/pragmas/populate-lambda/get-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/config/pragmas/populate-lambda/get-lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -14,10 +15,9 @@ 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
/* istanbul ignore else: clearer to be explicit here */
if (type === 'ws') return getWS(params)
}
10 changes: 7 additions & 3 deletions src/config/pragmas/populate-lambda/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')) {
Expand Down Expand Up @@ -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', '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] ] ] : [])
Expand Down
3 changes: 2 additions & 1 deletion src/config/pragmas/sort/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions src/config/pragmas/validate/_events.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
let { is } = require('../../../lib')
let { regex, size, unique } = require('./_lib')

/**
Expand Down Expand Up @@ -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}`)
}
}
})
}
}
19 changes: 18 additions & 1 deletion src/config/pragmas/validate/_scheduled.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ 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
size(name, 1, 242, '@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}`)
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions src/config/project/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/config/project/prefs/dotenv.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
13 changes: 8 additions & 5 deletions src/config/project/prefs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) ]
Expand All @@ -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)) {
Expand All @@ -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(' ')
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/defaults/function-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/env/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading