@voxgig/sdkgen-infrapack 0.0.15 → 0.0.16

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.
@@ -16,6 +16,11 @@ main: kit: target: 'seneca-provider': {
16
16
  test: { active: false }
17
17
  }
18
18
 
19
+ sdk: {
20
+ package: *'' | string
21
+ version: *'' | string
22
+ }
23
+
19
24
  sdk: dep: {
20
25
  kind: *'npm' | 'git' | 'release'
21
26
  ref: *'' | string
@@ -23,7 +23,12 @@ const SdkPin = cmp(function SdkPin(props: any) {
23
23
 
24
24
  File({ name: 'sdk-pin.json' }, () => {
25
25
  Content(JSON.stringify({
26
- note: 'GENERATED. The SDK this provider is generated from. ' +
26
+ note: provider.standalone ?
27
+ 'GENERATED. The SDK this provider depends on and is generated from. ' +
28
+ '`make sdk-src` fetches it; `make regen` replaces the API definition ' +
29
+ 'and guide in .sdk/ with its own and regenerates this repo. Set the version in ' +
30
+ '.sdk/model/project.aontu, not in this file.' :
31
+ 'GENERATED. The SDK this provider is generated from. ' +
27
32
  '`make sdk-src` fetches it; `make regen` regenerates this repo ' +
28
33
  'from it. Edit the SDK project model, not this file.',
29
34
  repo: provider.sdkRepoUrl,
@@ -1718,7 +1723,8 @@ const seneca = Seneca()
1718
1723
  },
1719
1724
  },
1720
1725
  })
1721
- .use('${provider.pkgName}')
1726
+ .use('${provider.pkgName}'${'' === provider.specBase ?
1727
+ `, { sdk: { base: 'https://${provider.lower}.example.com' } }` : ''})
1722
1728
 
1723
1729
  await seneca.ready()
1724
1730
 
@@ -1734,7 +1740,7 @@ await seneca.ready()
1734
1740
  `'${cparts.map((p: string) => 'some-' + p).join(
1735
1741
  null != subject.idsep && '' !== String(subject.idsep) ?
1736
1742
  String(subject.idsep) : '/')}'` :
1737
- 0 === subject.parents.length ? `'some-id'` :
1743
+ 0 === subject.parents.length ? (loadHasKey(subject) ? `'some-id'` : '') :
1738
1744
  `{ ` + subject.parents.map((p: string) => `${jsKey(p)}: 'some-${p}'`).join(', ') +
1739
1745
  `, id: 'some-id' }`
1740
1746
  Content(`const ${subject.name} = await seneca
@@ -1898,7 +1904,10 @@ whose logs cannot be read.
1898
1904
  each(provider.entities, (e: any) => {
1899
1905
  each(e.cmds, (cmd: any) => {
1900
1906
  const c = String(cmd.val$ ?? cmd)
1901
- Content(`| \`sys:entity,cmd:${c},zone:provider,base:${provider.lower},name:${e.name}\` | ${CMD_DESC[c]}. |
1907
+ const desc = 'save' !== c ? CMD_DESC[c] :
1908
+ !e.ops.includes('update') ? 'Create a record' :
1909
+ !e.ops.includes('create') ? 'Update a record' : CMD_DESC[c]
1910
+ Content(`| \`sys:entity,cmd:${c},zone:provider,base:${provider.lower},name:${e.name}\` | ${desc}. |
1902
1911
  `)
1903
1912
  })
1904
1913
  })
@@ -1978,9 +1987,15 @@ const sdk = seneca.export('${provider.pluginName}/sdk')()
1978
1987
 
1979
1988
  ## Contributing
1980
1989
 
1981
- This plugin is GENERATED. Changes belong in the SDK project's model and
1990
+ ${provider.standalone ?
1991
+ `This plugin is GENERATED, by the builder in \`.sdk/\` from the API definition
1992
+ of the SDK it depends on. Changes to the API belong in the SDK project's
1993
+ model, this package's own decisions in \`.sdk/model/project.aontu\`, and
1994
+ everything else in the components that build this target — anything edited
1995
+ elsewhere in this repository is overwritten by the next \`make regen\`.` :
1996
+ `This plugin is GENERATED. Changes belong in the SDK project's model and
1982
1997
  components, not here — anything edited in this repository is overwritten by
1983
- the next generation run.
1998
+ the next generation run.`}
1984
1999
 
1985
2000
  The [Senecajs org](http://senecajs.org) encourages open participation. If you
1986
2001
  feel you can help in any way, be it with bug reporting, documentation,
@@ -2677,6 +2692,7 @@ const DocHowto = cmp(function DocHowto(props: any) {
2677
2692
  // an absent value to the same thing, so a missing base is treated as absent
2678
2693
  // rather than printed as a default nobody can use.
2679
2694
  const liveBase = provider.liveBase || ''
2695
+ const specBase = provider.specBase || ''
2680
2696
 
2681
2697
  // The same choice the tests and the manual scripts make: fewest parent keys
2682
2698
  // (nothing to arrange), then most cmds. Recipes prefer it, so one entity
@@ -2698,6 +2714,7 @@ const DocHowto = cmp(function DocHowto(props: any) {
2698
2714
  // the API calls it. `apiKey` is that name, for the SDK-direct examples.
2699
2715
  const idf = (_e: any) => 'id'
2700
2716
  const apiKey = (e: any) => e.rk || 'id'
2717
+ const modelsId = (e: any) => null != e.ent?.id || null != (e.ent?.fields || {})[apiKey(e)]
2701
2718
 
2702
2719
  // A parent key's example value. This MIRRORS seedRecord rather than
2703
2720
  // inventing something more readable: the offline recipe below seeds with
@@ -2767,7 +2784,15 @@ const DocHowto = cmp(function DocHowto(props: any) {
2767
2784
 
2768
2785
  const eList = forCmd('list')
2769
2786
  const eLoad = forCmd('load')
2770
- const eSave = forCmd('save')
2787
+ const forOp = (op: string) => {
2788
+ const able = ents.filter((e: any) => e.ops.includes(op))
2789
+ return able.find((e: any) => e === subject) ||
2790
+ able.find((e: any) => 0 === e.parents.length) ||
2791
+ able[0] || null
2792
+ }
2793
+
2794
+ const eCreate = forOp('create')
2795
+ const eUpdate = forOp('update')
2771
2796
  const eRemove = forCmd('remove')
2772
2797
 
2773
2798
  // Sections as data, so the contents list and the sections cannot disagree.
@@ -2841,53 +2866,63 @@ genuinely wrong.`)
2841
2866
  }
2842
2867
 
2843
2868
 
2844
- if (null != eSave) {
2845
- const created = literal(createData(eSave))
2869
+ if (null != eCreate) {
2870
+ const created = literal(createData(eCreate))
2871
+ const updates = eCreate.ops.includes('update')
2846
2872
 
2847
- sec('Create a record', `\`make$\` builds an entity and \`save$\` writes it. An entity with no id
2848
- is a create:
2873
+ sec('Create a record', `\`make$\` builds an entity and \`save$\` writes it. ${updates ?
2874
+ 'An entity with no id\nis a create:' :
2875
+ `The API has no update\nfor a \`${eCreate.name}\`, so \`save$\` always creates one, even from an entity\nthat carries an id:`}
2849
2876
 
2850
2877
  \`\`\`js
2851
- const ${eSave.name} = await seneca
2852
- .entity('${canon(eSave)}')
2878
+ const ${eCreate.name} = await seneca
2879
+ .entity('${canon(eCreate)}')
2853
2880
  .make$(${created})
2854
2881
  .save$()
2855
2882
 
2856
- console.log(${eSave.name}.${idf(eSave)})
2883
+ console.log(${eCreate.name}${modelsId(eCreate) ? '.' + idf(eCreate) : ''})
2857
2884
  \`\`\`
2858
- ${0 === eSave.parents.length ? '' : `
2859
- Note that \`${eSave.parents.join('`, `')}\` travels in the DATA for a write,
2860
- not in a query: a \`${eSave.name}\` is created inside its parent.
2885
+ ${0 === eCreate.parents.length ? '' : `
2886
+ Note that \`${eCreate.parents.join('`, `')}\` travels in the DATA for a write,
2887
+ not in a query: a \`${eCreate.name}\` is created inside its parent.
2861
2888
  `}
2862
- \`save$\` resolves to the record as the API returned it, which is the only
2889
+ ${modelsId(eCreate) ?
2890
+ `\`save$\` resolves to the record as the API returned it, which is the only
2863
2891
  reliable source of the id. Read it from there rather than predicting it:
2864
2892
  what an API does with an id you supply on create is its own business, and
2865
- several ignore it entirely.`)
2893
+ several ignore it entirely.` :
2894
+ `\`save$\` resolves to the record as the API returned it. The API definition
2895
+ declares no id for a \`${eCreate.name}\`, so the record is the only place to
2896
+ read what identifies one.`}`)
2897
+ }
2866
2898
 
2867
- const f = changeable(eSave)
2868
2899
 
2869
- sec('Update a record', `The same call updates. \`save$\` dispatches on the id: an entity carrying
2870
- one is an update, an entity without one is a create. So the safe shape is
2900
+ if (null != eUpdate) {
2901
+ const f = changeable(eUpdate)
2902
+
2903
+ sec('Update a record', `${eUpdate.ops.includes('create') ?
2904
+ 'The same call updates. `save$` dispatches on the id: an entity carrying\none is an update, an entity without one is a create.' :
2905
+ `\`save$\` updates: the API has no create for a \`${eUpdate.name}\`.`} So the safe shape is
2871
2906
  load, change, save:
2872
2907
 
2873
- \`\`\`js${eSave.cmds.includes('load') ? `
2874
- const ${eSave.name} = await seneca
2875
- .entity('${canon(eSave)}')
2876
- .load$(${oneArgs(eSave)})
2908
+ \`\`\`js${eUpdate.cmds.includes('load') ? `
2909
+ const ${eUpdate.name} = await seneca
2910
+ .entity('${canon(eUpdate)}')
2911
+ .load$(${oneArgs(eUpdate)})
2877
2912
  ` : `
2878
- const ${eSave.name} = seneca
2879
- .entity('${canon(eSave)}')
2880
- .make$(${literal(0 === eSave.parents.length ?
2881
- { [idf(eSave)]: `${eSave.name}0` } :
2882
- { ...Object.fromEntries(eSave.parents.map(
2883
- (k: string) => [k, parentVal(eSave, k)])),
2884
- [idf(eSave)]: `${eSave.name}0` })})
2913
+ const ${eUpdate.name} = seneca
2914
+ .entity('${canon(eUpdate)}')
2915
+ .make$(${literal(0 === eUpdate.parents.length ?
2916
+ { [idf(eUpdate)]: `${eUpdate.name}0` } :
2917
+ { ...Object.fromEntries(eUpdate.parents.map(
2918
+ (k: string) => [k, parentVal(eUpdate, k)])),
2919
+ [idf(eUpdate)]: `${eUpdate.name}0` })})
2885
2920
  `}${null == f ? `
2886
2921
  // change the fields you need
2887
2922
  ` : `
2888
- ${eSave.name}.${f.name} = ${newValue(f)}
2923
+ ${eUpdate.name}.${f.name} = ${newValue(f)}
2889
2924
  `}
2890
- await ${eSave.name}.save$()
2925
+ await ${eUpdate.name}.save$()
2891
2926
  \`\`\`
2892
2927
 
2893
2928
  Mutating the record you loaded sends it as it stood plus your change, so
@@ -3027,13 +3062,16 @@ constructor, so \`base\` chooses the host:
3027
3062
  })
3028
3063
  \`\`\`
3029
3064
 
3030
- ${'' === liveBase ?
3065
+ ${'' === specBase ?
3031
3066
  `The API definition declares no server, so there is no default worth
3032
3067
  relying on: set \`base\` explicitly, or run against the mock instead (see
3033
3068
  [${OFFLINE_TITLE}](${anchor(OFFLINE_TITLE)})).` :
3034
- `The SDK's own default is \`${liveBase}\`, which is where the
3069
+ specBase === liveBase ?
3070
+ `The SDK's own default is \`${liveBase}\`, which is where the
3035
3071
  companion test server listens, so local development usually needs no
3036
- \`base\` at all.`}`)
3072
+ \`base\` at all.` :
3073
+ `The SDK's own default is \`${specBase}\`, the server the API definition
3074
+ declares, so \`base\` is needed only to reach another one.`}`)
3037
3075
 
3038
3076
 
3039
3077
  if (!provider.authActive) {
@@ -3321,10 +3359,15 @@ $ npm run repo-publish
3321
3359
  Only \`dist\`, the TypeScript sources and the licence file are published;
3322
3360
  the test suite and its build output stay in the repository.
3323
3361
 
3324
- Before publishing, check that \`package.json\` still depends on the
3362
+ ${'npm' === provider.sdkDepKind ?
3363
+ `Before publishing, check that \`package.json\` still depends on the
3325
3364
  published SDK by version range and not on a local path: a \`file:\`
3326
3365
  dependency left behind from local development installs perfectly on your
3327
- own machine and cannot be resolved by anybody else.
3366
+ own machine and cannot be resolved by anybody else.` :
3367
+ `Publish the SDK to npm first. \`package.json\` depends on it as
3368
+ \`${provider.sdkDep}\`, which everyone installing this package would
3369
+ have to fetch${'git' === provider.sdkDepKind ? ' with git' : ''}. Then drop \`sdk.dep\` from the model, regenerate,
3370
+ and check that the dependency is a version range.`}
3328
3371
 
3329
3372
  One last thing: this repository is GENERATED from the ${provider.api} API
3330
3373
  model by [@voxgig/sdkgen](https://github.com/voxgig/sdkgen). An edit made
@@ -3501,6 +3544,14 @@ Seneca({ legacy: false })
3501
3544
  if (live) {
3502
3545
  Content(` .use('${provider.pkgName}', { sdk: { base: '${provider.liveBase}' } })
3503
3546
  \`\`\`
3547
+ `)
3548
+ }
3549
+ else if ('' !== provider.specBase) {
3550
+ Content(` .use('${provider.pkgName}')
3551
+ \`\`\`
3552
+
3553
+ The SDK's default base URL is \`${provider.specBase}\`, the server the
3554
+ ${provider.api} definition declares. Pass \`sdk: { base }\` to reach another.
3504
3555
  `)
3505
3556
  }
3506
3557
  else {
@@ -3531,8 +3582,8 @@ Any option the \`${provider.sdkClass}\` constructor accepts:
3531
3582
 
3532
3583
  | Key | Effect |
3533
3584
  | --- | ------ |
3534
- | \`base\` | Base URL for API requests. ${live ?
3535
- `The SDK's own default is \`${provider.liveBase}\`.` :
3585
+ | \`base\` | Base URL for API requests. ${'' !== provider.specBase ?
3586
+ `The SDK's own default is \`${provider.specBase}\`, the server the API definition declares.` :
3536
3587
  'There is no default: this API declares no server, so it must be set.'} |
3537
3588
  | \`prefix\` / \`suffix\` | URL fragments placed around the path. |
3538
3589
  | \`headers\` | Headers sent on every request. These win over the \`authorization\` header the provider adds from a configured key. |
@@ -3618,7 +3669,9 @@ before any request is made, rather than issuing one that would 404.
3618
3669
  `)
3619
3670
  }
3620
3671
  if (e.cmds.includes('load')) {
3621
- Content(`| \`load$(q)\` | ${reqd([...e.parents, 'id'])} | One \`${e.name}\`, or \`null\` if not found. |
3672
+ Content(loadHasKey(e) ?
3673
+ `| \`load$(q)\` | ${reqd([...e.parents, 'id'])} | One \`${e.name}\`, or \`null\` if not found. |
3674
+ ` : `| \`load$(q)\` | ${0 < e.parents.length ? reqd(e.parents) : 'nothing: the route names no record'} | The one \`${e.name}\`, or \`null\` when \`id\` names one it does not carry. |
3622
3675
  `)
3623
3676
  }
3624
3677
  if (e.cmds.includes('save')) {
@@ -3636,7 +3689,8 @@ before any request is made, rather than issuing one that would 404.
3636
3689
 
3637
3690
  if ('id' !== apiKeyOf(e)) {
3638
3691
  Content(`
3639
- The API addresses \`${e.name}\` records by \`${apiKeyOf(e)}\`; the provider
3692
+ The API ${e.ops.some((op: string) => ['load', 'remove', 'update'].includes(op)) ?
3693
+ 'addresses' : 'identifies'} \`${e.name}\` records by \`${apiKeyOf(e)}\`; the provider
3640
3694
  carries that value as the entity's \`id\`, so every query and entity above
3641
3695
  uses \`id\`. A record the API returns with an unrelated \`id\` of its own
3642
3696
  ${false === e.parkfree ?
@@ -3661,7 +3715,7 @@ also defines are passed through unchanged in both directions.
3661
3715
  | ----- | ---- | ----- |
3662
3716
  `)
3663
3717
  each(e.fields, (f: any) => {
3664
- Content(`| \`${f.name}\` | ${f.kind} | ${f.name === (e.rk || 'id') ?
3718
+ Content(`| \`${f.name}\` | ${f.kind}${f.nullable ? ' or null' : ''} | ${f.name === (e.rk || 'id') ?
3665
3719
  ('id' === f.name ? 'Id field.' : 'API key; carried as the entity\'s `id`.') :
3666
3720
  e.parents.includes(f.name) ? ('' === f.parentEntity ?
3667
3721
  'Parent key. Required by every command.' :
@@ -4440,11 +4494,16 @@ rather than as a surprise in production.
4440
4494
  `)
4441
4495
 
4442
4496
  if ('' === provider.liveBase) {
4443
- Content(`The API definition declares no server, so this plugin has no default host: the
4497
+ Content(`${'' === provider.specBase ?
4498
+ `The API definition declares no server, so this plugin has no default host: the
4444
4499
  base URL arrives through the \`sdk.base\` option, supplied by whoever configures
4445
4500
  the plugin for a particular deployment. The tests therefore run entirely
4446
4501
  against the SDK's mock transport, which is the one host that is always
4447
- available.
4502
+ available.` :
4503
+ `The SDK's default host is \`${provider.specBase}\`, the server the API
4504
+ definition declares, and the \`sdk.base\` option points the plugin at another.
4505
+ Nothing declares a test server, so the tests run entirely against the SDK's
4506
+ mock transport, which is the one host that is always available.`}
4448
4507
 
4449
4508
 
4450
4509
  `)
@@ -4476,7 +4535,10 @@ that has to be applied again, silently, forever.
4476
4535
 
4477
4536
  The source of truth is the SDK project's model — the repository and tag named
4478
4537
  in \`sdk-pin.json\`, which \`make sdk-src\` fetches to \`${provider.sdkSrc}\` —
4479
- together with the sdkgen component that emits this target. A change to *what*
4538
+ together with the sdkgen component that emits this target.${provider.standalone ? `
4539
+ This repository builds itself: the builder in \`.sdk/\` carries a copy of that
4540
+ SDK's API definition, which \`make regen\` refreshes from the fetched source,
4541
+ and generation refuses to write when the copy no longer matches it.` : ''} A change to *what*
4480
4542
  the API offers belongs in the model; a change to
4481
4543
  *how* the provider expresses it belongs in the component. Both are versioned,
4482
4544
  both regenerate every provider built this way rather than just this one, and
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cmp, each,
3
3
  File, Content, Copy, Folder,
4
- entityCollection, entityOps, entityIdField, entityClassName,
4
+ entityCollection, entityOps, entityIdField, entityDataIdField, entityClassName,
5
5
  entityActions,
6
6
  opRequestShape, opParams, ownPoint, entityPath,
7
7
  collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
@@ -20,6 +20,10 @@ import {
20
20
  Tests, Scripts, Workflow, Readme, Docs, SdkPin, SDK_SRC_DIR,
21
21
  } from './Extras_seneca-provider'
22
22
  import { Gitignore } from './Gitignore_seneca-provider'
23
+ import { Makefile } from './Makefile_seneca-provider'
24
+ import {
25
+ standaloneBuilder, sdkIdentity, checkSdkSource,
26
+ } from './Standalone_seneca-provider'
23
27
 
24
28
 
25
29
 
@@ -208,7 +212,7 @@ function recordKey(ent: any): string {
208
212
  }
209
213
  }
210
214
 
211
- return '' !== fallback ? fallback : 'id'
215
+ return '' !== fallback ? fallback : (entityDataIdField(ent) || 'id')
212
216
  }
213
217
 
214
218
 
@@ -278,9 +282,18 @@ function parentKeys(ent: any): string[] {
278
282
  }
279
283
 
280
284
 
285
+ const sentinelKey = (type: any): string =>
286
+ String(type ?? '').replace(/[`$]/g, '').trim().toUpperCase()
287
+
288
+ const unionMembers = (type: any): any[] =>
289
+ Array.isArray(type) && 'ONE' === sentinelKey(type[0]) && Array.isArray(type[1]) ?
290
+ type[1] : []
291
+
292
+
281
293
  function fieldKind(type: any): string {
282
294
  if (Array.isArray(type)) {
283
- return 'string'
295
+ const member = unionMembers(type).find((m: any) => 'NULL' !== sentinelKey(m))
296
+ return null == member ? 'string' : fieldKind(member)
284
297
  }
285
298
 
286
299
  const t = String(type || '').toUpperCase()
@@ -294,6 +307,11 @@ function fieldKind(type: any): string {
294
307
  }
295
308
 
296
309
 
310
+ function fieldNullable(type: any): boolean {
311
+ return unionMembers(type).some((m: any) => 'NULL' === sentinelKey(m))
312
+ }
313
+
314
+
297
315
  function parentEntityOf(key: string, names: string[]): string {
298
316
  const stem = key.replace(/_id$/, '')
299
317
  return names.includes(stem) ? stem : ''
@@ -326,8 +344,10 @@ const Main = cmp(function Main(props: any) {
326
344
  const { target, ctx$ } = props
327
345
  const { model } = ctx$
328
346
 
347
+ const standalone = standaloneBuilder(target)
348
+
329
349
  const targets = model.main[KIT].target || {}
330
- if (null == targets.ts) {
350
+ if (!standalone && null == targets.ts) {
331
351
  throw new SdkGenError(
332
352
  'seneca-provider requires the `ts` target in the same SDK: it imports ' +
333
353
  'the TypeScript SDK that `ts` generates. Add it with:\n' +
@@ -347,8 +367,7 @@ const Main = cmp(function Main(props: any) {
347
367
  // included, so the two can never disagree.
348
368
  // The TypeScript SDK this provider WRAPS — a different target, so it
349
369
  // keeps its own name and does not follow this provider's alias.
350
- const sdkPkg = packageName(model, 'npm')
351
- const sdkVersion = packageVersion(model, 'ts')
370
+ const { sdkPkg, sdkVersion } = sdkIdentity(model, target)
352
371
 
353
372
  const entityColl = entityCollection(model)
354
373
 
@@ -434,6 +453,7 @@ const Main = cmp(function Main(props: any) {
434
453
  .map((f: any) => ({
435
454
  name: f.n,
436
455
  kind: fieldKind(f.t),
456
+ nullable: fieldNullable(f.t),
437
457
  parentEntity: parentEntityOf(f.n, entityNames),
438
458
  }))
439
459
 
@@ -444,6 +464,7 @@ const Main = cmp(function Main(props: any) {
444
464
  req.push({
445
465
  name: key,
446
466
  kind: 'string',
467
+ nullable: false,
447
468
  parentEntity: parentOf[key],
448
469
  })
449
470
  }
@@ -510,6 +531,7 @@ const Main = cmp(function Main(props: any) {
510
531
  Name, lower, ENV, sdkClass, pluginName, fileBase,
511
532
  sdkPkg, sdkVersion, entities,
512
533
  sdkDep,
534
+ standalone,
513
535
  // Whether that dependency comes from outside a registry. The generated
514
536
  // CI note says so, because "npm install is all you need" stops being
515
537
  // true the moment git or a tarball URL is in the path.
@@ -531,6 +553,8 @@ const Main = cmp(function Main(props: any) {
531
553
  version: packageVersion(model, target.name),
532
554
  liveBase,
533
555
  liveApp,
556
+ // The server the API definition declares, which is the SDK's own default.
557
+ specBase,
534
558
  publisher: PUBLISHER,
535
559
  publisherUrl: PUBLISHER_URL,
536
560
  probePath: (entities.find((e: any) =>
@@ -549,6 +573,10 @@ const Main = cmp(function Main(props: any) {
549
573
  authPrefix: resolveAuthPrefix(model),
550
574
  }
551
575
 
576
+ if (standalone) {
577
+ checkSdkSource(ctx$, provider, model)
578
+ }
579
+
552
580
  // `.gitignore` is EMITTED rather than copied — npm strips that filename
553
581
  // from the tarball, so as a template it reached only checkout users. See
554
582
  // Gitignore_seneca-provider. Called before the Copy, as every language
@@ -560,6 +588,7 @@ const Main = cmp(function Main(props: any) {
560
588
  replace: { ...ctx$.stdrep },
561
589
  })
562
590
 
591
+ Makefile({ provider })
563
592
  SdkPin({ provider })
564
593
  PackageJson({ provider, target })
565
594
  ProviderSource({ provider })
@@ -1218,7 +1247,12 @@ ${actionBranch('load',
1218
1247
  ` const hit = await ornull(() => this.shared.sdk.${e.acc}()[op$](${aq}))
1219
1248
  return null == hit ? null : entize(${out('hit')})
1220
1249
  `)}${guard('load', 'q')}${splitLine('load')} const res = await ornull(() => this.shared.sdk.${e.acc}().load(${sdkArg('load')}))
1221
- return null == res ? null : entize(${out('res', loadVals)})
1250
+ ${0 < addressKeys(e.ent, 'load').length || 0 < eparts.length ? '' :
1251
+ ` // The route names no record, so an id finds only the record carrying it.
1252
+ if (null != res && null != q.id && String(${jsProp('plain(res)', rk)}) !== String(q.id)) {
1253
+ return null
1254
+ }
1255
+ `} return null == res ? null : entize(${out('res', loadVals)})
1222
1256
  }
1223
1257
 
1224
1258
  `)
@@ -1268,7 +1302,12 @@ ${dropPark}
1268
1302
  // which matches a request against a stored record, then looked for a
1269
1303
  // record whose own \`id\` was that joined string and found none.
1270
1304
  delete data.id
1271
- ` : !keyed ? '' : `
1305
+ ` : !keyed ? '' : hasCreate && !hasUpdate && true !== e.rkoncreate ? `
1306
+ // The API assigns a ${e.name}'s \`${rk}\`, and a create names no record, so
1307
+ // Seneca's \`id\` is not sent: the id is read back from the response.
1308
+ const key: any = null
1309
+ ${dropPark} delete data.id
1310
+ ` : `
1272
1311
  // This API keys a ${e.name} by \`${rk}\`; Seneca carries it as \`id\`.
1273
1312
  // The key goes on the body under the API's own name, where the route
1274
1313
  // reads it, and into the match, which the SDK consults first.
@@ -1369,9 +1408,10 @@ ${provider.authActive ? `
1369
1408
  // the SDK was constructed with NO credential at all — the request went
1370
1409
  // out unauthenticated and failed much later as a 401 or a 404 on
1371
1410
  // anything private, with nothing at startup to point at the cause.
1372
- // \`apikey\` wins when both are set, so a config that has migrated is
1373
- // unaffected.
1374
- const apikey = res?.keymap?.apikey?.value ?? res?.keymap?.api?.value
1411
+ // \`apikey\` wins when both are set and it is not empty, so a config that
1412
+ // has migrated is unaffected.
1413
+ const apikey = [res?.keymap?.apikey?.value, res?.keymap?.api?.value]
1414
+ .find((value: any) => null != value && '' !== value)
1375
1415
 
1376
1416
  // Hand the credential to the SDK as \`apikey\`, NOT as an authorization
1377
1417
  // HEADER. The SDK's own auth stage owns that header: it reads
@@ -0,0 +1,216 @@
1
+ import {
2
+ Content,
3
+ File,
4
+ cmp,
5
+ } from '@voxgig/sdkgen'
6
+
7
+
8
+ const HEAD = `# GENERATED by @voxgig/sdkgen. Do not edit.
9
+
10
+ .PHONY: all build test clean reset sdk-src sdk-clean regen
11
+
12
+ all: build test
13
+
14
+ build:
15
+ \tnpm run build
16
+
17
+ test:
18
+ \tnpm test
19
+
20
+ clean:
21
+ \trm -rf dist dist-test .tsbuildinfo
22
+
23
+ reset:
24
+ \tnpm run reset
25
+
26
+ `
27
+
28
+
29
+ const INTRO_SDK = `# ---------------------------------------------------------------------------
30
+ # THIS REPOSITORY IS GENERATED, AND CAN REGENERATE ITSELF.
31
+ #
32
+ # Everything below fetches the SDK named in sdk-pin.json and runs the
33
+ # generator from it. Nothing here needs a sibling checkout or a particular
34
+ # directory layout: the pin names a repository and a TAG, and the checkout
35
+ # lands inside this repo at a fixed path.
36
+ #
37
+ # NOT a git submodule, deliberately. A submodule pins a commit in git's own
38
+ # plumbing, where a stale one is invisible in a normal diff and updating it is
39
+ # a second repository operation. sdk-pin.json is an ordinary committed file,
40
+ # regenerated from the model with everything else, so it moves with the SDK
41
+ # version it belongs to and shows up in review.
42
+ # ---------------------------------------------------------------------------`
43
+
44
+
45
+ // A provider whose builder is its own repository's .sdk/ (`output: root`).
46
+ const INTRO_SELF = `# ---------------------------------------------------------------------------
47
+ # THIS REPOSITORY IS GENERATED, BY ITS OWN BUILDER IN .sdk/.
48
+ #
49
+ # The builder generates this package and nothing else. Its API definition
50
+ # and guide are the SDK's: everything below fetches the SDK named in
51
+ # sdk-pin.json, and \`make regen\` replaces them with its own before
52
+ # generating, so the package is built from the same model as the SDK version
53
+ # it depends on.
54
+ #
55
+ # NOT a git submodule, deliberately. A submodule pins a commit in git's own
56
+ # plumbing, where a stale one is invisible in a normal diff and updating it is
57
+ # a second repository operation. sdk-pin.json is an ordinary committed file,
58
+ # regenerated from the model with everything else, so it moves with the SDK
59
+ # version it belongs to and shows up in review.
60
+ # ---------------------------------------------------------------------------`
61
+
62
+
63
+ const FETCH = `
64
+ SDK_PIN := sdk-pin.json
65
+ SDK_REPO := $(shell node -p "require('./$(SDK_PIN)').repo" 2>/dev/null)
66
+ SDK_TAG := $(shell node -p "require('./$(SDK_PIN)').tag" 2>/dev/null)
67
+ SDK_DIR := $(shell node -p "require('./$(SDK_PIN)').dir" 2>/dev/null)
68
+
69
+ # Point at a checkout you already have instead of fetching one:
70
+ # make sdk-src SDK_SRC_FROM=../path/to/sdk
71
+ SDK_SRC_FROM ?=
72
+
73
+ # Fetch (or update) the SDK source at the pinned tag.
74
+ #
75
+ # A TAG, not a branch: the point is the exact revision this repository was
76
+ # generated from, so that regenerating reproduces what is committed here
77
+ # rather than whatever main happens to say today.
78
+ sdk-src:
79
+ \t@set -e; \\
80
+ \ttest -n "$(SDK_REPO)" || { echo "sdk-src: no repo in $(SDK_PIN)"; exit 1; }; \\
81
+ \ttest -n "$(SDK_TAG)" || { echo "sdk-src: no tag in $(SDK_PIN)"; exit 1; }; \\
82
+ \tif [ -n "$(SDK_SRC_FROM)" ]; then \\
83
+ \t echo "sdk-src: using $(SDK_SRC_FROM) (not fetching)"; \\
84
+ \t rm -rf "$(SDK_DIR)"; mkdir -p "$$(dirname "$(SDK_DIR)")"; \\
85
+ \t ln -s "$$(cd "$(SDK_SRC_FROM)" && pwd)" "$(SDK_DIR)"; \\
86
+ \t exit 0; \\
87
+ \tfi; \\
88
+ \tif [ -L "$(SDK_DIR)" ]; then \\
89
+ \t echo "sdk-src: $(SDK_DIR) links to $$(readlink "$(SDK_DIR)"), which is left as it is;"; \\
90
+ \t echo " make sdk-clean first to fetch $(SDK_TAG) instead"; \\
91
+ \t exit 0; \\
92
+ \tfi; \\
93
+ \tif [ -d "$(SDK_DIR)/.git" ]; then \\
94
+ \t echo "sdk-src: fetching $(SDK_TAG) in $(SDK_DIR)"; \\
95
+ \t git -C "$(SDK_DIR)" fetch --depth 1 origin "refs/tags/$(SDK_TAG):refs/tags/$(SDK_TAG)" 2>/dev/null || \\
96
+ \t git -C "$(SDK_DIR)" fetch origin "refs/tags/$(SDK_TAG):refs/tags/$(SDK_TAG)"; \\
97
+ \t git -C "$(SDK_DIR)" checkout -q "refs/tags/$(SDK_TAG)"; \\
98
+ \telse \\
99
+ \t echo "sdk-src: cloning $(SDK_REPO) at $(SDK_TAG) into $(SDK_DIR)"; \\
100
+ \t rm -rf "$(SDK_DIR)"; mkdir -p "$$(dirname "$(SDK_DIR)")"; \\
101
+ \t git clone --depth 1 --branch "$(SDK_TAG)" "$(SDK_REPO).git" "$(SDK_DIR)"; \\
102
+ \tfi; \\
103
+ \techo "sdk-src: $(SDK_DIR) is at $(SDK_TAG)"
104
+
105
+ # Removes the fetched checkout AND the folder that held it. \`rm -rf\` on the
106
+ # checkout alone left an empty \`.sdksrc/\` behind — gitignored, so harmless,
107
+ # but it makes \`sdk-clean\` look like it did not finish. rmdir rather than
108
+ # another rm -rf: if anything else is in there, someone put it there, and
109
+ # removing it is not this target's business.
110
+ sdk-clean:
111
+ \trm -rf "$(SDK_DIR)"
112
+ \t@rmdir "$$(dirname "$(SDK_DIR)")" 2>/dev/null || true
113
+ `
114
+
115
+
116
+ const REGEN_SDK = `# Regenerate this repository from the pinned SDK.
117
+ #
118
+ # The SDK's model says where its provider goes, and that value is a fact about
119
+ # ONE developer's checkout layout — it cannot describe being cloned in here.
120
+ # So the destination is given to the generator AT RUN TIME instead:
121
+ #
122
+ # path ../.. — from the SDK checkout up to this repository root
123
+ # enclosing the output folder CONTAINS the SDK project, which the
124
+ # generator refuses unless asked, because one \`..\` too many
125
+ # writes a package over an unrelated repo
126
+ #
127
+ # See @voxgig/sdkgen docs/explanation/out-of-tree-targets.md.
128
+ # The generate-time output override arrived in @voxgig/sdkgen 4.16.0, and
129
+ # older versions IGNORE it SILENTLY: generation falls back to the destination
130
+ # in the SDK's own model, which from in here resolves to a folder that does
131
+ # not exist, gets skipped, and reports success having written nothing. That is
132
+ # not hypothetical — it is what this target did on its first end-to-end run,
133
+ # against a pin whose floor was set one release too low.
134
+ #
135
+ # So the floor is the version that actually ships the feature, not the version
136
+ # that happened to be current when it was written.
137
+ SDKGEN_MIN := 4.16.0
138
+
139
+ regen: sdk-src
140
+ \t@set -e; \\
141
+ \tif [ -L "$(SDK_DIR)" ]; then \\
142
+ \t echo "regen: $(SDK_DIR) is a symlink, and regenerating through one writes"; \\
143
+ \t echo " the package somewhere else. Node resolves a module's own"; \\
144
+ \t echo " directory through symlinks, so the generator computes its"; \\
145
+ \t echo " destination from where the checkout REALLY is — not from"; \\
146
+ \t echo " here — and \\\`enclosing\\\` then permits the result."; \\
147
+ \t echo " SDK_SRC_FROM is for building and linking against a local"; \\
148
+ \t echo " SDK (make sdk-src). To regenerate, use a fetched checkout:"; \\
149
+ \t echo " make sdk-clean && make regen"; \\
150
+ \t exit 1; \\
151
+ \tfi; \\
152
+ \techo "regen: building the generator in $(SDK_DIR)/.sdk"; \\
153
+ \tnpm --prefix "$(SDK_DIR)/.sdk" install; \\
154
+ \thave=$$(node -e "console.log(JSON.parse(require('fs').readFileSync('$(SDK_DIR)/.sdk/node_modules/@voxgig/sdkgen/package.json','utf8')).version)"); \\
155
+ \tnode -e 'const a=process.argv[1].split(".").map(Number),b=process.argv[2].split(".").map(Number); \\
156
+ \t for(let i=0;i<3;i++){if(a[i]>b[i])process.exit(0);if(a[i]<b[i])process.exit(1)} process.exit(0)' \\
157
+ \t "$$have" "$(SDKGEN_MIN)" || { \\
158
+ \t echo "regen: $(SDK_TAG) uses @voxgig/sdkgen $$have, and regenerating from"; \\
159
+ \t echo " inside this repo needs >= $(SDKGEN_MIN) (the generate-time output"; \\
160
+ \t echo " override). Older versions IGNORE it and silently write nothing."; \\
161
+ \t echo " Pin a newer SDK tag, or regenerate from a checkout of the SDK."; \\
162
+ \t exit 1; }; \\
163
+ \techo "regen: generating"; \\
164
+ \tcd "$(SDK_DIR)/.sdk" && \\
165
+ \t SDKGEN_EXTERNAL='{"seneca-provider":{"path":"../..","enclosing":true}}' \\
166
+ \t npm run generate
167
+ \t@echo "regen: done — review the diff in this repository"
168
+ `
169
+
170
+
171
+ const REGEN_SELF = `# Regenerate this repository with its own builder.
172
+ #
173
+ # The API definition and guide are replaced by the SDK source's first, so the
174
+ # builder generates from the SDK's model at the pinned tag, and generation
175
+ # checks the result against that source before writing. A linked checkout
176
+ # (SDK_SRC_FROM) is read as it is, and only this repository is written.
177
+ #
178
+ # To move to a newer SDK, set its version in .sdk/model/project.aontu and
179
+ # fetch that tag as you regenerate:
180
+ #
181
+ # make regen SDK_TAG=v1.2.3
182
+ regen: sdk-src
183
+ \t@set -e; \\
184
+ \ttest -f .sdk/package.json || { echo "regen: no builder at .sdk/"; exit 1; }; \\
185
+ \tfor d in def model/guide; do \\
186
+ \t test -d "$(SDK_DIR)/.sdk/$$d" || { echo "regen: no $(SDK_DIR)/.sdk/$$d"; exit 1; }; \\
187
+ \tdone; \\
188
+ \techo "regen: replacing the API definition and guide with those in $(SDK_DIR)"; \\
189
+ \tfor d in def model/guide; do \\
190
+ \t rm -rf ".sdk/$$d"; mkdir -p ".sdk/$$d"; cp -R "$(SDK_DIR)/.sdk/$$d/." ".sdk/$$d/"; \\
191
+ \tdone; \\
192
+ \techo "regen: installing the builder"; \\
193
+ \tcd .sdk && npm install && \\
194
+ \techo "regen: generating" && \\
195
+ \tnpm run generate
196
+ \t@echo "regen: done — review the diff in this repository"
197
+ `
198
+
199
+
200
+ const Makefile = cmp(function Makefile(props: any) {
201
+ const { provider } = props
202
+
203
+ File({ name: 'Makefile' }, () => {
204
+ Content([
205
+ HEAD,
206
+ provider.standalone ? INTRO_SELF : INTRO_SDK,
207
+ FETCH,
208
+ provider.standalone ? REGEN_SELF : REGEN_SDK,
209
+ ].join('\n'))
210
+ })
211
+ })
212
+
213
+
214
+ export {
215
+ Makefile,
216
+ }
@@ -0,0 +1,201 @@
1
+ import Path from 'node:path'
2
+
3
+ import {
4
+ packageName, packageVersion,
5
+ SdkGenError,
6
+ } from '@voxgig/sdkgen'
7
+
8
+ import {
9
+ KIT,
10
+ } from '@voxgig/apidef'
11
+
12
+
13
+ // `output: root: true` means this repository's own `.sdk/` is the builder:
14
+ // the SDK is released elsewhere, and is not a target of this project.
15
+ function standaloneBuilder(target: any): boolean {
16
+ return true === target?.output?.root
17
+ }
18
+
19
+
20
+ function sdkIdentity(model: any, target: any):
21
+ { sdkPkg: string, sdkVersion: string } {
22
+ const sdk = target?.sdk || {}
23
+ const pkg = String(sdk.package || '')
24
+ const version = String(sdk.version || '')
25
+ const hasTs = null != model?.main?.[KIT]?.target?.ts
26
+
27
+ if (!standaloneBuilder(target)) {
28
+ if ('' !== pkg || '' !== version) {
29
+ throw new SdkGenError(
30
+ target.name + ': `sdk.package` and `sdk.version` apply only to a ' +
31
+ 'provider generated at the root of its own repository (`output: ' +
32
+ 'root: true`). Generated from the SDK project, the SDK is its `ts` ' +
33
+ 'target: set `main.' + KIT + '.target.ts.publish.version` instead.')
34
+ }
35
+ return { sdkPkg: packageName(model, 'npm'), sdkVersion: packageVersion(model, 'ts') }
36
+ }
37
+
38
+ if ('' === version && !hasTs) {
39
+ throw new SdkGenError(
40
+ target.name + ': name the SDK version this provider depends on, as ' +
41
+ '`main.' + KIT + '.target.' + target.name + '.sdk.version`. Its ' +
42
+ 'builder has no `ts` target to read it from, and a default would pin ' +
43
+ 'whatever version it happens to be.')
44
+ }
45
+
46
+ return {
47
+ sdkPkg: '' !== pkg ? pkg : packageName(model, 'npm'),
48
+ sdkVersion: '' !== version ? version : packageVersion(model, 'ts'),
49
+ }
50
+ }
51
+
52
+
53
+ // What the SDK was generated from and the provider reads: titles,
54
+ // descriptions and contract metadata are left out.
55
+ const FIELD_KEYS = ['n', 't', 'r', 'a', 'ro', 'wo', 'fo']
56
+ const POINT_KEYS = ['a', 'k', 'm', 'o', 's', 'r', 't', 'g', 'q', 'gq']
57
+
58
+ function pick(obj: any, keys: string[]): Record<string, any> {
59
+ const out: Record<string, any> = {}
60
+ for (const key of keys) {
61
+ if (undefined !== obj?.[key]) {
62
+ out[key] = obj[key]
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+
69
+ // Keyed by path under `main.kit`, so a difference names where to look.
70
+ function apiSurface(model: any): Record<string, any> {
71
+ const kit = model?.main?.[KIT] || {}
72
+ const out: Record<string, any> = {
73
+ info: {
74
+ auth: kit.info?.auth,
75
+ security: kit.info?.security,
76
+ servers: (kit.info?.servers || []).map((server: any) => pick(server, ['url'])),
77
+ },
78
+ config: { auth: kit.config?.auth },
79
+ }
80
+
81
+ const coll = kit.entity || {}
82
+ for (const name of Object.keys(coll).sort()) {
83
+ const ent = coll[name]
84
+ if (null != ent && false !== ent.active) {
85
+ out['entity.' + name] = {
86
+ id: ent.id,
87
+ alias: ent.alias,
88
+ relations: { ancestors: ent.relations?.ancestors },
89
+ fields: Object.fromEntries(Object.entries(ent.fields || {})
90
+ .map(([key, field]) => [key, pick(field, FIELD_KEYS)])),
91
+ op: Object.fromEntries(Object.entries(ent.op || {})
92
+ .map(([key, op]: [string, any]) => [key, {
93
+ input: op?.input,
94
+ points: (op?.points || []).map((point: any) => pick(point, POINT_KEYS)),
95
+ }])),
96
+ }
97
+ }
98
+ }
99
+
100
+ return out
101
+ }
102
+
103
+
104
+ function brief(value: any): string {
105
+ const text = undefined === value ? 'absent' : JSON.stringify(value)
106
+ return 60 < text.length ? text.slice(0, 57) + '...' : text
107
+ }
108
+
109
+
110
+ // Keys ending in `$` are the generator's marks, present only on a live model.
111
+ function differences(here: any, there: any, path: string, out: string[]): void {
112
+ const node = (value: any) => null != value && 'object' === typeof value
113
+ if (node(here) && node(there) && Array.isArray(here) === Array.isArray(there)) {
114
+ const keys = Array.isArray(here) ?
115
+ Array.from({ length: Math.max(here.length, there.length) }, (_, i) => i) :
116
+ [...new Set([...Object.keys(here), ...Object.keys(there)])]
117
+ .filter((key) => !key.endsWith('$')).sort()
118
+ for (const key of keys) {
119
+ differences(here[key], there[key], Array.isArray(here) ?
120
+ path + '[' + key + ']' : path + '.' + key, out)
121
+ }
122
+ }
123
+ else if (JSON.stringify(here) !== JSON.stringify(there)) {
124
+ out.push(path + ': here ' + brief(here) + ', SDK ' + brief(there))
125
+ }
126
+ }
127
+
128
+
129
+ // The builder carries its own copy of the SDK's API definition, right only
130
+ // while it matches the SDK at the version depended on. `make sdk-src` fetches
131
+ // that SDK, whose compiled model is the comparison.
132
+ function checkSdkSource(ctx$: any, provider: any, model: any): void {
133
+ const fs = ctx$.fs()
134
+ const rel = provider.sdkSrc + '/.sdk/model/sdk.json'
135
+ const file = Path.join(ctx$.folder || '.', rel)
136
+
137
+ if (!fs.existsSync(file)) {
138
+ ctx$.log?.warn?.({
139
+ point: 'sdk-source-unchecked', file: rel,
140
+ note: 'seneca-provider: ' + rel + ' is absent, so this model was NOT ' +
141
+ 'checked against ' + provider.sdkPkg + ' ' + provider.sdkVersion +
142
+ '. `make sdk-src` fetches it.'
143
+ })
144
+ return
145
+ }
146
+
147
+ const sdk = JSON.parse(String(fs.readFileSync(file, 'utf8')))
148
+ const problems: string[] = []
149
+
150
+ // The SDK's own manifest, when fetched: this builder's naming rule need not
151
+ // be the one that built the SDK.
152
+ const manifestFile = Path.join(ctx$.folder || '.', provider.sdkSrc, 'ts', 'package.json')
153
+ const manifest = fs.existsSync(manifestFile) ?
154
+ JSON.parse(String(fs.readFileSync(manifestFile, 'utf8'))) : {}
155
+
156
+ const sdkVersion = String(manifest.version || packageVersion(sdk, 'ts'))
157
+ if (sdkVersion !== provider.sdkVersion) {
158
+ problems.push('version: this provider depends on ' + provider.sdkVersion +
159
+ ', the fetched SDK is ' + sdkVersion +
160
+ ' (make regen SDK_TAG=v' + provider.sdkVersion + ')')
161
+ }
162
+
163
+ const sdkPkg = String(manifest.name || packageName(sdk, 'npm'))
164
+ if (sdkPkg !== provider.sdkPkg) {
165
+ problems.push('package: this provider depends on ' + provider.sdkPkg +
166
+ ', the fetched SDK is ' + sdkPkg)
167
+ }
168
+
169
+ const here = apiSurface(model)
170
+ const there = apiSurface(sdk)
171
+
172
+ for (const part of [...new Set([...Object.keys(here), ...Object.keys(there)])].sort()) {
173
+ const found: string[] = []
174
+ differences(here[part], there[part], part, found)
175
+ problems.push(...found.slice(0, 5))
176
+ if (5 < found.length) {
177
+ problems.push(part + ': and ' + (found.length - 5) + ' more')
178
+ }
179
+ }
180
+
181
+ if (0 < problems.length) {
182
+ throw new SdkGenError(
183
+ 'seneca-provider: this builder\'s model does not match the SDK it ' +
184
+ 'depends on, as fetched to ' + provider.sdkSrc + ' (paths under main.' +
185
+ KIT + '):\n ' +
186
+ problems.join('\n ') +
187
+ '\n`make regen` replaces the API definition and guide in .sdk/ with the ' +
188
+ 'SDK\'s before generating. A difference that remains comes from a decision in ' +
189
+ 'the SDK project\'s own model, to be declared the same way in ' +
190
+ '.sdk/model/project.aontu, or from an @voxgig/apidef version other ' +
191
+ 'than the one in the SDK\'s .sdk/package.json.')
192
+ }
193
+ }
194
+
195
+
196
+ export {
197
+ standaloneBuilder,
198
+ sdkIdentity,
199
+ apiSurface,
200
+ checkSdkSource,
201
+ }
package/README.md CHANGED
@@ -27,7 +27,9 @@ sdkgen repository.
27
27
 
28
28
  These are consumer targets: they switch every standard generation phase off and
29
29
  emit their whole package from `Main`, and each one fails without the target it
30
- wraps, deliberately. Add `ts` to your project before `seneca-provider`.
30
+ wraps, deliberately. Add `ts` to your project before `seneca-provider`. The
31
+ one exception is a provider that carries its own builder (below), which names
32
+ the SDK it wraps instead.
31
33
 
32
34
  There is no manifest field for that requirement. Nothing in
33
35
  `sdkgen-package.json` says "this target needs that one", so this paragraph and
@@ -84,6 +86,75 @@ first look at the output.
84
86
  Set that in `model/project.aontu`, not in the target's own file — `target add`
85
87
  overwrites the latter.
86
88
 
89
+ ## Or the provider carries its own builder
90
+
91
+ The provider repository can instead hold a `.sdk/` of its own, which generates
92
+ the provider and nothing else, and leaves the SDK repository untouched. Its
93
+ `model/project.aontu` says so, and the standard `Root` that create-sdkgen
94
+ scaffolds reads it:
95
+
96
+ ```
97
+ main: kit: phase: top: active: false
98
+ main: kit: phase: build: active: false
99
+ main: kit: doc: active: false
100
+ main: kit: target: 'seneca-provider': output: root: true
101
+ main: kit: target: 'seneca-provider': sdk: version: '0.0.1'
102
+ ```
103
+
104
+ `output: root: true` generates the provider at the repository root, and is how
105
+ this target knows the builder is the provider's own. That builder has no `ts`
106
+ target, so it names the SDK itself: `sdk.version` is required, and
107
+ `sdk.package` overrides the derived package name. Both are refused in an SDK
108
+ project, where the `ts` target is the SDK.
109
+
110
+ The builder carries a copy of the SDK's API definition and guide. `make regen`
111
+ replaces them with those of the SDK fetched at the tag in `sdk-pin.json` before
112
+ generating. Generation then refuses to write when the builder's model and that
113
+ SDK's compiled model differ in what the SDK was generated from: an entity, an
114
+ operation's routes, parameters and actions, a field's type or requiredness, the
115
+ identity, the ancestors, the authentication or the servers. Titles and
116
+ descriptions are not compared. It also refuses when the package name or version
117
+ differ from the SDK's own `ts/package.json`, which is read rather than the name
118
+ re-derived, because the SDK's generator may name packages by a rule this
119
+ builder's does not share. Generated without the SDK fetched, it warns that the
120
+ check did not run.
121
+
122
+ A symlinked SDK checkout, from `make sdk-src SDK_SRC_FROM=<path>`, is read as
123
+ it is: `make sdk-src` never fetches or checks out a tag inside one.
124
+
125
+ To move to a newer SDK, set `sdk.version` and fetch its tag as you regenerate:
126
+
127
+ ```bash
128
+ make regen SDK_TAG=v0.0.2
129
+ ```
130
+
131
+ ## An SDK that is not on npm yet
132
+
133
+ The generated `package.json` depends on the SDK by version range, which
134
+ installs only once the SDK is published. Until then, `sdk.dep` points it
135
+ elsewhere:
136
+
137
+ | `sdk.dep` | The dependency |
138
+ |---|---|
139
+ | `kind: 'release', ref: 'v1.0.0'` | the `npm pack` tarball attached to that GitHub release |
140
+ | `kind: 'git', ref: '<tag>'` | `github:<owner>/<repo>#<tag>`, installed from the root of that tag's tree |
141
+ | `spec: '<anything>'` | the value, verbatim |
142
+
143
+ npm cannot install a package from a subfolder of a git repository, and an
144
+ sdkgen SDK keeps its TypeScript package in `ts/`. A git dependency therefore
145
+ needs a tag whose tree is that folder, cut in the SDK repository:
146
+
147
+ ```bash
148
+ git tag ts-v1.0.0 $(git commit-tree HEAD:ts -p HEAD -m "ts/ at v1.0.0")
149
+ git push origin ts-v1.0.0
150
+ ```
151
+
152
+ and then `sdk: dep: { kind: 'git', ref: 'ts-v1.0.0' }`. The tag carries what
153
+ the folder holds, so an SDK that commits `ts/dist` needs nothing built on
154
+ install. The generated CI passes `--allow-git=all`, which npm 12 needs for a
155
+ git dependency, and so would anyone installing a published provider that
156
+ depends on one: publish the SDK to npm before the provider.
157
+
87
158
  ## Parity
88
159
 
89
160
  Every target here declares `CONSUMER`. That is not a coverage tier alongside
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/sdkgen-infrapack",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Infrastructure-provider targets for the Voxgig SDK Generator: Seneca provider.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen-infrapack",
6
- "version": "0.0.15",
6
+ "version": "0.0.16",
7
7
  "engines": {
8
8
  "sdkgen": ">=4.25.0"
9
9
  },
@@ -1,131 +0,0 @@
1
- # GENERATED by @voxgig/sdkgen. Do not edit.
2
-
3
- .PHONY: all build test clean reset sdk-src sdk-clean regen
4
-
5
- all: build test
6
-
7
- build:
8
- npm run build
9
-
10
- test:
11
- npm test
12
-
13
- clean:
14
- rm -rf dist dist-test .tsbuildinfo
15
-
16
- reset:
17
- npm run reset
18
-
19
-
20
- # ---------------------------------------------------------------------------
21
- # THIS REPOSITORY IS GENERATED, AND CAN REGENERATE ITSELF.
22
- #
23
- # Everything below fetches the SDK named in sdk-pin.json and runs the
24
- # generator from it. Nothing here needs a sibling checkout or a particular
25
- # directory layout: the pin names a repository and a TAG, and the checkout
26
- # lands inside this repo at a fixed path.
27
- #
28
- # NOT a git submodule, deliberately. A submodule pins a commit in git's own
29
- # plumbing, where a stale one is invisible in a normal diff and updating it is
30
- # a second repository operation. sdk-pin.json is an ordinary committed file,
31
- # regenerated from the model with everything else, so it moves with the SDK
32
- # version it belongs to and shows up in review.
33
- # ---------------------------------------------------------------------------
34
-
35
- SDK_PIN := sdk-pin.json
36
- SDK_REPO := $(shell node -p "require('./$(SDK_PIN)').repo" 2>/dev/null)
37
- SDK_TAG := $(shell node -p "require('./$(SDK_PIN)').tag" 2>/dev/null)
38
- SDK_DIR := $(shell node -p "require('./$(SDK_PIN)').dir" 2>/dev/null)
39
-
40
- # Point at a checkout you already have instead of fetching one:
41
- # make sdk-src SDK_SRC_FROM=../path/to/sdk
42
- SDK_SRC_FROM ?=
43
-
44
- # Fetch (or update) the SDK source at the pinned tag.
45
- #
46
- # A TAG, not a branch: the point is the exact revision this repository was
47
- # generated from, so that regenerating reproduces what is committed here
48
- # rather than whatever main happens to say today.
49
- sdk-src:
50
- @set -e; \
51
- test -n "$(SDK_REPO)" || { echo "sdk-src: no repo in $(SDK_PIN)"; exit 1; }; \
52
- test -n "$(SDK_TAG)" || { echo "sdk-src: no tag in $(SDK_PIN)"; exit 1; }; \
53
- if [ -n "$(SDK_SRC_FROM)" ]; then \
54
- echo "sdk-src: using $(SDK_SRC_FROM) (not fetching)"; \
55
- rm -rf "$(SDK_DIR)"; mkdir -p "$$(dirname "$(SDK_DIR)")"; \
56
- ln -s "$$(cd "$(SDK_SRC_FROM)" && pwd)" "$(SDK_DIR)"; \
57
- exit 0; \
58
- fi; \
59
- if [ -d "$(SDK_DIR)/.git" ]; then \
60
- echo "sdk-src: fetching $(SDK_TAG) in $(SDK_DIR)"; \
61
- git -C "$(SDK_DIR)" fetch --depth 1 origin "refs/tags/$(SDK_TAG):refs/tags/$(SDK_TAG)" 2>/dev/null || \
62
- git -C "$(SDK_DIR)" fetch origin "refs/tags/$(SDK_TAG):refs/tags/$(SDK_TAG)"; \
63
- git -C "$(SDK_DIR)" checkout -q "refs/tags/$(SDK_TAG)"; \
64
- else \
65
- echo "sdk-src: cloning $(SDK_REPO) at $(SDK_TAG) into $(SDK_DIR)"; \
66
- rm -rf "$(SDK_DIR)"; mkdir -p "$$(dirname "$(SDK_DIR)")"; \
67
- git clone --depth 1 --branch "$(SDK_TAG)" "$(SDK_REPO).git" "$(SDK_DIR)"; \
68
- fi; \
69
- echo "sdk-src: $(SDK_DIR) is at $(SDK_TAG)"
70
-
71
- # Removes the fetched checkout AND the folder that held it. `rm -rf` on the
72
- # checkout alone left an empty `.sdksrc/` behind — gitignored, so harmless,
73
- # but it makes `sdk-clean` look like it did not finish. rmdir rather than
74
- # another rm -rf: if anything else is in there, someone put it there, and
75
- # removing it is not this target's business.
76
- sdk-clean:
77
- rm -rf "$(SDK_DIR)"
78
- @rmdir "$$(dirname "$(SDK_DIR)")" 2>/dev/null || true
79
-
80
- # Regenerate this repository from the pinned SDK.
81
- #
82
- # The SDK's model says where its provider goes, and that value is a fact about
83
- # ONE developer's checkout layout — it cannot describe being cloned in here.
84
- # So the destination is given to the generator AT RUN TIME instead:
85
- #
86
- # path ../.. — from the SDK checkout up to this repository root
87
- # enclosing the output folder CONTAINS the SDK project, which the
88
- # generator refuses unless asked, because one `..` too many
89
- # writes a package over an unrelated repo
90
- #
91
- # See @voxgig/sdkgen docs/explanation/out-of-tree-targets.md.
92
- # The generate-time output override arrived in @voxgig/sdkgen 4.16.0, and
93
- # older versions IGNORE it SILENTLY: generation falls back to the destination
94
- # in the SDK's own model, which from in here resolves to a folder that does
95
- # not exist, gets skipped, and reports success having written nothing. That is
96
- # not hypothetical — it is what this target did on its first end-to-end run,
97
- # against a pin whose floor was set one release too low.
98
- #
99
- # So the floor is the version that actually ships the feature, not the version
100
- # that happened to be current when it was written.
101
- SDKGEN_MIN := 4.16.0
102
-
103
- regen: sdk-src
104
- @set -e; \
105
- if [ -L "$(SDK_DIR)" ]; then \
106
- echo "regen: $(SDK_DIR) is a symlink, and regenerating through one writes"; \
107
- echo " the package somewhere else. Node resolves a module's own"; \
108
- echo " directory through symlinks, so the generator computes its"; \
109
- echo " destination from where the checkout REALLY is — not from"; \
110
- echo " here — and \`enclosing\` then permits the result."; \
111
- echo " SDK_SRC_FROM is for building and linking against a local"; \
112
- echo " SDK (make sdk-src). To regenerate, use a fetched checkout:"; \
113
- echo " make sdk-clean && make regen"; \
114
- exit 1; \
115
- fi; \
116
- echo "regen: building the generator in $(SDK_DIR)/.sdk"; \
117
- npm --prefix "$(SDK_DIR)/.sdk" install; \
118
- have=$$(node -e "console.log(JSON.parse(require('fs').readFileSync('$(SDK_DIR)/.sdk/node_modules/@voxgig/sdkgen/package.json','utf8')).version)"); \
119
- node -e 'const a=process.argv[1].split(".").map(Number),b=process.argv[2].split(".").map(Number); \
120
- for(let i=0;i<3;i++){if(a[i]>b[i])process.exit(0);if(a[i]<b[i])process.exit(1)} process.exit(0)' \
121
- "$$have" "$(SDKGEN_MIN)" || { \
122
- echo "regen: $(SDK_TAG) uses @voxgig/sdkgen $$have, and regenerating from"; \
123
- echo " inside this repo needs >= $(SDKGEN_MIN) (the generate-time output"; \
124
- echo " override). Older versions IGNORE it and silently write nothing."; \
125
- echo " Pin a newer SDK tag, or regenerate from a checkout of the SDK."; \
126
- exit 1; }; \
127
- echo "regen: generating"; \
128
- cd "$(SDK_DIR)/.sdk" && \
129
- SDKGEN_EXTERNAL='{"seneca-provider":{"path":"../..","enclosing":true}}' \
130
- npm run generate
131
- @echo "regen: done — review the diff in this repository"