@voxgig/apidef 0.2.0 → 1.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.
@@ -0,0 +1,185 @@
1
+
2
+
3
+ import { each, getx } from 'jostraca'
4
+
5
+ import type { TransformCtx, TransformSpec } from '../transform'
6
+
7
+ import { fixName } from '../transform'
8
+
9
+
10
+
11
+ async function fieldTransform(
12
+ ctx: TransformCtx,
13
+ tspec: TransformSpec,
14
+ model: any,
15
+ def: any
16
+ ) {
17
+ const { guide: { guide } } = ctx
18
+ let msg = 'fields: '
19
+
20
+ each(guide.entity, (guideEntity: any) => {
21
+
22
+ const entityModel = model.main.api.entity[guideEntity.key$]
23
+ let fieldCount = 0
24
+ each(guideEntity.path, (guidePath: any) => {
25
+ const pathdef = def.paths[guidePath.key$]
26
+
27
+ each(guidePath.op, (op: any) => {
28
+
29
+ if ('load' === op.key$) {
30
+ fieldCount = fieldbuild(entityModel, pathdef, op, guidePath, guideEntity, model)
31
+ }
32
+
33
+ })
34
+ })
35
+
36
+ msg += guideEntity.name + '=' + fieldCount + ' '
37
+ })
38
+
39
+ return { ok: true, msg }
40
+ }
41
+
42
+
43
+ function fieldbuild(
44
+ entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any
45
+ ) {
46
+ // console.log('FB-A', op, pathdef)
47
+ let fieldCount = 0
48
+ let fieldSets = getx(pathdef.get, 'responses 200 content "application/json" schema')
49
+
50
+ if (fieldSets) {
51
+ if (Array.isArray(fieldSets.allOf)) {
52
+ fieldSets = fieldSets.allOf
53
+ }
54
+ else if (fieldSets.properties) {
55
+ fieldSets = [fieldSets]
56
+ }
57
+ }
58
+
59
+ // console.log('TRANSFORM-FIELDSETS', fieldSets)
60
+
61
+ each(fieldSets, (fieldSet: any) => {
62
+ each(fieldSet.properties, (property: any) => {
63
+ // console.log(property)
64
+
65
+ const field =
66
+ (entityModel.field[property.key$] = entityModel.field[property.key$] || {})
67
+
68
+ field.name = property.key$
69
+ fixName(field, field.name)
70
+
71
+ field.type = property.type
72
+ fixName(field, field.type, 'type')
73
+
74
+ field.short = property.description
75
+
76
+ fieldCount++
77
+ // console.log('FB-ID', field.name, entityModel.param)
78
+ })
79
+ })
80
+
81
+ // Guess id field name using GET path param
82
+ if ('load' === op.key$) {
83
+ const getdef = pathdef.get
84
+ const getparams = getdef.parameters || []
85
+ if (1 === getparams.length) {
86
+ if (entityModel.op.load.path.match(RegExp('\\{' + getdef.parameters[0].name + '\\}$'))) {
87
+ entityModel.id.field = getdef.parameters[0].name
88
+ }
89
+ }
90
+ }
91
+
92
+ return fieldCount
93
+ }
94
+
95
+
96
+
97
+ export {
98
+ fieldTransform
99
+ }
100
+
101
+
102
+
103
+
104
+ /*
105
+
106
+ # API Specification Transform Guide
107
+
108
+
109
+ @"@voxgig/apidef/model/guide.jsonic"
110
+
111
+
112
+ guide: control: transform: openapi: order: `
113
+
114
+ top,
115
+ entity,
116
+ operation,
117
+ field,
118
+ customField,
119
+
120
+ `
121
+
122
+ guide: transform: customField: {
123
+ load: 'customField.js'
124
+ }
125
+
126
+
127
+ guide: entity: {
128
+ pet: path: {
129
+ '/pet/{petId}': {
130
+ op:{ load: 'get', create: 'post', save: 'put' }
131
+ }
132
+ }
133
+ pet: test: {
134
+ quick: {
135
+ active: true,
136
+ create: { id: 1, name:'Rex' },
137
+ load: { id: 1 },
138
+ }
139
+ }
140
+
141
+ # direct custom definition
142
+ pet: def: {}
143
+ }
144
+
145
+
146
+
147
+
148
+ const { each, getx } = require('jostraca')
149
+
150
+
151
+ async function customField(ctx, tspec, model, def) {
152
+ const { spec, util: {fixName} } = ctx
153
+
154
+ const nameField = {
155
+ name: 'name',
156
+ type: 'string',
157
+ short: 'Name of pet'
158
+ }
159
+ fixName(nameField, nameField.name)
160
+ fixName(nameField, nameField.type, 'type')
161
+
162
+ const ageField = {
163
+ name: 'age',
164
+ type: 'number',
165
+ short: 'Age of pet'
166
+ }
167
+ fixName(ageField, ageField.name)
168
+ fixName(ageField, ageField.type, 'type')
169
+
170
+
171
+ Object.assign(model.main.api.entity.pet.field, {
172
+ name: nameField,
173
+ age: ageField,
174
+ })
175
+
176
+ return { ok: true }
177
+ }
178
+
179
+
180
+ module.exports = {
181
+ customField
182
+ }
183
+
184
+
185
+ */
@@ -0,0 +1,22 @@
1
+
2
+ import { Jsonic } from '@jsonic/jsonic-next'
3
+
4
+ import { each, getx } from 'jostraca'
5
+
6
+ import type { TransformCtx, TransformSpec } from '../transform'
7
+
8
+
9
+ const { deep } = Jsonic.util
10
+
11
+ async function manualTransform(ctx: TransformCtx, tspec: TransformSpec, model: any, def: any) {
12
+ const { guide: { guide: { manual } } } = ctx
13
+
14
+ deep(model, manual)
15
+
16
+ return { ok: true, msg: 'manual' }
17
+ }
18
+
19
+
20
+ export {
21
+ manualTransform
22
+ }
@@ -0,0 +1,125 @@
1
+
2
+
3
+ import { each, getx } from 'jostraca'
4
+
5
+ import type { TransformCtx, TransformSpec } from '../transform'
6
+
7
+ import { fixName } from '../transform'
8
+
9
+
10
+
11
+ async function operationTransform(
12
+ ctx: TransformCtx,
13
+ tspec: TransformSpec,
14
+ model: any,
15
+ def: any
16
+ ) {
17
+ const { guide: { guide } } = ctx
18
+ let msg = 'operations: '
19
+
20
+ const paramBuilder = (paramMap: any, paramDef: any,
21
+ entityModel: any, pathdef: any,
22
+ op: any, path: any, entity: any, model: any) => {
23
+
24
+ paramMap[paramDef.name] = {
25
+ required: paramDef.required
26
+ }
27
+ fixName(paramMap[paramDef.name], paramDef.name)
28
+
29
+ const type = paramDef.schema ? paramDef.schema.type : paramDef.type
30
+ fixName(paramMap[paramDef.name], type, 'type')
31
+ }
32
+
33
+
34
+ const queryBuilder = (queryMap: any, queryDef: any,
35
+ entityModel: any, pathdef: any,
36
+ op: any, path: any, entity: any, model: any) => {
37
+ queryMap[queryDef.name] = {
38
+ required: queryDef.required
39
+ }
40
+ fixName(queryMap[queryDef.name], queryDef.name)
41
+
42
+ const type = queryDef.schema ? queryDef.schema.type : queryDef.type
43
+ fixName(queryMap[queryDef.name], type, 'type')
44
+ }
45
+
46
+ const opBuilder: any = {
47
+ any: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
48
+ const em = entityModel.op[op.key$] = {
49
+ path: path.key$,
50
+ method: op.val$,
51
+ param: {},
52
+ query: {},
53
+ }
54
+ fixName(em, op.key$)
55
+
56
+ // Params are in the path
57
+ if (0 < path.params$.length) {
58
+ let params = getx(pathdef[op.val$], 'parameters?in=path') || []
59
+ if (Array.isArray(params)) {
60
+ params.reduce((a: any, p: any) =>
61
+ (paramBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.param)
62
+ }
63
+ }
64
+
65
+ // Queries are after the ?
66
+ let queries = getx(pathdef[op.val$], 'parameters?in!=path') || []
67
+ if (Array.isArray(queries)) {
68
+ queries.reduce((a: any, p: any) =>
69
+ (queryBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.query)
70
+ }
71
+
72
+ return em
73
+ },
74
+
75
+
76
+ list: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
77
+ return opBuilder.any(entityModel, pathdef, op, path, entity, model)
78
+ },
79
+
80
+ load: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
81
+ return opBuilder.any(entityModel, pathdef, op, path, entity, model)
82
+ },
83
+
84
+ create: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
85
+ return opBuilder.any(entityModel, pathdef, op, path, entity, model)
86
+ },
87
+
88
+ save: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
89
+ return opBuilder.any(entityModel, pathdef, op, path, entity, model)
90
+ },
91
+
92
+ remove: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
93
+ return opBuilder.any(entityModel, pathdef, op, path, entity, model)
94
+ },
95
+
96
+ }
97
+
98
+
99
+
100
+ each(guide.entity, (guideEntity: any) => {
101
+ let opcount = 0
102
+ const entityModel = model.main.api.entity[guideEntity.key$]
103
+ each(guideEntity.path, (guidePath: any) => {
104
+ const pathdef = def.paths[guidePath.key$]
105
+
106
+ each(guidePath.op, (op: any) => {
107
+ const opbuild = opBuilder[op.key$]
108
+
109
+ if (opbuild) {
110
+ opbuild(entityModel, pathdef, op, guidePath, guideEntity, model)
111
+ opcount++
112
+ }
113
+ })
114
+ })
115
+
116
+ msg += guideEntity.name + '=' + opcount + ' '
117
+ })
118
+
119
+ return { ok: true, msg }
120
+ }
121
+
122
+
123
+ export {
124
+ operationTransform
125
+ }
@@ -0,0 +1,21 @@
1
+
2
+ import { each, getx } from 'jostraca'
3
+
4
+ import type { TransformCtx, TransformSpec } from '../transform'
5
+
6
+ import { fixName } from '../transform'
7
+
8
+
9
+ async function topTransform(ctx: TransformCtx, tspec: TransformSpec, model: any, def: any) {
10
+ const { spec } = ctx
11
+
12
+ fixName(model.main.api, spec.meta.name)
13
+ model.main.def.desc = def.info.description
14
+
15
+ return { ok: true, msg: spec.meta.name }
16
+ }
17
+
18
+
19
+ export {
20
+ topTransform
21
+ }
@@ -0,0 +1,193 @@
1
+ /* Copyright (c) 2024 Voxgig, MIT License */
2
+
3
+
4
+ import Path from 'node:path'
5
+
6
+ import { getx, each, camelify } from 'jostraca'
7
+
8
+
9
+ import { topTransform } from './transform/top'
10
+ import { entityTransform } from './transform/entity'
11
+ import { operationTransform } from './transform/operation'
12
+ import { fieldTransform } from './transform/field'
13
+ import { manualTransform } from './transform/manual'
14
+
15
+
16
+
17
+ type TransformCtx = {
18
+ log: any,
19
+ spec: any,
20
+ guide: any,
21
+ opts: any,
22
+ util: any,
23
+ defpath: string,
24
+ }
25
+
26
+ type TransformSpec = {
27
+ transform: Transform[]
28
+ }
29
+
30
+ type TransformResult = {
31
+ ok: boolean
32
+ msg: string
33
+ }
34
+
35
+ type Transform = (
36
+ ctx: TransformCtx,
37
+ tspec: TransformSpec,
38
+ model: any,
39
+ def: any,
40
+ ) => Promise<TransformResult>
41
+
42
+ type ProcessResult = {
43
+ ok: boolean
44
+ msg: string,
45
+ results: TransformResult[]
46
+ }
47
+
48
+
49
+ const TRANSFORM: Record<string, Transform> = {
50
+ top: topTransform,
51
+ entity: entityTransform,
52
+ operation: operationTransform,
53
+ field: fieldTransform,
54
+ manual: manualTransform,
55
+ }
56
+
57
+
58
+
59
+
60
+ async function resolveTransforms(ctx: TransformCtx): Promise<TransformSpec> {
61
+ const { log, guide: { guide } } = ctx
62
+
63
+ const tspec: TransformSpec = {
64
+ transform: []
65
+ }
66
+
67
+ // TODO: parameterize
68
+ const defkind = 'openapi'
69
+ const transformNames = guide.control.transform[defkind].order
70
+ .split(/\s*,\s*/)
71
+ .map((t: string) => t.trim())
72
+ .filter((t: string) => '' != t)
73
+
74
+ log.info({ point: 'transform', note: 'order', order: transformNames })
75
+
76
+ for (const tn of transformNames) {
77
+ log.debug({ what: 'transform', transform: tn })
78
+ const transform = await resolveTransform(tn, ctx)
79
+ tspec.transform.push(transform)
80
+ }
81
+
82
+ return tspec
83
+ }
84
+
85
+
86
+ async function resolveTransform(tn: string, ctx: TransformCtx) {
87
+ const { log, defpath, guide: { guide } } = ctx
88
+
89
+ let transform = TRANSFORM[tn]
90
+ if (transform) {
91
+ return transform
92
+ }
93
+
94
+ const tdef = guide.transform[tn]
95
+ if (null == tdef) {
96
+ const err = new Error('Unknown transform: ' + tn)
97
+ log.error({ what: 'transform', transform: tn, fail: 'unknown', err })
98
+ throw err
99
+ }
100
+
101
+ if (!tn.startsWith('custom')) {
102
+ const err =
103
+ new Error('Custom transform name must start with "custom": ' + tn)
104
+ log.error({ what: 'transform', transform: tn, fail: 'prefix', err })
105
+ throw err
106
+ }
107
+
108
+ const customtpath = Path.join(defpath, tdef.load)
109
+ try {
110
+ const transformModule = require(customtpath)
111
+ transform = transformModule[tn]
112
+ }
113
+ catch (e: any) {
114
+ const err = new Error('Custom transform not found: ' +
115
+ customtpath + ': ' + e.message)
116
+ log.error({ what: 'transform', transform: tn, fail: 'require', err })
117
+ throw err
118
+ }
119
+
120
+ return transform
121
+ }
122
+
123
+
124
+
125
+ async function processTransforms(
126
+ ctx: TransformCtx,
127
+ spec: TransformSpec,
128
+ model: any,
129
+ def: any
130
+ ): Promise<ProcessResult> {
131
+ const pres: ProcessResult = {
132
+ ok: true,
133
+ msg: '',
134
+ results: []
135
+ }
136
+
137
+ for (let tI = 0; tI < spec.transform.length; tI++) {
138
+ const transform = spec.transform[tI]
139
+
140
+ try {
141
+ const tres = await transform(ctx, spec, model, def)
142
+ pres.ok = pres.ok && tres.ok
143
+ pres.msg += tres.msg + '\n'
144
+ pres.results.push(tres)
145
+ }
146
+ catch (err: any) {
147
+ pres.ok = false
148
+ pres.msg += err.message + '\n'
149
+ pres.results.push({
150
+ ok: false,
151
+ msg: err.message
152
+ })
153
+ }
154
+ }
155
+
156
+ return pres
157
+ }
158
+
159
+
160
+
161
+
162
+
163
+ /*
164
+ function extractFields(properties: any) {
165
+ const fieldMap = each(properties)
166
+ .reduce((a: any, p: any) => (a[p.key$] =
167
+ { name: p.key$, kind: camelify(p.type) }, a), {})
168
+ return fieldMap
169
+ }
170
+ */
171
+
172
+
173
+ function fixName(base: any, name: string, prop = 'name') {
174
+ base[prop.toLowerCase()] = name.toLowerCase()
175
+ base[camelify(prop)] = camelify(name)
176
+ base[prop.toUpperCase()] = name.toUpperCase()
177
+ }
178
+
179
+
180
+
181
+
182
+
183
+ export type {
184
+ TransformCtx,
185
+ TransformSpec,
186
+ }
187
+
188
+
189
+ export {
190
+ fixName,
191
+ resolveTransforms,
192
+ processTransforms,
193
+ }
@@ -1,14 +0,0 @@
1
-
2
- guide: entity: &: {
3
- name: .$KEY
4
- path: &: {
5
- op: {}
6
- }
7
- }
8
-
9
-
10
- guide: feature: &: {
11
- name: .$KEY
12
- active: *false | boolean
13
- }
14
-