@voxgig/apidef 8.12.1 → 8.14.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 (56) hide show
  1. package/dist/apidef.d.ts +2 -2
  2. package/dist/apidef.js +2 -2
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.d.ts +5 -1
  5. package/dist/builder/entity/entity.js +17 -1
  6. package/dist/builder/entity/entity.js.map +1 -1
  7. package/dist/builder/flow/flowHeuristic01.js +14 -14
  8. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  9. package/dist/model.d.ts +35 -33
  10. package/dist/resolved.d.ts +10 -6
  11. package/dist/resolved.js +65 -15
  12. package/dist/resolved.js.map +1 -1
  13. package/dist/transform/args.js +19 -19
  14. package/dist/transform/args.js.map +1 -1
  15. package/dist/transform/clean.js +4 -0
  16. package/dist/transform/clean.js.map +1 -1
  17. package/dist/transform/contract.d.ts +0 -2
  18. package/dist/transform/contract.js +6 -75
  19. package/dist/transform/contract.js.map +1 -1
  20. package/dist/transform/entity.d.ts +2 -1
  21. package/dist/transform/entity.js +13 -1
  22. package/dist/transform/entity.js.map +1 -1
  23. package/dist/transform/field.js +64 -64
  24. package/dist/transform/field.js.map +1 -1
  25. package/dist/transform/flowstep.js +42 -42
  26. package/dist/transform/flowstep.js.map +1 -1
  27. package/dist/transform/graphql.js +8 -8
  28. package/dist/transform/graphql.js.map +1 -1
  29. package/dist/transform/operation.js +9 -9
  30. package/dist/transform/operation.js.map +1 -1
  31. package/dist/transform/select.js +13 -13
  32. package/dist/transform/select.js.map +1 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/utility.d.ts +2 -1
  36. package/dist/utility.js +5 -0
  37. package/dist/utility.js.map +1 -1
  38. package/model/apidef.aon +128 -183
  39. package/model/guide.aon +3 -2
  40. package/package.json +5 -3
  41. package/src/apidef.ts +4 -3
  42. package/src/builder/entity/entity.ts +18 -1
  43. package/src/builder/flow/flowHeuristic01.ts +14 -14
  44. package/src/model.ts +36 -37
  45. package/src/resolved.ts +62 -17
  46. package/src/transform/args.ts +20 -20
  47. package/src/transform/clean.ts +5 -0
  48. package/src/transform/contract.ts +6 -71
  49. package/src/transform/entity.ts +13 -1
  50. package/src/transform/field.ts +66 -69
  51. package/src/transform/flowstep.ts +42 -42
  52. package/src/transform/graphql.ts +8 -8
  53. package/src/transform/operation.ts +9 -9
  54. package/src/transform/select.ts +13 -13
  55. package/src/types.ts +1 -1
  56. package/src/utility.ts +7 -0
@@ -35,7 +35,8 @@ function resolveEntity(
35
35
  each(kit.entity, ((entity: any, entityName: string) => {
36
36
  const entityFile = (null == opts.outprefix ? '' : opts.outprefix) + entityName + '.aon'
37
37
 
38
- let entityJSONIC = formatJSONIC(entity).trim()
38
+ const { model, relations } = entityAncestorSource(entity)
39
+ let entityJSONIC = formatJSONIC(model).trim()
39
40
  entityJSONIC = entityJSONIC.substring(1, entityJSONIC.length - 1)
40
41
 
41
42
  const fieldAliasesSrc = fieldAliases(entity)
@@ -45,6 +46,7 @@ function resolveEntity(
45
46
  `main: ${KIT}: entity: ${entity.name}: {\n\n` +
46
47
  ` alias: field: ${fieldAliasesSrc}\n` +
47
48
  entityJSONIC +
49
+ relations +
48
50
  '\n\n}\n'
49
51
 
50
52
  entityFiles.push({ name: entityFile, src: entitySrc })
@@ -65,6 +67,20 @@ function resolveEntity(
65
67
  }
66
68
  }
67
69
 
70
+ function entityAncestorSource(entity: any): { model: any, relations: string } {
71
+ const model = { ...entity }
72
+ const ancestors: string[][] = entity.relations?.ancestors ?? []
73
+ if (null != entity.relations) {
74
+ model.relations = { ...entity.relations }
75
+ delete model.relations.ancestors
76
+ if (0 === Object.keys(model.relations).length) delete model.relations
77
+ }
78
+ const chains = ancestors.map(chain => ' [' + chain.map(name =>
79
+ 'path(' + JSON.stringify('$.main.kit.entity.' + name) + ')').join(' ') + ']')
80
+ return { model, relations: 0 === chains.length ? '' :
81
+ '\n relations: ancestors: [\n' + chains.join('\n') + '\n ]' }
82
+ }
83
+
68
84
 
69
85
  function gcEntityFiles(
70
86
  fs: any,
@@ -126,4 +142,5 @@ function fieldAliases(_entity: any): string {
126
142
  export {
127
143
  resolveEntity,
128
144
  gcEntityFiles,
145
+ entityAncestorSource,
129
146
  }
@@ -81,11 +81,11 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
81
81
 
82
82
  let num = (i * size(apiEntity.fields) * 10)
83
83
  each(apiEntity.fields, (field) => {
84
- ent[field.name] =
85
- 'number' === field.type ? num :
86
- 'boolean' === field.type ? 0 === num % 2 :
87
- 'object' === field.type ? {} :
88
- 'array' === field.type ? [] :
84
+ ent[field.n] =
85
+ 'number' === field.t ? num :
86
+ 'boolean' === field.t ? 0 === num % 2 :
87
+ 'object' === field.t ? {} :
88
+ 'array' === field.t ? [] :
89
89
  's' + (num.toString(16))
90
90
  num++
91
91
  })
@@ -108,9 +108,9 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
108
108
  const point = findMainLoadPoint(entop.load)
109
109
 
110
110
  // Get additional required match properties
111
- each(point?.args.params, (param: any) => {
112
- if (param.required) {
113
- let ancestorName = param.name
111
+ each(point?.g.params, (param: any) => {
112
+ if (param.r) {
113
+ let ancestorName = param.n
114
114
  let ancestorEntity = apimodel.main.api.entity[ancestorName]
115
115
 
116
116
  if (null == ancestorEntity) {
@@ -122,10 +122,10 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
122
122
  flow.model.param[`${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID`] = {
123
123
  [ancestorEntity.name + '01']: ancestorEntity.NAME + '01'
124
124
  }
125
- am[param.name] =
125
+ am[param.n] =
126
126
  `\`dm$=p.${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID.${ancestorEntity.name}01\``
127
127
 
128
- data[`${nom(apiEntity, 'NAME')}01`][param.name] = ancestorEntity.NAME + '01'
128
+ data[`${nom(apiEntity, 'NAME')}01`][param.n] = ancestorEntity.NAME + '01'
129
129
  }
130
130
  }
131
131
  })
@@ -197,7 +197,7 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
197
197
 
198
198
 
199
199
  function findMainLoadPoint(op: ModelOp): ModelPoint | undefined {
200
- let cands = op.points.filter(a => 'id' === getelem(a.segments, -1)?.var)
200
+ let cands = op.points.filter(a => 'id' === getelem(a.s, -1)?.var)
201
201
  return cands[0]
202
202
  }
203
203
 
@@ -207,12 +207,12 @@ function makeUpdateData(name: string, apiEntity: any, flow: any, id: string) {
207
207
  const data = flow.model.test.entity[apiEntity.name]
208
208
 
209
209
  const dataFields =
210
- each(apiEntity.field).filter(f => 'id' !== f.name && !f.name.includes('_id'))
211
- const stringFields = each(dataFields).filter(f => 'string' === f.type)
210
+ each(apiEntity.fields).filter(f => 'id' !== f.n && !f.n.includes('_id'))
211
+ const stringFields = each(dataFields).filter(f => 'string' === f.t)
212
212
 
213
213
  if (0 < size(stringFields)) {
214
214
  const f = stringFields[0]
215
- ud[f.name] = data[id][f.name] + '-`$WHEN`'
215
+ ud[f.n] = data[id][f.n] + '-`$WHEN`'
216
216
  }
217
217
 
218
218
  return ud
package/src/model.ts CHANGED
@@ -61,17 +61,18 @@ type ModelFieldOp = {
61
61
 
62
62
 
63
63
  type ModelField = {
64
- name: string
65
- type: any
66
- req: boolean
64
+ n: string
65
+ h: string
66
+ t: any
67
+ r: boolean
67
68
  op: Partial<Record<OpName, ModelFieldOp>>
68
69
 
69
- short?: string
70
+ sh?: string
70
71
 
71
- readOnly?: boolean
72
- writeOnly?: boolean
73
- deprecated?: boolean
74
- format?: string
72
+ ro?: boolean
73
+ wo?: boolean
74
+ de?: boolean
75
+ fo?: string
75
76
  union?: {
76
77
  count: number
77
78
  branches: number
@@ -80,18 +81,14 @@ type ModelField = {
80
81
  }
81
82
 
82
83
 
83
- // Operation argument/parameter definition.
84
- // `example` captures a value the spec advertises (parameter `example`,
85
- // the first entry of `examples`, or `schema.example`/`schema.default`).
86
- // Test generators use this for required params in live test setup so the
87
- // generated request actually satisfies the API contract.
84
+ // `ex` holds an advertised parameter example or schema default.
88
85
  type ModelArg = {
89
- name: string
90
- orig: string
91
- type: any
92
- kind: ArgKind
93
- reqd: boolean
94
- example?: any
86
+ n: string
87
+ or?: string
88
+ t: any
89
+ k: ArgKind
90
+ r: boolean
91
+ ex?: any
95
92
  }
96
93
 
97
94
 
@@ -133,29 +130,30 @@ type ModelPathSegment = {
133
130
 
134
131
 
135
132
  type ModelPoint = {
136
- contract?: { version: number, id: string, source: string, json: string }
137
- orig: string
138
- kind?: PointKind
139
- graphql?: ModelGraphql
140
- method: MethodName
141
- segments: ModelPathSegment[]
142
- rename: Partial<{
133
+ co?: { version: 2, id: string, source: string }
134
+ li?: boolean | Record<string, any>
135
+ o: string
136
+ k?: PointKind
137
+ gq?: ModelGraphql
138
+ m: MethodName
139
+ s: ModelPathSegment[]
140
+ r: Partial<{
143
141
  param: Record<string, string>
144
142
  query: Record<string, string>
145
143
  header: Record<string, string>
146
144
  cookie: Record<string, string>
147
145
  }>
148
- args: Partial<{
146
+ g: Partial<{
149
147
  params: ModelArg[]
150
148
  query: ModelArg[]
151
149
  header: ModelArg[]
152
150
  cookie: ModelArg[]
153
151
  }>
154
- transform: {
152
+ t: {
155
153
  req?: any
156
154
  res?: any
157
155
  }
158
- select: {
156
+ q: {
159
157
  exist: string[]
160
158
  $action?: string
161
159
  }
@@ -173,7 +171,7 @@ type ModelEntity = {
173
171
  Name?: string
174
172
  NAME?: string
175
173
  op: ModelOpMap
176
- fields: ModelField[]
174
+ fields: Record<string, ModelField>
177
175
  id?: {
178
176
  name: string
179
177
  field: string
@@ -197,7 +195,7 @@ type ModelEntityFlow = {
197
195
  // Per-step input cluster. Test-generators name the variables they emit by
198
196
  // reading these slots, falling back to derived defaults. All fields are
199
197
  // optional — `newFlowStep` in transform/flowstep.ts guarantees the input
200
- // object itself exists, so consumers don't need to null-check `step.input`.
198
+ // object itself exists, so consumers don't need to null-check `step.i`.
201
199
  type ModelEntityFlowStepInput = {
202
200
  ref?: string
203
201
  entvar?: string
@@ -226,12 +224,13 @@ type ModelEntityFlowStepSpec = {
226
224
 
227
225
 
228
226
  type ModelEntityFlowStep = {
229
- op: OpName
230
- input: ModelEntityFlowStepInput
231
- match: Record<string, any>
232
- data: Record<string, any>
233
- spec: ModelEntityFlowStepSpec[]
234
- valid: ModelEntityFlowStepValidator[]
227
+ a?: boolean
228
+ o: OpName
229
+ i: ModelEntityFlowStepInput
230
+ m: Record<string, any>
231
+ d: Record<string, any>
232
+ s: ModelEntityFlowStepSpec[]
233
+ v: ModelEntityFlowStepValidator[]
235
234
  }
236
235
 
237
236
 
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,