@architect/inventory 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## [2.2.0] 2021-11-16
6
+
7
+ ### Added
8
+
9
+ - Finally formalized `@tables-streams`, the fully customizable successor to `@tables` with `stream true`
10
+ - Added `@tables-indexes` pragma
11
+ - `@tables-indexes` has identical semantics as (and will eventually supersede) `@indexes`
12
+ - Until Arc 10.0 + Inventory 3.0, consumers should now check both `inv.indexes` AND `inv.tables-indexes`
13
+
14
+ ---
15
+
16
+ ## [2.1.3] 2021-11-04
17
+
18
+ ### Fixed
19
+
20
+ - Hardened runtime validation by ensuring non-string values will fail gracefully
21
+
22
+ ---
23
+
24
+ ## [2.1.2] 2021-10-28
25
+
26
+ ### Added
27
+
28
+ - Added memory / timeout configuration validation
29
+
30
+
31
+ ### Changed
32
+
33
+ - Improved layer validation error formatting
34
+
35
+ ---
36
+
5
37
  ## [2.1.1] 2021-10-13
6
38
 
7
39
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@architect/inventory",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Architect project resource enumeration utility",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -23,21 +23,28 @@
23
23
  "@architect/asap": "~4.1.0",
24
24
  "@architect/parser": "~5.0.2",
25
25
  "@architect/utils": "~3.0.4",
26
- "lambda-runtimes": "~1.0.1"
26
+ "lambda-runtimes": "~1.1.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@architect/eslint-config": "~2.0.1",
30
30
  "aws-sdk": "2.880.0",
31
31
  "aws-sdk-mock": "~5.4.0",
32
32
  "cross-env": "~7.0.3",
33
- "eslint": "~8.0.1",
34
- "mock-fs": "~5.1.1",
33
+ "eslint": "~8.2.0",
34
+ "mock-fs": "~5.1.2",
35
35
  "mock-require": "~3.0.3",
36
36
  "nyc": "~15.1.0",
37
37
  "tap-spec": "^5.0.0",
38
- "tape": "^5.3.1"
38
+ "tape": "^5.3.2"
39
39
  },
40
40
  "eslintConfig": {
41
41
  "extends": "@architect/eslint-config"
42
+ },
43
+ "nyc": {
44
+ "check-coverage": true,
45
+ "branches": 100,
46
+ "lines": 100,
47
+ "functions": 100,
48
+ "statements": 100
42
49
  }
43
50
  }
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- [<img src="https://s3-us-west-2.amazonaws.com/arc.codes/architect-logo-500b@2x.png" width=500>](https://www.npmjs.com/package/@architect/inventory)
1
+ [<img src="https://assets.arc.codes/architect-logo-500b@2x.png" width=500>](https://www.npmjs.com/package/@architect/inventory)
2
2
 
3
3
  ## [`@architect/inventory`](https://www.npmjs.com/package/@architect/inventory)
4
4
 
@@ -1,7 +1,7 @@
1
1
  let { all: allPragmas } = require('../../lib/pragmas')
2
2
 
3
3
  // Get all pragmas except special cases
4
- let isSpecial = p => p === 'shared' || p === 'views'
4
+ let isSpecial = p => [ 'shared', 'views' ].includes(p)
5
5
  let visitors = allPragmas.map(p => {
6
6
  // eslint-disable-next-line
7
7
  if (!isSpecial(p)) return require(`./${p}`)
@@ -21,6 +21,9 @@ module.exports = function configureArcPragmas ({ arc, inventory }, errors) {
21
21
  visitors.forEach(visitor => {
22
22
  // Expects pragma visitors to have function name of: `configure${pragma}`
23
23
  let name = visitor.name.replace('configure', '').toLowerCase()
24
+ // Special cases for dasherization
25
+ if (name === 'tablesindexes') name = 'tables-indexes'
26
+ if (name === 'tablesstreams') name = 'tables-streams'
24
27
  pragmas[name] = visitor({ arc, inventory, errors })
25
28
  })
26
29
 
@@ -1,5 +1,5 @@
1
- let is = require('../../lib/is')
2
1
  let validate = require('./validate')
2
+ let { getIndexes } = require('./tables-indexes')
3
3
 
4
4
  module.exports = function configureIndexes ({ arc, errors }) {
5
5
  if (!arc.indexes || !arc.indexes.length) return null
@@ -7,43 +7,12 @@ module.exports = function configureIndexes ({ arc, errors }) {
7
7
  errors.push(`Specifying @indexes requires specifying corresponding @tables`)
8
8
  return null
9
9
  }
10
+ if (arc['tables-indexes']?.length && arc.indexes?.length) {
11
+ errors.push(`Either @tables-indexes or @indexes can be specified, but not both`)
12
+ return null
13
+ }
10
14
 
11
- let isCustomName = key => is.string(key) && key.toLowerCase() === 'name'
12
- function error (item) { errors.push(`Invalid @indexes item: ${item}`) }
13
-
14
- let indexes = arc.indexes.map(index => {
15
- if (is.object(index)) {
16
- let name = Object.keys(index)[0]
17
- let partitionKey = null
18
- let partitionKeyType = null
19
- let sortKey = null
20
- let sortKeyType = null
21
- let indexName = null
22
- Object.entries(index[name]).forEach(([ key, value ]) => {
23
- if (is.sortKey(value)) {
24
- sortKey = key
25
- sortKeyType = value.replace('**', '')
26
- }
27
- else if (is.primaryKey(value)) {
28
- partitionKey = key
29
- partitionKeyType = value.replace('*', '')
30
- }
31
- else if (isCustomName(key)) {
32
- indexName = value
33
- }
34
- })
35
- return {
36
- indexName,
37
- name,
38
- partitionKey,
39
- partitionKeyType,
40
- sortKey,
41
- sortKeyType,
42
- }
43
- }
44
- error(index)
45
- }).filter(Boolean) // Invalid indexes may create undefined entries in the map
46
-
15
+ let indexes = getIndexes(arc, 'indexes', errors)
47
16
  validate.indexes(indexes, '@indexes', errors)
48
17
 
49
18
  return indexes
@@ -2,23 +2,29 @@ let { join } = require('path')
2
2
  let { existsSync } = require('fs')
3
3
  let is = require('../../../lib/is')
4
4
 
5
- module.exports = function populateStreams ({ type, item, dir, cwd, errors }) {
5
+ module.exports = function populateTablesStreams ({ type, item, dir, cwd, errors }) {
6
6
  if (type === 'tables' && is.object(item)) {
7
7
  let name = Object.keys(item)[0]
8
- // Check for the legacy dir from before `@tables tablename stream true` generated an @streams item
8
+ // Check for the legacy dir from before `@tables tablename stream true` generated a @tables-streams item
9
9
  let legacySrc = join(cwd, dir, name)
10
10
  let streamSrc = join(cwd, 'src', 'streams', name)
11
- let src = existsSync(legacySrc) ? legacySrc : streamSrc
11
+ let tablesStreamsSrc = join(cwd, 'src', 'tables-streams', name)
12
+
13
+ let src
14
+ if (existsSync(legacySrc)) src = legacySrc
15
+ else if (existsSync(streamSrc)) src = streamSrc // TODO [remove] in 10.0
16
+ else src = tablesStreamsSrc
17
+
12
18
  let table = name
13
19
  return { name, src, table }
14
20
  }
15
- else if (type === 'streams' && is.string(item)) {
21
+ else if (type === 'tables-streams' && is.string(item)) {
16
22
  let name = item
17
23
  let src = join(cwd, dir, name)
18
24
  let table = name
19
25
  return { name, src, table }
20
26
  }
21
- else if (type === 'streams' && is.object(item)) {
27
+ else if (type === 'tables-streams' && is.object(item)) {
22
28
  let name = Object.keys(item)[0]
23
29
  let src = item[name].src
24
30
  ? join(cwd, item[name].src)
@@ -1,3 +1,4 @@
1
+ let is = require('../../../lib/is')
1
2
  let { aliases, runtimes, runtimeList } = require('lambda-runtimes')
2
3
 
3
4
  // Runtime interpolater
@@ -18,5 +19,9 @@ module.exports = function getRuntime (config) {
18
19
  config.runtimeAlias = runtime
19
20
  }
20
21
  }
22
+ else if (is.defined(runtime)) {
23
+ // Someone did something funky like specify a number or bool, so coerce and let it fail validation
24
+ config.runtime = `${config.runtime}`
25
+ }
21
26
  return config
22
27
  }
@@ -10,7 +10,7 @@ let getEvents = require('./_events')
10
10
  let getPlugins = require('./_plugins')
11
11
  let getScheduled = require('./_scheduled')
12
12
  let getWS = require('./_websockets')
13
- let getStreams = require('./_streams')
13
+ let getTablesStreams = require('./_tables-streams')
14
14
 
15
15
  /**
16
16
  * Build out the Lambda tree
@@ -87,6 +87,8 @@ function populateLambda (type, pragma, inventory, errors) {
87
87
  return lambdas
88
88
  }
89
89
 
90
+ let ts = 'tables-streams'
91
+
90
92
  function getLambda (params) {
91
93
  let { type } = params
92
94
  params.dir = `src/${type}/`
@@ -96,8 +98,8 @@ function getLambda (params) {
96
98
  if (type === 'plugins') return getPlugins(params)
97
99
  if (type === 'queues') return getEvents(params) // Effectively the same as events
98
100
  if (type === 'scheduled') return getScheduled(params)
99
- if (type === 'streams') return getStreams(params)
100
- if (type === 'tables') return getStreams(params) // Shortcut for creating streams
101
+ if (type === ts) return getTablesStreams(params)
102
+ if (type === 'tables') return getTablesStreams(params) // Shortcut for creating streams
101
103
  /* istanbul ignore else */ /* Clearer to be explicit here */
102
104
  if (type === 'ws') return getWS(params)
103
105
  }
@@ -108,7 +110,7 @@ module.exports = {
108
110
  plugins: populateLambda.bind({}, 'plugins'),
109
111
  queues: populateLambda.bind({}, 'queues'),
110
112
  scheduled: populateLambda.bind({}, 'scheduled'),
111
- streams: populateLambda.bind({}, 'streams'),
112
113
  tables: populateLambda.bind({}, 'tables'),
114
+ [ts]: populateLambda.bind({}, ts),
113
115
  ws: populateLambda.bind({}, 'ws'),
114
116
  }
@@ -0,0 +1,59 @@
1
+ let is = require('../../lib/is')
2
+ let validate = require('./validate')
3
+
4
+ function configureTablesIndexes ({ arc, errors }) {
5
+ if (!arc['tables-indexes'] || !arc['tables-indexes'].length) return null
6
+ if (arc['tables-indexes'] && !arc.tables) {
7
+ errors.push(`Specifying @tables-indexes requires specifying corresponding @tables`)
8
+ return null
9
+ }
10
+ if (arc['tables-indexes']?.length && arc.indexes?.length) {
11
+ errors.push(`Either @tables-indexes or @indexes can be specified, but not both`)
12
+ return null
13
+ }
14
+
15
+ let indexes = getIndexes(arc, 'tables-indexes', errors)
16
+ validate.indexes(indexes, '@tables-indexes', errors)
17
+
18
+ return indexes
19
+ }
20
+
21
+ let getIndexes = (arc, pragma, errors) => {
22
+ let isCustomName = key => is.string(key) && key.toLowerCase() === 'name'
23
+ function error (item) { errors.push(`Invalid @${pragma} item: ${item}`) }
24
+ return arc[pragma].map(index => {
25
+ if (is.object(index)) {
26
+ let name = Object.keys(index)[0]
27
+ let partitionKey = null
28
+ let partitionKeyType = null
29
+ let sortKey = null
30
+ let sortKeyType = null
31
+ let indexName = null
32
+ Object.entries(index[name]).forEach(([ key, value ]) => {
33
+ if (is.sortKey(value)) {
34
+ sortKey = key
35
+ sortKeyType = value.replace('**', '')
36
+ }
37
+ else if (is.primaryKey(value)) {
38
+ partitionKey = key
39
+ partitionKeyType = value.replace('*', '')
40
+ }
41
+ else if (isCustomName(key)) {
42
+ indexName = value
43
+ }
44
+ })
45
+ return {
46
+ indexName,
47
+ name,
48
+ partitionKey,
49
+ partitionKeyType,
50
+ sortKey,
51
+ sortKeyType,
52
+ }
53
+ }
54
+ error(index)
55
+ }).filter(Boolean) // Invalid indexes may create undefined entries in the map
56
+ }
57
+
58
+ configureTablesIndexes.getIndexes = getIndexes
59
+ module.exports = configureTablesIndexes
@@ -3,15 +3,15 @@ let validate = require('./validate')
3
3
  let is = require('../../lib/is')
4
4
 
5
5
  /**
6
- * `@streams` (formerly `@tables`)
6
+ * `@tables-streams`
7
7
  * - Originally `@tables {tablename} stream true` created a lambda at src/tables/{tablename}
8
- * - This was superseded by `@streams`; `@tables` remains for backwards compat and as a convenience for creating `@streams`
8
+ * - This was superseded by `@tables-streams`; `@tables` remains for backwards compat and as a convenience for creating `@tables-streams`
9
9
  * - If a project has an existing `@tables` Lambda, we'll continue using that so long as the directory exists
10
10
  */
11
- module.exports = function configureStreams ({ arc, inventory, errors }) {
12
- if (!arc.streams && !arc.tables) return null
13
- if (arc.streams && !arc.tables) {
14
- errors.push(`Specifying @streams requires specifying corresponding @tables`)
11
+ module.exports = function configureTablesStreams ({ arc, inventory, errors }) {
12
+ if (!arc['tables-streams'] && !arc.tables) return null
13
+ if (arc['tables-streams'] && !arc.tables) {
14
+ errors.push(`Specifying @tables-streams requires specifying corresponding @tables`)
15
15
  return null
16
16
  }
17
17
 
@@ -22,21 +22,21 @@ module.exports = function configureStreams ({ arc, inventory, errors }) {
22
22
  }
23
23
  else tables = null
24
24
 
25
- // Populate @streams
26
- let streams = populate.streams(arc.streams, inventory, errors)
25
+ // Populate @tables-streams
26
+ let streams = populate['tables-streams'](arc['tables-streams'], inventory, errors)
27
27
 
28
28
  if (tables && streams) {
29
29
  let uniqueTables = tables.filter(t => !streams.some(s => s.table === t.table))
30
30
  let merged = streams.concat(uniqueTables)
31
- validate.streams(merged, errors)
31
+ validate.tablesStreams(merged, errors)
32
32
  return merged
33
33
  }
34
34
  else if (streams) {
35
- validate.streams(streams, errors)
35
+ validate.tablesStreams(streams, errors)
36
36
  return streams
37
37
  }
38
38
  else if (tables) {
39
- validate.streams(tables, errors)
39
+ validate.tablesStreams(tables, errors)
40
40
  return tables
41
41
  }
42
42
  return null
@@ -53,7 +53,6 @@ function validateCron (schedule, errors) {
53
53
  if (!year.toString().match(minHrYr)) expErr('year', year)
54
54
  }
55
55
 
56
-
57
56
  let singular = [ 'minute', 'hour', 'day' ]
58
57
  let plural = [ 'minutes', 'hours', 'days' ]
59
58
  function validateRate (schedule, errors) {
@@ -69,11 +68,7 @@ function validateRate (schedule, errors) {
69
68
 
70
69
  // Value must be a >0 number
71
70
  if (!is.number(value) || !(value > 0)) {
72
- expErr('rate value must be a number greater than 0', value)
73
- }
74
- // Value must be a whole number
75
- else if (!value.toString().match(/^\d+$/)) {
76
- expErr('rate value must be a whole number', value)
71
+ expErr('rate value must be a whole number greater than 0', value)
77
72
  }
78
73
  // Interval must be a string
79
74
  if (!is.string(interval)) {
@@ -0,0 +1,19 @@
1
+ let { regex, size, unique } = require('./_lib')
2
+
3
+ /**
4
+ * Validate @tables-streams (& @tables streams true)
5
+ *
6
+ * Where possible, attempts to follow DynamoDB validation
7
+ * See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
8
+ */
9
+ module.exports = function validateTablesStreams (tablesStreams, errors) {
10
+ if (tablesStreams.length) {
11
+ unique(tablesStreams, '@tables-streams', errors)
12
+
13
+ tablesStreams.forEach(stream => {
14
+ let { name } = stream
15
+ size(name, 3, 255, '@tables-streams', errors)
16
+ regex(name, 'veryLooseName', '@tables-streams', errors)
17
+ })
18
+ }
19
+ }
@@ -47,7 +47,7 @@ module.exports = function validateTablesAndIndexes (pragma, pragmaName, errors)
47
47
  }
48
48
  })
49
49
  if (foundDupe) {
50
- let err = `Duplicate @indexes value: '${index.name}'`
50
+ let err = `Duplicate ${pragmaName} value: '${index.name}'`
51
51
  if (!errors.includes(err)) errors.push(err)
52
52
  }
53
53
  })
@@ -1,18 +1,18 @@
1
1
  /* eslint-disable global-require */
2
2
  module.exports = {
3
3
  // Pragmas and project validation
4
- aws: require('./_aws'),
5
- events: require('./_events'),
6
- http: require('./_http'),
7
- indexes: require('./_tables'), // Same ruleset as @tables (more or less)
8
- proxy: require('./_proxy'),
9
- tables: require('./_tables'),
10
- queues: require('./_events'), // Same ruleset as @events
11
- scheduled: require('./_scheduled'),
12
- shared: require('./_shared'), // Also includes @views
13
- streams: require('./_streams'),
14
- websockets: require('./_websockets'),
4
+ aws: require('./_aws'),
5
+ events: require('./_events'),
6
+ http: require('./_http'),
7
+ indexes: require('./_tables'), // Same ruleset as @tables (more or less)
8
+ proxy: require('./_proxy'),
9
+ tables: require('./_tables'),
10
+ queues: require('./_events'), // Same ruleset as @events
11
+ scheduled: require('./_scheduled'),
12
+ shared: require('./_shared'), // Also includes @views
13
+ tablesStreams: require('./_tables-streams'),
14
+ websockets: require('./_websockets'),
15
15
 
16
16
  // Misc
17
- validate: require('./_lib')
17
+ validate: require('./_lib')
18
18
  }
@@ -60,8 +60,9 @@ module.exports = function inventoryDefaults (params = {}) {
60
60
  scheduled: null,
61
61
  shared: null,
62
62
  static: null,
63
- streams: null,
64
63
  tables: null,
64
+ 'tables-indexes': null,
65
+ 'tables-streams': null,
65
66
  views: null,
66
67
  ws: null,
67
68
  // Collection of all Lambda paths
package/src/get.js CHANGED
@@ -8,7 +8,7 @@ module.exports = function _get (inventory) {
8
8
  if (pragma === null) return null
9
9
  if (is.array(pragma)) {
10
10
  // Handle arrays of named entities or string values
11
- let finder = i => i && i.name && i.name === name || i === name
11
+ let finder = i => i?.name === name || i === name
12
12
  if (multipleResults.includes(prag)) {
13
13
  let results = pragma.filter(finder)
14
14
  return results.length ? results : undefined
@@ -37,5 +37,6 @@ module.exports = function _get (inventory) {
37
37
  // Everything is uniquely named except in certain special-case pragmas
38
38
  // These refer to other pragmas, and thus may allow multiple same/same-named entities
39
39
  let multipleResults = [
40
- 'indexes'
40
+ 'tables-indexes',
41
+ 'indexes',
41
42
  ]
package/src/lib/is.js CHANGED
@@ -4,9 +4,10 @@ module.exports = {
4
4
  // Types
5
5
  array: item => Array.isArray(item),
6
6
  bool: item => typeof item === 'boolean',
7
- number: item => typeof item === 'number',
7
+ number: item => Number.isInteger(item),
8
8
  object: item => typeof item === 'object' && !Array.isArray(item),
9
9
  string: item => typeof item === 'string',
10
+ defined: item => typeof item !== 'undefined' && item !== null,
10
11
  // Filesystem
11
12
  exists: path => existsSync(path),
12
13
  folder: path => existsSync(path) && lstatSync(path).isDirectory(),
@@ -6,7 +6,7 @@ module.exports = {
6
6
  'cdn',
7
7
  'events',
8
8
  'http',
9
- 'indexes',
9
+ 'indexes', // -> transitioning to @tables-indexes
10
10
  'macros',
11
11
  'plugins',
12
12
  'proxy',
@@ -14,8 +14,9 @@ module.exports = {
14
14
  'scheduled',
15
15
  'shared',
16
16
  'static',
17
- 'streams',
18
17
  'tables',
18
+ 'tables-indexes',
19
+ 'tables-streams',
19
20
  'views',
20
21
  'ws',
21
22
  ],
@@ -26,7 +27,7 @@ module.exports = {
26
27
  'plugins',
27
28
  'queues',
28
29
  'scheduled',
29
- 'streams',
30
+ 'tables-streams',
30
31
  'ws',
31
32
  ],
32
33
  }
@@ -0,0 +1,64 @@
1
+ let is = require('../lib/is')
2
+ let { lambdas } = require('../lib/pragmas')
3
+ let { aliases, runtimeList } = require('lambda-runtimes')
4
+ let allRuntimes = runtimeList.concat([ 'deno' ])
5
+
6
+ /**
7
+ * Configuration validator
8
+ */
9
+ module.exports = function configValidator (params, inventory, errors) {
10
+ let { runtime: globalRuntime, memory: globalMemory, timeout: globalTimeout } = inventory.aws
11
+
12
+ /**
13
+ * Global config
14
+ */
15
+ // Memory
16
+ if (is.defined(globalMemory) && invalidMemory(globalMemory)) {
17
+ errors.push(invalidMemoryMsg(`${globalMemory} MB (@aws)`))
18
+ }
19
+ // Runtime
20
+ if ((globalRuntime && !is.string(globalRuntime)) ||
21
+ (globalRuntime && !allRuntimes.includes(globalRuntime) &&
22
+ !aliases[globalRuntime] && !aliases[globalRuntime.toLowerCase()])) {
23
+ errors.push(`Invalid project-level runtime: ${globalRuntime}`)
24
+ }
25
+ // Timeout
26
+ if (is.defined(globalTimeout) && invalidTimeout(globalTimeout)) {
27
+ errors.push(invalidTimeoutMsg(`${globalTimeout} seconds (@aws)`))
28
+ }
29
+
30
+ /**
31
+ * Lambda config
32
+ */
33
+ lambdas.forEach(p => {
34
+ let pragma = inventory[p]
35
+ if (pragma) pragma.forEach(({ name, config }) => {
36
+ let { memory, runtime, timeout } = config
37
+
38
+ // Memory
39
+ if (invalidMemory(memory) && memory !== globalMemory) {
40
+ errors.push(invalidMemoryMsg(`${memory} MB (@${p} ${name})`))
41
+ }
42
+ // Runtime
43
+ if (!allRuntimes.includes(runtime) && runtime !== globalRuntime) {
44
+ errors.push(`Invalid runtime: ${runtime} (@${p} ${name})`)
45
+ }
46
+ // Timeout
47
+ if (invalidTimeout(timeout) && timeout !== globalTimeout) {
48
+ errors.push(invalidTimeoutMsg(`${timeout} seconds (@${p} ${name})`))
49
+ }
50
+ })
51
+ })
52
+ }
53
+
54
+ // Memory
55
+ let minMemory = 128
56
+ let maxMemory = 10240
57
+ let invalidMemory = memory => !is.number(memory) || (memory < minMemory) || (memory > maxMemory)
58
+ let invalidMemoryMsg = info => `Invalid Lambda memory setting: ${info}, memory must be between ${minMemory} - ${maxMemory} MB`
59
+
60
+ // Timeout
61
+ let minTimeout = 1
62
+ let maxTimeout = 1 * 60 * 15 // 15 mins
63
+ let invalidTimeout = timeout => !is.number(timeout) || (timeout < minTimeout) || (timeout > maxTimeout)
64
+ let invalidTimeoutMsg = info => `Invalid Lambda timeout setting: ${info}, timeout must be between ${minTimeout} - ${maxTimeout} seconds`
@@ -1,4 +1,4 @@
1
- let runtimes = require('./runtimes')
1
+ let config = require('./config')
2
2
  let layers = require('./layers')
3
3
  let tablesChildren = require('./tables-children')
4
4
  let errorFmt = require('../lib/error-fmt')
@@ -12,9 +12,8 @@ module.exports = function finalValidation (params, inventory) {
12
12
  /**
13
13
  * Deal with vendor configuration errors
14
14
  */
15
-
16
- // Blow up on any non-matching runtimes
17
- runtimes(params, inventory, errors)
15
+ // Analyze function configuration
16
+ config(params, inventory, errors)
18
17
 
19
18
  // Ensure layer configuration will work, AWS blows up with awful errors on this
20
19
  layers(params, inventory, errors)
@@ -32,8 +31,7 @@ module.exports = function finalValidation (params, inventory) {
32
31
  /**
33
32
  * Deal with project validation errors
34
33
  */
35
-
36
- // Ensure @tables children (@streams, @indexes) have parent tables present
34
+ // Ensure @tables children (@tables-streams, @indexes) have parent tables present
37
35
  tablesChildren(inventory, errors)
38
36
 
39
37
  if (errors.length) {
@@ -1,12 +1,14 @@
1
1
  let { sep } = require('path')
2
2
  let { lambdas } = require('../lib/pragmas')
3
- let validateARN = require('./arn')
3
+ let is = require('../lib/is')
4
+ let plural = arr => arr.length > 1 ? 's' : ''
4
5
 
5
6
  /**
6
7
  * Layer validator
7
8
  */
8
9
  module.exports = function layerValidator (params, inventory, errors) {
9
- let { region } = inventory.aws
10
+ let { _project } = inventory
11
+ let { region, layers: globalLayers } = inventory.aws
10
12
  let { cwd, validateLayers = true } = params
11
13
 
12
14
  // Shouldn't be possible because we backfill region, but jic
@@ -15,39 +17,69 @@ module.exports = function layerValidator (params, inventory, errors) {
15
17
  // Allow for manual opt-out of layer validation
16
18
  if (!validateLayers) return
17
19
 
18
- // Walk the tree of layer configs, starting with @aws
19
- Object.entries(inventory).forEach(([ i ]) => {
20
- let item = inventory[i]
21
- if (i === 'aws') {
22
- let location = inventory._project.manifest &&
23
- inventory._project.manifest.replace(cwd, '')
24
- let layers = item.layers
25
- validateLayer({ layers, region, location })
26
- }
27
- else if (lambdas.includes(i) && item) {
28
- item.forEach(entry => {
29
- // Probably unnecessary if no configFile is present but why not, let's be extra safe
30
- let location = entry.configFile && entry.configFile.replace(cwd, '')
31
- let layers = entry.config.layers
32
- validateLayer({ layers, region, location })
33
- })
34
- }
20
+ /**
21
+ * Global config
22
+ */
23
+ let location = _project?.manifest?.replace(cwd, '')
24
+ validateLayer({ layers: globalLayers, region, location, errors })
25
+
26
+ /**
27
+ * Lambda config
28
+ */
29
+ lambdas.forEach(p => {
30
+ let pragma = inventory[p]
31
+ if (pragma) pragma.forEach(({ config, configFile }) => {
32
+ let location = configFile?.replace(cwd, '')
33
+ validateLayer({ layers: config.layers, region, location, errors })
34
+ })
35
35
  })
36
+ }
36
37
 
37
- function validateLayer ({ layers, region, location }) {
38
- let loc = location && location.startsWith(sep) ? location.substr(1) : location
39
- let lambda = loc ? ` - Lambda: ${loc}\n` : ''
40
- if (!layers || !layers.length) return
41
- else {
42
- if (layers.length > 5) {
43
- let list = ` - Layers:\n ${layers.map(l => ` - ${l}`).join('\n')}`
44
- errors.push(`Lambda can only be configured with up to 5 layers\n${lambda}${list}`)
45
- }
46
- // CloudFormation fails without a helpful error if any layers aren't in the same region as the app because CloudFormation
47
- for (let arn of layers) {
48
- let arnError = validateARN({ arn, region, loc })
49
- if (arnError) errors.push(arnError)
50
- }
38
+ function validateLayer ({ layers, region, location, errors }) {
39
+ let loc = location && location.startsWith(sep) ? location.substr(1) : location
40
+ let config = loc ? ` (${loc})` : ''
41
+ if (!layers || !layers.length) return
42
+ else {
43
+ if (layers.length > 5) {
44
+ let list = `${layers.map(l => ` - ${l}`).join('\n')}`
45
+ errors.push(`Lambdas can only be configured with up to 5 layers, got ${layers.length} layers${config}:\n${list}`)
51
46
  }
47
+ // CloudFormation fails without a helpful error if any layers aren't in the same region as the app because CloudFormation
48
+ let arnErrors = validateARN({ layers, region, config })
49
+ if (arnErrors) errors.push(arnErrors)
50
+ }
51
+ }
52
+
53
+ // Validates Lambda layer / policy ARNs, prob can't be used for other kinds of ARN
54
+ function validateARN ({ layers, region, config }) {
55
+ let invalidArns = []
56
+ let badRegions = []
57
+ layers.forEach(arn => {
58
+ let parts = is.string(arn) && arn.split(':')
59
+ // Invalid
60
+ if (!is.string(arn) ||
61
+ !arn.startsWith('arn:') ||
62
+ parts.length !== 8) {
63
+ return invalidArns.push(` - ${arn}`)
64
+ }
65
+ // Bad region
66
+ let layerRegion = parts[3]
67
+ if (region !== layerRegion) {
68
+ badRegions.push(
69
+ ` - Layer ARN: ${arn}\n` +
70
+ ` - Layer region: ${layerRegion}`
71
+ )
72
+ }
73
+ })
74
+ let err = ''
75
+ if (invalidArns.length) {
76
+ err += `Invalid ARN${plural(invalidArns)}${config}:\n` +
77
+ invalidArns.join('\n')
78
+ }
79
+ if (badRegions.length) {
80
+ err += `Layer${plural(badRegions)} ` +
81
+ `not in app's region of ${region}${config}:\n` +
82
+ badRegions.join('\n')
52
83
  }
84
+ if (err) return err
53
85
  }
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Ensure @tables children (@streams, @indexes) have parent tables present
2
+ * Ensure @tables children (@tables-streams, @indexes) have parent tables present
3
3
  * - If not, configuration is invalid
4
4
  */
5
5
  module.exports = function validateTablesChildren (inventory, errors) {
6
- let { indexes, streams, tables } = inventory
6
+ let { indexes, 'tables-streams': tablesStreams, tables } = inventory
7
7
 
8
8
  function check (table, type) {
9
9
  if (!tables.some(t => t.name === table)) {
10
10
  errors.push(`@${type} ${table} missing corresponding table`)
11
11
  }
12
12
  }
13
- if (streams) {
14
- streams.forEach(stream => check(stream.table, 'streams'))
13
+ if (tablesStreams) {
14
+ tablesStreams.forEach(stream => check(stream.table, 'tables-streams'))
15
15
  }
16
16
  if (indexes) {
17
17
  indexes.forEach(index => check(index.name, 'indexes'))
@@ -1,19 +0,0 @@
1
- let { regex, size, unique } = require('./_lib')
2
-
3
- /**
4
- * Validate @streams (& @tables streams true)
5
- *
6
- * Where possible, attempts to follow DynamoDB validation
7
- * See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
8
- */
9
- module.exports = function validateStreams (streams, errors) {
10
- if (streams.length) {
11
- unique(streams, '@streams', errors)
12
-
13
- streams.forEach(stream => {
14
- let { name } = stream
15
- size(name, 3, 255, '@streams', errors)
16
- regex(name, 'veryLooseName', '@streams', errors)
17
- })
18
- }
19
- }
@@ -1,24 +0,0 @@
1
- let is = require('../lib/is')
2
-
3
- // Validates Lambda layer / policy ARNs, prob can't be used for other kinds of ARN
4
- module.exports = function validateARN ({ arn, region, loc }) {
5
- if (!is.string(arn) ||
6
- !arn.startsWith('arn:') ||
7
- arn.split(':').length !== 8) {
8
- /* istanbul ignore next */
9
- let lambda = loc ? `in ${loc}` : ''
10
- return `Invalid ARN${lambda}: ${arn}`
11
- }
12
-
13
- let parts = arn.split(':')
14
- let layerRegion = parts[3]
15
- if (region !== layerRegion) {
16
- /* istanbul ignore next */
17
- let lambda = loc ? ` - Lambda: ${loc}\n` : ''
18
- let err = `Lambda layers must be in the same region as app\n` + lambda +
19
- ` - App region: ${region}\n` +
20
- ` - Layer ARN: ${arn}\n` +
21
- ` - Layer region: ${layerRegion}`
22
- return err
23
- }
24
- }
@@ -1,28 +0,0 @@
1
- let { lambdas } = require('../lib/pragmas')
2
- let { aliases, runtimeList } = require('lambda-runtimes')
3
-
4
- /**
5
- * Runtime validator
6
- */
7
- module.exports = function runtimeValidator (params, inventory, errors) {
8
-
9
- let allRuntimes = runtimeList.concat([ 'deno' ])
10
- let globalRuntime = inventory.aws?.runtime
11
- if (globalRuntime &&
12
- !allRuntimes.includes(globalRuntime) &&
13
- !aliases[globalRuntime]) {
14
- errors.push(`Invalid project-level runtime: ${globalRuntime}`)
15
- }
16
-
17
- // Walk the tree of layer configs, starting with @aws
18
- lambdas.forEach(p => {
19
- let pragma = inventory[p]
20
- if (pragma) pragma.forEach(entry => {
21
- let runtime = entry.config.runtime
22
- if (runtime === globalRuntime) return
23
- if (!allRuntimes.includes(runtime)) {
24
- errors.push(`Invalid runtime: ${runtime} (@${p} ${entry.name})`)
25
- }
26
- })
27
- })
28
- }