@voxgig/apidef 1.9.0 → 2.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.d.ts +3 -29
- package/dist/apidef.js +84 -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 +125 -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 +178 -0
- package/dist/guide/heuristic01.js.map +1 -0
- package/dist/guide.d.ts +2 -0
- package/dist/guide.js +62 -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 +4 -2
- 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 +22 -11
- 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 +21 -10
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +682 -0
- package/dist/types.js +38 -0
- package/dist/types.js.map +1 -0
- package/dist/utility.d.ts +4 -0
- package/dist/utility.js +59 -0
- package/dist/utility.js.map +1 -0
- package/model/apidef.jsonic +28 -24
- package/package.json +8 -6
- package/src/apidef.ts +117 -263
- 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 +165 -0
- package/src/builder/flow/flowHeuristic01.ts~ +45 -0
- package/src/builder/flow.ts +60 -0
- package/src/guide/heuristic01.ts +225 -0
- package/src/guide.ts +91 -0
- package/src/parse.ts +6 -4
- package/src/resolver.ts +91 -0
- package/src/transform/entity.ts +10 -7
- package/src/transform/field.ts +9 -92
- package/src/transform/operation.ts +39 -25
- package/src/transform/top.ts +11 -9
- package/src/transform.ts +23 -11
- package/src/types.ts +88 -0
- package/src/utility.ts +83 -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,165 @@
|
|
|
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
|
|
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
|
+
param: {
|
|
41
|
+
[`${model.NAME}_TEST_${apiEntity.NAME}_ENTID`]: idmap
|
|
42
|
+
},
|
|
43
|
+
test: { entity: { [apiEntity.Name]: {} } },
|
|
44
|
+
step: []
|
|
45
|
+
} as any)
|
|
46
|
+
|
|
47
|
+
names(flow, flow.name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const data = flow.model.test.entity[apiEntity.Name]
|
|
51
|
+
|
|
52
|
+
refs.map((ref, i) => {
|
|
53
|
+
const id = idmap[ref]
|
|
54
|
+
const ent: any = data[id] = {}
|
|
55
|
+
|
|
56
|
+
let num = (i * size(apiEntity.field) * 10)
|
|
57
|
+
each(apiEntity.field, (field) => {
|
|
58
|
+
ent[field.name] =
|
|
59
|
+
'number' === field.type ? num :
|
|
60
|
+
'boolean' === field.type ? 0 === num % 2 :
|
|
61
|
+
'object' === field.type ? {} :
|
|
62
|
+
'array' === field.type ? [] :
|
|
63
|
+
's' + (num.toString(16))
|
|
64
|
+
num++
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
ent.id = id
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const steps = flow.model.step
|
|
72
|
+
|
|
73
|
+
let num = 0
|
|
74
|
+
let name
|
|
75
|
+
|
|
76
|
+
if (apiEntity.op.load) {
|
|
77
|
+
name = `load_${apiEntity.name}${num}`
|
|
78
|
+
steps.push({
|
|
79
|
+
name,
|
|
80
|
+
kind: 'entity',
|
|
81
|
+
entity: `${apiEntity.name}`,
|
|
82
|
+
action: 'load',
|
|
83
|
+
match: {
|
|
84
|
+
id: `\`dm$=p.${model.NAME}_TEST_${apiEntity.NAME}_ENTID.${apiEntity.name}01\``
|
|
85
|
+
},
|
|
86
|
+
valid: {
|
|
87
|
+
'`$OPEN`': true,
|
|
88
|
+
id: `\`dm$=s.${name}.match.id\``
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (apiEntity.op.update && apiEntity.op.load) {
|
|
94
|
+
num++
|
|
95
|
+
name = `update_${apiEntity.name}${num}`
|
|
96
|
+
const id = idmap[`${apiEntity.name}01`]
|
|
97
|
+
const loadref = `load_${apiEntity.name}${num - 1}`
|
|
98
|
+
const reqdata = makeUpdateData(name, apiEntity, flow, id)
|
|
99
|
+
const valid = makeUpdateValid(name, apiEntity, flow, id, reqdata)
|
|
100
|
+
steps.push({
|
|
101
|
+
name,
|
|
102
|
+
ref: loadref,
|
|
103
|
+
action: 'update',
|
|
104
|
+
reqdata,
|
|
105
|
+
valid: {
|
|
106
|
+
'`$OPEN`': true,
|
|
107
|
+
id: `\`dm$=s.${loadref}.match.id\``,
|
|
108
|
+
...valid
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
num++
|
|
113
|
+
name = `load_${apiEntity.name}${num}`
|
|
114
|
+
|
|
115
|
+
steps.push({
|
|
116
|
+
name,
|
|
117
|
+
kind: 'entity',
|
|
118
|
+
entity: `${apiEntity.name}`,
|
|
119
|
+
action: 'load',
|
|
120
|
+
match: {
|
|
121
|
+
id: `\`dm$=p.${model.NAME}_TEST_${apiEntity.NAME}_ENTID.${apiEntity.name}01\``
|
|
122
|
+
},
|
|
123
|
+
valid: {
|
|
124
|
+
'`$OPEN`': true,
|
|
125
|
+
id: `\`dm$=s.${loadref}.match.id\``,
|
|
126
|
+
...valid
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
return flow
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
function makeUpdateData(name: string, apiEntity: any, flow: any, id: string) {
|
|
138
|
+
const ud: any = {}
|
|
139
|
+
const data = flow.model.test.entity[apiEntity.Name]
|
|
140
|
+
|
|
141
|
+
const dataFields = each(apiEntity.field).filter(f => 'id' !== f.name && !f.name.includes('_id'))
|
|
142
|
+
const stringFields = each(dataFields).filter(f => 'string' === f.type)
|
|
143
|
+
|
|
144
|
+
if (0 < size(stringFields)) {
|
|
145
|
+
const f = stringFields[0]
|
|
146
|
+
ud[f.name] = data[id][f.name] + '-`$WHEN`'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ud
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
function makeUpdateValid(name: string, apiEntity: any, flow: any, id: string, reqdata: any) {
|
|
154
|
+
const vd: any = {}
|
|
155
|
+
|
|
156
|
+
each(reqdata, (n) => {
|
|
157
|
+
vd[n.key$] = `\`dm$=s.${name}.reqdata.${n.key$}\``
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return vd
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
flowHeuristic01
|
|
165
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { each, snakify, names } from 'jostraca'
|
|
4
|
+
|
|
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 apiEntity = ctx.apimodel.main.api.entity[entity.name]
|
|
24
|
+
|
|
25
|
+
const flow: any = {
|
|
26
|
+
name: 'Basic' + apiEntity.Name
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
flow.model = ({
|
|
30
|
+
name: flow.Name,
|
|
31
|
+
param: {},
|
|
32
|
+
test: { entity: { [apiEntity.Name]: {} } },
|
|
33
|
+
step: []
|
|
34
|
+
} as any)
|
|
35
|
+
|
|
36
|
+
names(flow, flow.name)
|
|
37
|
+
|
|
38
|
+
return flow
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
flowHeuristic01
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
return function flowBuilder() {
|
|
25
|
+
|
|
26
|
+
Folder({ name: 'flow' }, () => {
|
|
27
|
+
const barrel = [
|
|
28
|
+
'# Flows\n'
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
each(flows, (flow: any) => {
|
|
32
|
+
let flowfile =
|
|
33
|
+
Path.join(ctx.opts.folder, 'flow',
|
|
34
|
+
(null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) +
|
|
35
|
+
flow.Name + 'Flow.jsonic')
|
|
36
|
+
|
|
37
|
+
let flowModelSrc = formatJsonSrc(JSON.stringify(flow.model, null, 2))
|
|
38
|
+
|
|
39
|
+
let flowsrc = `# ${flow.Name}Flow
|
|
40
|
+
|
|
41
|
+
main: sdk: flow: ${flow.Name}Flow:
|
|
42
|
+
` + flowModelSrc
|
|
43
|
+
|
|
44
|
+
barrel.push(`@"${Path.basename(flowfile)}"`)
|
|
45
|
+
|
|
46
|
+
File({ name: Path.basename(flowfile) }, () => Content(flowsrc))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
File({
|
|
50
|
+
name: (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'flow-index.jsonic'
|
|
51
|
+
}, () => Content(barrel.join('\n')))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
makeFlowBuilder
|
|
60
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { each, snakify, names } from 'jostraca'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async function heuristic01(ctx: any): Promise<Record<string, any>> {
|
|
8
|
+
let guide = ctx.model.main.api.guide
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const entityDescs = resolveEntityDescs(ctx)
|
|
12
|
+
|
|
13
|
+
// console.log('entityDescs')
|
|
14
|
+
// console.dir(entityDescs, { depth: null })
|
|
15
|
+
|
|
16
|
+
guide = {
|
|
17
|
+
control: guide.control,
|
|
18
|
+
entity: entityDescs,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return guide
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const METHOD_IDOP: Record<string, string> = {
|
|
26
|
+
get: 'load',
|
|
27
|
+
post: 'create',
|
|
28
|
+
put: 'update',
|
|
29
|
+
patch: 'update',
|
|
30
|
+
delete: 'remove',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function resolveEntityDescs(ctx: any) {
|
|
35
|
+
const entityDescs: Record<string, any> = {}
|
|
36
|
+
const paths = ctx.def.paths
|
|
37
|
+
|
|
38
|
+
each(paths, (pathdef, pathname) => {
|
|
39
|
+
// look for rightmmost /entname/{entid}
|
|
40
|
+
const m = pathname.match(/\/([a-zA-Z0-1_-]+)\/\{([a-zA-Z0-1_-]+)\}$/)
|
|
41
|
+
if (m) {
|
|
42
|
+
let origentname = snakify(m[1])
|
|
43
|
+
let entname = depluralize(origentname)
|
|
44
|
+
|
|
45
|
+
let entdesc = (entityDescs[entname] = entityDescs[entname] || { name: entname })
|
|
46
|
+
entdesc.plural = origentname
|
|
47
|
+
|
|
48
|
+
names(entdesc, entname)
|
|
49
|
+
|
|
50
|
+
entdesc.alias = entdesc.alias || {}
|
|
51
|
+
|
|
52
|
+
if ('id' != m[2]) {
|
|
53
|
+
entdesc.alias.id = m[2]
|
|
54
|
+
entdesc.alias[m[2]] = 'id'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
entdesc.path = (entdesc.path || {})
|
|
58
|
+
|
|
59
|
+
const op: Record<string, any> = {}
|
|
60
|
+
entdesc.path[pathname] = { op }
|
|
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[origentname]) {
|
|
75
|
+
// TODO: use quotes when @voxgig/struct updated to support them
|
|
76
|
+
// transform.resform = '`body."'+origentname+'"`'
|
|
77
|
+
transform.resform = '`body.' + origentname + '`'
|
|
78
|
+
}
|
|
79
|
+
else if (resbody[entname]) {
|
|
80
|
+
transform.resform = '`body.' + entname + '`'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const reqdef = mdef.requestBody?.content?.['application/json']?.schema?.properties
|
|
85
|
+
if (reqdef) {
|
|
86
|
+
if (reqdef[origentname]) {
|
|
87
|
+
transform.reqform = { [origentname]: '`reqdata`' }
|
|
88
|
+
}
|
|
89
|
+
else if (reqdef[entname]) {
|
|
90
|
+
transform.reqform = { [entname]: '`reqdata`' }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
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
|
+
|
|
108
|
+
each(paths, (pathdef, pathname) => {
|
|
109
|
+
// look for rightmmost /entname/{entid}
|
|
110
|
+
const m = pathname.match(/\/([a-zA-Z0-1_-]+)$/)
|
|
111
|
+
if (m) {
|
|
112
|
+
let origentname = snakify(m[1])
|
|
113
|
+
let entname = depluralize(origentname)
|
|
114
|
+
|
|
115
|
+
let entdesc = entityDescs[entname]
|
|
116
|
+
|
|
117
|
+
if (entdesc) {
|
|
118
|
+
entdesc.path = (entdesc.path || {})
|
|
119
|
+
|
|
120
|
+
if (pathdef.get) {
|
|
121
|
+
const op: Record<string, any> = { list: { method: 'get' } }
|
|
122
|
+
entdesc.path[pathname] = { op }
|
|
123
|
+
|
|
124
|
+
const transform: Record<string, any> = {}
|
|
125
|
+
const mdef = pathdef.get
|
|
126
|
+
const resokdef = mdef.responses[200] || mdef.responses[201]
|
|
127
|
+
const resbody = resokdef?.content?.['application/json']?.schema
|
|
128
|
+
if (resbody) {
|
|
129
|
+
if (resbody[origentname]) {
|
|
130
|
+
// TODO: use quotes when @voxgig/struct updated to support them
|
|
131
|
+
// transform.resform = '`body."'+origentname+'"`'
|
|
132
|
+
transform.resform = '`body.' + origentname + '`'
|
|
133
|
+
}
|
|
134
|
+
else if (resbody[entname]) {
|
|
135
|
+
transform.resform = '`body.' + entname + '`'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (0 < Object.entries(transform).length) {
|
|
140
|
+
op.transform = transform
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return entityDescs
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
function depluralize(word: string): string {
|
|
152
|
+
if (!word || word.length === 0) {
|
|
153
|
+
return word
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Common irregular plurals
|
|
157
|
+
const irregulars: Record<string, string> = {
|
|
158
|
+
'children': 'child',
|
|
159
|
+
'men': 'man',
|
|
160
|
+
'women': 'woman',
|
|
161
|
+
'teeth': 'tooth',
|
|
162
|
+
'feet': 'foot',
|
|
163
|
+
'geese': 'goose',
|
|
164
|
+
'mice': 'mouse',
|
|
165
|
+
'people': 'person',
|
|
166
|
+
'data': 'datum',
|
|
167
|
+
'criteria': 'criterion',
|
|
168
|
+
'phenomena': 'phenomenon',
|
|
169
|
+
'indices': 'index',
|
|
170
|
+
'matrices': 'matrix',
|
|
171
|
+
'vertices': 'vertex',
|
|
172
|
+
'analyses': 'analysis',
|
|
173
|
+
'axes': 'axis',
|
|
174
|
+
'crises': 'crisis',
|
|
175
|
+
'diagnoses': 'diagnosis',
|
|
176
|
+
'oases': 'oasis',
|
|
177
|
+
'theses': 'thesis',
|
|
178
|
+
'appendices': 'appendix'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (irregulars[word]) {
|
|
182
|
+
return irregulars[word]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Rules for regular plurals (applied in order)
|
|
186
|
+
|
|
187
|
+
// -ies -> -y (cities -> city)
|
|
188
|
+
if (word.endsWith('ies') && word.length > 3) {
|
|
189
|
+
return word.slice(0, -3) + 'y'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -ves -> -f or -fe (wolves -> wolf, knives -> knife)
|
|
193
|
+
if (word.endsWith('ves')) {
|
|
194
|
+
const stem = word.slice(0, -3)
|
|
195
|
+
// Check if it should be -fe (like knife, wife, life)
|
|
196
|
+
if (['kni', 'wi', 'li'].includes(stem)) {
|
|
197
|
+
return stem + 'fe'
|
|
198
|
+
}
|
|
199
|
+
return stem + 'f'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// -oes -> -o (potatoes -> potato)
|
|
203
|
+
if (word.endsWith('oes')) {
|
|
204
|
+
return word.slice(0, -2)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// -ses, -xes, -zes, -shes, -ches -> remove -es (boxes -> box)
|
|
208
|
+
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes') ||
|
|
209
|
+
word.endsWith('shes') || word.endsWith('ches')) {
|
|
210
|
+
return word.slice(0, -2)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// -s -> remove -s (cats -> cat)
|
|
214
|
+
if (word.endsWith('s') && !word.endsWith('ss')) {
|
|
215
|
+
return word.slice(0, -1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// If none of the rules apply, return as is
|
|
219
|
+
return word
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
heuristic01
|
|
225
|
+
}
|