@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.
package/src/apidef.ts CHANGED
@@ -1,64 +1,127 @@
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'
13
10
 
14
- import { getx, each, camelify } from 'jostraca'
11
+ import { prettyPino, Pino } from '@voxgig/util'
12
+
13
+
14
+ import {
15
+ resolveTransforms,
16
+ processTransforms,
17
+ fixName,
18
+ } from './transform'
15
19
 
16
20
 
17
21
  type ApiDefOptions = {
18
22
  fs?: any
23
+ pino?: ReturnType<typeof Pino>
24
+ debug?: boolean | string
19
25
  }
20
26
 
21
27
 
28
+ type ApiDefSpec = {
29
+ def: string
30
+ model: string,
31
+ kind: string,
32
+ meta: Record<string, any>,
33
+ }
22
34
 
23
35
 
24
36
  function ApiDef(opts: ApiDefOptions = {}) {
25
37
  const fs = opts.fs || Fs
38
+ const pino = prettyPino('apidef', opts)
39
+
40
+ const log = pino.child({ cmp: 'apidef' })
26
41
 
27
42
 
28
43
  async function watch(spec: any) {
44
+ log.info({ point: 'watch-start' })
45
+ log.debug({ point: 'watch-spec', spec })
46
+
29
47
  await generate(spec)
30
48
 
31
49
  const fsw = new FSWatcher()
32
50
 
33
51
  fsw.on('change', (...args: any[]) => {
34
- // console.log('APIDEF CHANGE', args)
52
+ log.trace({ watch: 'change', file: args[0] })
35
53
  generate(spec)
36
54
  })
37
55
 
38
- // console.log('APIDEF-WATCH', spec.def)
56
+ log.trace({ watch: 'add', what: 'def', file: spec.def })
39
57
  fsw.add(spec.def)
40
58
 
41
- // console.log('APIDEF-WATCH', spec.guide)
59
+ log.trace({ watch: 'add', what: 'guide', file: spec.guilde })
42
60
  fsw.add(spec.guide)
43
61
  }
44
62
 
45
63
 
46
- async function generate(spec: any) {
47
- // console.log('APIDEF.generate')
64
+ async function generate(spec: ApiDefSpec) {
65
+ const start = Date.now()
66
+
67
+ // TODO: validate spec
68
+
69
+ const defpath = Path.normalize(spec.def)
70
+
71
+ log.info({ point: 'generate-start', note: 'defpath', defpath, start })
72
+ log.debug({ point: 'generate-spec', spec })
73
+
74
+ // TODO: Validate spec
75
+ const ctx = {
76
+ log,
77
+ spec,
78
+ guide: {},
79
+ opts,
80
+ util: { fixName },
81
+ defpath: Path.dirname(defpath)
82
+ }
83
+
84
+
48
85
 
49
86
  const guide = await resolveGuide(spec, opts)
50
- const transform = resolveTranform(spec, guide, opts)
51
87
 
52
- const source = fs.readFileSync(spec.def, 'utf8')
88
+ if (null == guide) {
89
+ return
90
+ }
91
+
92
+
93
+ log.debug({ point: 'guide', guide })
94
+
95
+ ctx.guide = guide
96
+ const transformSpec = await resolveTransforms(ctx)
97
+ log.debug({ point: 'transform', spec: transformSpec })
98
+
99
+
100
+ let source
101
+ try {
102
+ source = fs.readFileSync(spec.def, 'utf8')
103
+ }
104
+ catch (err: any) {
105
+ log.error({ read: 'fail', what: 'def', file: defpath, err })
106
+ throw err
107
+ }
53
108
 
54
- const modelBasePath = Path.dirname(spec.model)
55
109
 
56
110
  const config = await createConfig({})
57
- const bundle = await bundleFromString({
58
- source,
59
- config,
60
- dereference: true,
61
- })
111
+ let bundle
112
+
113
+ try {
114
+ bundle = await bundleFromString({
115
+ source,
116
+ config,
117
+ dereference: true,
118
+ })
119
+ }
120
+ catch (err: any) {
121
+ log.error({ parse: 'fail', what: 'openapi', file: defpath, err })
122
+ throw err
123
+ }
124
+
62
125
 
63
126
  const model = {
64
127
  main: {
@@ -71,11 +134,18 @@ function ApiDef(opts: ApiDefOptions = {}) {
71
134
 
72
135
  try {
73
136
  const def = bundle.bundle.parsed
74
- // console.dir(def, { depth: null })
75
- transform(def, model)
137
+ const processResult = await processTransforms(ctx, transformSpec, model, def)
138
+
139
+ if (!processResult.ok) {
140
+ log.error({ process: 'fail', what: 'transform', result: processResult })
141
+ throw new Error('Transform failed: ' + processResult.msg)
142
+ }
143
+ else {
144
+ log.debug({ process: 'result', what: 'transform', result: processResult })
145
+ }
76
146
  }
77
147
  catch (err: any) {
78
- console.log('APIDEF ERROR', err)
148
+ log.error({ process: 'fail', what: 'transform', err })
79
149
  throw err
80
150
  }
81
151
 
@@ -83,34 +153,19 @@ function ApiDef(opts: ApiDefOptions = {}) {
83
153
  let modelSrc = JSON.stringify(modelapi, null, 2)
84
154
  modelSrc = modelSrc.substring(1, modelSrc.length - 1)
85
155
 
86
- fs.writeFileSync(
87
- spec.model,
88
- modelSrc
89
- )
156
+ writeChanged('api-model', spec.model, modelSrc)
90
157
 
91
158
 
159
+ const modelBasePath = Path.dirname(spec.model)
92
160
  const defFilePath = Path.join(modelBasePath, 'def.jsonic')
93
161
 
94
162
  const modelDef = { main: { def: model.main.def } }
95
163
  let modelDefSrc = JSON.stringify(modelDef, null, 2)
96
164
  modelDefSrc = modelDefSrc.substring(1, modelDefSrc.length - 1)
97
165
 
98
- let existingSrc: string = ''
99
- if (fs.existsSync(defFilePath)) {
100
- existingSrc = fs.readFileSync(defFilePath, 'utf8')
101
- }
102
-
103
- let writeModelDef = existingSrc !== modelDefSrc
104
- // console.log('APIDEF', writeModelDef)
105
-
106
- // Only write the model def if it has changed
107
- if (writeModelDef) {
108
- fs.writeFileSync(
109
- defFilePath,
110
- modelDefSrc
111
- )
112
- }
166
+ writeChanged('def-model', defFilePath, modelDefSrc)
113
167
 
168
+ log.info({ point: 'generate-end', note: 'success', break: true })
114
169
 
115
170
  return {
116
171
  ok: true,
@@ -119,6 +174,47 @@ function ApiDef(opts: ApiDefOptions = {}) {
119
174
  }
120
175
 
121
176
 
177
+
178
+ function writeChanged(what: string, path: string, content: string) {
179
+ let exists = false
180
+ let changed = false
181
+ let action = ''
182
+ try {
183
+ let existingContent: string = ''
184
+ path = Path.normalize(path)
185
+
186
+ exists = fs.existsSync(path)
187
+
188
+ if (exists) {
189
+ action = 'read'
190
+ existingContent = fs.readFileSync(path, 'utf8')
191
+ }
192
+
193
+ changed = existingContent !== content
194
+
195
+ log.info({
196
+ point: 'write-' + what,
197
+ note: 'changed,file',
198
+ write: 'file', what, skip: !changed, exists, changed,
199
+ contentLength: content.length, file: path
200
+ })
201
+
202
+ if (changed) {
203
+ action = 'write'
204
+ fs.writeFileSync(path, content)
205
+ }
206
+ }
207
+ catch (err: any) {
208
+ log.error({
209
+ fail: action, what, file: path, exists, changed,
210
+ contentLength: content.length, err
211
+ })
212
+ throw err
213
+ }
214
+ }
215
+
216
+
217
+
122
218
  async function resolveGuide(spec: any, _opts: any) {
123
219
  if (null == spec.guide) {
124
220
  spec.guide = spec.def + '-guide.jsonic'
@@ -127,34 +223,70 @@ function ApiDef(opts: ApiDefOptions = {}) {
127
223
  const path = Path.normalize(spec.guide)
128
224
  let src: string
129
225
 
130
- // console.log('APIDEF resolveGuide', path)
226
+ let action = ''
227
+ let exists = false
228
+ try {
131
229
 
132
- if (fs.existsSync(path)) {
133
- src = fs.readFileSync(path, 'utf8')
134
- }
135
- else {
136
- src = `
230
+ action = 'exists'
231
+ let exists = fs.existsSync(path)
232
+
233
+ log.debug({ read: 'file', what: 'guide', file: path, exists })
234
+
235
+ if (exists) {
236
+ action = 'read'
237
+ src = fs.readFileSync(path, 'utf8')
238
+ }
239
+ else {
240
+ src = `
137
241
  # API Specification Transform Guide
138
242
 
139
- @"node_modules/@voxgig/apidef/model/guide.jsonic"
243
+ @"@voxgig/apidef/model/guide.jsonic"
140
244
 
141
245
  guide: entity: {
142
246
 
143
247
  }
144
248
 
249
+ guide: control: transform: openapi: order: \`
250
+ top,
251
+ entity,
252
+ operation,
253
+ field,
254
+ manual,
255
+ \`
256
+
145
257
  `
146
- fs.writeFileSync(path, src)
258
+ action = 'write'
259
+ fs.writeFileSync(path, src)
260
+ }
261
+ }
262
+ catch (err: any) {
263
+ log.error({ fail: action, what: 'guide', file: path, exists, err })
264
+ throw err
147
265
  }
148
266
 
149
- const aopts = {}
267
+ const aopts = { path }
150
268
  const root = Aontu(src, aopts)
151
269
  const hasErr = root.err && 0 < root.err.length
152
270
 
153
- // TODO: collect all errors
154
271
  if (hasErr) {
155
- // console.log(root.err)
156
- // throw new Error(root.err[0])
157
- throw root.err[0].err
272
+ for (let serr of root.err) {
273
+ let err: any = new Error('Guide model: ' + serr.msg)
274
+ err.cause$ = [serr]
275
+
276
+ if ('syntax' === serr.why) {
277
+ err.uxmsg$ = true
278
+ }
279
+
280
+ log.error({ fail: 'parse', point: 'guide-parse', file: path, err })
281
+
282
+ if (err.uxmsg$) {
283
+ return
284
+ }
285
+ else {
286
+ err.rooterrs$ = root.err
287
+ throw err
288
+ }
289
+ }
158
290
  }
159
291
 
160
292
  let genctx = new Context({ root })
@@ -162,27 +294,19 @@ guide: entity: {
162
294
 
163
295
  // TODO: collect all errors
164
296
  if (genctx.err && 0 < genctx.err.length) {
165
- // console.log(genctx.err)
166
- throw new Error(JSON.stringify(genctx.err[0]))
297
+ const err: any = new Error('Guide build error:\n' +
298
+ (genctx.err.map((pe: any) => pe.msg)).join('\n'))
299
+ log.error({ fail: 'build', what: 'guide', file: path, err })
300
+ err.errs = () => genctx.err
301
+ throw err
167
302
  }
168
303
 
169
- // console.log('GUIDE')
170
- // console.dir(guide, { depth: null })
171
-
172
304
  const pathParts = Path.parse(path)
173
305
  spec.guideModelPath = Path.join(pathParts.dir, pathParts.name + '.json')
174
306
 
175
307
  const updatedSrc = JSON.stringify(guide, null, 2)
176
308
 
177
- // 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
- }
182
-
183
- if (existingSrc !== updatedSrc) {
184
- fs.writeFileSync(spec.guideModelPath, updatedSrc)
185
- }
309
+ writeChanged('guide-model', spec.guideModelPath, updatedSrc)
186
310
 
187
311
  return guide
188
312
  }
@@ -197,195 +321,6 @@ guide: entity: {
197
321
 
198
322
 
199
323
 
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
324
  export type {
390
325
  ApiDefOptions,
391
326
  }
@@ -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
+ }