@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.
- package/dist/apidef.d.ts +2 -2
- package/dist/apidef.js +2 -2
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/entity.d.ts +5 -1
- package/dist/builder/entity/entity.js +17 -1
- package/dist/builder/entity/entity.js.map +1 -1
- package/dist/builder/flow/flowHeuristic01.js +14 -14
- package/dist/builder/flow/flowHeuristic01.js.map +1 -1
- package/dist/model.d.ts +35 -33
- package/dist/resolved.d.ts +10 -6
- package/dist/resolved.js +65 -15
- package/dist/resolved.js.map +1 -1
- package/dist/transform/args.js +19 -19
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/clean.js +4 -0
- package/dist/transform/clean.js.map +1 -1
- package/dist/transform/contract.d.ts +0 -2
- package/dist/transform/contract.js +6 -75
- package/dist/transform/contract.js.map +1 -1
- package/dist/transform/entity.d.ts +2 -1
- package/dist/transform/entity.js +13 -1
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +64 -64
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/flowstep.js +42 -42
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/graphql.js +8 -8
- package/dist/transform/graphql.js.map +1 -1
- package/dist/transform/operation.js +9 -9
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.js +13 -13
- package/dist/transform/select.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +5 -0
- package/dist/utility.js.map +1 -1
- package/model/apidef.aon +128 -183
- package/model/guide.aon +3 -2
- package/package.json +5 -3
- package/src/apidef.ts +4 -3
- package/src/builder/entity/entity.ts +18 -1
- package/src/builder/flow/flowHeuristic01.ts +14 -14
- package/src/model.ts +36 -37
- package/src/resolved.ts +62 -17
- package/src/transform/args.ts +20 -20
- package/src/transform/clean.ts +5 -0
- package/src/transform/contract.ts +6 -71
- package/src/transform/entity.ts +13 -1
- package/src/transform/field.ts +66 -69
- package/src/transform/flowstep.ts +42 -42
- package/src/transform/graphql.ts +8 -8
- package/src/transform/operation.ts +9 -9
- package/src/transform/select.ts +13 -13
- package/src/types.ts +1 -1
- 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
|
-
|
|
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.
|
|
85
|
-
'number' === field.
|
|
86
|
-
'boolean' === field.
|
|
87
|
-
'object' === field.
|
|
88
|
-
'array' === field.
|
|
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?.
|
|
112
|
-
if (param.
|
|
113
|
-
let ancestorName = param.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
211
|
-
const stringFields = each(dataFields).filter(f => 'string' === f.
|
|
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.
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
n: string
|
|
65
|
+
h: string
|
|
66
|
+
t: any
|
|
67
|
+
r: boolean
|
|
67
68
|
op: Partial<Record<OpName, ModelFieldOp>>
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
sh?: string
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
+
g: Partial<{
|
|
149
147
|
params: ModelArg[]
|
|
150
148
|
query: ModelArg[]
|
|
151
149
|
header: ModelArg[]
|
|
152
150
|
cookie: ModelArg[]
|
|
153
151
|
}>
|
|
154
|
-
|
|
152
|
+
t: {
|
|
155
153
|
req?: any
|
|
156
154
|
res?: any
|
|
157
155
|
}
|
|
158
|
-
|
|
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.
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
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.
|
|
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,
|
|
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, {
|
|
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
|
|
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,
|
|
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
|
}
|
package/src/transform/args.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
-
return 'mutation' === mpoint.
|
|
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["
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
145
|
-
kind,
|
|
146
|
-
|
|
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.
|
|
151
|
+
marg.ex = example
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
if (argdef.nullable) {
|
|
155
|
-
marg.
|
|
155
|
+
marg.t = ['`$ONE`', '`$NULL`', marg.t]
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const argsKey = (marg.
|
|
159
|
-
let kindargs = (mpoint.
|
|
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.
|
|
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.
|
|
167
|
+
mpoint.g[key as keyof typeof mpoint.g]?.sort(cmp)
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
package/src/transform/clean.ts
CHANGED
|
@@ -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.
|
|
59
|
-
const method = path?.[point.
|
|
60
|
-
const graphql = def.query?.[point.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/src/transform/entity.ts
CHANGED
|
@@ -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,
|