@architect/inventory 3.1.1 → 3.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,33 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## [3.2.0] 2022-07-24
6
+
7
+ ### Added
8
+
9
+ - Added support for new setter plugin APIs, specifically: `@proxy`, `@shared`, `@static`, `@tables`, `@tables-indexes`, `@views`
10
+ - Added new `@static` setting: `compression`
11
+
12
+
13
+ ### Changed
14
+
15
+ - Lambdas defined in the userland Architect project manifest now override conflicting Lambdas returned by plugins (instead of throwing validation errors); fixes #1352
16
+ - Plugin developers can now use a `required` flag to enforce a validation error should their plugin conflict with userland Lambdas
17
+ - `@tables` and `@tables-indexes` can now accept lower case key types (e.g. `*string` instead of `*String`)
18
+ - `@tables` and `@tables-indexes` can also accept `*` and `**` as a shortcut for string-type primary and sort keys
19
+ - Changed plugin function property tags from `plugin|type` to `_plugin|_type` to indicate internal property namespacing
20
+ - Added `@static` pragma validation
21
+ - Fixed obscure case where `@static` `ignore` setting might only use the first list item
22
+
23
+
24
+ ### Fixed
25
+
26
+ - Fixed issue where Lambdas created by plugins that returned arrays did not have their `plugin` and `type` properties set
27
+ - Fixed issue where an absolute path in `@shared|views` `src` would incorrectly resolve
28
+ - Fixed issue where `@views` might incorrectly return a validation error when only HTTP setter plugins are used to define `@http` routes
29
+
30
+ ---
31
+
5
32
  ## [3.1.1] 2022-05-09
6
33
 
7
34
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@architect/inventory",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Architect project resource enumeration utility",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -2,10 +2,10 @@ let { join } = require('path')
2
2
  let { existsSync } = require('fs')
3
3
  let { is, normalizeSrc, pragmas, tidyError, validationPatterns } = require('../../lib')
4
4
  let { lambdas } = pragmas
5
- let nonLambdaSetters = [ 'customLambdas', 'env', 'runtimes' ]
5
+ let nonLambdaSetters = [ 'customLambdas', 'env', 'proxy', 'runtimes', 'shared', 'static', 'views', 'tables', 'tables-indexes' ]
6
6
  let setters = [ ...lambdas, ...nonLambdaSetters ]
7
- let pluginMethods = [ 'deploy', 'sandbox' ] // TODO add more!
8
- let reservedNames = [ '_methods', 'events', 'queues', 'static', 'tables' ]
7
+ let pluginMethods = [ 'deploy', 'sandbox' ]
8
+ let reservedNames = [ '_methods' ]
9
9
 
10
10
  module.exports = function getPluginModules ({ arc, inventory, errors }) {
11
11
  if (!arc?.plugins?.length && !arc?.macros?.length) return null
@@ -67,8 +67,8 @@ module.exports = function getPluginModules ({ arc, inventory, errors }) {
67
67
  return
68
68
  }
69
69
  if (!methods.set[setter]) methods.set[setter] = []
70
- fn.plugin = name
71
- fn.type = type
70
+ fn._plugin = name
71
+ fn._type = type
72
72
  methods.set[setter].push(fn)
73
73
  }
74
74
  })
@@ -83,8 +83,8 @@ module.exports = function getPluginModules ({ arc, inventory, errors }) {
83
83
  }
84
84
  if (!methods[method]) methods[method] = {}
85
85
  if (!methods[method][hook]) methods[method][hook] = []
86
- fn.plugin = name
87
- fn.type = type
86
+ fn._plugin = name
87
+ fn._type = type
88
88
  methods[method][hook].push(fn)
89
89
  })
90
90
  }
@@ -101,14 +101,18 @@ module.exports = function getPluginModules ({ arc, inventory, errors }) {
101
101
  }
102
102
 
103
103
  function getPath (cwd, srcDir, name) {
104
- let paths = [
105
- join(cwd, 'src', srcDir, `${name}.js`),
106
- join(cwd, 'src', srcDir, name),
107
- join(cwd, 'node_modules', name),
108
- join(cwd, 'node_modules', `@${name}`),
109
- ]
110
- if (existsSync(paths[0])) return paths[0]
111
- else if (existsSync(paths[1])) return paths[1]
112
- else if (existsSync(paths[2])) return paths[2]
113
- else if (existsSync(paths[3])) return paths[3]
104
+ let path1 = join(cwd, 'src', srcDir, `${name}.js`)
105
+ let path2 = join(cwd, 'src', srcDir, name)
106
+ let path3 = join(cwd, 'node_modules', name)
107
+ let path4 = join(cwd, 'node_modules', `@${name}`)
108
+ /**/ if (existsSync(path1)) return path1
109
+ else if (existsSync(path2)) return path2
110
+ else if (existsSync(path3)) return path3
111
+ else if (existsSync(path4)) return path4
112
+ try {
113
+ return require.resolve(name)
114
+ }
115
+ catch {
116
+ return
117
+ }
114
118
  }
@@ -25,25 +25,26 @@ function populateLambda (type, params) {
25
25
  var result = fn({ arc: invCopy._project.arc, inventory: { inv: invCopy } })
26
26
  }
27
27
  catch (err) {
28
- err.message = `Setter plugin exception: plugin: ${fn.plugin}, method: set.${type}`
28
+ err.message = `Setter plugin exception: plugin: ${fn._plugin}, method: set.${type}`
29
29
  + `\n` + err.message
30
30
  throw err
31
31
  }
32
32
  if (!result ||
33
33
  (!is.object(result) && !is.array(result)) ||
34
34
  (is.array(result) && result.some(r => !is.object(r)))) {
35
- errors.push(`Setter plugins must return a valid response: plugin: ${fn.plugin}, method: set.${type}`)
35
+ errors.push(`Setter plugins must return a valid response: plugin: ${fn._plugin}, method: set.${type}`)
36
36
  return []
37
37
  }
38
38
  if (is.array(result)) {
39
- result.forEach(item => {
40
- item.plugin = fn.plugin
41
- item.type = fn.type
39
+ result.forEach((item, i) => {
40
+ item._plugin = fn._plugin
41
+ item._type = fn._type
42
+ result[i] = item
42
43
  })
43
44
  }
44
45
  else {
45
- result.plugin = fn.plugin
46
- result.type = fn.type
46
+ result._plugin = fn._plugin
47
+ result._type = fn._type
47
48
  }
48
49
  return result
49
50
  })
@@ -52,6 +53,15 @@ function populateLambda (type, params) {
52
53
 
53
54
  let pragmaLambda = populate(type, pragma || arc[type], inventory, errors) || []
54
55
  let aggregate = [ ...pluginLambda, ...pragmaLambda ]
56
+
57
+ // Filter plugins, if appropriate: not required, but do conflict with a manifest entry
58
+ aggregate = aggregate.filter(lambda => {
59
+ if (lambda._plugin && !lambda.required &&
60
+ aggregate.some(({ name, _plugin }) => !_plugin && name === lambda.name)) {
61
+ return false
62
+ }
63
+ return true
64
+ })
55
65
  return aggregate.length ? aggregate : null
56
66
  }
57
67
 
@@ -135,6 +145,12 @@ function populate (type, pragma, inventory, errors, plugin) {
135
145
  // Final tidying of any undefined properties
136
146
  Object.keys(lambda).forEach(k => !is.defined(lambda[k]) && delete lambda[k])
137
147
 
148
+ // Pass through the plugin tag and required status
149
+ if (item._plugin) {
150
+ lambda._plugin = item._plugin
151
+ if (item.required === true) lambda.required = true
152
+ }
153
+
138
154
  lambdas.push(lambda)
139
155
  }
140
156
 
@@ -0,0 +1,117 @@
1
+ let { deepFrozenCopy } = require('@architect/utils')
2
+ let { is } = require('../../../lib')
3
+
4
+ /**
5
+ * Build out resource pragmas (e.g. `@tables`) via plugins
6
+ * Returns an array of resources
7
+ */
8
+ function resources (params) {
9
+ let { errors, template, plugins, inventory, type, valid } = params
10
+ if (plugins) {
11
+ let invCopy = deepFrozenCopy(inventory)
12
+ let pluginResults = plugins.flatMap(fn => {
13
+ try {
14
+ var result = fn({ arc: invCopy._project.arc, inventory: { inv: invCopy } })
15
+ }
16
+ catch (err) {
17
+ err.message = `Setter plugin exception: plugin: ${fn._plugin}, method: set.${type}`
18
+ + `\n` + err.message
19
+ throw err
20
+ }
21
+ if (!result ||
22
+ (!is.object(result) && !is.array(result)) ||
23
+ (is.array(result) && result.some(r => !is.object(r)))) {
24
+ errors.push(`Setter plugins must return a valid response: plugin: ${fn._plugin}, method: set.${type}`)
25
+ return []
26
+ }
27
+ if (is.array(result)) {
28
+ result.forEach((item, i) => {
29
+ item = populateTemplate(template, item)
30
+ item._plugin = fn._plugin
31
+ item._type = fn._type
32
+ result[i] = item
33
+ })
34
+ }
35
+ else {
36
+ result = populateTemplate(template, result)
37
+ result._plugin = fn._plugin
38
+ result._type = fn._type
39
+ }
40
+ return result
41
+ })
42
+ // Validation pass
43
+ let validationErrors = []
44
+ pluginResults.forEach(item => {
45
+ let errors = validate(item, valid, type)
46
+ validationErrors.push(...errors)
47
+ })
48
+ if (validationErrors.length) {
49
+ errors = errors.push(...validationErrors)
50
+ return []
51
+ }
52
+ return pluginResults
53
+ }
54
+ return
55
+ }
56
+
57
+ function populateTemplate (template, item) {
58
+ let newItem = JSON.parse(JSON.stringify(template))
59
+ Object.entries(item).forEach(([ setting, value ]) => {
60
+ if (is.defined(value)) newItem[setting] = value
61
+ })
62
+ return newItem
63
+ }
64
+
65
+ function validate (item, valid, type) {
66
+ if (!valid) return []
67
+ return Object.entries(valid).map(([ setting, value ]) => {
68
+ if (!is[value](item[setting])) return `Invalid plugin-generated @${type} resource: ${setting}: ${item[setting]}`
69
+ }).filter(Boolean)
70
+ }
71
+
72
+ /**
73
+ * Build out settings pragmas (e.g. `@static`) via plugins
74
+ * Returns an object of settings
75
+ */
76
+ function settings (params) {
77
+ let { errors, settings, plugins, inventory, type, valid } = params
78
+ if (plugins) {
79
+ let invCopy = deepFrozenCopy(inventory)
80
+ let pluginSettings = settings
81
+ let foundError = false
82
+ plugins.forEach(fn => {
83
+ try {
84
+ var result = fn({ arc: invCopy._project.arc, inventory: { inv: invCopy } })
85
+ }
86
+ catch (err) {
87
+ err.message = `Setter plugin exception: plugin: ${fn._plugin}, method: set.${type}`
88
+ + `\n` + err.message
89
+ throw err
90
+ }
91
+ if (!result || !is.object(result)) {
92
+ errors.push(`Setter plugins must return a valid response: plugin: ${fn._plugin}, method: set.${type}`)
93
+ foundError = true
94
+ }
95
+ else {
96
+ Object.entries(result).forEach(([ setting, value ]) => {
97
+ if (is.defined(settings[setting]) && is.defined(value)) {
98
+ pluginSettings[setting] = value
99
+ }
100
+ })
101
+ }
102
+ })
103
+ if (foundError) return null
104
+
105
+ // Validation pass
106
+ let validationErrors = validate(pluginSettings, valid, type)
107
+ if (validationErrors.length) {
108
+ errors = errors.push(...validationErrors)
109
+ return null
110
+ }
111
+
112
+ return pluginSettings
113
+ }
114
+ return settings
115
+ }
116
+
117
+ module.exports = { resources, settings }
@@ -1,19 +1,43 @@
1
+ let populate = require('./populate-other')
1
2
  let validate = require('./validate')
2
3
 
3
- module.exports = function configureProxy ({ arc, errors }) {
4
- if (arc.proxy && !arc.http) {
5
- errors.push('@proxy requires @http')
4
+ module.exports = function configureProxy ({ arc, inventory, errors }) {
5
+ let proxySetters = inventory.plugins?._methods?.set?.proxy
6
+ let httpSetters = inventory.plugins?._methods?.set?.http
7
+ if ((arc.proxy || proxySetters) &&
8
+ (!arc.http && !httpSetters)) {
9
+ errors.push('Specifying @proxy requires specifying @http')
6
10
  return null
7
11
  }
8
- if (!arc.proxy || !arc.http) return null
12
+ if (!arc.proxy && !proxySetters) return null
9
13
 
10
- let proxy = {}
11
- let envs = [ 'testing', 'staging', 'production' ]
12
- envs.forEach(env => {
13
- let setting = arc.proxy.find(s => (s[0] && s[0] === env) && s[1])
14
- if (!setting) errors.push(`@proxy ${env} environment not found or invalid`)
15
- else proxy[env] = setting[1]
14
+ let proxy = {
15
+ testing: null,
16
+ staging: null,
17
+ production: null,
18
+ }
19
+
20
+ proxy = populate.settings({
21
+ errors,
22
+ settings: proxy,
23
+ plugins: proxySetters,
24
+ inventory,
25
+ type: 'proxy',
26
+ valid: {
27
+ testing: 'string',
28
+ staging: 'string',
29
+ production: 'string',
30
+ },
16
31
  })
32
+ if (proxy === null) return null
33
+
34
+ if (arc?.proxy?.length) {
35
+ Object.keys(proxy).forEach(env => {
36
+ let setting = arc.proxy.find(s => (s[0] && s[0] === env) && s[1])
37
+ if (!setting) errors.push(`@proxy ${env} environment not found or invalid`)
38
+ else proxy[env] = setting[1]
39
+ })
40
+ }
17
41
 
18
42
  validate.proxy(proxy, errors)
19
43
 
@@ -1,4 +1,5 @@
1
1
  let { join } = require('path')
2
+ let populate = require('./populate-other')
2
3
  let validate = require('./validate')
3
4
  let { is, pragmas } = require('../../lib')
4
5
  let lambdas = pragmas.lambdas.concat('customLambdas')
@@ -9,20 +10,34 @@ module.exports = function configureShared ({ arc, pragmas, inventory, errors })
9
10
  let { cwd, src: projSrc } = inventory._project
10
11
  let src = join(projSrc, 'shared')
11
12
  let shared = {
12
- src,
13
- shared: [] // Revert to null later if none are defined
13
+ src: null,
14
+ shared: []
15
+ }
16
+
17
+ let foundSrcSetting = false
18
+ let pluginSrc = populate.settings({
19
+ errors,
20
+ settings: shared,
21
+ plugins: inventory.plugins?._methods?.set?.shared,
22
+ inventory,
23
+ type: 'shared',
24
+ valid: { src: 'string' },
25
+ })
26
+ // Shared setters only support src, and do not support specifying Lambdas
27
+ // Lambda paths have not yet been reified in Inventory
28
+ if (is.string(pluginSrc?.src)) {
29
+ shared.src = pluginSrc.src
30
+ foundSrcSetting = true
14
31
  }
15
32
 
16
33
  // First pass to get + check shared folder (if any)
17
- let foundSrc = false
18
34
  if (arc?.shared?.length) {
19
35
  for (let share of arc.shared) {
20
36
  if (is.array(share)) {
21
37
  let key = share[0]?.toLowerCase()
22
38
  if (key === 'src' && is.string(share[1])) {
23
39
  shared.src = share[1]
24
- foundSrc = true
25
- validate.shared(shared.src, cwd, errors)
40
+ foundSrcSetting = true
26
41
  continue
27
42
  }
28
43
  if (key === 'src' && !is.string(share[1])) {
@@ -32,11 +47,14 @@ module.exports = function configureShared ({ arc, pragmas, inventory, errors })
32
47
  }
33
48
  }
34
49
 
50
+ if (foundSrcSetting) validate.shared(shared.src, cwd, errors)
51
+ else shared.src = src
52
+
35
53
  // Exit if configured shared folder doesn't exist
36
54
  if (!is.exists(shared.src)) return null
37
55
 
38
56
  // Proceeding from here resets all shared config, so make sure it's only if specific shared are specified
39
- let some = arc.shared?.length && !(arc?.shared?.length === 1 && foundSrc)
57
+ let some = arc.shared?.length && !(arc?.shared?.length === 1 && foundSrcSetting)
40
58
  if (some) {
41
59
  // Reset shared settings
42
60
  for (let pragma of lambdas) {
@@ -1,13 +1,17 @@
1
+ let populate = require('./populate-other')
1
2
  let { asapSrc, is } = require('../../lib')
3
+ let validate = require('./validate')
2
4
 
3
- module.exports = function configureStatic ({ arc, inventory }) {
5
+ module.exports = function configureStatic ({ arc, inventory, errors }) {
6
+ let staticSetters = inventory.plugins?._methods?.set?.static
4
7
  let httpSetters = inventory.plugins?._methods?.set?.http
5
8
 
6
9
  // @static is inferred by @http
7
- if (!arc.static && !arc.http && !httpSetters) return null
10
+ if (!arc.static && !staticSetters && !arc.http && !httpSetters) return null
8
11
 
9
12
  let staticPragma = arc.static || []
10
13
  let _static = {
14
+ compression: false, // Arc applied default
11
15
  fingerprint: null,
12
16
  folder: 'public', // Arc applied default
13
17
  ignore: null,
@@ -24,17 +28,23 @@ module.exports = function configureStatic ({ arc, inventory }) {
24
28
  if (isDisabled) return false
25
29
  }
26
30
 
31
+ _static = populate.settings({
32
+ errors,
33
+ settings: _static,
34
+ plugins: staticSetters,
35
+ inventory,
36
+ type: 'static',
37
+ })
38
+
27
39
  let settings = Object.entries(_static).map(([ setting ]) => setting)
40
+ let validSetting = key => settings.includes(key)
28
41
  for (let setting of staticPragma) {
29
- let validSetting = key => settings.includes(key)
42
+ // The ignore setting can come in a couple shapes, so we have to handle those
30
43
  if (setting.ignore) {
31
44
  _static.ignore = setting.ignore
32
45
  }
33
- else if (is.array(setting) &&
34
- setting.length === 2 &&
35
- validSetting(setting[0])) {
36
- let isIgnore = setting[0] === 'ignore'
37
- _static[setting[0]] = isIgnore ? [ setting[1] ] : setting[1]
46
+ else if (is.array(setting) && validSetting(setting[0])) {
47
+ _static[setting[0]] = setting[0] === 'ignore' ? [ ...setting.slice(1) ] : setting[1]
38
48
  }
39
49
  }
40
50
 
@@ -44,5 +54,7 @@ module.exports = function configureStatic ({ arc, inventory }) {
44
54
  inventory._project.asapSrc = asapSrc()
45
55
  }
46
56
 
57
+ validate.static(_static, errors)
58
+
47
59
  return _static
48
60
  }
@@ -1,52 +1,72 @@
1
- let { is } = require('../../lib')
1
+ let populate = require('./populate-other')
2
+ let { is, capitalize } = require('../../lib')
2
3
  let validate = require('./validate')
3
4
 
4
- module.exports = function configureTablesIndexes ({ arc, errors }) {
5
- if (!arc['tables-indexes'] || !arc['tables-indexes'].length) return null
6
- if (arc['tables-indexes'] && !arc.tables) {
5
+ module.exports = function configureTablesIndexes ({ arc, inventory, errors }) {
6
+ let $indexes = 'tables-indexes' // It's quite long!
7
+ let indexesSetters = inventory.plugins?._methods?.set?.[$indexes]
8
+ let tablesSetters = inventory.plugins?._methods?.set?.tables
9
+ if ((!arc[$indexes] || !arc[$indexes].length) && !indexesSetters) return null
10
+ if ((arc[$indexes] || indexesSetters) &&
11
+ (!arc.tables && !tablesSetters)) {
7
12
  errors.push(`Specifying @tables-indexes requires specifying corresponding @tables`)
8
13
  return null
9
14
  }
10
15
 
11
- let indexes = getIndexes(arc, 'tables-indexes', errors)
12
- validate.tablesIndexes(indexes, '@tables-indexes', errors)
16
+ let indexTemplate = () => ({
17
+ name: undefined,
18
+ partitionKey: null,
19
+ partitionKeyType: null,
20
+ sortKey: null,
21
+ sortKeyType: null,
22
+ indexName: null,
23
+ })
13
24
 
14
- return indexes
15
- }
25
+ let indexes = []
26
+ let plugins = populate.resources({
27
+ errors,
28
+ template: indexTemplate(),
29
+ plugins: indexesSetters,
30
+ inventory,
31
+ type: 'indexes',
32
+ valid: { name: 'string' }
33
+ })
34
+ if (plugins) indexes.push(...plugins)
16
35
 
17
- let getIndexes = (arc, pragma, errors) => {
18
- let isCustomName = key => is.string(key) && key.toLowerCase() === 'name'
19
- function error (item) { errors.push(`Invalid @${pragma} item: ${item}`) }
20
- return arc[pragma].map(index => {
36
+ let userland = arc?.[$indexes]?.map(index => {
21
37
  if (is.object(index)) {
22
- let name = Object.keys(index)[0]
23
- let partitionKey = null
24
- let partitionKeyType = null
25
- let sortKey = null
26
- let sortKeyType = null
27
- let indexName = null
28
- Object.entries(index[name]).forEach(([ key, value ]) => {
38
+ let i = indexTemplate()
39
+ i.name = Object.keys(index)[0]
40
+ Object.entries(index[i.name]).forEach(([ key, value ]) => {
29
41
  if (is.sortKey(value)) {
30
- sortKey = key
31
- sortKeyType = value.replace('**', '')
42
+ i.sortKey = key
43
+ i.sortKeyType = value.replace('**', '').toLowerCase()
44
+ if (!i.sortKeyType) i.sortKeyType = 'string'
32
45
  }
33
46
  else if (is.primaryKey(value)) {
34
- partitionKey = key
35
- partitionKeyType = value.replace('*', '')
47
+ i.partitionKey = key
48
+ i.partitionKeyType = value.replace('*', '').toLowerCase()
49
+ if (!i.partitionKeyType) i.partitionKeyType = 'string'
36
50
  }
37
- else if (isCustomName(key)) {
38
- indexName = value
51
+ else if (key?.toLowerCase() === 'name') {
52
+ i.indexName = value
39
53
  }
40
54
  })
41
- return {
42
- indexName,
43
- name,
44
- partitionKey,
45
- partitionKeyType,
46
- sortKey,
47
- sortKeyType,
48
- }
55
+ return i
49
56
  }
50
- error(index)
51
- }).filter(Boolean) // Invalid indexes may create undefined entries in the map
57
+ errors.push(`Invalid @${$indexes} item: ${index}`)
58
+ }).filter(Boolean) // Invalid indexes or plugins may create undefined entries in the map
59
+ if (userland) indexes.push(...userland)
60
+
61
+ // Normalize key type casing
62
+ if (indexes.length) indexes = indexes.map(index => {
63
+ let { sortKeyType, partitionKeyType } = index
64
+ if (sortKeyType) index.sortKeyType = capitalize(index.sortKeyType)
65
+ if (partitionKeyType) index.partitionKeyType = capitalize(index.partitionKeyType)
66
+ return index
67
+ })
68
+
69
+ validate.tablesIndexes(indexes, '@tables-indexes', errors)
70
+
71
+ return indexes
52
72
  }
@@ -1,54 +1,70 @@
1
- let { is } = require('../../lib')
1
+ let populate = require('./populate-other')
2
+ let { is, capitalize } = require('../../lib')
2
3
  let validate = require('./validate')
3
4
 
4
- module.exports = function configureTables ({ arc, errors }) {
5
- if (!arc.tables || !arc.tables.length) return null
5
+ module.exports = function configureTables ({ arc, inventory, errors }) {
6
+ let tablesSetters = inventory.plugins?._methods?.set?.tables
7
+ if ((!arc.tables || !arc.tables.length) && !tablesSetters) return null
6
8
 
7
9
  let pitrLong = 'PointInTimeRecovery' // It's just so long
10
+ let tableTemplate = () => ({
11
+ name: undefined,
12
+ partitionKey: null,
13
+ partitionKeyType: null,
14
+ sortKey: null,
15
+ sortKeyType: null,
16
+ stream: null,
17
+ ttl: null,
18
+ encrypt: null,
19
+ pitr: null,
20
+ })
8
21
 
9
- let tables = arc.tables.map(table => {
22
+ let tables = []
23
+ let plugins = populate.resources({
24
+ errors,
25
+ template: tableTemplate(),
26
+ plugins: tablesSetters,
27
+ inventory,
28
+ type: 'tables',
29
+ valid: { name: 'string' }
30
+ })
31
+ if (plugins) tables.push(...plugins)
32
+
33
+ let userland = arc?.tables?.map(table => {
10
34
  if (is.object(table)) {
11
- let name = Object.keys(table)[0]
12
- let partitionKey = null
13
- let partitionKeyType = null
14
- let sortKey = null
15
- let sortKeyType = null
16
- let stream = null
17
- let ttl = null
18
- let encrypt = null
19
- let pitr = null
20
- let pitrOld // Old opt, remove in some future breaking change
21
- Object.entries(table[name]).forEach(([ key, value ]) => {
35
+ let t = tableTemplate()
36
+ t.name = Object.keys(table)[0]
37
+ Object.entries(table[t.name]).forEach(([ key, value ]) => {
22
38
  if (is.sortKey(value)) {
23
- sortKey = key
24
- sortKeyType = value.replace('**', '')
39
+ t.sortKey = key
40
+ t.sortKeyType = value.replace('**', '').toLowerCase()
41
+ if (!t.sortKeyType) t.sortKeyType = 'string'
25
42
  }
26
43
  else if (is.primaryKey(value)) {
27
- partitionKey = key
28
- partitionKeyType = value.replace('*', '')
44
+ t.partitionKey = key
45
+ t.partitionKeyType = value.replace('*', '').toLowerCase()
46
+ if (!t.partitionKeyType) t.partitionKeyType = 'string'
29
47
  }
30
- if (key === 'stream') stream = value
31
- if (value === 'TTL') ttl = key
32
- if (key === 'encrypt') encrypt = value
33
- if (key === 'pitr') pitr = value
34
- if (key === pitrLong) pitrOld = value
48
+ if (key === 'stream') t.stream = value
49
+ if (value === 'TTL') t.ttl = key
50
+ if (key === 'encrypt') t.encrypt = value
51
+ if (key === 'PITR') t.pitr = value
52
+ if (key === 'pitr') t.pitr = value
53
+ if (key === pitrLong) t.PointInTimeRecovery = value
35
54
  })
36
- let t = {
37
- name,
38
- partitionKey,
39
- partitionKeyType,
40
- sortKey,
41
- sortKeyType,
42
- stream,
43
- ttl,
44
- encrypt,
45
- pitr,
46
- }
47
- if (pitrOld !== undefined) t.PointInTimeRecovery = pitrOld
48
55
  return t
49
56
  }
50
57
  errors.push(`Invalid @tables item: ${table}`)
51
- }).filter(Boolean) // Invalid tables may create undefined entries in the map
58
+ }).filter(Boolean) // Invalid tables or plugins may create undefined entries in the map
59
+ if (userland) tables.push(...userland)
60
+
61
+ // Normalize key type casing
62
+ if (tables.length) tables = tables.map(table => {
63
+ let { sortKeyType, partitionKeyType } = table
64
+ if (sortKeyType) table.sortKeyType = capitalize(table.sortKeyType)
65
+ if (partitionKeyType) table.partitionKeyType = capitalize(table.partitionKeyType)
66
+ return table
67
+ })
52
68
 
53
69
  validate.tables(tables, '@tables', errors)
54
70
 
@@ -22,10 +22,19 @@ function size (value, min, max, pragmaName, errors) {
22
22
 
23
23
  function unique (lambdas, pragmaName, errors) {
24
24
  if (!is.array(lambdas)) throw ReferenceError(`Invalid Lambda array: ${lambdas}`)
25
- let names = []
26
- if (lambdas.length) lambdas.forEach(({ name }) => {
27
- let err = `Duplicate ${pragmaName} item: ${name}`
28
- if (names.includes(name) && !errors.includes(err)) errors.push(err)
25
+ let names = [] // List of names we've looked at
26
+ let namesErrored = [] // List of any offending names
27
+ if (lambdas.length) lambdas.forEach(({ name, pragma }) => {
28
+ if (names.includes(name) && !namesErrored.includes(name)) {
29
+ let err = `Duplicate ${pragmaName} item: ${name}`
30
+
31
+ // If we find a plugin Lambda, that means the filter let it through because it's required by the plugin, so we must now error
32
+ let foundPlugin = lambdas.find(l => l._plugin && l.name === name)
33
+ if (foundPlugin) err = `Plugin requires conflicting ${pragmaName} item: ${name}, plugin: ${foundPlugin._plugin}, method: set.${pragma}`
34
+
35
+ namesErrored.push(name)
36
+ errors.push(err)
37
+ }
29
38
  names.push(name)
30
39
  })
31
40
  }
@@ -2,7 +2,7 @@ let { join, resolve, sep } = require('path')
2
2
  let { is } = require('../../../lib')
3
3
 
4
4
  module.exports = function validateShared (src, cwd, errors) {
5
- let path = src && resolve(join(cwd, src))
5
+ let path = src && src.startsWith(cwd) ? src : resolve(join(cwd, src))
6
6
 
7
7
  if (!is.exists(path)) errors.push(`Directory not found: ${src}`)
8
8
  else if (!is.folder(path)) errors.push(`Must be a directory: ${src}`)
@@ -0,0 +1,40 @@
1
+ let { is } = require('../../../lib')
2
+
3
+ module.exports = function validateStatic (_static, errors) {
4
+ // `folder` validation happens elsewhere
5
+ let {
6
+ compression,
7
+ fingerprint,
8
+ ignore,
9
+ prefix,
10
+ prune,
11
+ spa,
12
+ staging,
13
+ production,
14
+ } = _static
15
+
16
+ if (compression && ![ true, 'br', 'gzip' ].includes(compression)) {
17
+ errors.push(`Compression must be 'br' or 'gzip'`)
18
+ }
19
+ if (fingerprint && ![ true, 'external' ].includes(fingerprint)) {
20
+ errors.push(`Fingerprint must be true or 'external'`)
21
+ }
22
+ if (ignore && (!is.array(ignore) || !ignore.every(i => is.string(i)))) {
23
+ errors.push(`Ignore must be a list of one or more strings`)
24
+ }
25
+ if (prefix && !is.string(prefix)) {
26
+ errors.push(`Prefix must be a string`)
27
+ }
28
+ if (!is.nullish(prune) && !is.bool(prune)) {
29
+ errors.push(`Prune must be a boolean`)
30
+ }
31
+ if (!is.nullish(spa) && !is.bool(spa)) {
32
+ errors.push(`spa must be a string`)
33
+ }
34
+ if (!is.nullish(staging) && !is.string(staging)) {
35
+ errors.push(`Staging must be a string`)
36
+ }
37
+ if (!is.nullish(production) && !is.string(production)) {
38
+ errors.push(`Production must be a string`)
39
+ }
40
+ }
@@ -10,12 +10,13 @@ let { deepStrictEqual } = require('assert')
10
10
  module.exports = function validateTablesAndIndexes (pragma, pragmaName, errors) {
11
11
  if (pragma?.length) {
12
12
  pragma.forEach(table => {
13
- let { name, indexName, partitionKey, sortKey } = table
13
+ let { name, indexName, partitionKey, partitionKeyType, sortKey } = table
14
14
 
15
15
  size(name, 3, 255, pragmaName, errors)
16
16
  regex(name, 'veryLooseName', pragmaName, errors)
17
17
 
18
18
  if (!partitionKey) errors.push(`Invalid ${pragmaName} item (partition key required): '${name}'`)
19
+ if (!partitionKeyType) errors.push(`Invalid ${pragmaName} item (partition key type required): '${name}'`)
19
20
  if (indexName) {
20
21
  size(indexName, 3, 255, pragmaName, errors)
21
22
  regex(indexName, 'veryLooseName', pragmaName, errors)
@@ -8,6 +8,7 @@ module.exports = {
8
8
  queues: require('./_events'), // Same ruleset as @events
9
9
  scheduled: require('./_scheduled'),
10
10
  shared: require('./_shared'), // Also includes @views
11
+ static: require('./_static'),
11
12
  tables: require('./_tables'),
12
13
  tablesIndexes: require('./_tables'), // Same ruleset as @tables (more or less)
13
14
  tablesStreams: require('./_tables-streams'),
@@ -1,31 +1,47 @@
1
1
  let { join } = require('path')
2
+ let populate = require('./populate-other')
2
3
  let validate = require('./validate')
3
4
  let { is } = require('../../lib')
4
5
 
5
6
  module.exports = function configureViews ({ arc, pragmas, inventory, errors }) {
6
- if (arc.views && !arc.http) {
7
+ let httpSetters = inventory.plugins?._methods?.set?.http
8
+ if (arc.views && (!arc.http && !httpSetters)) {
7
9
  errors.push('@views requires @http')
8
10
  return null
9
11
  }
10
- if (!arc.http) return null
12
+ if (!arc.http && !httpSetters) return null
11
13
 
12
14
  let { cwd, src: projSrc } = inventory._project
13
15
  let src = join(projSrc, 'views')
14
16
  let views = {
15
- src,
16
- views: [] // Revert to null later if none are defined
17
+ src: null,
18
+ views: []
19
+ }
20
+
21
+ let foundSrcSetting = false
22
+ let pluginSrc = populate.settings({
23
+ errors,
24
+ settings: views,
25
+ plugins: inventory.plugins?._methods?.set?.views,
26
+ inventory,
27
+ type: 'views',
28
+ valid: { src: 'string' },
29
+ })
30
+ // Views setters only support src, and do not support specifying Lambdas
31
+ // Lambda paths have not yet been reified in Inventory
32
+ if (is.string(pluginSrc?.src)) {
33
+ views.src = pluginSrc.src
34
+ foundSrcSetting = true
17
35
  }
18
36
 
19
37
  // First pass to get + check views folder (if any)
20
- let foundSrc = false
21
38
  if (arc?.views?.length) {
22
39
  for (let view of arc.views) {
23
40
  if (is.array(view)) {
24
41
  let key = view[0]?.toLowerCase()
25
42
  if (key === 'src' && is.string(view[1])) {
26
43
  views.src = view[1]
27
- foundSrc = true
28
- validate.shared(views.src, cwd, errors)
44
+ foundSrcSetting = true
29
45
  continue
30
46
  }
31
47
  if (key === 'src' && !is.string(view[1])) {
@@ -35,11 +51,14 @@ module.exports = function configureViews ({ arc, pragmas, inventory, errors }) {
35
51
  }
36
52
  }
37
53
 
54
+ if (foundSrcSetting) validate.shared(views.src, cwd, errors)
55
+ else views.src = src
56
+
38
57
  // Exit if default views folder doesn't exist
39
58
  if (!is.exists(views.src)) return null
40
59
 
41
60
  // Proceeding from here resets all views config, so make sure it's only if specific views are specified
42
- let some = arc.views?.length && !(arc?.views?.length === 1 && foundSrc)
61
+ let some = arc.views?.length && !(arc?.views?.length === 1 && foundSrcSetting)
43
62
  if (some) {
44
63
  // Reset views settings
45
64
  for (let route of pragmas.http) {
package/src/lib/index.js CHANGED
@@ -9,6 +9,9 @@ let pragmas = require('./pragmas')
9
9
  * Why take up a whole fs block when smol libs can just live here?
10
10
  */
11
11
 
12
+ // Capitalize a string (used to normalize table/index key types)
13
+ let capitalize = str => str[0].toUpperCase() + str.substr(1)
14
+
12
15
  // For setting `lambda.build`, compiled + transpiled are effectively the same
13
16
  let compiledRuntimes = [ 'compiled', 'transpiled' ]
14
17
 
@@ -29,6 +32,7 @@ let tidyError = err => err.message + `\n` + err.stack.split('\n').slice(1).join(
29
32
 
30
33
  module.exports = {
31
34
  asapSrc,
35
+ capitalize,
32
36
  compiledRuntimes,
33
37
  errorFmt,
34
38
  getLambdaDirs,
package/src/lib/is.js CHANGED
@@ -1,19 +1,32 @@
1
1
  let { existsSync, lstatSync } = require('fs')
2
2
 
3
+ // Types
4
+ let array = item => Array.isArray(item)
5
+ let bool = item => typeof item === 'boolean'
6
+ let defined = item => typeof item !== 'undefined'
7
+ let fn = item => typeof item === 'function'
8
+ let nullish = item => typeof item === 'undefined' || item === null
9
+ let number = item => Number.isInteger(item)
10
+ let object = item => typeof item === 'object' && !Array.isArray(item)
11
+ let string = item => typeof item === 'string'
12
+ // Filesystem
13
+ let exists = path => existsSync(path)
14
+ let folder = path => existsSync(path) && lstatSync(path).isDirectory()
15
+ // Pragma-specific stuff
16
+ let primaryKey = val => string(val) && [ '*', '*string', '*number' ].includes(val.toLowerCase())
17
+ let sortKey = val => string(val) && [ '**', '**string', '**number' ].includes(val.toLowerCase())
18
+
3
19
  module.exports = {
4
- // Types
5
- array: item => Array.isArray(item),
6
- bool: item => typeof item === 'boolean',
7
- defined: item => typeof item !== 'undefined',
8
- fn: item => typeof item === 'function',
9
- nullish: item => typeof item === 'undefined' || item === null,
10
- number: item => Number.isInteger(item),
11
- object: item => typeof item === 'object' && !Array.isArray(item),
12
- string: item => typeof item === 'string',
13
- // Filesystem
14
- exists: path => existsSync(path),
15
- folder: path => existsSync(path) && lstatSync(path).isDirectory(),
16
- // Pragma-specific stuff
17
- primaryKey: val => typeof val === 'string' && (val.startsWith('*String') || val.startsWith('*Number')),
18
- sortKey: val => typeof val === 'string' && (val.startsWith('**String') || val.startsWith('**Number')),
20
+ array,
21
+ bool,
22
+ defined,
23
+ fn,
24
+ nullish,
25
+ number,
26
+ object,
27
+ string,
28
+ exists,
29
+ folder,
30
+ primaryKey,
31
+ sortKey,
19
32
  }