@voxgig/apidef 6.5.0 → 7.0.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.js +39 -2
- package/dist/apidef.js.map +1 -1
- package/dist/guide/graphql01.d.ts +39 -0
- package/dist/guide/graphql01.js +331 -0
- package/dist/guide/graphql01.js.map +1 -0
- package/dist/guide/guide.js +70 -10
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +1 -0
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/model.d.ts +22 -1
- package/dist/parse/graphql.d.ts +46 -0
- package/dist/parse/graphql.js +205 -0
- package/dist/parse/graphql.js.map +1 -0
- package/dist/parse.js +19 -0
- package/dist/parse.js.map +1 -1
- package/dist/transform/args.js +49 -4
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/entity.js +51 -3
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +71 -1
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/graphql.d.ts +6 -0
- package/dist/transform/graphql.js +227 -0
- package/dist/transform/graphql.js.map +1 -0
- package/dist/transform/select.js +17 -2
- package/dist/transform/select.js.map +1 -1
- package/dist/transform/top.js +31 -2
- package/dist/transform/top.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +52 -4
- package/dist/utility.js.map +1 -1
- package/model/apidef.aontu +23 -0
- package/model/guide.aontu +22 -0
- package/package.json +12 -4
- package/src/apidef.ts +44 -2
- package/src/guide/graphql01.ts +516 -0
- package/src/guide/guide.ts +90 -12
- package/src/guide/heuristic01.ts +1 -0
- package/src/model.ts +46 -1
- package/src/parse/graphql.ts +302 -0
- package/src/parse.ts +23 -0
- package/src/transform/args.ts +54 -5
- package/src/transform/entity.ts +63 -3
- package/src/transform/field.ts +89 -2
- package/src/transform/graphql.ts +311 -0
- package/src/transform/select.ts +19 -2
- package/src/transform/top.ts +31 -2
- package/src/types.ts +40 -0
- package/src/utility.ts +54 -4
package/src/model.ts
CHANGED
|
@@ -90,13 +90,54 @@ type ModelArg = {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// Transport a point speaks. 'http' is the default and covers every
|
|
94
|
+
// OpenAPI-derived point; 'graphql' points carry a `graphql` block instead
|
|
95
|
+
// of relying on method+path (they synthesize method 'POST' and empty
|
|
96
|
+
// parts so the HTTP-shaped machinery downstream keeps working unchanged).
|
|
97
|
+
type PointKind = 'http' | 'graphql'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Pagination descriptor for a GraphQL list op. `nodes`/`cursor`/`more` are
|
|
101
|
+
// dotted paths relative to the unwrapped connection object.
|
|
102
|
+
type ModelGraphqlPage = {
|
|
103
|
+
style: string
|
|
104
|
+
nodes: string
|
|
105
|
+
cursor: string
|
|
106
|
+
more: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// One GraphQL variable binding: `name` is the variable as it appears in the
|
|
111
|
+
// operation document, `from` the op argument it is read from, `gqltype` the
|
|
112
|
+
// declared GraphQL type (e.g. 'String!').
|
|
113
|
+
type ModelGraphqlVar = {
|
|
114
|
+
name: string
|
|
115
|
+
from: string
|
|
116
|
+
gqltype: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
// GraphQL wire data for a point. `doc` is the complete operation document,
|
|
121
|
+
// rendered single-line with sorted selection fields so output stays
|
|
122
|
+
// byte-stable and schema drift shows up in model diffs.
|
|
123
|
+
type ModelGraphql = {
|
|
124
|
+
optype: 'query' | 'mutation'
|
|
125
|
+
field: string
|
|
126
|
+
doc: string
|
|
127
|
+
vars: ModelGraphqlVar[]
|
|
128
|
+
page?: ModelGraphqlPage
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// One concrete endpoint that can satisfy an operation. An entity op
|
|
94
133
|
// (load/list/create/...) carries an array of these — apidef chooses
|
|
95
134
|
// between them at runtime via `select.exist` matching against reqmatch /
|
|
96
135
|
// reqdata. (Originally named `ModelTarget`; renamed for consistency with
|
|
97
136
|
// the field name `points` and the runtime utility `MakePoint`.)
|
|
98
137
|
type ModelPoint = {
|
|
99
138
|
orig: string
|
|
139
|
+
kind?: PointKind
|
|
140
|
+
graphql?: ModelGraphql
|
|
100
141
|
method: MethodName
|
|
101
142
|
parts: string[]
|
|
102
143
|
rename: Partial<{
|
|
@@ -210,6 +251,10 @@ type ModelEntityFlowStep = {
|
|
|
210
251
|
export type {
|
|
211
252
|
OpName,
|
|
212
253
|
ArgKind,
|
|
254
|
+
PointKind,
|
|
255
|
+
ModelGraphql,
|
|
256
|
+
ModelGraphqlVar,
|
|
257
|
+
ModelGraphqlPage,
|
|
213
258
|
NamesCluster,
|
|
214
259
|
Model,
|
|
215
260
|
ModelEntityRelations,
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/* Copyright (c) 2024-2026 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
// GraphQL ingestion: normalise an SDL document or an introspection result
|
|
4
|
+
// into the plain `def` structure the guide and transform stages consume.
|
|
5
|
+
//
|
|
6
|
+
// The OpenAPI parser hands downstream stages the spec object itself, with
|
|
7
|
+
// `$ref`s resolved in place. GraphQL has no equivalent literal document, so
|
|
8
|
+
// this builds an explicit graph instead:
|
|
9
|
+
//
|
|
10
|
+
// def.types — every named type, keyed by type name
|
|
11
|
+
// def.query — root Query fields, keyed by field name
|
|
12
|
+
// def.mutation — root Mutation fields, keyed by field name
|
|
13
|
+
// def.servers — synthesised from the `endpoint` option (a schema carries
|
|
14
|
+
// no deployment URL, but transform/top.ts requires one)
|
|
15
|
+
// def.info — synthesised; SDL has no info block
|
|
16
|
+
//
|
|
17
|
+
// Type references are held as NAME STRINGS, never object pointers, so the
|
|
18
|
+
// result is acyclic and JSON-serialisable by construction — GraphQL type
|
|
19
|
+
// graphs are freely recursive (Issue.team.issues), and apidef writes
|
|
20
|
+
// `<def>.full.json` under the debug flag.
|
|
21
|
+
|
|
22
|
+
import { relativizePath } from '../utility'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// A single argument on a root field or a type field.
|
|
26
|
+
type GqlArg = {
|
|
27
|
+
name: string
|
|
28
|
+
gqltype: string // rendered GraphQL type, e.g. 'String!' or '[Int!]'
|
|
29
|
+
type: string // named (unwrapped) type, e.g. 'String'
|
|
30
|
+
reqd: boolean
|
|
31
|
+
deflt?: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// A field on an object/interface type, or a root field.
|
|
36
|
+
type GqlField = {
|
|
37
|
+
name: string
|
|
38
|
+
gqltype: string
|
|
39
|
+
type: string // named (unwrapped) type
|
|
40
|
+
reqd: boolean
|
|
41
|
+
list: boolean
|
|
42
|
+
args: GqlArg[]
|
|
43
|
+
deprecated: boolean
|
|
44
|
+
desc?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// A named type in the schema. `fields` is present for OBJECT, INTERFACE and
|
|
49
|
+
// INPUT_OBJECT kinds; `values` for ENUM; `possible` for UNION/INTERFACE.
|
|
50
|
+
type GqlType = {
|
|
51
|
+
name: string
|
|
52
|
+
kind: string // OBJECT | INPUT_OBJECT | ENUM | SCALAR | INTERFACE | UNION
|
|
53
|
+
fields: Record<string, GqlField>
|
|
54
|
+
values?: string[]
|
|
55
|
+
possible?: string[]
|
|
56
|
+
interfaces?: string[]
|
|
57
|
+
desc?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
type GqlDef = {
|
|
62
|
+
graphql: true
|
|
63
|
+
info: Record<string, any>
|
|
64
|
+
servers: { url: string }[]
|
|
65
|
+
types: Record<string, GqlType>
|
|
66
|
+
query: Record<string, GqlField>
|
|
67
|
+
mutation: Record<string, GqlField>
|
|
68
|
+
subscription: Record<string, GqlField>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// `graphql` is an OPTIONAL peer dependency: REST-only consumers should not
|
|
73
|
+
// have to install it. Resolve it lazily, and fail with an actionable message
|
|
74
|
+
// rather than a bare MODULE_NOT_FOUND.
|
|
75
|
+
function loadGraphQL(meta: { file: string }): any {
|
|
76
|
+
try {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
78
|
+
return require('graphql')
|
|
79
|
+
}
|
|
80
|
+
catch (err: any) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'@voxgig/apidef: parse: GraphQL: the "graphql" package is required to' +
|
|
83
|
+
' parse GraphQL schemas - install it alongside @voxgig/apidef' +
|
|
84
|
+
` (${relativizePath(meta.file)})`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// Introspection JSON arrives either bare (`{__schema:...}`) or wrapped in a
|
|
91
|
+
// GraphQL response envelope (`{data:{__schema:...}}`).
|
|
92
|
+
function asIntrospection(source: string): any {
|
|
93
|
+
const trimmed = source.trimStart()
|
|
94
|
+
if (!trimmed.startsWith('{')) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let parsed: any
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(source)
|
|
101
|
+
}
|
|
102
|
+
catch (err: any) {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (null != parsed?.__schema) {
|
|
107
|
+
return parsed
|
|
108
|
+
}
|
|
109
|
+
if (null != parsed?.data?.__schema) {
|
|
110
|
+
return parsed.data
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
// Render a type reference to its GraphQL source form ('[Issue!]!') and its
|
|
118
|
+
// named form ('Issue'), plus the required/list flags the classifier keys on.
|
|
119
|
+
function describeType(G: any, gtype: any) {
|
|
120
|
+
const gqltype = String(gtype)
|
|
121
|
+
const named = G.getNamedType(gtype)
|
|
122
|
+
return {
|
|
123
|
+
gqltype,
|
|
124
|
+
type: named.name,
|
|
125
|
+
reqd: G.isNonNullType(gtype),
|
|
126
|
+
list: G.isListType(G.isNonNullType(gtype) ? gtype.ofType : gtype),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
function buildArgs(G: any, gargs: any[]): GqlArg[] {
|
|
132
|
+
return (gargs || []).map((ga: any) => {
|
|
133
|
+
const d = describeType(G, ga.type)
|
|
134
|
+
const arg: GqlArg = {
|
|
135
|
+
name: ga.name,
|
|
136
|
+
gqltype: d.gqltype,
|
|
137
|
+
type: d.type,
|
|
138
|
+
reqd: d.reqd,
|
|
139
|
+
}
|
|
140
|
+
if (undefined !== ga.defaultValue && null !== ga.defaultValue) {
|
|
141
|
+
arg.deflt = ga.defaultValue
|
|
142
|
+
}
|
|
143
|
+
return arg
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function buildField(G: any, gfield: any): GqlField {
|
|
149
|
+
const d = describeType(G, gfield.type)
|
|
150
|
+
const field: GqlField = {
|
|
151
|
+
name: gfield.name,
|
|
152
|
+
gqltype: d.gqltype,
|
|
153
|
+
type: d.type,
|
|
154
|
+
reqd: d.reqd,
|
|
155
|
+
list: d.list,
|
|
156
|
+
args: buildArgs(G, gfield.args),
|
|
157
|
+
deprecated: null != gfield.deprecationReason,
|
|
158
|
+
}
|
|
159
|
+
if (null != gfield.description && '' !== gfield.description) {
|
|
160
|
+
field.desc = gfield.description
|
|
161
|
+
}
|
|
162
|
+
return field
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
function fieldMap(G: any, gtype: any): Record<string, GqlField> {
|
|
167
|
+
const out: Record<string, GqlField> = {}
|
|
168
|
+
const gfields = gtype.getFields ? gtype.getFields() : {}
|
|
169
|
+
// Sorted: downstream output must be byte-stable.
|
|
170
|
+
for (const name of Object.keys(gfields).sort()) {
|
|
171
|
+
out[name] = buildField(G, gfields[name])
|
|
172
|
+
}
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
function typeKind(G: any, gtype: any): string {
|
|
178
|
+
if (G.isObjectType(gtype)) return 'OBJECT'
|
|
179
|
+
if (G.isInputObjectType(gtype)) return 'INPUT_OBJECT'
|
|
180
|
+
if (G.isEnumType(gtype)) return 'ENUM'
|
|
181
|
+
if (G.isInterfaceType(gtype)) return 'INTERFACE'
|
|
182
|
+
if (G.isUnionType(gtype)) return 'UNION'
|
|
183
|
+
if (G.isScalarType(gtype)) return 'SCALAR'
|
|
184
|
+
return 'UNKNOWN'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function buildTypes(G: any, schema: any): Record<string, GqlType> {
|
|
189
|
+
const out: Record<string, GqlType> = {}
|
|
190
|
+
const typeMap = schema.getTypeMap()
|
|
191
|
+
|
|
192
|
+
for (const name of Object.keys(typeMap).sort()) {
|
|
193
|
+
// Introspection meta types (__Schema, __Type, ...) are not API surface.
|
|
194
|
+
if (name.startsWith('__')) {
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const gtype = typeMap[name]
|
|
199
|
+
const kind = typeKind(G, gtype)
|
|
200
|
+
|
|
201
|
+
const desc: GqlType = {
|
|
202
|
+
name,
|
|
203
|
+
kind,
|
|
204
|
+
fields: ('OBJECT' === kind || 'INTERFACE' === kind || 'INPUT_OBJECT' === kind) ?
|
|
205
|
+
fieldMap(G, gtype) : {},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if ('ENUM' === kind) {
|
|
209
|
+
desc.values = gtype.getValues().map((v: any) => v.name).sort()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if ('UNION' === kind) {
|
|
213
|
+
desc.possible = schema.getPossibleTypes(gtype).map((t: any) => t.name).sort()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if ('INTERFACE' === kind) {
|
|
217
|
+
desc.possible = schema.getPossibleTypes(gtype).map((t: any) => t.name).sort()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ('OBJECT' === kind || 'INTERFACE' === kind) {
|
|
221
|
+
const ifaces = (gtype.getInterfaces ? gtype.getInterfaces() : [])
|
|
222
|
+
.map((t: any) => t.name).sort()
|
|
223
|
+
if (0 < ifaces.length) {
|
|
224
|
+
desc.interfaces = ifaces
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (null != gtype.description && '' !== gtype.description) {
|
|
229
|
+
desc.desc = gtype.description
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
out[name] = desc
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return out
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
function rootFields(G: any, gtype: any): Record<string, GqlField> {
|
|
240
|
+
return null == gtype ? {} : fieldMap(G, gtype)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// Parse a GraphQL schema (SDL text or introspection JSON) into `def`.
|
|
245
|
+
//
|
|
246
|
+
// `opts.endpoint` is REQUIRED: a schema declares no deployment URL, but a
|
|
247
|
+
// usable SDK needs a base URL and transform/top.ts fails the build without
|
|
248
|
+
// `servers[0].url`.
|
|
249
|
+
async function parseGraphQL(
|
|
250
|
+
source: string,
|
|
251
|
+
meta: { file: string },
|
|
252
|
+
opts?: { endpoint?: string, title?: string, version?: string }
|
|
253
|
+
): Promise<GqlDef> {
|
|
254
|
+
const G = loadGraphQL(meta)
|
|
255
|
+
|
|
256
|
+
const endpoint = opts?.endpoint
|
|
257
|
+
if (null == endpoint || '' === String(endpoint).trim()) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
'@voxgig/apidef: parse: GraphQL: an endpoint option is required' +
|
|
260
|
+
' (a GraphQL schema declares no server URL)' +
|
|
261
|
+
` (${relativizePath(meta.file)})`
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let schema: any
|
|
266
|
+
const introspection = asIntrospection(source)
|
|
267
|
+
|
|
268
|
+
if (null != introspection) {
|
|
269
|
+
schema = G.buildClientSchema(introspection)
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
schema = G.buildSchema(source)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const def: GqlDef = {
|
|
276
|
+
graphql: true,
|
|
277
|
+
info: {
|
|
278
|
+
title: opts?.title ?? '',
|
|
279
|
+
version: opts?.version ?? '',
|
|
280
|
+
description: '',
|
|
281
|
+
},
|
|
282
|
+
servers: [{ url: endpoint }],
|
|
283
|
+
types: buildTypes(G, schema),
|
|
284
|
+
query: rootFields(G, schema.getQueryType()),
|
|
285
|
+
mutation: rootFields(G, schema.getMutationType()),
|
|
286
|
+
subscription: rootFields(G, schema.getSubscriptionType()),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return def
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
parseGraphQL,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type {
|
|
298
|
+
GqlDef,
|
|
299
|
+
GqlType,
|
|
300
|
+
GqlField,
|
|
301
|
+
GqlArg,
|
|
302
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { Yaml } from '@tabnas/yaml'
|
|
|
5
5
|
|
|
6
6
|
import { relativizePath } from './utility'
|
|
7
7
|
|
|
8
|
+
import { parseGraphQL } from './parse/graphql'
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
// NOTE: @tabnas/yaml types its Plugin against @tabnas/parser, while
|
|
10
12
|
// Jsonic.use expects @tabnas/jsonic's own (structurally identical) Plugin
|
|
@@ -46,6 +48,27 @@ async function parse(kind: string, source: any, meta: { file: string }) {
|
|
|
46
48
|
throw pe
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
else if ('GraphQL' === kind) {
|
|
52
|
+
|
|
53
|
+
validateSource(kind, source, meta)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const def = await parseGraphQL(source, meta, (meta as any).graphql)
|
|
57
|
+
return def
|
|
58
|
+
}
|
|
59
|
+
catch (pe: any) {
|
|
60
|
+
// Already-decorated errors (missing endpoint, missing package) carry
|
|
61
|
+
// the package prefix; only raw parser failures need wrapping.
|
|
62
|
+
if ('string' === typeof pe.message &&
|
|
63
|
+
!pe.message.startsWith('@voxgig/apidef:')) {
|
|
64
|
+
pe.message =
|
|
65
|
+
`@voxgig/apidef: parse: syntax: ${pe.message}` +
|
|
66
|
+
` (${relativizePath(meta.file)})`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw pe
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
else {
|
|
50
73
|
throw new Error(
|
|
51
74
|
`@voxgig/apidef: parse: unknown kind: ${kind}` +
|
package/src/transform/args.ts
CHANGED
|
@@ -41,11 +41,35 @@ const argsTransform: Transform = async function(
|
|
|
41
41
|
each(mop.points, (mpoint: ModelPoint) => {
|
|
42
42
|
const argdefs: ParameterDef[] = []
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if ('graphql' === mpoint.kind) {
|
|
45
|
+
// GraphQL root-field arguments become 'param' args, so the existing
|
|
46
|
+
// arg machinery (select.exist matching, request typing, test
|
|
47
|
+
// generation) works on them unchanged. Input-object arguments are
|
|
48
|
+
// the request body and are bound as variables by the document
|
|
49
|
+
// renderer instead, so they are not surfaced as params here.
|
|
50
|
+
const fielddef: any = graphqlFieldDef(def, mpoint)
|
|
51
|
+
for (const arg of (fielddef?.args ?? [])) {
|
|
52
|
+
const argtype = def.types?.[arg.type]
|
|
53
|
+
if (null != argtype && 'INPUT_OBJECT' === argtype.kind) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
argdefs.push({
|
|
57
|
+
name: arg.name,
|
|
58
|
+
in: 'path',
|
|
59
|
+
// A schema default makes a non-null argument omittable by the
|
|
60
|
+
// caller, so it is not required of the SDK caller either.
|
|
61
|
+
required: arg.reqd && undefined === arg.deflt,
|
|
62
|
+
schema: { type: gqlScalarType(arg.type) },
|
|
63
|
+
} as any)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const pathdef: PathDef = def.paths[mpoint.orig]
|
|
68
|
+
argdefs.push(...(pathdef.parameters ?? []))
|
|
69
|
+
|
|
70
|
+
const opdef: MethodDef = (pathdef as any)[mpoint.method.toLowerCase()]
|
|
71
|
+
argdefs.push(...(opdef?.parameters ?? []))
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
resolveArgs(ment, mop, mpoint, argdefs)
|
|
51
75
|
})
|
|
@@ -59,6 +83,31 @@ const argsTransform: Transform = async function(
|
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
|
|
86
|
+
// Locate the normalised root-field descriptor a GraphQL point came from.
|
|
87
|
+
function graphqlFieldDef(def: any, mpoint: ModelPoint): any {
|
|
88
|
+
const field = mpoint.graphql?.field ?? mpoint.orig
|
|
89
|
+
return 'mutation' === mpoint.graphql?.optype ?
|
|
90
|
+
def.mutation?.[field] : def.query?.[field]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
// Map a GraphQL named type onto the JSON-schema-ish scalar names the
|
|
95
|
+
// existing arg/field typing understands.
|
|
96
|
+
//
|
|
97
|
+
// Only the built-in scalars have a known JSON shape. A custom scalar can be
|
|
98
|
+
// anything — JSON/JSONObject accept objects and arrays, DateTime is a
|
|
99
|
+
// string, Upload is a file handle — so anything unrecognised stays
|
|
100
|
+
// unconstrained rather than being wrongly advertised (and validated) as a
|
|
101
|
+
// string. ID and String are the two custom-free string cases.
|
|
102
|
+
function gqlScalarType(typeName: string): string | undefined {
|
|
103
|
+
return 'Int' === typeName ? 'integer' :
|
|
104
|
+
'Float' === typeName ? 'number' :
|
|
105
|
+
'Boolean' === typeName ? 'boolean' :
|
|
106
|
+
('String' === typeName || 'ID' === typeName) ? 'string' :
|
|
107
|
+
undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
62
111
|
const ARG_KIND: Record<string, ModelArg["kind"]> = {
|
|
63
112
|
'query': 'query',
|
|
64
113
|
'header': 'header',
|
package/src/transform/entity.ts
CHANGED
|
@@ -42,7 +42,10 @@ const entityTransform: Transform = async function(
|
|
|
42
42
|
// ID. Move "/people" onto person here; this also clears the way for
|
|
43
43
|
// sensible flow generation (one entity, one collection, multiple
|
|
44
44
|
// sub-resources).
|
|
45
|
-
|
|
45
|
+
// Path-shaped collection merging is meaningless for root-field guides.
|
|
46
|
+
if (true !== ctx.def?.graphql) {
|
|
47
|
+
mergeCollectionPaths(guide, ctx.log)
|
|
48
|
+
}
|
|
46
49
|
|
|
47
50
|
each(guide.entity, (guideEntity: GuideEntity, entname: string) => {
|
|
48
51
|
// `active: false` in guide.aontu drops the entity. The guide model has
|
|
@@ -58,8 +61,18 @@ const entityTransform: Transform = async function(
|
|
|
58
61
|
|
|
59
62
|
ctx.log.debug({ point: 'guide-entity', note: entname })
|
|
60
63
|
|
|
61
|
-
const
|
|
62
|
-
|
|
64
|
+
const graphql = true === ctx.def?.graphql
|
|
65
|
+
|
|
66
|
+
const paths$ = graphql ?
|
|
67
|
+
resolveFieldList(guideEntity, ctx.def) :
|
|
68
|
+
resolvePathList(guideEntity, ctx.def)
|
|
69
|
+
|
|
70
|
+
// Ancestry is inferred from literal/{param} path pairs, which root
|
|
71
|
+
// fields do not have; GraphQL relations come from the schema instead
|
|
72
|
+
// (see transform/graphql.ts).
|
|
73
|
+
const relations = graphql ?
|
|
74
|
+
{ ancestors: [] } :
|
|
75
|
+
buildRelations(guideEntity, paths$)
|
|
63
76
|
|
|
64
77
|
const modelent: ModelEntity = {
|
|
65
78
|
name: entname,
|
|
@@ -214,6 +227,53 @@ function resolvePathList(guideEntity: GuideEntity, def: { paths: Record<string,
|
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
|
|
230
|
+
// Root-field equivalent of resolvePathList for GraphQL guides. A root field
|
|
231
|
+
// has no path to split, so `parts` stays empty (GraphQL points address the
|
|
232
|
+
// single endpoint and carry their operation document instead) and `def` is
|
|
233
|
+
// the normalised root-field descriptor rather than a path item.
|
|
234
|
+
function resolveFieldList(guideEntity: GuideEntity, def: any) {
|
|
235
|
+
const paths$: PathDesc[] = []
|
|
236
|
+
|
|
237
|
+
each((guideEntity as any).field, (guideField: GuidePath, orig: string) => {
|
|
238
|
+
if (!guideActive(guideField)) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The root field lives under query or mutation depending on the op type
|
|
243
|
+
// the guide recorded.
|
|
244
|
+
const optype = Object.values(guideField.op ?? {})
|
|
245
|
+
.map((o: any) => o.optype)
|
|
246
|
+
.find((t: any) => null != t) ?? 'query'
|
|
247
|
+
|
|
248
|
+
const fielddef = 'mutation' === optype ?
|
|
249
|
+
def.mutation?.[orig] : def.query?.[orig]
|
|
250
|
+
|
|
251
|
+
// The guide expresses GraphQL renames as `rename: arg:` (root fields
|
|
252
|
+
// have arguments, not path params), while the model's arg machinery
|
|
253
|
+
// reads `rename.param`. Translate so a user override actually applies.
|
|
254
|
+
const grename: any = guideField.rename ?? {}
|
|
255
|
+
const rename: any = null != grename.arg ?
|
|
256
|
+
{ ...grename, param: { ...(grename.param ?? {}), ...grename.arg } } :
|
|
257
|
+
grename
|
|
258
|
+
|
|
259
|
+
const pathdesc: PathDesc = {
|
|
260
|
+
orig,
|
|
261
|
+
parts: [],
|
|
262
|
+
rename,
|
|
263
|
+
method: '', // operation collectOps will copy and assign per op
|
|
264
|
+
op: guideField.op,
|
|
265
|
+
def: fielddef,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
paths$.push(pathdesc)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
; (guideEntity as any).paths$ = paths$
|
|
272
|
+
|
|
273
|
+
return paths$
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
217
277
|
|
|
218
278
|
function buildRelations(guideEntity: any, paths$: PathDesc[]) {
|
|
219
279
|
// An ancestor is a literal collection segment (e.g. "rems") followed by
|
package/src/transform/field.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { each, getx } from 'jostraca'
|
|
|
4
4
|
|
|
5
5
|
import type { TransformResult, Transform } from '../transform'
|
|
6
6
|
|
|
7
|
-
import { validator,
|
|
7
|
+
import { validator, canonizeField, inferFieldType, normalizeFieldName, envelopeProp } from '../utility'
|
|
8
8
|
|
|
9
9
|
import { KIT } from '../types'
|
|
10
10
|
|
|
@@ -93,7 +93,10 @@ function resolveOpFields(
|
|
|
93
93
|
|
|
94
94
|
for (let fielddef of fielddefs) {
|
|
95
95
|
const fieldname = (fielddef as any).key$ as string
|
|
96
|
-
|
|
96
|
+
// Field names are WIRE identifiers — see canonizeField. Using the
|
|
97
|
+
// entity-name canonizer here renamed modelType -> model_type and
|
|
98
|
+
// items -> item, so the SDK read keys the server never sends.
|
|
99
|
+
const name = canonizeField(normalizeFieldName(fieldname))
|
|
97
100
|
const mfield: ModelField = {
|
|
98
101
|
name,
|
|
99
102
|
type: inferFieldType(name, validator(fielddef.type)),
|
|
@@ -107,12 +110,96 @@ function resolveOpFields(
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
|
|
113
|
+
// GraphQL entity fields come straight from the object type: every
|
|
114
|
+
// non-deprecated scalar field, minus any that require arguments (selecting
|
|
115
|
+
// `download(format: Format!)` without binding its argument makes every
|
|
116
|
+
// operation using the fragment fail GraphQL validation), plus one id-stub
|
|
117
|
+
// reference per to-one relation.
|
|
118
|
+
function findGraphqlFieldDefs(
|
|
119
|
+
ment: ModelEntity,
|
|
120
|
+
mpoint: ModelPoint,
|
|
121
|
+
def: any
|
|
122
|
+
): SchemaDef[] {
|
|
123
|
+
const typeName = (mpoint.graphql as any)?.entityType$ ??
|
|
124
|
+
(ment as any).orig$ ?? ''
|
|
125
|
+
const gtype = def.types?.[typeName]
|
|
126
|
+
|
|
127
|
+
if (null == gtype) {
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const out: SchemaDef[] = []
|
|
132
|
+
|
|
133
|
+
// Sorted by construction in parse/graphql.ts, so output stays byte-stable.
|
|
134
|
+
for (const fname of Object.keys(gtype.fields)) {
|
|
135
|
+
const f = gtype.fields[fname]
|
|
136
|
+
|
|
137
|
+
if (f.deprecated) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A field taking required arguments cannot appear in a fixed fragment.
|
|
142
|
+
if (f.args.some((a: any) => a.reqd)) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ftype = def.types?.[f.type]
|
|
147
|
+
const kind = ftype?.kind
|
|
148
|
+
|
|
149
|
+
if ('SCALAR' === kind || 'ENUM' === kind) {
|
|
150
|
+
out.push({
|
|
151
|
+
key$: fname,
|
|
152
|
+
// Enum values are always strings; scalars map by name, with unknown
|
|
153
|
+
// custom scalars left unconstrained.
|
|
154
|
+
type: 'ENUM' === kind ? 'string' : gqlFieldType(f.type),
|
|
155
|
+
required: f.reqd,
|
|
156
|
+
} as any)
|
|
157
|
+
}
|
|
158
|
+
else if (('OBJECT' === kind || 'INTERFACE' === kind) && !f.list) {
|
|
159
|
+
// To-one relation. The default fragment selects `team { id }`, so the
|
|
160
|
+
// response carries a nested stub object — declare it as such. Naming a
|
|
161
|
+
// flat `team_id` here would advertise a field the wire never returns,
|
|
162
|
+
// since nothing flattens the response.
|
|
163
|
+
const idField = ftype.fields?.id
|
|
164
|
+
if (null != idField) {
|
|
165
|
+
out.push({
|
|
166
|
+
key$: fname,
|
|
167
|
+
type: 'object',
|
|
168
|
+
required: false,
|
|
169
|
+
} as any)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// GraphQL named type -> the type names the field typing understands.
|
|
179
|
+
//
|
|
180
|
+
// Built-ins only: a custom scalar (JSON, JSONObject, Upload, ...) can hold
|
|
181
|
+
// any JSON value, so advertising it as a string would misdescribe the data
|
|
182
|
+
// and make generated validation reject values the schema accepts. Enums are
|
|
183
|
+
// mapped by the caller, which knows they are strings.
|
|
184
|
+
function gqlFieldType(typeName: string): string | undefined {
|
|
185
|
+
return 'Int' === typeName ? 'integer' :
|
|
186
|
+
'Float' === typeName ? 'number' :
|
|
187
|
+
'Boolean' === typeName ? 'boolean' :
|
|
188
|
+
('String' === typeName || 'ID' === typeName) ? 'string' :
|
|
189
|
+
undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
110
193
|
function findFieldDefs(
|
|
111
194
|
_ment: ModelEntity,
|
|
112
195
|
mop: ModelOp,
|
|
113
196
|
mpoint: ModelPoint,
|
|
114
197
|
def: any
|
|
115
198
|
): SchemaDef[] {
|
|
199
|
+
if ('graphql' === mpoint.kind) {
|
|
200
|
+
return findGraphqlFieldDefs(_ment, mpoint, def)
|
|
201
|
+
}
|
|
202
|
+
|
|
116
203
|
const fielddefs: SchemaDef[] = []
|
|
117
204
|
const pathdef = def.paths[mpoint.orig]
|
|
118
205
|
|