@voxgig/apidef 0.2.0 → 0.3.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/src/apidef.ts CHANGED
@@ -1,64 +1,133 @@
1
- /* Copyright (c) 2024 Richard Rodger, MIT License */
1
+ /* Copyright (c) 2024 Voxgig, MIT License */
2
2
 
3
3
  import * as Fs from 'node:fs'
4
-
5
4
  import Path from 'node:path'
6
5
 
7
6
 
8
7
  import { bundleFromString, createConfig } from '@redocly/openapi-core'
9
-
10
8
  import { FSWatcher } from 'chokidar'
11
-
12
9
  import { Aontu, Context } from 'aontu'
10
+ import Pino from 'pino'
11
+ import PinoPretty from 'pino-pretty'
12
+
13
+
13
14
 
14
- import { getx, each, camelify } from 'jostraca'
15
+
16
+ import {
17
+ resolveTransforms,
18
+ processTransforms,
19
+ fixName,
20
+ } from './transform'
15
21
 
16
22
 
17
23
  type ApiDefOptions = {
18
24
  fs?: any
25
+ pino?: ReturnType<typeof Pino>
26
+ debug?: boolean | string
19
27
  }
20
28
 
21
29
 
30
+ type ApiDefSpec = {
31
+ def: string
32
+ model: string,
33
+ kind: string,
34
+ meta: Record<string, any>,
35
+ }
22
36
 
23
37
 
24
38
  function ApiDef(opts: ApiDefOptions = {}) {
25
39
  const fs = opts.fs || Fs
40
+ let pino = opts.pino
41
+
42
+ if (null == pino) {
43
+ let pretty = PinoPretty({ sync: true })
44
+ const level = null == opts.debug ? 'info' :
45
+ true === opts.debug ? 'debug' :
46
+ 'string' == typeof opts.debug ? opts.debug :
47
+ 'info'
48
+
49
+ pino = Pino({
50
+ name: 'apidef',
51
+ level,
52
+ },
53
+ pretty
54
+ )
55
+ }
56
+
57
+
58
+ const log = pino.child({ cmp: 'apidef' })
26
59
 
27
60
 
28
61
  async function watch(spec: any) {
62
+ log.info({ point: 'watch-start' })
63
+ log.debug({ point: 'watch-spec', spec })
64
+
29
65
  await generate(spec)
30
66
 
31
67
  const fsw = new FSWatcher()
32
68
 
33
69
  fsw.on('change', (...args: any[]) => {
34
- // console.log('APIDEF CHANGE', args)
70
+ log.trace({ watch: 'change', file: args[0] })
35
71
  generate(spec)
36
72
  })
37
73
 
38
- // console.log('APIDEF-WATCH', spec.def)
74
+ log.trace({ watch: 'add', what: 'def', file: spec.def })
39
75
  fsw.add(spec.def)
40
76
 
41
- // console.log('APIDEF-WATCH', spec.guide)
77
+ log.trace({ watch: 'add', what: 'guide', file: spec.guilde })
42
78
  fsw.add(spec.guide)
43
79
  }
44
80
 
45
81
 
46
- async function generate(spec: any) {
47
- // console.log('APIDEF.generate')
82
+ async function generate(spec: ApiDefSpec) {
83
+ const start = Date.now()
84
+
85
+ log.info({ point: 'generate-start', start })
86
+ log.debug({ point: 'generate-spec', spec })
87
+
88
+ // TODO: Validate spec
89
+ const ctx = {
90
+ log,
91
+ spec,
92
+ guide: {},
93
+ opts,
94
+ util: { fixName },
95
+ defpath: Path.dirname(spec.def)
96
+ }
48
97
 
49
98
  const guide = await resolveGuide(spec, opts)
50
- const transform = resolveTranform(spec, guide, opts)
99
+ log.debug({ point: 'guide', guide })
51
100
 
52
- const source = fs.readFileSync(spec.def, 'utf8')
101
+ ctx.guide = guide
102
+ const transformSpec = await resolveTransforms(ctx)
103
+ log.debug({ point: 'transform', spec: transformSpec })
104
+
105
+
106
+ let source
107
+ try {
108
+ source = fs.readFileSync(spec.def, 'utf8')
109
+ }
110
+ catch (err: any) {
111
+ log.error({ read: 'fail', what: 'def', file: spec.def, err })
112
+ throw err
113
+ }
53
114
 
54
- const modelBasePath = Path.dirname(spec.model)
55
115
 
56
116
  const config = await createConfig({})
57
- const bundle = await bundleFromString({
58
- source,
59
- config,
60
- dereference: true,
61
- })
117
+ let bundle
118
+
119
+ try {
120
+ bundle = await bundleFromString({
121
+ source,
122
+ config,
123
+ dereference: true,
124
+ })
125
+ }
126
+ catch (err: any) {
127
+ log.error({ parse: 'fail', what: 'openapi', file: spec.def, err })
128
+ throw err
129
+ }
130
+
62
131
 
63
132
  const model = {
64
133
  main: {
@@ -71,11 +140,18 @@ function ApiDef(opts: ApiDefOptions = {}) {
71
140
 
72
141
  try {
73
142
  const def = bundle.bundle.parsed
74
- // console.dir(def, { depth: null })
75
- transform(def, model)
143
+ const processResult = await processTransforms(ctx, transformSpec, model, def)
144
+
145
+ if (!processResult.ok) {
146
+ log.error({ process: 'fail', what: 'transform', result: processResult })
147
+ throw new Error('Transform failed: ' + processResult.msg)
148
+ }
149
+ else {
150
+ log.debug({ process: 'result', what: 'transform', result: processResult })
151
+ }
76
152
  }
77
153
  catch (err: any) {
78
- console.log('APIDEF ERROR', err)
154
+ log.error({ process: 'fail', what: 'transform', err })
79
155
  throw err
80
156
  }
81
157
 
@@ -83,18 +159,19 @@ function ApiDef(opts: ApiDefOptions = {}) {
83
159
  let modelSrc = JSON.stringify(modelapi, null, 2)
84
160
  modelSrc = modelSrc.substring(1, modelSrc.length - 1)
85
161
 
86
- fs.writeFileSync(
87
- spec.model,
88
- modelSrc
89
- )
162
+ writeChanged('api-model', spec.model, modelSrc)
90
163
 
91
164
 
165
+ const modelBasePath = Path.dirname(spec.model)
92
166
  const defFilePath = Path.join(modelBasePath, 'def.jsonic')
93
167
 
94
168
  const modelDef = { main: { def: model.main.def } }
95
169
  let modelDefSrc = JSON.stringify(modelDef, null, 2)
96
170
  modelDefSrc = modelDefSrc.substring(1, modelDefSrc.length - 1)
97
171
 
172
+ writeChanged('def-model', defFilePath, modelDefSrc)
173
+
174
+ /*
98
175
  let existingSrc: string = ''
99
176
  if (fs.existsSync(defFilePath)) {
100
177
  existingSrc = fs.readFileSync(defFilePath, 'utf8')
@@ -110,7 +187,7 @@ function ApiDef(opts: ApiDefOptions = {}) {
110
187
  modelDefSrc
111
188
  )
112
189
  }
113
-
190
+ */
114
191
 
115
192
  return {
116
193
  ok: true,
@@ -119,6 +196,43 @@ function ApiDef(opts: ApiDefOptions = {}) {
119
196
  }
120
197
 
121
198
 
199
+
200
+ function writeChanged(what: string, path: string, content: string) {
201
+ let exists = false
202
+ let changed = false
203
+ let action = ''
204
+ try {
205
+ let existingContent: string = ''
206
+ exists = fs.existsSync(path)
207
+
208
+ if (exists) {
209
+ action = 'read'
210
+ existingContent = fs.readFileSync(path, 'utf8')
211
+ }
212
+
213
+ changed = existingContent !== content
214
+
215
+ log.info({
216
+ write: 'file', what, skip: !changed, exists, changed,
217
+ contentLength: content.length, file: path
218
+ })
219
+
220
+ if (changed) {
221
+ action = 'write'
222
+ fs.writeFileSync(path, content)
223
+ }
224
+ }
225
+ catch (err: any) {
226
+ log.error({
227
+ fail: action, what, file: path, exists, changed,
228
+ contentLength: content.length, err
229
+ })
230
+ throw err
231
+ }
232
+ }
233
+
234
+
235
+
122
236
  async function resolveGuide(spec: any, _opts: any) {
123
237
  if (null == spec.guide) {
124
238
  spec.guide = spec.def + '-guide.jsonic'
@@ -127,34 +241,58 @@ function ApiDef(opts: ApiDefOptions = {}) {
127
241
  const path = Path.normalize(spec.guide)
128
242
  let src: string
129
243
 
130
- // console.log('APIDEF resolveGuide', path)
244
+ let action = ''
245
+ let exists = false
246
+ try {
131
247
 
132
- if (fs.existsSync(path)) {
133
- src = fs.readFileSync(path, 'utf8')
134
- }
135
- else {
136
- src = `
248
+ action = 'exists'
249
+ let exists = fs.existsSync(path)
250
+
251
+ log.debug({ read: 'file', what: 'guide', file: path, exists })
252
+
253
+ if (exists) {
254
+ action = 'read'
255
+ src = fs.readFileSync(path, 'utf8')
256
+ }
257
+ else {
258
+ src = `
137
259
  # API Specification Transform Guide
138
260
 
139
- @"node_modules/@voxgig/apidef/model/guide.jsonic"
261
+ @"@voxgig/apidef/model/guide.jsonic"
140
262
 
141
263
  guide: entity: {
142
264
 
143
265
  }
144
266
 
267
+ guide: control: transform: openapi: order: \`
268
+ top,
269
+ entity,
270
+ operation,
271
+ field,
272
+ manual,
273
+ \`
274
+
145
275
  `
146
- fs.writeFileSync(path, src)
276
+ action = 'write'
277
+ fs.writeFileSync(path, src)
278
+ }
279
+ }
280
+ catch (err: any) {
281
+ log.error({ fail: action, what: 'guide', file: path, exists, err })
282
+ throw err
147
283
  }
148
284
 
149
- const aopts = {}
285
+
286
+ const aopts = { path }
150
287
  const root = Aontu(src, aopts)
151
288
  const hasErr = root.err && 0 < root.err.length
152
289
 
153
- // TODO: collect all errors
154
290
  if (hasErr) {
155
- // console.log(root.err)
156
- // throw new Error(root.err[0])
157
- throw root.err[0].err
291
+ const err: any = new Error('Guide parse error:\n' +
292
+ (root.err.map((pe: any) => pe.msg)).join('\n'))
293
+ log.error({ fail: 'parse', what: 'guide', file: path, err })
294
+ err.errs = () => root.err
295
+ throw err
158
296
  }
159
297
 
160
298
  let genctx = new Context({ root })
@@ -162,27 +300,29 @@ guide: entity: {
162
300
 
163
301
  // TODO: collect all errors
164
302
  if (genctx.err && 0 < genctx.err.length) {
165
- // console.log(genctx.err)
166
- throw new Error(JSON.stringify(genctx.err[0]))
303
+ const err: any = new Error('Guide build error:\n' +
304
+ (genctx.err.map((pe: any) => pe.msg)).join('\n'))
305
+ log.error({ fail: 'build', what: 'guide', file: path, err })
306
+ err.errs = () => genctx.err
307
+ throw err
167
308
  }
168
309
 
169
- // console.log('GUIDE')
170
- // console.dir(guide, { depth: null })
171
-
172
310
  const pathParts = Path.parse(path)
173
311
  spec.guideModelPath = Path.join(pathParts.dir, pathParts.name + '.json')
174
312
 
175
313
  const updatedSrc = JSON.stringify(guide, null, 2)
176
314
 
315
+ writeChanged('guide-model', spec.guideModelPath, updatedSrc)
316
+
177
317
  // console.log('APIDEF resolveGuide write', spec.guideModelPath, src !== updatedSrc)
178
- let existingSrc = ''
179
- if (fs.existsSync(spec.guideModelPath)) {
180
- existingSrc = fs.readFileSync(spec.guideModelPath, 'utf8')
181
- }
318
+ // let existingSrc = ''
319
+ // if (fs.existsSync(spec.guideModelPath)) {
320
+ // existingSrc = fs.readFileSync(spec.guideModelPath, 'utf8')
321
+ // }
182
322
 
183
- if (existingSrc !== updatedSrc) {
184
- fs.writeFileSync(spec.guideModelPath, updatedSrc)
185
- }
323
+ // if (existingSrc !== updatedSrc) {
324
+ // fs.writeFileSync(spec.guideModelPath, updatedSrc)
325
+ // }
186
326
 
187
327
  return guide
188
328
  }
@@ -197,195 +337,6 @@ guide: entity: {
197
337
 
198
338
 
199
339
 
200
-
201
- function resolveTranform(spec: any, guide: any, opts: any) {
202
- return makeOpenAPITransform(spec, guide, opts)
203
- }
204
-
205
-
206
- function extractFields(properties: any) {
207
- const fieldMap = each(properties)
208
- .reduce((a: any, p: any) => (a[p.key$] =
209
- { name: p.key$, kind: camelify(p.type) }, a), {})
210
- return fieldMap
211
- }
212
-
213
-
214
- function fixName(base: any, name: string, prop = 'name') {
215
- base[prop.toLowerCase()] = name.toLowerCase()
216
- base[camelify(prop)] = camelify(name)
217
- base[prop.toUpperCase()] = name.toUpperCase()
218
- }
219
-
220
-
221
- function makeOpenAPITransform(spec: any, guideModel: any, opts: any) {
222
-
223
- const paramBuilder = (paramMap: any, paramDef: any,
224
- entityModel: any, pathdef: any,
225
- op: any, path: any, entity: any, model: any) => {
226
-
227
- paramMap[paramDef.name] = {
228
- required: paramDef.required
229
- }
230
- fixName(paramMap[paramDef.name], paramDef.name)
231
-
232
- const type = paramDef.schema ? paramDef.schema.type : paramDef.type
233
- fixName(paramMap[paramDef.name], type, 'type')
234
- }
235
-
236
-
237
- const queryBuilder = (queryMap: any, queryDef: any,
238
- entityModel: any, pathdef: any,
239
- op: any, path: any, entity: any, model: any) => {
240
- queryMap[queryDef.name] = {
241
- required: queryDef.required
242
- }
243
- fixName(queryMap[queryDef.name], queryDef.name)
244
-
245
- const type = queryDef.schema ? queryDef.schema.type : queryDef.type
246
- fixName(queryMap[queryDef.name], type, 'type')
247
- }
248
-
249
-
250
- const opBuilder: any = {
251
- any: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
252
- const em = entityModel.op[op.key$] = {
253
- path: path.key$,
254
- method: op.val$,
255
- param: {},
256
- query: {},
257
- }
258
- fixName(em, op.key$)
259
-
260
- // Params are in the path
261
- if (0 < path.params.length) {
262
- let params = getx(pathdef[op.val$], 'parameters?in=path') || []
263
- if (Array.isArray(params)) {
264
- params.reduce((a: any, p: any) =>
265
- (paramBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.param)
266
- }
267
- }
268
-
269
- // Queries are after the ?
270
- let queries = getx(pathdef[op.val$], 'parameters?in!=path') || []
271
- if (Array.isArray(queries)) {
272
- queries.reduce((a: any, p: any) =>
273
- (queryBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.query)
274
- }
275
-
276
- return em
277
- },
278
-
279
-
280
- list: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
281
- return opBuilder.any(entityModel, pathdef, op, path, entity, model)
282
- },
283
-
284
- load: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
285
- return opBuilder.any(entityModel, pathdef, op, path, entity, model)
286
- },
287
-
288
- create: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
289
- return opBuilder.any(entityModel, pathdef, op, path, entity, model)
290
- },
291
-
292
- save: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
293
- return opBuilder.any(entityModel, pathdef, op, path, entity, model)
294
- },
295
-
296
- remove: (entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any) => {
297
- return opBuilder.any(entityModel, pathdef, op, path, entity, model)
298
- },
299
-
300
- }
301
-
302
-
303
- function fieldbuild(
304
- entityModel: any, pathdef: any, op: any, path: any, entity: any, model: any
305
- ) {
306
- // console.log(pathdef)
307
-
308
- let fieldSets = getx(pathdef.get, 'responses 200 content "application/json" schema')
309
-
310
- if (fieldSets) {
311
- if (Array.isArray(fieldSets.allOf)) {
312
- fieldSets = fieldSets.allOf
313
- }
314
- else if (fieldSets.properties) {
315
- fieldSets = [fieldSets]
316
- }
317
- }
318
-
319
- each(fieldSets, (fieldSet: any) => {
320
- each(fieldSet.properties, (property: any) => {
321
- // console.log(property)
322
-
323
- const field =
324
- (entityModel.field[property.key$] = entityModel.field[property.key$] || {})
325
-
326
- field.name = property.key$
327
- fixName(field, field.name)
328
-
329
- field.type = property.type
330
- fixName(field, field.type, 'type')
331
-
332
- field.short = property.description
333
- })
334
- })
335
- }
336
-
337
-
338
- return function OpenAPITransform(def: any, model: any) {
339
- fixName(model.main.api, spec.meta.name)
340
-
341
- // console.log('OpenAPITransform', guideModel)
342
-
343
- model.main.def.desc = def.info.description
344
-
345
-
346
- each(guideModel.guide.entity, (entity: any) => {
347
- // console.log('ENTITY', entity)
348
-
349
- const entityModel: any = model.main.api.entity[entity.key$] = {
350
- op: {},
351
- field: {},
352
- cmd: {},
353
- }
354
-
355
- fixName(entityModel, entity.key$)
356
-
357
- each(entity.path, (path: any) => {
358
- const pathdef = def.paths[path.key$]
359
-
360
- if (null == pathdef) {
361
- throw new Error('APIDEF: path not found in OpenAPI: ' + path.key$ +
362
- ' (entity: ' + entity.name + ')')
363
- }
364
-
365
- path.parts = path.key$.split('/')
366
- path.params = path.parts
367
- .filter((p: string) => p.startsWith('{'))
368
- .map((p: string) => p.substring(1, p.length - 1))
369
-
370
- // console.log('ENTITY-PATH', entity, path)
371
-
372
- each(path.op, (op: any) => {
373
- const opbuild = opBuilder[op.key$]
374
-
375
- if (opbuild) {
376
- opbuild(entityModel, pathdef, op, path, entity, model)
377
- }
378
-
379
- if ('load' === op.key$) {
380
- fieldbuild(entityModel, pathdef, op, path, entity, model)
381
- }
382
- })
383
- })
384
- })
385
- }
386
- }
387
-
388
-
389
340
  export type {
390
341
  ApiDefOptions,
391
342
  }
@@ -0,0 +1,54 @@
1
+
2
+
3
+ import { each } from 'jostraca'
4
+
5
+ import type { TransformCtx, TransformSpec } from '../transform'
6
+
7
+ import { fixName } from '../transform'
8
+
9
+
10
+
11
+ async function entityTransform(ctx: TransformCtx, tspec: TransformSpec, model: any, def: any) {
12
+ const { guide: { guide } } = ctx
13
+ let msg = ''
14
+
15
+ each(guide.entity, (guideEntity: any) => {
16
+
17
+ const entityModel: any = model.main.api.entity[guideEntity.key$] = {
18
+ op: {},
19
+ field: {},
20
+ cmd: {},
21
+ id: {
22
+ name: 'id',
23
+ field: 'id',
24
+ }
25
+ }
26
+
27
+ fixName(entityModel, guideEntity.key$)
28
+
29
+ each(guideEntity.path, (guidePath: any) => {
30
+ const pathdef = def.paths[guidePath.key$]
31
+
32
+ if (null == pathdef) {
33
+ throw new Error('APIDEF: path not found in OpenAPI: ' + guidePath.key$ +
34
+ ' (entity: ' + guideEntity.name + ')')
35
+ }
36
+
37
+ guidePath.parts$ = guidePath.key$.split('/')
38
+ guidePath.params$ = guidePath.parts$
39
+ .filter((p: string) => p.startsWith('{'))
40
+ .map((p: string) => p.substring(1, p.length - 1))
41
+
42
+ })
43
+
44
+ msg += guideEntity.name + ' '
45
+
46
+ })
47
+
48
+ return { ok: true, msg }
49
+ }
50
+
51
+
52
+ export {
53
+ entityTransform
54
+ }