@voxgig/apidef 6.3.6 → 6.3.8

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.
Files changed (45) hide show
  1. package/dist/apidef.d.ts +2 -2
  2. package/dist/apidef.js +27 -6
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.js.map +1 -1
  5. package/dist/builder/entity/info.js.map +1 -1
  6. package/dist/builder/entity.js.map +1 -1
  7. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  8. package/dist/builder/flow.js.map +1 -1
  9. package/dist/guide/guide.js +49 -6
  10. package/dist/guide/guide.js.map +1 -1
  11. package/dist/guide/heuristic01.js +99 -89
  12. package/dist/guide/heuristic01.js.map +1 -1
  13. package/dist/parse.js +151 -18
  14. package/dist/parse.js.map +1 -1
  15. package/dist/resolver.js.map +1 -1
  16. package/dist/transform/args.js.map +1 -1
  17. package/dist/transform/clean.js.map +1 -1
  18. package/dist/transform/entity.js.map +1 -1
  19. package/dist/transform/field.js +14 -0
  20. package/dist/transform/field.js.map +1 -1
  21. package/dist/transform/flow.js.map +1 -1
  22. package/dist/transform/flowstep.js.map +1 -1
  23. package/dist/transform/operation.js.map +1 -1
  24. package/dist/transform/select.js.map +1 -1
  25. package/dist/transform/top.d.ts +2 -1
  26. package/dist/transform/top.js +32 -2
  27. package/dist/transform/top.js.map +1 -1
  28. package/dist/transform.d.ts +8 -7
  29. package/dist/transform.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/types.d.ts +97 -84
  32. package/dist/types.js.map +1 -1
  33. package/dist/utility.d.ts +6 -1
  34. package/dist/utility.js +169 -8
  35. package/dist/utility.js.map +1 -1
  36. package/package.json +8 -8
  37. package/src/apidef.ts +28 -5
  38. package/src/guide/guide.ts +50 -6
  39. package/src/guide/heuristic01.ts +37 -28
  40. package/src/parse.ts +161 -20
  41. package/src/transform/field.ts +16 -1
  42. package/src/transform/top.ts +37 -2
  43. package/src/tsconfig.json +1 -0
  44. package/src/types.ts +5 -0
  45. package/src/utility.ts +186 -8
package/src/utility.ts CHANGED
@@ -6,7 +6,7 @@ import { snakify, camelify, kebabify, each } from 'jostraca'
6
6
  import { decircular } from '@voxgig/util'
7
7
 
8
8
  import {
9
- slice, merge, inject, clone, isnode, walk, transform, select,
9
+ slice, merge, inject, clone, isnode, walk, transform, select, keysof,
10
10
  Injection,
11
11
  M_VAL,
12
12
  M_KEYPRE,
@@ -134,7 +134,12 @@ function formatJsonSrc(jsonsrc: string) {
134
134
  //
135
135
  // Keys are lowercase; depluralize() does a case-insensitive lookup
136
136
  // and reapplies the caller's casing on the way out.
137
- const IRREGULARS: Record<string, string> = {
137
+ //
138
+ // Null-prototype: these tables are indexed by spec-derived names, so a
139
+ // schema or path segment called `constructor` / `__proto__` / `toString`
140
+ // would otherwise resolve to the inherited Object member and be returned
141
+ // as a "match" — crashing matchCase() on a function. See NULL_PROTO_NOTE.
142
+ const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
138
143
  'analytics': 'analytics',
139
144
  'analyses': 'analysis',
140
145
  'appendices': 'appendix',
@@ -198,7 +203,18 @@ const IRREGULARS: Record<string, string> = {
198
203
  'vertices': 'vertex',
199
204
  'women': 'woman',
200
205
  'yes': 'yes',
201
- }
206
+ })
207
+
208
+ // NULL_PROTO_NOTE: every plain-object lookup table in this module whose keys
209
+ // come from an API spec is built with a null prototype
210
+ // (`Object.assign(Object.create(null), {...})`). Without it, `TABLE[name]`
211
+ // inherits from Object.prototype, so `TABLE['constructor']` yields the Object
212
+ // constructor (truthy, a function) and `TABLE['__proto__']` yields
213
+ // Object.prototype (truthy, an object). Both then flow into code expecting a
214
+ // string. A spec with `components.schemas.Constructor` is enough to reach
215
+ // this: canonize -> depluralize -> matchCase -> `.toLowerCase is not a
216
+ // function`, failing the whole build at the guide stage. Keep new tables
217
+ // null-prototype, or use a Map (CANONIZE_CACHE already does).
202
218
 
203
219
  // Sorted longest-first so the most specific IRREGULARS suffix wins.
204
220
  // Without this, 'women' would be shadowed by 'men' (3 < 5) under
@@ -235,12 +251,13 @@ function matchCase(source: string, target: string): string {
235
251
  // inherit the override without signature churn. apidef is
236
252
  // single-model-per-process; if that ever changes, switch this to a
237
253
  // per-context map.
238
- let CUSTOM_PLURALS: Record<string, string> = {}
254
+ // Null-prototype: see NULL_PROTO_NOTE above.
255
+ let CUSTOM_PLURALS: Record<string, string> = Object.create(null)
239
256
  let CUSTOM_PLURAL_KEYS: string[] = []
240
257
 
241
258
 
242
259
  function setCustomPlurals(plurals: Record<string, string> | undefined | null) {
243
- CUSTOM_PLURALS = {}
260
+ CUSTOM_PLURALS = Object.create(null)
244
261
  if (plurals) {
245
262
  for (const k of Object.keys(plurals)) {
246
263
  // Skip null/undefined values so a partially-typed model entry
@@ -905,7 +922,17 @@ function renderJSONIC(
905
922
  }
906
923
 
907
924
 
908
- const VALID_CANON: Record<string, string> = {
925
+ // Canonical type-sentinel vocabulary. VALID_CANON maps an OpenAPI type NAME
926
+ // to its `$SENTINEL` form; CANON_ONE is the union sentinel produced by
927
+ // `validator` for a multi-type (`['`$ONE`', [member, ...]]`). Both are part
928
+ // of the public API so downstream consumers (e.g. @voxgig/sdkgen's
929
+ // sentinel -> language-type table) can verify they cover the full set
930
+ // instead of hand-syncing against this file.
931
+ //
932
+ // Null-prototype (see NULL_PROTO_NOTE): `type` values come from the spec, so
933
+ // a schema declaring `type: constructor` would otherwise return the Object
934
+ // constructor here rather than falling through to the 'Any' default.
935
+ const VALID_CANON: Record<string, string> = Object.assign(Object.create(null), {
909
936
  'string': '`$STRING`',
910
937
  'number': '`$NUMBER`',
911
938
  'integer': '`$INTEGER`',
@@ -914,7 +941,9 @@ const VALID_CANON: Record<string, string> = {
914
941
  'array': '`$ARRAY`',
915
942
  'object': '`$OBJECT`',
916
943
  'any': '`$ANY`',
917
- }
944
+ })
945
+
946
+ const CANON_ONE = '`$ONE`'
918
947
 
919
948
 
920
949
  function validator(torig: undefined | string | string[]): any {
@@ -924,7 +953,7 @@ function validator(torig: undefined | string | string[]): any {
924
953
  return canon
925
954
  }
926
955
  else if (Array.isArray(torig)) {
927
- return ['`$ONE`', torig.map((t: string) => validator(t))]
956
+ return [CANON_ONE, torig.map((t: string) => validator(t))]
928
957
  }
929
958
  else {
930
959
  return '`$ANY`'
@@ -1376,6 +1405,150 @@ export type {
1376
1405
  PathMatch
1377
1406
  }
1378
1407
 
1408
+
1409
+ // A response property only "wraps" the entity when it is itself a structured
1410
+ // value that could contain the entity: an object, an array, a $ref, or a
1411
+ // composed (allOf/oneOf/anyOf) schema. A scalar property (string, integer,
1412
+ // number, boolean) that merely shares the entity's name is a field of the
1413
+ // entity, not a wrapper, so the response must not be unwrapped down to it.
1414
+ function isEntityWrapperProp(propSchema: any): boolean {
1415
+ if (null == propSchema || 'object' !== typeof propSchema) {
1416
+ return false
1417
+ }
1418
+ if (null != propSchema.$ref) {
1419
+ return true
1420
+ }
1421
+ if (null != propSchema.properties ||
1422
+ null != propSchema.items ||
1423
+ null != propSchema.allOf ||
1424
+ null != propSchema.oneOf ||
1425
+ null != propSchema.anyOf) {
1426
+ return true
1427
+ }
1428
+ const t = propSchema.type
1429
+ return 'object' === t || 'array' === t
1430
+ }
1431
+
1432
+
1433
+ // A response body that is nothing but a single wrapper property is an
1434
+ // ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
1435
+ // `{items: [...]}`, `{results: [...]}`. Return that property's name so the
1436
+ // caller can unwrap to it, or null when the body is the result itself.
1437
+ //
1438
+ // Two conditions keep this from firing on a response that IS the entity:
1439
+ //
1440
+ // 1. EXACTLY ONE property. A body with siblings is a structure in its own
1441
+ // right, not a wrapper — `{ok, id}` from a delete, or any paged
1442
+ // `{results, next}`, must be handed over whole.
1443
+ // 2. The property's SHAPE matches the operation's cardinality. A `list`
1444
+ // unwraps only to an array, every other op only to a non-array. So a
1445
+ // single-entity op facing `{items: [...]}` is left alone rather than
1446
+ // silently yielding a list, and vice versa.
1447
+ //
1448
+ // A one-field entity whose sole field is itself structured can still be
1449
+ // unwrapped wrongly; that is the residual cost of the spec not saying which
1450
+ // it means. Naming the wrapper after the entity remains the unambiguous
1451
+ // signal, and is still checked first.
1452
+ function envelopeProp(resprops: any, opname: string): string | null {
1453
+ const keys = keysof(resprops)
1454
+ if (1 !== keys.length) {
1455
+ return null
1456
+ }
1457
+
1458
+ const key = keys[0]
1459
+ const prop = resprops[key]
1460
+ if (!isEntityWrapperProp(prop)) {
1461
+ return null
1462
+ }
1463
+
1464
+ const islist = propIsList(prop)
1465
+ if (null == islist || islist !== ('list' === opname)) {
1466
+ return null
1467
+ }
1468
+
1469
+ return key
1470
+ }
1471
+
1472
+
1473
+ // Is this schema a collection? null when the schema does not say.
1474
+ //
1475
+ // `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
1476
+ // structured, but a composed schema carries no outer `type` or `items` — so
1477
+ // reading those alone silently called it a non-list. A `list` then kept its
1478
+ // envelope, and worse, a single-entity op unwrapped to an array-valued
1479
+ // property. Composed branches are inspected instead, and unanimity required:
1480
+ // a union that is an array in one branch and an object in another does not
1481
+ // say what the caller will get, and an envelope is not worth guessing at.
1482
+ function propIsList(schema: any): boolean | null {
1483
+ if (null == schema || 'object' !== typeof schema) {
1484
+ return null
1485
+ }
1486
+
1487
+ const branches = schema.allOf ?? schema.oneOf ?? schema.anyOf
1488
+ if (Array.isArray(branches)) {
1489
+ if (0 === branches.length) {
1490
+ return null
1491
+ }
1492
+ const first = propIsList(branches[0])
1493
+ if (null == first) {
1494
+ return null
1495
+ }
1496
+ for (const branch of branches) {
1497
+ if (propIsList(branch) !== first) {
1498
+ return null
1499
+ }
1500
+ }
1501
+ return first
1502
+ }
1503
+
1504
+ return 'array' === schema.type || null != schema.items
1505
+ }
1506
+
1507
+
1508
+ // The request BODY a closed schema permits, as a transform mapping.
1509
+ //
1510
+ // `additionalProperties: false` is the spec saying the server rejects any
1511
+ // property it did not declare. When a body says that, sending the caller's
1512
+ // whole request payload is wrong: an op's payload also carries its PATH
1513
+ // params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
1514
+ // request over that one extra key. Restricting the body to the declared
1515
+ // properties is then not a heuristic — it is what the spec asked for.
1516
+ //
1517
+ // Returns null for an open or property-less schema, where `reqdata` (send
1518
+ // everything) remains the right default: an open body accepts extras, and
1519
+ // with no declared properties there is nothing to restrict to.
1520
+ //
1521
+ // The KEY is the property's wire name — that is what goes on the wire and
1522
+ // what the server matches against. The SOURCE is read by the field's
1523
+ // CANONICAL name, because that is the only name the caller ever sees:
1524
+ // findFieldDefs runs every property through `canonize(normalizeFieldName())`,
1525
+ // so a spec property `UserName` reaches the generated request type as
1526
+ // `user_name`. Reading `reqdata.UserName` would find nothing and send
1527
+ // undefined.
1528
+ function closedBodyTransform(schema: any): Record<string, string> | null {
1529
+ if (null == schema || 'object' !== typeof schema) {
1530
+ return null
1531
+ }
1532
+ if (false !== schema.additionalProperties) {
1533
+ return null
1534
+ }
1535
+
1536
+ const names = keysof(schema.properties)
1537
+ if (0 === names.length) {
1538
+ return null
1539
+ }
1540
+
1541
+ // Null prototype: a spec is free to declare a property called `__proto__`,
1542
+ // and on an ordinary object that assignment sets the prototype instead of
1543
+ // creating an own property — the mapping would vanish, and with it the
1544
+ // whole body restriction if it were the only one.
1545
+ const out: Record<string, string> = Object.create(null)
1546
+ for (const name of names) {
1547
+ out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`'
1548
+ }
1549
+ return out
1550
+ }
1551
+
1379
1552
  export {
1380
1553
  nom,
1381
1554
  getdlog,
@@ -1390,6 +1563,8 @@ export {
1390
1563
  makeWarner,
1391
1564
  formatJSONIC,
1392
1565
  validator,
1566
+ VALID_CANON,
1567
+ CANON_ONE,
1393
1568
  canonize,
1394
1569
  canonizeCmpName,
1395
1570
  stripSchemaNamespace,
@@ -1408,5 +1583,8 @@ export {
1408
1583
  getModelPath,
1409
1584
  sortedKeys,
1410
1585
  sortedEntries,
1586
+ isEntityWrapperProp,
1587
+ envelopeProp,
1588
+ closedBodyTransform,
1411
1589
 
1412
1590
  }