@voxgig/apidef 8.13.0 → 8.15.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.
Files changed (67) hide show
  1. package/bin/voxgig-apidef +1 -174
  2. package/dist/apidef.d.ts +2 -2
  3. package/dist/apidef.js +2 -2
  4. package/dist/apidef.js.map +1 -1
  5. package/dist/builder/entity/entity.d.ts +5 -1
  6. package/dist/builder/entity/entity.js +17 -1
  7. package/dist/builder/entity/entity.js.map +1 -1
  8. package/dist/builder/flow/flowHeuristic01.js +14 -14
  9. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  10. package/dist/cli.d.ts +38 -0
  11. package/dist/cli.js +279 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/guide/guide.js +8 -5
  14. package/dist/guide/guide.js.map +1 -1
  15. package/dist/model.d.ts +35 -33
  16. package/dist/resolved.d.ts +10 -6
  17. package/dist/resolved.js +65 -15
  18. package/dist/resolved.js.map +1 -1
  19. package/dist/transform/args.js +19 -19
  20. package/dist/transform/args.js.map +1 -1
  21. package/dist/transform/clean.js +4 -0
  22. package/dist/transform/clean.js.map +1 -1
  23. package/dist/transform/contract.d.ts +0 -2
  24. package/dist/transform/contract.js +6 -75
  25. package/dist/transform/contract.js.map +1 -1
  26. package/dist/transform/entity.d.ts +2 -1
  27. package/dist/transform/entity.js +13 -1
  28. package/dist/transform/entity.js.map +1 -1
  29. package/dist/transform/field.js +64 -64
  30. package/dist/transform/field.js.map +1 -1
  31. package/dist/transform/flowstep.js +42 -42
  32. package/dist/transform/flowstep.js.map +1 -1
  33. package/dist/transform/graphql.js +8 -8
  34. package/dist/transform/graphql.js.map +1 -1
  35. package/dist/transform/operation.js +9 -9
  36. package/dist/transform/operation.js.map +1 -1
  37. package/dist/transform/select.js +13 -13
  38. package/dist/transform/select.js.map +1 -1
  39. package/dist/transform/top.js +1 -1
  40. package/dist/transform/top.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/types.d.ts +1 -1
  43. package/dist/utility.d.ts +2 -1
  44. package/dist/utility.js +6 -1
  45. package/dist/utility.js.map +1 -1
  46. package/model/apidef.aon +128 -157
  47. package/model/guide.aon +3 -2
  48. package/package.json +5 -3
  49. package/src/apidef.ts +4 -3
  50. package/src/builder/entity/entity.ts +18 -1
  51. package/src/builder/flow/flowHeuristic01.ts +14 -14
  52. package/src/cli.ts +341 -0
  53. package/src/guide/guide.ts +8 -4
  54. package/src/model.ts +36 -37
  55. package/src/resolved.ts +62 -17
  56. package/src/transform/args.ts +20 -20
  57. package/src/transform/clean.ts +5 -0
  58. package/src/transform/contract.ts +6 -71
  59. package/src/transform/entity.ts +13 -1
  60. package/src/transform/field.ts +66 -69
  61. package/src/transform/flowstep.ts +42 -42
  62. package/src/transform/graphql.ts +8 -8
  63. package/src/transform/operation.ts +9 -9
  64. package/src/transform/select.ts +13 -13
  65. package/src/transform/top.ts +1 -1
  66. package/src/types.ts +1 -1
  67. package/src/utility.ts +8 -1
package/src/resolved.ts CHANGED
@@ -2,7 +2,22 @@
2
2
 
3
3
  // See docs/design/resolved-spec-capability.md
4
4
 
5
- import { graphqlInputTypes } from './transform/contract'
5
+ // An operation needs its argument types, including recursive input objects.
6
+ // Output types are represented by the field and generated invocation selection;
7
+ // copying the entire connected output graph per operation is quadratic in API size.
8
+ function graphqlInputTypes(field: any, types: any): any {
9
+ const out: any = {}
10
+ function visit(name: string) {
11
+ if (!types[name] || Object.prototype.hasOwnProperty.call(out, name)) return
12
+ const type = types[name]
13
+ out[name] = type
14
+ if (type.kind === 'INPUT_OBJECT') {
15
+ for (const child of Object.values(type.fields || {}) as any[]) visit(child.type)
16
+ }
17
+ }
18
+ for (const arg of field.args || []) visit(arg.type)
19
+ return out
20
+ }
6
21
 
7
22
 
8
23
  const METHODS = [
@@ -16,6 +31,8 @@ type OperationFacts = {
16
31
  }
17
32
 
18
33
 
34
+ type OperationSelector = { entity: string, op: string }
35
+
19
36
  type ResolvedSpec = {
20
37
  version: 1
21
38
  kind: string
@@ -24,16 +41,14 @@ type ResolvedSpec = {
24
41
  // during parse, so a consumer reading the file itself would miss lookups.
25
42
  def: any
26
43
 
27
- operation(method: string, path: string): OperationFacts | undefined
44
+ operation(method: string, path: string, selector?: OperationSelector): OperationFacts | undefined
28
45
  }
29
46
 
30
47
 
31
- // The single definition of a resolved operation, shared by contractTransform
32
- // and by consumers of the capability, so the two cannot disagree.
33
- function operationFacts(def: any, point: { method: string, orig: string }): OperationFacts | undefined {
34
- const path = def?.paths?.[point.orig]
35
- const method = path?.[String(point.method).toLowerCase()]
36
- const graphql = def?.query?.[point.orig] || def?.mutation?.[point.orig]
48
+ function operationFacts(def: any, point: { m: string, o: string }): OperationFacts | undefined {
49
+ const path = def?.paths?.[point.o]
50
+ const method = path?.[String(point.m).toLowerCase()]
51
+ const graphql = def?.query?.[point.o] || def?.mutation?.[point.o]
37
52
 
38
53
  if (!method && !graphql) return undefined
39
54
 
@@ -67,21 +82,21 @@ function operationFacts(def: any, point: { method: string, orig: string }): Oper
67
82
  }
68
83
 
69
84
 
70
- // Every described operation, keyed 'METHOD path' as `point.contract.id` is.
85
+ // Every described operation, keyed 'METHOD path' as `point.co.id` is.
71
86
  function operationIndex(def: any): { [id: string]: OperationFacts } {
72
87
  const out: { [id: string]: OperationFacts } = {}
73
88
 
74
89
  for (const path of Object.keys(def?.paths || {})) {
75
90
  for (const method of METHODS) {
76
91
  if (null == def.paths[path]?.[method]) continue
77
- const facts = operationFacts(def, { method, orig: path })
92
+ const facts = operationFacts(def, { m: method, o: path })
78
93
  if (facts) out[method.toUpperCase() + ' ' + path] = facts
79
94
  }
80
95
  }
81
96
 
82
97
  for (const kind of ['query', 'mutation']) {
83
98
  for (const field of Object.keys(def?.[kind] || {})) {
84
- const facts = operationFacts(def, { method: 'POST', orig: field })
99
+ const facts = operationFacts(def, { m: 'POST', o: field })
85
100
  if (facts) out['POST ' + field] = facts
86
101
  }
87
102
  }
@@ -90,20 +105,49 @@ function operationIndex(def: any): { [id: string]: OperationFacts } {
90
105
  }
91
106
 
92
107
 
93
- function makeResolved(kind: string, def: any): ResolvedSpec {
108
+ function operationGuide(guide: any, method: string, path: string, graphql: boolean,
109
+ selector?: OperationSelector): any {
110
+ const matches: any[] = []
111
+ for (const [entityName, entity] of Object.entries(guide?.entity || {}) as any) {
112
+ if (selector && selector.entity !== entityName) continue
113
+ const ops = entity[graphql ? 'field' : 'path']?.[path]?.op || {}
114
+ for (const [opName, op] of Object.entries(ops) as any) {
115
+ if (selector && selector.op !== opName) continue
116
+ if (op.method && op.method.toUpperCase() !== method.toUpperCase()) continue
117
+ if (op.contract !== undefined || op.live !== undefined) matches.push(op)
118
+ }
119
+ }
120
+ if (matches.length > 1) {
121
+ throw new Error('Ambiguous operation guide for ' + method + ' ' + path + '; select entity and op')
122
+ }
123
+ return matches[0]
124
+ }
125
+
126
+ function makeResolved(kind: string, def: any, guide: () => any = () => undefined): ResolvedSpec {
94
127
  return {
95
128
  version: 1,
96
129
  kind,
97
130
  def,
98
- operation: (method: string, path: string) =>
99
- operationFacts(def, { method, orig: path }),
131
+ operation: (method: string, path: string, selector?: OperationSelector) => {
132
+ const facts = operationFacts(def, { m: method, o: path })
133
+ if (!facts) return undefined
134
+ const op = operationGuide(guide(), method, path, facts.protocol === 'graphql', selector)
135
+ for (const key of ['requestBody', 'responses', 'parameters', 'security']) {
136
+ if (op?.contract?.[key] !== undefined) {
137
+ facts[key] = op.contract[key]
138
+ ;(facts.factSources ??= {})[key] = 'guide'
139
+ }
140
+ }
141
+ if (op?.live !== undefined) facts.live = op.live
142
+ return facts
143
+ },
100
144
  }
101
145
  }
102
146
 
103
147
 
104
148
  // Tolerates a missing context: apidef also runs outside a model build.
105
- function publishResolved(ctx: any, kind: string, def: any): ResolvedSpec {
106
- const resolved = makeResolved(kind, def)
149
+ function publishResolved(ctx: any, kind: string, def: any, guide?: () => any): ResolvedSpec {
150
+ const resolved = makeResolved(kind, def, guide)
107
151
  if (null != ctx && 'object' === typeof ctx) {
108
152
  ctx.state = ctx.state || {}
109
153
  ctx.state.apidef = { ...(ctx.state.apidef || {}), resolved }
@@ -114,7 +158,7 @@ function publishResolved(ctx: any, kind: string, def: any): ResolvedSpec {
114
158
 
115
159
  function resolvedSpec(carrier: any): ResolvedSpec | undefined {
116
160
  if (null == carrier || 'object' !== typeof carrier) return undefined
117
- return carrier.state?.apidef?.resolved ??
161
+ return carrier.resolved ?? carrier.ctx?.resolved ?? carrier.state?.apidef?.resolved ??
118
162
  carrier.ctx?.state?.apidef?.resolved ??
119
163
  carrier.apidef?.resolved ??
120
164
  undefined
@@ -122,6 +166,7 @@ function resolvedSpec(carrier: any): ResolvedSpec | undefined {
122
166
 
123
167
 
124
168
  export type {
169
+ OperationSelector,
125
170
  OperationFacts,
126
171
  ResolvedSpec,
127
172
  }
@@ -41,7 +41,7 @@ const argsTransform: Transform = async function(
41
41
  each(mop.points, (mpoint: ModelPoint) => {
42
42
  const argdefs: ParameterDef[] = []
43
43
 
44
- if ('graphql' === mpoint.kind) {
44
+ if ('graphql' === mpoint.k) {
45
45
  // GraphQL root-field arguments become 'param' args, so the existing
46
46
  // arg machinery (select.exist matching, request typing, test
47
47
  // generation) works on them unchanged. Input-object arguments are
@@ -64,10 +64,10 @@ const argsTransform: Transform = async function(
64
64
  }
65
65
  }
66
66
  else {
67
- const pathdef: PathDef = def.paths[mpoint.orig]
67
+ const pathdef: PathDef = def.paths[mpoint.o]
68
68
  argdefs.push(...((pathdef as any)?.parameters ?? []))
69
69
 
70
- const opdef: MethodDef = (pathdef as any)?.[mpoint.method.toLowerCase()]
70
+ const opdef: MethodDef = (pathdef as any)?.[mpoint.m.toLowerCase()]
71
71
  argdefs.push(...(opdef?.parameters ?? []))
72
72
  }
73
73
 
@@ -85,8 +85,8 @@ const argsTransform: Transform = async function(
85
85
 
86
86
  // Locate the normalised root-field descriptor a GraphQL point came from.
87
87
  function graphqlFieldDef(def: any, mpoint: ModelPoint): any {
88
- const field = mpoint.graphql?.field ?? mpoint.orig
89
- return 'mutation' === mpoint.graphql?.optype ?
88
+ const field = mpoint.gq?.field ?? mpoint.o
89
+ return 'mutation' === mpoint.gq?.optype ?
90
90
  def.mutation?.[field] : def.query?.[field]
91
91
  }
92
92
 
@@ -100,7 +100,7 @@ function gqlScalarType(typeName: string): string | undefined {
100
100
  }
101
101
 
102
102
 
103
- const ARG_KIND: Record<string, ModelArg["kind"]> = {
103
+ const ARG_KIND: Record<string, ModelArg["k"]> = {
104
104
  'query': 'query',
105
105
  'header': 'header',
106
106
  'path': 'param',
@@ -122,11 +122,11 @@ function resolveArgs(
122
122
  const ref = (argdef as any)?.$ref
123
123
  ctx?.warn?.({
124
124
  note: `Parameter with no name on entity=${ment.name} op=${mop.name}` +
125
- ` path=${mpoint.orig} is dropped` +
125
+ ` path=${mpoint.o} is dropped` +
126
126
  (null == ref ? '.' : `: \`$ref\` "${ref}" resolves to nothing.`) +
127
127
  ' A parameter needs a `name`, or a reference that resolves to one.',
128
128
  entity: ment.name,
129
- path: mpoint.orig,
129
+ path: mpoint.o,
130
130
  op: mop.name,
131
131
  })
132
132
  return
@@ -136,35 +136,35 @@ function resolveArgs(
136
136
  // Rename map can be keyed by either the spec original (camelCase) or by
137
137
  // the snakified form depending on which path went through heuristic01.
138
138
  // Try both before falling through to `orig`.
139
- const renameMap = mpoint.rename[kind]
139
+ const renameMap = mpoint.r[kind]
140
140
  const name = renameMap?.[specName] ?? renameMap?.[orig] ?? orig
141
141
  const marg: ModelArg = {
142
- name,
143
- orig,
144
- type: inferFieldType(name, validator(argdef.schema?.type)),
145
- kind,
146
- reqd: !!argdef.required
142
+ n: name,
143
+ or: orig,
144
+ t: inferFieldType(name, validator(argdef.schema?.type)),
145
+ k: kind,
146
+ r: !!argdef.required
147
147
  }
148
148
 
149
149
  const example = resolveArgExample(argdef)
150
150
  if (undefined !== example) {
151
- marg.example = example
151
+ marg.ex = example
152
152
  }
153
153
 
154
154
  if (argdef.nullable) {
155
- marg.type = ['`$ONE`', '`$NULL`', marg.type]
155
+ marg.t = ['`$ONE`', '`$NULL`', marg.t]
156
156
  }
157
157
 
158
- const argsKey = (marg.kind === 'param' ? 'params' : marg.kind) as keyof typeof mpoint.args
159
- let kindargs = (mpoint.args[argsKey] = mpoint.args[argsKey] ?? [])
158
+ const argsKey = (marg.k === 'param' ? 'params' : marg.k) as keyof typeof mpoint.g
159
+ let kindargs = (mpoint.g[argsKey] = mpoint.g[argsKey] ?? [])
160
160
  kindargs.push(marg)
161
161
  touchedKeys.add(argsKey)
162
162
  })
163
163
 
164
164
  // Sort once after all args are collected
165
- const cmp = (a: ModelArg, b: ModelArg) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
165
+ const cmp = (a: ModelArg, b: ModelArg) => a.n < b.n ? -1 : a.n > b.n ? 1 : 0
166
166
  for (const key of touchedKeys) {
167
- mpoint.args[key as keyof typeof mpoint.args]?.sort(cmp)
167
+ mpoint.g[key as keyof typeof mpoint.g]?.sort(cmp)
168
168
  }
169
169
  }
170
170
 
@@ -1,5 +1,6 @@
1
1
 
2
2
  import type { TransformResult, Transform } from '../transform'
3
+ import { KIT } from '../types'
3
4
 
4
5
  import { walk, isempty, isnode, ismap, islist } from '@voxgig/struct'
5
6
 
@@ -54,6 +55,10 @@ const cleanTransform: Transform = async function(
54
55
 
55
56
  ctx.apimodel = cur[0]
56
57
 
58
+ for (const entity of Object.values(ctx.apimodel.main?.[KIT]?.entity ?? {}) as any[]) {
59
+ entity.fields ??= {}
60
+ }
61
+
57
62
  return { ok: true, msg: 'clean' }
58
63
  }
59
64
 
@@ -1,89 +1,24 @@
1
1
  // Point contracts. See docs/design/resolved-spec-capability.md
2
2
  import type { Transform } from '../transform'
3
3
 
4
- import { operationFacts } from '../resolved'
5
-
6
-
7
- export function contractJSON(value: any): string {
8
- function walk(root: any, base: string): any {
9
- // One memo per fact, so refs stay local to it.
10
- const seen = new Map<any, string>()
11
- function copy(v: any, path: string): any {
12
- if (v === null || typeof v !== 'object') return v
13
- if (seen.has(v)) return { $ref: seen.get(v) }
14
- seen.set(v, path)
15
- const out: any = Array.isArray(v) ? v.map((item, i) => copy(item, path + '/' + i)) : {}
16
- if (!Array.isArray(v)) for (const k of Object.keys(v).sort()) {
17
- if (!k.endsWith('$') && !k.startsWith('x-') && undefined !== v[k]) out[k] = copy(v[k], path + '/' + k.replace(/~/g, '~0').replace(/\//g, '~1'))
18
- }
19
- return out
20
- }
21
- return copy(root, base)
22
- }
23
-
24
- if (null === value || 'object' !== typeof value || Array.isArray(value)) {
25
- return JSON.stringify(walk(value, '#'))
26
- }
27
-
28
- const out: any = {}
29
- for (const k of Object.keys(value).sort()) {
30
- if (k.endsWith('$') || k.startsWith('x-') || undefined === value[k]) continue
31
- out[k] = walk(value[k], '#/' + k.replace(/~/g, '~0').replace(/\//g, '~1'))
32
- }
33
- return JSON.stringify(out)
34
- }
35
-
36
- // An operation needs its argument types, including recursive input objects.
37
- // Output types are represented by the field and generated invocation selection;
38
- // copying the entire connected output graph per operation is quadratic in API size.
39
- export function graphqlInputTypes(field: any, types: any): any {
40
- const out: any = {}
41
- function visit(name: string) {
42
- if (!types[name] || Object.prototype.hasOwnProperty.call(out, name)) return
43
- const type = types[name]
44
- out[name] = type
45
- if (type.kind === 'INPUT_OBJECT') {
46
- for (const child of Object.values(type.fields || {}) as any[]) visit(child.type)
47
- }
48
- }
49
- for (const arg of field.args || []) visit(arg.type)
50
- return out
51
- }
52
-
53
4
  export const contractTransform: Transform = async (ctx: any) => {
54
5
  const def = ctx.def || {}
55
6
  for (const entity of Object.values(ctx.apimodel.main.kit.entity || {}) as any[]) {
56
7
  for (const op of Object.values(entity.op || {}) as any[]) {
57
8
  for (const point of op?.points || []) {
58
- const path = def.paths?.[point.orig]
59
- const method = path?.[point.method.toLowerCase()]
60
- const graphql = def.query?.[point.orig] || def.mutation?.[point.orig]
9
+ const path = def.paths?.[point.o]
10
+ const method = path?.[point.m.toLowerCase()]
11
+ const graphql = def.query?.[point.o] || def.mutation?.[point.o]
61
12
  if (!method && !graphql) continue
62
- const facts: any = operationFacts(def, point)
63
- if (null == facts) continue
64
-
65
- // A property of the point, not of the definition.
66
- if (graphql) facts.invocation = point.graphql
67
- const guideOp = ctx.guide?.entity?.[entity.name]?.[graphql ? 'field' : 'path']?.[point.orig]?.op?.[op.name]
13
+ const guideOp = ctx.guide?.entity?.[entity.name]?.[graphql ? 'field' : 'path']?.[point.o]?.op?.[op.name]
68
14
  const hint = guideOp?.live
69
- for (const key of ['requestBody', 'responses', 'parameters', 'security']) {
70
- if (guideOp?.contract?.[key] !== undefined) {
71
- facts[key] = guideOp.contract[key]
72
- ;(facts.factSources ??= {})[key] = 'guide'
73
- }
74
- }
75
15
  if (hint !== undefined) {
76
- facts.live = hint
77
- point.live = hint
16
+ point.li = hint
78
17
  }
79
18
  // Identity only; facts come from the capability. See
80
19
  // docs/design/resolved-spec-capability.md
81
- point.contract = { version: 1, id: point.method + ' ' + point.orig,
20
+ point.co = { version: 2, id: point.m + ' ' + point.o,
82
21
  source: graphql ? 'graphql' : def.swagger ? 'swagger2' : 'openapi3' }
83
-
84
- if (ctx.opts?.contractJson) {
85
- point.contract.json = contractJSON(facts)
86
- }
87
22
  }
88
23
  }
89
24
  }
@@ -59,7 +59,7 @@ const entityTransform: Transform = async function(
59
59
  const modelent: ModelEntity = {
60
60
  name: entname,
61
61
  op: {},
62
- fields: [],
62
+ fields: {},
63
63
  relations,
64
64
  }
65
65
 
@@ -68,9 +68,20 @@ const entityTransform: Transform = async function(
68
68
  msg += guideEntity.name + ' '
69
69
  })
70
70
 
71
+ filterEntityAncestors(kit.entity)
71
72
  return { ok: true, msg }
72
73
  }
73
74
 
75
+ function filterEntityAncestors(entities: Record<string, any>) {
76
+ for (const [name, entity] of Object.entries(entities)) {
77
+ if (null == entity.relations) continue
78
+ entity.relations.ancestors = (entity.relations.ancestors ?? [])
79
+ .map((chain: string[]) => chain.filter(ancestor => ancestor !== name &&
80
+ Object.prototype.hasOwnProperty.call(entities, ancestor)))
81
+ .filter((chain: string[]) => 0 < chain.length)
82
+ }
83
+ }
84
+
74
85
 
75
86
  // Move "/X" paths onto the entity that owns "/X/{id}" or "/X/{id}/sub".
76
87
  // Only acts when the path "/X" sits on a different entity than the
@@ -301,6 +312,7 @@ function suffix(p: string[], c: string[]): boolean {
301
312
 
302
313
 
303
314
  export {
315
+ filterEntityAncestors,
304
316
  resolvePathList,
305
317
  buildRelations,
306
318
  entityTransform,