@voxgig/apidef 1.9.0 → 2.0.2
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 +3 -29
- package/dist/apidef.js +65 -186
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/apiEntity.d.ts +3 -0
- package/dist/builder/entity/apiEntity.js +51 -0
- package/dist/builder/entity/apiEntity.js.map +1 -0
- package/dist/builder/entity/def.d.ts +3 -0
- package/dist/builder/entity/def.js +19 -0
- package/dist/builder/entity/def.js.map +1 -0
- package/dist/builder/entity.d.ts +2 -0
- package/dist/builder/entity.js +30 -0
- package/dist/builder/entity.js.map +1 -0
- package/dist/builder/flow/flowHeuristic01.d.ts +2 -0
- package/dist/builder/flow/flowHeuristic01.js +153 -0
- package/dist/builder/flow/flowHeuristic01.js.map +1 -0
- package/dist/builder/flow.d.ts +2 -0
- package/dist/builder/flow.js +41 -0
- package/dist/builder/flow.js.map +1 -0
- package/dist/guide/heuristic01.d.ts +2 -0
- package/dist/guide/heuristic01.js +119 -0
- package/dist/guide/heuristic01.js.map +1 -0
- package/dist/guide.d.ts +2 -0
- package/dist/guide.js +60 -0
- package/dist/guide.js.map +1 -0
- package/dist/parse.d.ts +1 -1
- package/dist/parse.js +5 -4
- package/dist/parse.js.map +1 -1
- package/dist/resolver.d.ts +2 -0
- package/dist/resolver.js +62 -0
- package/dist/resolver.js.map +1 -0
- package/dist/transform/entity.js +25 -4
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +4 -84
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.d.ts +2 -2
- package/dist/transform/operation.js +60 -30
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/top.d.ts +2 -2
- package/dist/transform/top.js +5 -4
- package/dist/transform/top.js.map +1 -1
- package/dist/transform.d.ts +1 -1
- package/dist/transform.js +20 -10
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +691 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/utility.d.ts +5 -0
- package/dist/utility.js +84 -0
- package/dist/utility.js.map +1 -0
- package/model/apidef.jsonic +28 -24
- package/package.json +9 -7
- package/src/apidef.ts +94 -271
- package/src/builder/entity/apiEntity.ts +88 -0
- package/src/builder/entity/def.ts +44 -0
- package/src/builder/entity.ts +54 -0
- package/src/builder/flow/flowHeuristic01.ts +200 -0
- package/src/builder/flow.ts +61 -0
- package/src/guide/heuristic01.ts +178 -0
- package/src/guide.ts +87 -0
- package/src/parse.ts +6 -4
- package/src/resolver.ts +91 -0
- package/src/transform/entity.ts +36 -10
- package/src/transform/field.ts +9 -92
- package/src/transform/operation.ts +112 -46
- package/src/transform/top.ts +11 -9
- package/src/transform.ts +22 -11
- package/src/types.ts +89 -0
- package/src/utility.ts +161 -0
- package/dist/transform/manual.d.ts +0 -3
- package/dist/transform/manual.js +0 -12
- package/dist/transform/manual.js.map +0 -1
- package/src/transform/manual.ts +0 -29
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
import Path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { each, File, Folder, Content } from 'jostraca'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ApiDefOptions,
|
|
10
|
+
} from '../../types'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
formatJsonSrc,
|
|
14
|
+
} from '../../utility'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function resolveApiEntity(
|
|
19
|
+
apimodel: any,
|
|
20
|
+
opts: ApiDefOptions,
|
|
21
|
+
) {
|
|
22
|
+
const barrel = [
|
|
23
|
+
'# Entity Models\n'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const entityFiles: { name: string, src: string }[] = []
|
|
27
|
+
|
|
28
|
+
each(apimodel.main.api.entity, ((entity: any, entityName: string) => {
|
|
29
|
+
const entityFile = (null == opts.outprefix ? '' : opts.outprefix) + entityName + '.jsonic'
|
|
30
|
+
|
|
31
|
+
const entityJSON =
|
|
32
|
+
JSON.stringify(entity, null, 2)
|
|
33
|
+
|
|
34
|
+
const fieldAliasesSrc = fieldAliases(entity)
|
|
35
|
+
|
|
36
|
+
const entitySrc =
|
|
37
|
+
`# Entity: ${entity.name}\n\n` +
|
|
38
|
+
`main: api: entity: ${entity.name}: {\n\n` +
|
|
39
|
+
` alias: field: ${fieldAliasesSrc}\n` +
|
|
40
|
+
formatJsonSrc(entityJSON.substring(1, entityJSON.length - 1)) +
|
|
41
|
+
'\n\n}\n'
|
|
42
|
+
|
|
43
|
+
entityFiles.push({ name: entityFile, src: entitySrc })
|
|
44
|
+
|
|
45
|
+
barrel.push(`@"${Path.basename(entityFile)}"`)
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
const indexFile = (null == opts.outprefix ? '' : opts.outprefix) + 'api-entity-index.jsonic'
|
|
49
|
+
|
|
50
|
+
return function apiEntityBuilder() {
|
|
51
|
+
Folder({ name: 'api' }, () => {
|
|
52
|
+
each(entityFiles, (entityFile) => {
|
|
53
|
+
File({ name: entityFile.name }, () => Content(entityFile.src))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
File({ name: indexFile }, () => Content(barrel.join('\n')))
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fieldAliases(entity: any) {
|
|
63
|
+
// HEURISTIC: id may be name_id or nameId
|
|
64
|
+
const fieldAliases =
|
|
65
|
+
each(entity.op, (op: any) =>
|
|
66
|
+
each(op.param))
|
|
67
|
+
.flat()
|
|
68
|
+
.reduce((a: any, p: any) =>
|
|
69
|
+
|
|
70
|
+
(entity.field[p.keys] ? null :
|
|
71
|
+
(p.key$.toLowerCase().includes(entity.name) ?
|
|
72
|
+
(a[p.key$] = 'id', a.id = p.key$) :
|
|
73
|
+
null)
|
|
74
|
+
|
|
75
|
+
, a), {})
|
|
76
|
+
|
|
77
|
+
const fieldAliasesSrc =
|
|
78
|
+
JSON.stringify(fieldAliases, null, 2)
|
|
79
|
+
.replace(/\n/g, '\n ')
|
|
80
|
+
|
|
81
|
+
return fieldAliasesSrc
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
resolveApiEntity
|
|
88
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
import Path from 'node:path'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ApiDefOptions,
|
|
8
|
+
ApiModel,
|
|
9
|
+
} from '../../types'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
File,
|
|
14
|
+
Folder,
|
|
15
|
+
Content
|
|
16
|
+
} from 'jostraca'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
function resolveDef(
|
|
20
|
+
apimodel: ApiModel,
|
|
21
|
+
opts: ApiDefOptions,
|
|
22
|
+
) {
|
|
23
|
+
const defFile =
|
|
24
|
+
(null == opts.outprefix ? '' : opts.outprefix) + 'api-def.jsonic'
|
|
25
|
+
|
|
26
|
+
const modelDef = { main: { def: apimodel.main.def } }
|
|
27
|
+
let modelDefSrc = JSON.stringify(modelDef, null, 2)
|
|
28
|
+
|
|
29
|
+
modelDefSrc =
|
|
30
|
+
'# API Definition\n\n' +
|
|
31
|
+
modelDefSrc.substring(1, modelDefSrc.length - 1).replace(/\n /g, '\n')
|
|
32
|
+
|
|
33
|
+
return function defBuilder() {
|
|
34
|
+
Folder({ name: 'api' }, () => {
|
|
35
|
+
File({ name: defFile }, () => Content(modelDefSrc))
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
resolveDef
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
import Path from 'node:path'
|
|
4
|
+
|
|
5
|
+
// import { each } from 'jostraca'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// import type {
|
|
9
|
+
// ApiDefOptions,
|
|
10
|
+
// Log,
|
|
11
|
+
// FsUtil,
|
|
12
|
+
// } from '../types'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// import {
|
|
16
|
+
// writeChanged
|
|
17
|
+
// } from '../utility'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
resolveApiEntity
|
|
22
|
+
} from './entity/apiEntity'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
resolveDef
|
|
26
|
+
} from './entity/def'
|
|
27
|
+
|
|
28
|
+
// import {
|
|
29
|
+
// resolveSdkEntity
|
|
30
|
+
// } from './entity/sdkEntity'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async function makeEntityBuilder(ctx: any) {
|
|
35
|
+
const { apimodel, opts } = ctx
|
|
36
|
+
|
|
37
|
+
const apiEntityBuilder = resolveApiEntity(apimodel, opts)
|
|
38
|
+
const defBuilder = resolveDef(apimodel, opts)
|
|
39
|
+
// const sdkEntityBuilder = resolveSdkEntity(apimodel, opts)
|
|
40
|
+
|
|
41
|
+
return function entityBuilder() {
|
|
42
|
+
apiEntityBuilder()
|
|
43
|
+
defBuilder()
|
|
44
|
+
// sdkEntityBuilder()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
makeEntityBuilder
|
|
54
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
|
|
2
|
+
import { size } from '@voxgig/struct'
|
|
3
|
+
|
|
4
|
+
import { each, names } from 'jostraca'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async function flowHeuristic01(ctx: any): Promise<any[]> {
|
|
8
|
+
let entity = ctx.model.main.api.guide.entity
|
|
9
|
+
|
|
10
|
+
const flows: any[] = []
|
|
11
|
+
|
|
12
|
+
each(entity, (entity: any) => {
|
|
13
|
+
flows.push(resolveBasicEntityFlow(ctx, entity))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return flows
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function resolveBasicEntityFlow(ctx: any, entity: any) {
|
|
23
|
+
const { apimodel, model } = ctx
|
|
24
|
+
const apiEntity = apimodel.main.api.entity[entity.name]
|
|
25
|
+
|
|
26
|
+
const flow: any = {
|
|
27
|
+
name: 'Basic' + apiEntity.Name + 'Flow'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const refs = [
|
|
31
|
+
`${apiEntity.name}01`,
|
|
32
|
+
`${apiEntity.name}02`,
|
|
33
|
+
`${apiEntity.name}03`,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const idmap = refs.reduce((a: any, ref) => (a[ref] = ref.toUpperCase(), a), {})
|
|
37
|
+
|
|
38
|
+
flow.model = ({
|
|
39
|
+
name: flow.name,
|
|
40
|
+
active: true,
|
|
41
|
+
param: {
|
|
42
|
+
[`${model.NAME}_TEST_${apiEntity.NAME}_ENTID`]: idmap,
|
|
43
|
+
[`${model.NAME}_TEST_LIVE`]: "FALSE",
|
|
44
|
+
[`${model.NAME}_TEST_EXPLAIN`]: "FALSE",
|
|
45
|
+
},
|
|
46
|
+
test: { entity: { [apiEntity.name]: {} } },
|
|
47
|
+
step: []
|
|
48
|
+
} as any)
|
|
49
|
+
|
|
50
|
+
names(flow, flow.name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
const data = flow.model.test.entity[apiEntity.name]
|
|
54
|
+
|
|
55
|
+
refs.map((ref, i) => {
|
|
56
|
+
const id = idmap[ref]
|
|
57
|
+
const ent: any = data[id] = {}
|
|
58
|
+
|
|
59
|
+
let num = (i * size(apiEntity.field) * 10)
|
|
60
|
+
each(apiEntity.field, (field) => {
|
|
61
|
+
ent[field.name] =
|
|
62
|
+
'number' === field.type ? num :
|
|
63
|
+
'boolean' === field.type ? 0 === num % 2 :
|
|
64
|
+
'object' === field.type ? {} :
|
|
65
|
+
'array' === field.type ? [] :
|
|
66
|
+
's' + (num.toString(16))
|
|
67
|
+
num++
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
ent.id = id
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
const steps = flow.model.step
|
|
75
|
+
|
|
76
|
+
let num = 0
|
|
77
|
+
let name
|
|
78
|
+
|
|
79
|
+
const am: any = {}
|
|
80
|
+
|
|
81
|
+
if (apiEntity.op.load) {
|
|
82
|
+
|
|
83
|
+
// Get additional required match properties
|
|
84
|
+
each(apiEntity.op.load.param, (param: any) => {
|
|
85
|
+
if (param.required) {
|
|
86
|
+
let ancestorName = param.name
|
|
87
|
+
let ancestorEntity = apimodel.main.api.entity[ancestorName]
|
|
88
|
+
|
|
89
|
+
if (null == ancestorEntity) {
|
|
90
|
+
ancestorName = ancestorName.replace('_id', '')
|
|
91
|
+
ancestorEntity = apimodel.main.api.entity[ancestorName]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (ancestorEntity && ancestorName !== apiEntity.name) {
|
|
95
|
+
flow.model.param[`${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID`] = {
|
|
96
|
+
[ancestorEntity.name + '01']: ancestorEntity.NAME + '01'
|
|
97
|
+
}
|
|
98
|
+
am[param.name] =
|
|
99
|
+
`\`dm$=p.${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID.${ancestorEntity.name}01\``
|
|
100
|
+
|
|
101
|
+
data[`${apiEntity.NAME}01`][param.name] = ancestorEntity.NAME + '01'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
name = `load_${apiEntity.name}${num}`
|
|
108
|
+
steps.push({
|
|
109
|
+
name,
|
|
110
|
+
kind: 'entity',
|
|
111
|
+
entity: `${apiEntity.name}`,
|
|
112
|
+
action: 'load',
|
|
113
|
+
match: {
|
|
114
|
+
id: `\`dm$=p.${model.NAME}_TEST_${apiEntity.NAME}_ENTID.${apiEntity.name}01\``,
|
|
115
|
+
...am,
|
|
116
|
+
},
|
|
117
|
+
valid: {
|
|
118
|
+
'`$OPEN`': true,
|
|
119
|
+
id: `\`dm$=s.${name}.match.id\``,
|
|
120
|
+
...am,
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (apiEntity.op.update && apiEntity.op.load) {
|
|
126
|
+
num++
|
|
127
|
+
name = `update_${apiEntity.name}${num}`
|
|
128
|
+
const id = idmap[`${apiEntity.name}01`]
|
|
129
|
+
const loadref = `load_${apiEntity.name}${num - 1}`
|
|
130
|
+
const reqdata = makeUpdateData(name, apiEntity, flow, id)
|
|
131
|
+
const valid = makeUpdateValid(name, apiEntity, flow, id, reqdata)
|
|
132
|
+
steps.push({
|
|
133
|
+
name,
|
|
134
|
+
ref: loadref,
|
|
135
|
+
action: 'update',
|
|
136
|
+
reqdata,
|
|
137
|
+
valid: {
|
|
138
|
+
'`$OPEN`': true,
|
|
139
|
+
id: `\`dm$=s.${loadref}.match.id\``,
|
|
140
|
+
...am,
|
|
141
|
+
...valid
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
num++
|
|
146
|
+
name = `load_${apiEntity.name}${num}`
|
|
147
|
+
|
|
148
|
+
steps.push({
|
|
149
|
+
name,
|
|
150
|
+
kind: 'entity',
|
|
151
|
+
entity: `${apiEntity.name}`,
|
|
152
|
+
action: 'load',
|
|
153
|
+
match: {
|
|
154
|
+
id: `\`dm$=p.${model.NAME}_TEST_${apiEntity.NAME}_ENTID.${apiEntity.name}01\``,
|
|
155
|
+
...am,
|
|
156
|
+
},
|
|
157
|
+
valid: {
|
|
158
|
+
'`$OPEN`': true,
|
|
159
|
+
id: `\`dm$=s.${loadref}.match.id\``,
|
|
160
|
+
...am,
|
|
161
|
+
...valid
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
return flow
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
function makeUpdateData(name: string, apiEntity: any, flow: any, id: string) {
|
|
173
|
+
const ud: any = {}
|
|
174
|
+
const data = flow.model.test.entity[apiEntity.name]
|
|
175
|
+
|
|
176
|
+
const dataFields = each(apiEntity.field).filter(f => 'id' !== f.name && !f.name.includes('_id'))
|
|
177
|
+
const stringFields = each(dataFields).filter(f => 'string' === f.type)
|
|
178
|
+
|
|
179
|
+
if (0 < size(stringFields)) {
|
|
180
|
+
const f = stringFields[0]
|
|
181
|
+
ud[f.name] = data[id][f.name] + '-`$WHEN`'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return ud
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function makeUpdateValid(name: string, apiEntity: any, flow: any, id: string, reqdata: any) {
|
|
189
|
+
const vd: any = {}
|
|
190
|
+
|
|
191
|
+
each(reqdata, (n) => {
|
|
192
|
+
vd[n.key$] = `\`dm$=s.${name}.reqdata.${n.key$}\``
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return vd
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export {
|
|
199
|
+
flowHeuristic01
|
|
200
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import Path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { File, Content, Folder, each } from 'jostraca'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
formatJsonSrc,
|
|
8
|
+
} from '../utility'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import { flowHeuristic01 } from './flow/flowHeuristic01'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async function makeFlowBuilder(ctx: any) {
|
|
15
|
+
let flows: any[] = []
|
|
16
|
+
|
|
17
|
+
if ('heuristic01' === ctx.opts.strategy) {
|
|
18
|
+
flows = await flowHeuristic01(ctx)
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
return function flowBuilder() {
|
|
26
|
+
|
|
27
|
+
Folder({ name: 'flow' }, () => {
|
|
28
|
+
const barrel = [
|
|
29
|
+
'# Flows\n'
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
each(flows, (flow: any) => {
|
|
33
|
+
let flowfile =
|
|
34
|
+
Path.join(ctx.opts.folder, 'flow',
|
|
35
|
+
(null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) +
|
|
36
|
+
flow.Name + '.jsonic')
|
|
37
|
+
|
|
38
|
+
let flowModelSrc = formatJsonSrc(JSON.stringify(flow.model, null, 2))
|
|
39
|
+
|
|
40
|
+
let flowsrc = `# ${flow.Name}
|
|
41
|
+
|
|
42
|
+
main: sdk: flow: ${flow.Name}:
|
|
43
|
+
` + flowModelSrc
|
|
44
|
+
|
|
45
|
+
barrel.push(`@"${Path.basename(flowfile)}"`)
|
|
46
|
+
|
|
47
|
+
File({ name: Path.basename(flowfile) }, () => Content(flowsrc))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
File({
|
|
51
|
+
name: (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'flow-index.jsonic'
|
|
52
|
+
}, () => Content(barrel.join('\n')))
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
makeFlowBuilder
|
|
61
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { each, snakify, names } from 'jostraca'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import { depluralize } from '../utility'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
type EntityDesc = {
|
|
10
|
+
name: string
|
|
11
|
+
origname: string
|
|
12
|
+
plural: string
|
|
13
|
+
path: Record<string, EntityPathDesc>
|
|
14
|
+
alias: Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type EntityPathDesc = {
|
|
19
|
+
op: Record<string, any>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function heuristic01(ctx: any): Promise<Record<string, any>> {
|
|
23
|
+
let guide = ctx.model.main.api.guide
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const entityDescs = resolveEntityDescs(ctx)
|
|
27
|
+
|
|
28
|
+
// console.log('entityDescs')
|
|
29
|
+
// console.dir(entityDescs, { depth: null })
|
|
30
|
+
|
|
31
|
+
guide = {
|
|
32
|
+
control: guide.control,
|
|
33
|
+
entity: entityDescs,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return guide
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const METHOD_IDOP: Record<string, string> = {
|
|
41
|
+
get: 'load',
|
|
42
|
+
post: 'create',
|
|
43
|
+
put: 'update',
|
|
44
|
+
patch: 'update',
|
|
45
|
+
delete: 'remove',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function resolveEntityDescs(ctx: any) {
|
|
50
|
+
const entityDescs: Record<string, any> = {}
|
|
51
|
+
const paths = ctx.def.paths
|
|
52
|
+
|
|
53
|
+
// Analyze paths ending in .../foo/{foo}
|
|
54
|
+
each(paths, (pathdef, pathstr) => {
|
|
55
|
+
|
|
56
|
+
// Look for rightmmost /entname/{entid}.
|
|
57
|
+
const m = pathstr.match(/\/([a-zA-Z0-1_-]+)\/\{([a-zA-Z0-1_-]+)\}$/)
|
|
58
|
+
if (m) {
|
|
59
|
+
const entdesc = resolveEntity(entityDescs, pathstr, m[1], m[2])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
each(pathdef, (mdef, method) => {
|
|
63
|
+
const opname = METHOD_IDOP[method]
|
|
64
|
+
if (null == opname) return;
|
|
65
|
+
|
|
66
|
+
const transform: Record<string, any> = {
|
|
67
|
+
// reqform: '`reqdata`',
|
|
68
|
+
// resform: '`body`',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resokdef = mdef.responses[200] || mdef.responses[201]
|
|
72
|
+
const resbody = resokdef?.content?.['application/json']?.schema
|
|
73
|
+
if (resbody) {
|
|
74
|
+
if (resbody[entdesc.origname]) {
|
|
75
|
+
transform.resform = '`body.' + entdesc.origname + '`'
|
|
76
|
+
}
|
|
77
|
+
else if (resbody[entdesc.name]) {
|
|
78
|
+
transform.resform = '`body.' + entdesc.name + '`'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reqdef = mdef.requestBody?.content?.['application/json']?.schema?.properties
|
|
83
|
+
if (reqdef) {
|
|
84
|
+
if (reqdef[entdesc.origname]) {
|
|
85
|
+
transform.reqform = { [entdesc.origname]: '`reqdata`' }
|
|
86
|
+
}
|
|
87
|
+
else if (reqdef[entdesc.origname]) {
|
|
88
|
+
transform.reqform = { [entdesc.origname]: '`reqdata`' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const op = entdesc.path[pathstr].op
|
|
94
|
+
|
|
95
|
+
op[opname] = {
|
|
96
|
+
// TODO: in actual guide, remove "standard" method ops since redundant
|
|
97
|
+
method,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (0 < Object.entries(transform).length) {
|
|
101
|
+
op[opname].transform = transform
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Analyze paths ending in .../foo
|
|
108
|
+
each(paths, (pathdef, pathstr) => {
|
|
109
|
+
|
|
110
|
+
// Look for rightmmost /entname.
|
|
111
|
+
const m = pathstr.match(/\/([a-zA-Z0-1_-]+)$/)
|
|
112
|
+
if (m) {
|
|
113
|
+
const entdesc = resolveEntity(entityDescs, pathstr, m[1])
|
|
114
|
+
|
|
115
|
+
if (pathdef.get) {
|
|
116
|
+
const op: Record<string, any> = { list: { method: 'get' } }
|
|
117
|
+
entdesc.path[pathstr] = { op }
|
|
118
|
+
|
|
119
|
+
const transform: Record<string, any> = {}
|
|
120
|
+
const mdef = pathdef.get
|
|
121
|
+
const resokdef = mdef.responses[200] || mdef.responses[201]
|
|
122
|
+
const resbody = resokdef?.content?.['application/json']?.schema
|
|
123
|
+
if (resbody) {
|
|
124
|
+
if (resbody[entdesc.origname]) {
|
|
125
|
+
transform.resform = '`body.' + entdesc.origname + '`'
|
|
126
|
+
}
|
|
127
|
+
else if (resbody[entdesc.name]) {
|
|
128
|
+
transform.resform = '`body.' + entdesc.name + '`'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (0 < Object.entries(transform).length) {
|
|
133
|
+
op.transform = transform
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return entityDescs
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
function resolveEntity(
|
|
144
|
+
entityDescs: Record<string, EntityDesc>,
|
|
145
|
+
pathStr: string,
|
|
146
|
+
pathName: string,
|
|
147
|
+
pathParam?: string
|
|
148
|
+
)
|
|
149
|
+
: EntityDesc {
|
|
150
|
+
let origentname = snakify(pathName)
|
|
151
|
+
let entname = depluralize(origentname)
|
|
152
|
+
|
|
153
|
+
let entdesc = (entityDescs[entname] = entityDescs[entname] || { name: entname })
|
|
154
|
+
entdesc.plural = origentname
|
|
155
|
+
entdesc.origname = origentname
|
|
156
|
+
|
|
157
|
+
names(entdesc, entname)
|
|
158
|
+
|
|
159
|
+
entdesc.alias = entdesc.alias || {}
|
|
160
|
+
|
|
161
|
+
if (null != pathParam) {
|
|
162
|
+
const pathParamCanon = snakify(pathParam)
|
|
163
|
+
if ('id' != pathParamCanon) {
|
|
164
|
+
entdesc.alias.id = pathParamCanon
|
|
165
|
+
entdesc.alias[pathParamCanon] = 'id'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
entdesc.path = (entdesc.path || {})
|
|
170
|
+
entdesc.path[pathStr] = { op: {} }
|
|
171
|
+
|
|
172
|
+
return entdesc
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
heuristic01
|
|
178
|
+
}
|
package/src/guide.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
|
|
2
|
+
import Path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { File, Content, each } from 'jostraca'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import { heuristic01 } from './guide/heuristic01'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async function resolveGuide(ctx: any) {
|
|
11
|
+
let guide: Record<string, any> = ctx.model.main.api.guide
|
|
12
|
+
|
|
13
|
+
if ('heuristic01' === ctx.opts.strategy) {
|
|
14
|
+
guide = await heuristic01(ctx)
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
guide = cleanGuide(guide)
|
|
21
|
+
|
|
22
|
+
ctx.model.main.api.guide = guide
|
|
23
|
+
|
|
24
|
+
const guideFile =
|
|
25
|
+
Path.join(ctx.opts.folder,
|
|
26
|
+
(null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'guide.jsonic')
|
|
27
|
+
|
|
28
|
+
const guideBlocks = [
|
|
29
|
+
'# Guide',
|
|
30
|
+
'',
|
|
31
|
+
'main: api: guide: { ',
|
|
32
|
+
'',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
guideBlocks.push(...each(guide.entity, (entity, entityname) => {
|
|
37
|
+
guideBlocks.push(`\nentity: ${entityname}: {`)
|
|
38
|
+
|
|
39
|
+
each(entity.path, (path, pathname) => {
|
|
40
|
+
guideBlocks.push(` path: '${pathname}': op: {`)
|
|
41
|
+
|
|
42
|
+
each(path.op, (op, opname) => {
|
|
43
|
+
guideBlocks.push(` '${opname}': method: ${op.method}`)
|
|
44
|
+
if (op.transform?.reqform) {
|
|
45
|
+
guideBlocks.push(
|
|
46
|
+
` '${opname}': transform: reqform: ${JSON.stringify(op.transform.reqform)}`)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
guideBlocks.push(` }`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
guideBlocks.push(`}`)
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
guideBlocks.push('}')
|
|
57
|
+
|
|
58
|
+
const guideSrc = guideBlocks.join('\n')
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
File({ name: Path.basename(guideFile) }, () => Content(guideSrc))
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
function cleanGuide(guide: Record<string, any>): Record<string, any> {
|
|
68
|
+
const clean: Record<string, any> = {
|
|
69
|
+
control: guide.control,
|
|
70
|
+
entity: {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
each(guide.entity, (entity: any, name: string) => {
|
|
74
|
+
let ent: any = clean.entity[name] = clean.entity[name] = { name, path: {} }
|
|
75
|
+
|
|
76
|
+
each(entity.path, (path: any, pathname: string) => {
|
|
77
|
+
ent.path[pathname] = path
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return clean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
resolveGuide
|
|
87
|
+
}
|