@toa.io/norm 1.0.0-alpha.284 → 1.0.0-alpha.287

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +1 -196
  2. package/package.json +5 -5
  3. package/src/.component/.expand/bridge.js +1 -1
  4. package/src/.component/.expand/entity.js +1 -1
  5. package/src/.component/.expand/events.js +1 -1
  6. package/src/.component/.expand/extensions.js +1 -1
  7. package/src/.component/.expand/operations.js +1 -1
  8. package/src/.component/.expand/properties.js +1 -1
  9. package/src/.component/.expand/receivers.js +3 -2
  10. package/src/.component/.expand/version.js +11 -13
  11. package/src/.component/.normalize/entity.js +7 -0
  12. package/src/.component/.normalize/events.js +4 -1
  13. package/src/.component/.normalize/index.js +1 -0
  14. package/src/.component/.normalize/operations.js +9 -1
  15. package/src/.component/.normalize/receivers.js +4 -1
  16. package/src/.component/collapse.js +30 -6
  17. package/src/.component/defaults.js +3 -4
  18. package/src/.component/dependencies.js +7 -4
  19. package/src/.component/dereference.js +14 -13
  20. package/src/.component/expand.js +11 -2
  21. package/src/.component/extensions.js +2 -1
  22. package/src/.component/index.js +1 -0
  23. package/src/.component/merge.js +3 -5
  24. package/src/.component/migrations.js +57 -0
  25. package/src/.component/normalize.js +9 -1
  26. package/src/.component/schema.yaml +29 -39
  27. package/src/.component/validate.js +23 -3
  28. package/src/.context/.dependencies/connectors.js +3 -3
  29. package/src/.context/.dependencies/describe.js +4 -1
  30. package/src/.context/.dependencies/extensions.js +18 -13
  31. package/src/.context/.dependencies/load.js +6 -4
  32. package/src/.context/.dependencies/resolve.js +1 -1
  33. package/src/.context/dereference.js +3 -1
  34. package/src/.context/normalize.js +5 -0
  35. package/src/.context/schema.yaml +6 -4
  36. package/src/component.js +14 -5
  37. package/src/context.js +9 -5
  38. package/src/entity.js +15 -0
  39. package/src/index.js +1 -0
  40. package/src/shortcuts.js +0 -1
  41. package/test/component/collapse.fixtures.js +28 -34
  42. package/test/component/collapse.test.js +41 -0
  43. package/test/component/dependencies.test.js +6 -6
  44. package/test/component/dereference.fixtures.js +13 -17
  45. package/test/component/dereference.test.js +12 -3
  46. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  47. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  48. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  49. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  50. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  51. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  52. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  53. package/test/component/migrations.test.js +62 -0
  54. package/test/component/normalize.test.js +61 -13
  55. package/test/component/validate.fixtures.js +14 -8
  56. package/test/component/validate.test.js +113 -59
  57. package/test/context/complete.test.js +5 -1
  58. package/test/context/dereference.test.js +3 -2
  59. package/test/context/expand.test.js +4 -2
  60. package/test/context/normalize.test.js +14 -1
  61. package/test/context/validate.test.js +40 -20
  62. package/test/shortcuts.test.js +2 -1
  63. package/types/component.d.ts +14 -3
  64. package/types/context/declaration.d.ts +1 -1
  65. package/types/context.d.ts +10 -9
  66. package/types/entity.d.ts +7 -0
  67. package/types/index.d.ts +1 -0
@@ -28,8 +28,7 @@ const define = async (root, manifest, property) => {
28
28
  // default bridge
29
29
  const definition = await scan(manifest.bridge, root, property)
30
30
 
31
- if (definition === undefined)
32
- return
31
+ if (definition === undefined) return
33
32
 
34
33
  const items = Object.entries(definition)
35
34
 
@@ -57,7 +56,7 @@ const define = async (root, manifest, property) => {
57
56
  const cache = {}
58
57
 
59
58
  // the promise is what is remembered, so a bridge is loaded once
60
- function req (mod) {
59
+ function req(mod) {
61
60
  cache[mod] ??= import(mod)
62
61
 
63
62
  return cache[mod]
@@ -66,8 +65,7 @@ function req (mod) {
66
65
  const scan = async (bridge, root, property) => {
67
66
  const { define } = await req(bridge)
68
67
 
69
- if (property in define)
70
- return define[property](root)
68
+ if (property in define) return define[property](root)
71
69
  else return undefined
72
70
  }
73
71
 
@@ -0,0 +1,57 @@
1
+ import { basename, extname, resolve } from 'node:path'
2
+ import { readFile } from 'node:fs/promises'
3
+
4
+ import glob from 'fast-glob'
5
+ import { yaml } from '@toa.io/generic'
6
+
7
+ /**
8
+ * The migrations a component declares, in the order they are applied. The file name without its
9
+ * extension is the migration's id, and sorting those ids is what orders them — so a name is
10
+ * written to sort: `0001-indexes`, then `0002-default-role`.
11
+ *
12
+ * What a file contains is the storage's business; this reads it and hands it over.
13
+ */
14
+ export const migrations = async (root, manifest) => {
15
+ const paths = await glob(resolve(root, DIRECTORY, '*' + EXTENSIONS), GLOB)
16
+
17
+ if (paths.length === 0) return
18
+
19
+ if (manifest.entity === undefined)
20
+ throw new Error(`Component at '${root}' declares migrations but stores nothing`)
21
+
22
+ const ids = new Map()
23
+
24
+ // sorted, so the two files a conflict names are the same two on every machine
25
+ for (const path of paths.sort()) {
26
+ const id = basename(path, extname(path))
27
+ const found = ids.get(id)
28
+
29
+ if (found !== undefined)
30
+ throw new Error(
31
+ `Component at '${root}' has more than one ${DIRECTORY}/${id}: ` +
32
+ `${basename(found)} and ${basename(path)}`
33
+ )
34
+
35
+ ids.set(id, path)
36
+ }
37
+
38
+ manifest.entity.migrations = await Promise.all(
39
+ Array.from(ids, async ([id, path]) => ({ id, steps: await read(path) }))
40
+ )
41
+ }
42
+
43
+ /**
44
+ * A migration is a list of steps. A file that is not one is refused here rather than at the
45
+ * storage, where the component it belongs to is no longer known.
46
+ */
47
+ async function read(path) {
48
+ const steps = yaml.load(await readFile(path, 'utf8'))
49
+
50
+ if (!Array.isArray(steps)) throw new Error(`Migration '${path}' is not a list of steps`)
51
+
52
+ return steps
53
+ }
54
+
55
+ const DIRECTORY = 'migrations'
56
+ const EXTENSIONS = '.{yaml,yml,json}'
57
+ const GLOB = { onlyFiles: true, absolute: true }
@@ -1,6 +1,14 @@
1
- import { events, operations, receivers } from './.normalize/index.js'
1
+ import { entity, events, operations, receivers } from './.normalize/index.js'
2
2
 
3
+ /**
4
+ * What a manifest does not say but the runtime needs said. The schema states what a value may
5
+ * be; what it is where nothing was written is decided here, so that validating a manifest is
6
+ * reading it rather than completing it.
7
+ */
3
8
  export const normalize = async (component) => {
9
+ component.namespace ??= 'default'
10
+
11
+ entity(component)
4
12
  operations(component)
5
13
  await events(component)
6
14
  receivers(component)
@@ -51,7 +51,6 @@ properties:
51
51
  namespace:
52
52
  type: string
53
53
  pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
54
- default: "default"
55
54
  not:
56
55
  oneOf:
57
56
  - const: "system"
@@ -72,43 +71,42 @@ properties:
72
71
  properties:
73
72
  storage:
74
73
  type: string
75
- default: "@toa.io/storages.mongodb"
76
- schema:
77
- $ref: https://json-schema.org/draft/2019-09/schema
74
+ properties:
78
75
  type: object
79
- properties:
80
- type:
81
- default: object
82
- const: object
83
- properties:
84
- type: object
85
- propertyNames:
86
- oneOf:
87
- - $ref: "#/definitions/name"
88
- - enum: [_version, _created, _updated, _deleted]
89
- unique:
76
+ # a system property is named like any other, so nothing is enumerated beside them
77
+ propertyNames:
78
+ $ref: "#/definitions/name"
79
+ additionalProperties:
80
+ $ref: https://json-schema.org/draft/2019-09/schema
81
+ required:
82
+ type: array
83
+ uniqueItems: true
84
+ items:
85
+ $ref: "#/definitions/name"
86
+ blank:
87
+ # what a record holds before anything is written to it; a system property is the
88
+ # runtime's to write, so none may be named here
90
89
  type: object
91
- patternProperties:
92
- ".*":
93
- type: array
94
- items:
90
+ propertyNames:
91
+ not:
92
+ enum: [id, VERSION, CREATED, UPDATED, DELETED]
93
+ migrations:
94
+ type: array
95
+ items:
96
+ type: object
97
+ properties:
98
+ id:
95
99
  type: string
96
- index:
97
- type: object
98
- patternProperties:
99
- ".*":
100
- type: object
101
- patternProperties:
102
- ".*":
103
- type: string
104
- enum: [asc, desc, hash, text]
100
+ # what a step means belongs to the storage, which is the only thing that reads it
101
+ steps:
102
+ type: array
103
+ required: [id, steps]
104
+ additionalProperties: false
105
105
  associated:
106
106
  type: boolean
107
- default: false
108
107
  custom:
109
108
  type: boolean
110
- default: false
111
- required: [schema]
109
+ required: [properties]
112
110
  additionalProperties: false
113
111
 
114
112
  bindings:
@@ -155,7 +153,6 @@ properties:
155
153
  $ref: https://json-schema.org/draft/2019-09/schema
156
154
  output:
157
155
  $ref: https://json-schema.org/draft/2019-09/schema
158
- default: {}
159
156
  errors:
160
157
  # what a reply may carry as a code: the error schema allows either
161
158
  type: array
@@ -228,10 +225,8 @@ properties:
228
225
  properties:
229
226
  scope:
230
227
  const: none
231
- default: none
232
228
  query:
233
229
  const: false
234
- default: false
235
230
  - if: # effect
236
231
  properties:
237
232
  type:
@@ -239,7 +234,6 @@ properties:
239
234
  then:
240
235
  properties:
241
236
  scope:
242
- default: none
243
237
  enum: [object, objects, stream, none]
244
238
  additionalProperties: false
245
239
  additionalProperties: false
@@ -260,10 +254,8 @@ properties:
260
254
  $ref: "#/definitions/binding"
261
255
  conditioned:
262
256
  type: boolean
263
- default: false
264
257
  subjective:
265
258
  type: boolean
266
- default: false
267
259
  required: [bridge, path]
268
260
  additionalProperties: false
269
261
 
@@ -287,10 +279,8 @@ properties:
287
279
  type: string
288
280
  conditioned:
289
281
  type: boolean
290
- default: false
291
282
  adaptive:
292
283
  type: boolean
293
- default: false
294
284
  arguments:
295
285
  type: array
296
286
  required: [operation]
@@ -4,7 +4,9 @@ import { readFileSync } from 'node:fs'
4
4
  import { yaml } from '@toa.io/generic'
5
5
  import * as schemas from '@toa.io/schemas'
6
6
 
7
- const object = yaml.load(readFileSync(path.resolve(import.meta.dirname, 'schema.yaml'), 'utf8'))
7
+ const object = yaml.load(
8
+ readFileSync(path.resolve(import.meta.dirname, 'schema.yaml'), 'utf8')
9
+ )
8
10
  const schema = schemas.schema(object)
9
11
 
10
12
  export const validate = async (manifest) => {
@@ -12,10 +14,24 @@ export const validate = async (manifest) => {
12
14
 
13
15
  if (error) throw error
14
16
 
17
+ if (manifest.entity !== undefined) entity(manifest)
15
18
  if (manifest.events !== undefined) await events(manifest)
16
19
  if (manifest.receivers !== undefined) receivers(manifest)
17
20
  }
18
21
 
22
+ /** What only the entity's own declaration can answer: whether a name it uses is one it declares. */
23
+ const entity = (manifest) => {
24
+ const { properties, required, blank } = manifest.entity
25
+
26
+ for (const name of required ?? [])
27
+ if (properties[name] === undefined)
28
+ throw new Error(`Entity requires property '${name}', which is not defined`)
29
+
30
+ for (const name of Object.keys(blank ?? {}))
31
+ if (properties[name] === undefined)
32
+ throw new Error(`Entity blank names property '${name}', which is not defined`)
33
+ }
34
+
19
35
  const events = async (manifest) => {
20
36
  for (const [label, event] of Object.entries(manifest.events)) {
21
37
  const { properties } = await import(event.binding)
@@ -29,11 +45,15 @@ const events = async (manifest) => {
29
45
  const receivers = (manifest) => {
30
46
  for (const [locator, receiver] of Object.entries(manifest.receivers)) {
31
47
  if (manifest.operations?.[receiver.operation] === undefined) {
32
- throw new Error(`Receiver '${locator}' refers to undefined operation '${receiver.operation}'`)
48
+ throw new Error(
49
+ `Receiver '${locator}' refers to undefined operation '${receiver.operation}'`
50
+ )
33
51
  }
34
52
 
35
53
  if (!TYPES.has(manifest.operations[receiver.operation].type)) {
36
- throw new Error(`Receiver '${locator}' must refer to an operation of the allowed types: ${Array.from(TYPES).join(', ')}`)
54
+ throw new Error(
55
+ `Receiver '${locator}' must refer to an operation of the allowed types: ${Array.from(TYPES).join(', ')}`
56
+ )
37
57
  }
38
58
  }
39
59
  }
@@ -3,10 +3,10 @@ import { resolve } from 'node:path'
3
3
  export const connectors = (context, extracted) => {
4
4
  const connectors = {}
5
5
 
6
- const components = (context.components === undefined
6
+ const components =
7
+ (context.components === undefined
7
8
  ? extracted
8
- : context.components.concat(extracted)
9
- ) ?? []
9
+ : context.components.concat(extracted)) ?? []
10
10
 
11
11
  for (const component of components) {
12
12
  if (component.entity !== undefined) {
@@ -3,4 +3,7 @@
3
3
  * @param {Object} manifest
4
4
  * @returns {toa.norm.context.dependencies.Instance}
5
5
  */
6
- export const describe = (component, manifest = undefined) => ({ locator: component.locator, manifest })
6
+ export const describe = (component, manifest = undefined) => ({
7
+ locator: component.locator,
8
+ manifest
9
+ })
@@ -10,7 +10,12 @@ export const extensions = async (context) => {
10
10
  const declared = await extractDeclaredServices(context, extensions)
11
11
 
12
12
  const extracted = declared.concat(
13
- await extractExtensionComponents(components.concat(declared), extensions, context.annotations))
13
+ await extractExtensionComponents(
14
+ components.concat(declared),
15
+ extensions,
16
+ context.annotations
17
+ )
18
+ )
14
19
 
15
20
  components.push(...extracted)
16
21
 
@@ -27,7 +32,7 @@ export const extensions = async (context) => {
27
32
  return { extensions, components: extracted }
28
33
  }
29
34
 
30
- async function extractDeclaredServices (context, extensions) {
35
+ async function extractDeclaredServices(context, extensions) {
31
36
  const extracted = []
32
37
 
33
38
  for (const composition of context.compositions ?? [])
@@ -35,18 +40,20 @@ async function extractDeclaredServices (context, extensions) {
35
40
  if (reference in extensions) continue
36
41
 
37
42
  try {
38
- extracted.push(...await extract(reference, extensions, context.annotations))
43
+ extracted.push(...(await extract(reference, extensions, context.annotations)))
39
44
  } catch (e) {
40
- throw new Error(`Composition '${composition.name}' lists '${reference}', ` +
41
- `which cannot be resolved: ${e.message}`, { cause: e })
45
+ throw new Error(
46
+ `Composition '${composition.name}' lists '${reference}', ` +
47
+ `which cannot be resolved: ${e.message}`,
48
+ { cause: e }
49
+ )
42
50
  }
43
51
  }
44
52
 
45
53
  return extracted
46
54
  }
47
55
 
48
- async function extractExtensionComponents (components, extensions, annotations) {
49
-
56
+ async function extractExtensionComponents(components, extensions, annotations) {
50
57
  const extracted = []
51
58
 
52
59
  for (const component of components) {
@@ -55,12 +62,11 @@ async function extractExtensionComponents (components, extensions, annotations)
55
62
  for (const reference of Object.keys(component.extensions)) {
56
63
  if (reference in extensions) continue
57
64
 
58
- extracted.push(...await extract(reference, extensions, annotations))
65
+ extracted.push(...(await extract(reference, extensions, annotations)))
59
66
  }
60
67
  }
61
68
 
62
- if (extracted.length === 0)
63
- return extracted
69
+ if (extracted.length === 0) return extracted
64
70
 
65
71
  const deeper = await extractExtensionComponents(extracted, extensions, annotations)
66
72
 
@@ -75,7 +81,7 @@ async function extractExtensionComponents (components, extensions, annotations)
75
81
  * @param {object} [annotations]
76
82
  * @returns {Promise<Array<toa.norm.Component>>}
77
83
  */
78
- async function extract (reference, extensions, annotations) {
84
+ async function extract(reference, extensions, annotations) {
79
85
  extensions[reference] = []
80
86
 
81
87
  const { metadata, module: mod } = await loadDependency(reference)
@@ -86,8 +92,7 @@ async function extract (reference, extensions, annotations) {
86
92
  const annotation = annotations?.[metadata?.name ?? reference]
87
93
  const extracted = []
88
94
 
89
- for (const path of mod.components(annotation).paths)
90
- extracted.push(await load(path))
95
+ for (const path of mod.components(annotation).paths) extracted.push(await load(path))
91
96
 
92
97
  return extracted
93
98
  }
@@ -11,16 +11,18 @@ const require = createRequire(import.meta.url)
11
11
  * @param {string} path
12
12
  * @returns {Promise<{ metadata: object, module: object }>}
13
13
  */
14
- export async function load (path) {
14
+ export async function load(path) {
15
15
  const metadata = loadMetadata(path)
16
16
  const module = await loadModule(path)
17
17
 
18
18
  return { metadata, module }
19
19
  }
20
20
 
21
- function loadMetadata (reference) {
21
+ function loadMetadata(reference) {
22
22
  try {
23
- return JSON.parse(readFileSync(require.resolve(join(reference, 'package.json')), 'utf8'))
23
+ return JSON.parse(
24
+ readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
25
+ )
24
26
  } catch {
25
27
  return null
26
28
  }
@@ -30,7 +32,7 @@ function loadMetadata (reference) {
30
32
  * @param {string} reference
31
33
  * @returns {Promise<object>}
32
34
  */
33
- async function loadModule (reference) {
35
+ async function loadModule(reference) {
34
36
  const path = require.resolve(reference)
35
37
 
36
38
  return await import(pathToFileURL(path).href)
@@ -38,7 +38,7 @@ export const resolve = async (references, annotations = {}) => {
38
38
  * @param {string} reference
39
39
  * @returns {object | null}
40
40
  */
41
- async function optional (reference) {
41
+ async function optional(reference) {
42
42
  try {
43
43
  return (await load(reference)).module
44
44
  } catch {
@@ -12,7 +12,9 @@ export const dereference = (context) => {
12
12
  const component = components[id]
13
13
 
14
14
  if (component === undefined)
15
- throw new Error(`Composition '${composition.name}' lists an unknown component '${id}'.`)
15
+ throw new Error(
16
+ `Composition '${composition.name}' lists an unknown component '${id}'.`
17
+ )
16
18
 
17
19
  return component
18
20
  })
@@ -12,4 +12,9 @@ export const normalize = (context) => {
12
12
  }
13
13
 
14
14
  if (typeof context.registry === 'string') context.registry = { base: context.registry }
15
+
16
+ // what an image is built for, where the Context does not say
17
+ if (context.registry !== undefined) context.registry.platforms ??= PLATFORMS
15
18
  }
19
+
20
+ const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']
@@ -40,10 +40,12 @@ properties:
40
40
  minItems: 1
41
41
  items:
42
42
  type: string
43
- default:
44
- - linux/amd64
45
- - linux/arm/v7
46
- - linux/arm64
43
+ services:
44
+ description: >
45
+ Where an extension service's image comes from. `build`, which absence means,
46
+ builds it into `registry.base` the way a composition is built. `published` uses
47
+ the image the extension ships, which is neither built nor pushed.
48
+ enum: [build, published]
47
49
  # required: [base]
48
50
  compositions:
49
51
  type: array
package/src/component.js CHANGED
@@ -5,7 +5,17 @@ import { yaml as jsyaml } from '@toa.io/generic'
5
5
  import { find } from '@toa.io/generic'
6
6
  import { Locator } from '@toa.io/core'
7
7
 
8
- import { expand, merge, validate, collapse, dereference, defaults, normalize, extensions } from './.component/index.js'
8
+ import {
9
+ expand,
10
+ merge,
11
+ migrations,
12
+ validate,
13
+ collapse,
14
+ dereference,
15
+ defaults,
16
+ normalize,
17
+ extensions
18
+ } from './.component/index.js'
9
19
 
10
20
  export const component = async (path) => {
11
21
  const manifest = await load(path)
@@ -23,7 +33,7 @@ const load = async (path, base, proto = false) => {
23
33
  if (base !== undefined) path = find(path, base, MANIFEST)
24
34
 
25
35
  const file = join(path, MANIFEST)
26
- const manifest = await read(file) ?? {}
36
+ const manifest = (await read(file)) ?? {}
27
37
 
28
38
  manifest.path = path
29
39
 
@@ -31,6 +41,7 @@ const load = async (path, base, proto = false) => {
31
41
  await expand(manifest)
32
42
 
33
43
  await merge(path, manifest)
44
+ await migrations(path, manifest)
34
45
 
35
46
  if (manifest.prototype !== null) {
36
47
  const prototype = await load(manifest.prototype, path, true)
@@ -46,8 +57,6 @@ const load = async (path, base, proto = false) => {
46
57
 
47
58
  const MANIFEST = 'manifest.toa.yaml'
48
59
 
49
-
50
-
51
60
  /**
52
61
  * Reads a YAML file, resolving anchors into distinct objects so that
53
62
  * mutating one node cannot reach another.
@@ -55,7 +64,7 @@ const MANIFEST = 'manifest.toa.yaml'
55
64
  * @param {string} path
56
65
  * @return {Promise<object>}
57
66
  */
58
- async function read (path) {
67
+ async function read(path) {
59
68
  const object = jsyaml.load(await readFile(path, 'utf8'))
60
69
 
61
70
  return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
package/src/context.js CHANGED
@@ -6,7 +6,14 @@ import { yaml as jsyaml } from '@toa.io/generic'
6
6
 
7
7
  import { component } from './component.js'
8
8
 
9
- import { dependencies, normalize, complete, dereference, expand, validate } from './.context/index.js'
9
+ import {
10
+ dependencies,
11
+ normalize,
12
+ complete,
13
+ dereference,
14
+ expand,
15
+ validate
16
+ } from './.context/index.js'
10
17
 
11
18
  export const context = async (root, environment = process.env.TOA_ENV) => {
12
19
  const path = resolve(root, CONTEXT)
@@ -34,11 +41,8 @@ export const context = async (root, environment = process.env.TOA_ENV) => {
34
41
  const CONTEXT = 'context.toa.yaml'
35
42
  const COMPONENTS = 'components/*'
36
43
 
37
-
38
44
  const GLOB = { onlyDirectories: true, absolute: true }
39
45
 
40
-
41
-
42
46
  /**
43
47
  * Reads a YAML file, resolving anchors into distinct objects so that
44
48
  * mutating one node cannot reach another.
@@ -46,7 +50,7 @@ const GLOB = { onlyDirectories: true, absolute: true }
46
50
  * @param {string} path
47
51
  * @return {Promise<object>}
48
52
  */
49
- async function read (path) {
53
+ async function read(path) {
50
54
  const object = jsyaml.load(await readFile(path, 'utf8'))
51
55
 
52
56
  return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
package/src/entity.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What a component declares about its records is a property map and a list of the properties a
3
+ * whole one has. A validator wants a schema, so it is assembled here rather than declared: one
4
+ * that a stored record must fit, and one for a changeset, which is whatever subset of the same
5
+ * properties an assignment writes.
6
+ */
7
+ export const schema = (entity) =>
8
+ entity.required === undefined
9
+ ? changeset(entity)
10
+ : { type: 'object', properties: entity.properties, required: entity.required }
11
+
12
+ export const changeset = (entity) => ({
13
+ type: 'object',
14
+ properties: entity.properties
15
+ })
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { context } from './context.js'
2
2
  export { component } from './component.js'
3
3
  export * as shortcuts from './shortcuts.js'
4
+ export * as entity from './entity.js'
package/src/shortcuts.js CHANGED
@@ -41,7 +41,6 @@ export const SHORTCUTS = {
41
41
  node: '@toa.io/bridges.node',
42
42
  bash: '@toa.io/bridges.bash',
43
43
  mongodb: '@toa.io/storages.mongodb',
44
- sql: '@toa.io/storages.sql',
45
44
  queues: '@toa.io/storages.queues',
46
45
  exposition: '@toa.io/extensions.exposition',
47
46
  realtime: '@toa.io/extensions.realtime',
@@ -3,51 +3,45 @@ import { generate } from 'randomstring'
3
3
  const entity = {
4
4
  manifest: {
5
5
  entity: {
6
- schema: {
7
- properties: {
8
- foo: {
9
- type: 'string'
10
- },
11
- bar: {
12
- type: 'integer'
13
- }
6
+ properties: {
7
+ foo: {
8
+ type: 'string'
14
9
  },
15
- required: ['foo']
16
- }
10
+ bar: {
11
+ type: 'integer'
12
+ }
13
+ },
14
+ required: ['foo']
17
15
  }
18
16
  },
19
17
  prototype: {
20
18
  entity: {
21
- schema: {
22
- properties: {
23
- foo: {
24
- default: 'ok'
25
- },
26
- baz: {
27
- type: 'boolean'
28
- }
19
+ properties: {
20
+ foo: {
21
+ minLength: 1
29
22
  },
30
- required: ['baz']
31
- }
23
+ baz: {
24
+ type: 'boolean'
25
+ }
26
+ },
27
+ required: ['baz']
32
28
  }
33
29
  },
34
30
  result: {
35
31
  entity: {
36
- schema: {
37
- properties: {
38
- foo: {
39
- type: 'string',
40
- default: 'ok'
41
- },
42
- bar: {
43
- type: 'integer'
44
- },
45
- baz: {
46
- type: 'boolean'
47
- }
32
+ properties: {
33
+ foo: {
34
+ type: 'string',
35
+ minLength: 1
48
36
  },
49
- required: ['foo', 'baz']
50
- }
37
+ bar: {
38
+ type: 'integer'
39
+ },
40
+ baz: {
41
+ type: 'boolean'
42
+ }
43
+ },
44
+ required: ['foo', 'baz']
51
45
  }
52
46
  }
53
47
  }