@voxgig/sdkgen 1.3.1 → 1.3.2

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 (88) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/action/action.d.ts +2 -1
  3. package/dist/action/action.js +13 -3
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/feature.js +3 -5
  6. package/dist/action/feature.js.map +1 -1
  7. package/dist/action/target.js +9 -7
  8. package/dist/action/target.js.map +1 -1
  9. package/dist/cmp/Deploy.js +3 -3
  10. package/dist/cmp/Deploy.js.map +1 -1
  11. package/dist/helpers/opShape.js +38 -6
  12. package/dist/helpers/opShape.js.map +1 -1
  13. package/dist/sdkgen.d.ts +2 -2
  14. package/dist/sdkgen.js +5 -4
  15. package/dist/sdkgen.js.map +1 -1
  16. package/dist/utility.d.ts +2 -1
  17. package/dist/utility.js +20 -1
  18. package/dist/utility.js.map +1 -1
  19. package/model/sdkgen.aontu +1 -1
  20. package/package.json +1 -1
  21. package/project/.sdk/model/feature/feature-index.aontu +5 -0
  22. package/project/.sdk/src/cmp/go/Config_go.ts +3 -2
  23. package/project/.sdk/src/cmp/go/ReadmeEntity_go.ts +35 -9
  24. package/project/.sdk/src/cmp/go/ReadmeExamplesTest_go.ts +18 -14
  25. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +6 -2
  26. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +6 -2
  27. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +51 -30
  28. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +66 -13
  29. package/project/.sdk/src/cmp/go/ReadmeTopHowto_go.ts +4 -0
  30. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +51 -16
  31. package/project/.sdk/src/cmp/go/utility_go.ts +70 -0
  32. package/project/.sdk/src/cmp/js/Config_js.ts +3 -6
  33. package/project/.sdk/src/cmp/js/ReadmeEntity_js.ts +23 -6
  34. package/project/.sdk/src/cmp/js/ReadmeQuick_js.ts +31 -7
  35. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +33 -10
  36. package/project/.sdk/src/cmp/js/ReadmeTopQuick_js.ts +24 -14
  37. package/project/.sdk/src/cmp/js/ReadmeTopTest_js.ts +15 -3
  38. package/project/.sdk/src/cmp/lua/Config_lua.ts +3 -2
  39. package/project/.sdk/src/cmp/lua/ReadmeEntity_lua.ts +41 -6
  40. package/project/.sdk/src/cmp/lua/ReadmeQuick_lua.ts +94 -20
  41. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +51 -8
  42. package/project/.sdk/src/cmp/lua/ReadmeTopQuick_lua.ts +31 -2
  43. package/project/.sdk/src/cmp/lua/ReadmeTopTest_lua.ts +13 -2
  44. package/project/.sdk/src/cmp/php/Config_php.ts +3 -2
  45. package/project/.sdk/src/cmp/php/ReadmeEntity_php.ts +34 -6
  46. package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +95 -19
  47. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +44 -8
  48. package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +26 -2
  49. package/project/.sdk/src/cmp/php/ReadmeTopTest_php.ts +13 -2
  50. package/project/.sdk/src/cmp/py/Config_py.ts +3 -2
  51. package/project/.sdk/src/cmp/py/ReadmeEntity_py.ts +26 -8
  52. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +95 -21
  53. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +37 -11
  54. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +28 -2
  55. package/project/.sdk/src/cmp/py/ReadmeTopTest_py.ts +16 -3
  56. package/project/.sdk/src/cmp/rb/Config_rb.ts +3 -2
  57. package/project/.sdk/src/cmp/rb/ReadmeEntity_rb.ts +26 -8
  58. package/project/.sdk/src/cmp/rb/ReadmeQuick_rb.ts +96 -21
  59. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +36 -10
  60. package/project/.sdk/src/cmp/rb/ReadmeTopQuick_rb.ts +28 -2
  61. package/project/.sdk/src/cmp/rb/ReadmeTopTest_rb.ts +13 -2
  62. package/project/.sdk/src/cmp/ts/Config_ts.ts +3 -6
  63. package/project/.sdk/src/cmp/ts/ReadmeEntity_ts.ts +22 -6
  64. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +56 -18
  65. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +33 -10
  66. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +21 -14
  67. package/project/.sdk/src/cmp/ts/ReadmeTopTest_ts.ts +15 -3
  68. package/project/.sdk/tm/py/test/test_struct_utility.py +1 -1
  69. package/project/.sdk/tm/rb/Makefile +3 -1
  70. package/src/action/action.ts +15 -3
  71. package/src/action/feature.ts +4 -8
  72. package/src/action/target.ts +11 -9
  73. package/src/cmp/Deploy.ts +3 -3
  74. package/src/helpers/opShape.ts +45 -7
  75. package/src/sdkgen.ts +4 -3
  76. package/src/utility.ts +24 -1
  77. package/project/.sdk/model/feature/feature-index.jsonic +0 -5
  78. /package/project/.sdk/model/feature/{log.jsonic → log.aontu} +0 -0
  79. /package/project/.sdk/model/feature/{test.jsonic → test.aontu} +0 -0
  80. /package/project/.sdk/model/target/{go-cli.jsonic → go-cli.aontu} +0 -0
  81. /package/project/.sdk/model/target/{go-mcp.jsonic → go-mcp.aontu} +0 -0
  82. /package/project/.sdk/model/target/{go.jsonic → go.aontu} +0 -0
  83. /package/project/.sdk/model/target/{js.jsonic → js.aontu} +0 -0
  84. /package/project/.sdk/model/target/{lua.jsonic → lua.aontu} +0 -0
  85. /package/project/.sdk/model/target/{php.jsonic → php.aontu} +0 -0
  86. /package/project/.sdk/model/target/{py.jsonic → py.aontu} +0 -0
  87. /package/project/.sdk/model/target/{rb.jsonic → rb.aontu} +0 -0
  88. /package/project/.sdk/model/target/{ts.jsonic → ts.aontu} +0 -0
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, canonToType, canonKey, entityIdField } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, canonKey, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -9,13 +9,14 @@ import {
9
9
 
10
10
  // A type-correct Ruby literal for a field's canonical type — the create body
11
11
  // is EXECUTED by the doc test, so it must carry a real value per field.
12
- function rbLit(type: any): string {
12
+ // Strings render the quoted placeholder.
13
+ function rbLit(type: any, placeholder: string = 'example'): string {
13
14
  const k = canonKey(type)
14
15
  if ('INTEGER' === k || 'NUMBER' === k) return '1'
15
16
  if ('BOOLEAN' === k) return 'true'
16
17
  if ('ARRAY' === k) return '[]'
17
18
  if ('OBJECT' === k) return '{}'
18
- return '"example"'
19
+ return `"${placeholder}"`
19
20
  }
20
21
 
21
22
 
@@ -107,11 +108,23 @@ const ReadmeEntity = cmp(function ReadmeEntity(props: any) {
107
108
  }
108
109
 
109
110
  if (opnames.includes('load')) {
111
+ // The id key plus every REQUIRED match key (parent path params like
112
+ // page_id) — the same shape the runtime resolves path params from, so
113
+ // the example always works.
114
+ const loadItems = opRequestShape(entity, 'load').items
115
+ .filter((it: any) => !it.optional || it.name === idF)
116
+ .sort((a: any, b: any) =>
117
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
118
+ const loadArg = 0 < loadItems.length
119
+ ? `{ ${loadItems.map((it: any) =>
120
+ `"${it.name}" => ${rbLit(it.type,
121
+ it.name === idF ? entity.name + '_id' : it.name)}`).join(', ')} }`
122
+ : ''
110
123
  Content(`#### Example: Load
111
124
 
112
125
  \`\`\`ruby
113
126
  # load returns the bare ${entity.Name} record (raises on error).
114
- ${entity.name} = client.${entity.Name}.load(${idF ? `{ "${idF}" => "${entity.name}_id" }` : ''})
127
+ ${entity.name} = client.${entity.Name}.load(${loadArg})
115
128
  \`\`\`
116
129
 
117
130
  `)
@@ -129,16 +142,21 @@ ${entity.name}s = client.${entity.Name}.list
129
142
  }
130
143
 
131
144
  if (opnames.includes('create')) {
145
+ // Members come from the SAME shape the runtime validates
146
+ // (opRequestShape): every required member must appear — including a
147
+ // required id and parent keys like page_id — with a real, executable
148
+ // literal (the doc test RUNS this block, so a comment placeholder
149
+ // would break it).
150
+ const createItems = opRequestShape(entity, 'create').items
151
+ .filter((it: any) => !it.optional)
132
152
  Content(`#### Example: Create
133
153
 
134
154
  \`\`\`ruby
135
155
  ${entity.name} = client.${entity.Name}.create({
136
156
  `)
137
- each(fields, (field: any) => {
138
- if ('id' !== field.name && field.req) {
139
- Content(` "${field.name}" => ${rbLit(field.type)}, # ${canonToType(field.type, target.name)}
157
+ createItems.map((it: any) => {
158
+ Content(` "${it.name}" => ${rbLit(it.type, 'example_' + it.name)}, # ${canonToType(it.type, target.name)}
140
159
  `)
141
- }
142
160
  })
143
161
  Content(`})
144
162
  \`\`\`
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive, envName, canonKey, opRequestShape, entityIdField, entityDataIdField } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, envName, canonKey, opRequestShape, entityIdField, entityDataIdField, entityOps } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -14,8 +14,16 @@ const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
14
14
  const entity = getModelPath(model, `main.${KIT}.entity`)
15
15
 
16
16
  const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
17
+
18
+ // Find a nested entity if available: one with a parent chain
19
+ // (relations.ancestors), an active load op, and a required non-id load
20
+ // param to demonstrate (the parent key, e.g. page_id).
17
21
  const nestedEntity = Object.values(entity).find((e: any) =>
18
- e.active !== false && e.ancestors && e.ancestors.length > 0
22
+ e.active !== false &&
23
+ e.relations && e.relations.ancestors && 0 < e.relations.ancestors.length &&
24
+ entityOps(e).includes('load') &&
25
+ opRequestShape(e, 'load').items.some((it: any) =>
26
+ !it.optional && it.name !== entityIdField(e))
19
27
  ) as any
20
28
 
21
29
  const ctor = isAuthActive(model)
@@ -35,7 +43,7 @@ client = ${ctor}
35
43
  if (exampleEntity) {
36
44
  const eName = nom(exampleEntity, 'Name')
37
45
  const article = /^[aeiou]/i.test(eName) ? "an" : "a"
38
- const opnames = Object.keys(exampleEntity.op || {})
46
+ const opnames = entityOps(exampleEntity)
39
47
  // Model-driven id keys: `idF` is the load-MATCH key (null when the entity
40
48
  // has no id-like field — a response-wrapped spec); when null, load/remove
41
49
  // take no argument. `dataIdF` is the id on the RETURNED record's data type —
@@ -45,6 +53,19 @@ client = ${ctor}
45
53
  const idF = entityIdField(exampleEntity)
46
54
  const dataIdF = entityDataIdField(exampleEntity)
47
55
 
56
+ // A type-correct, executable Ruby literal for a param: numeric/boolean/
57
+ // array/hash params render a typed literal; strings render the quoted
58
+ // placeholder (the doc test EXECUTES these blocks, so a comment
59
+ // placeholder would not parse).
60
+ const rbLit = (type: any, placeholder: string = 'example'): string => {
61
+ const k = canonKey(type)
62
+ if ('INTEGER' === k || 'NUMBER' === k) return '1'
63
+ if ('BOOLEAN' === k) return 'true'
64
+ if ('ARRAY' === k) return '[]'
65
+ if ('OBJECT' === k) return '{}'
66
+ return `"${placeholder}"`
67
+ }
68
+
48
69
  // Model-driven display field: the entity's first non-id string field
49
70
  // (falling back to any non-id field), so the list example prints a real
50
71
  // column instead of a hardcoded "name" the entity may not have.
@@ -75,13 +96,62 @@ end
75
96
  `)
76
97
  }
77
98
 
78
- if (opnames.includes('load')) {
99
+ if (nestedEntity) {
100
+ const neName = nom(nestedEntity, 'Name')
101
+ const neArticle = /^[aeiou]/i.test(neName) ? "an" : "a"
102
+ const neVar = neName.toLowerCase()
103
+
104
+ // Model-driven match: every REQUIRED load-match key — the same shape
105
+ // the runtime resolves path params from, so the example always works.
106
+ // Parent keys (e.g. page_id) first, the entity's own id last.
107
+ const neIdF = entityIdField(nestedEntity)
108
+ const neRequired = opRequestShape(nestedEntity, 'load').items
109
+ .filter((it: any) => !it.optional)
110
+ .sort((a: any, b: any) =>
111
+ (a.name === neIdF ? 1 : 0) - (b.name === neIdF ? 1 : 0))
112
+ const parentItem = neRequired.find((it: any) => it.name !== neIdF) as any
113
+ const parentParam = parentItem && parentItem.name
114
+ const parentName = parentParam ? parentParam.replace(/_id$/, '') : 'its parent'
115
+ const neMatch = neRequired.map((it: any) =>
116
+ `"${it.name}" => ${rbLit(it.type,
117
+ it.name === neIdF ? 'example_id' : 'example_' + it.name)}`)
118
+
119
+ Content(`### 3. Load ${neArticle} ${neName.toLowerCase()}
120
+
121
+ ${neName} is nested under ${parentName}, so provide the \`${parentParam}\`.
122
+
123
+ \`\`\`ruby
124
+ begin
125
+ # load returns the bare ${neName} record (raises on error).
126
+ ${neVar} = client.${neName}.load({ ${neMatch.join(', ')} })
127
+ puts ${neVar}
128
+ rescue => err
129
+ warn "load failed: #{err}"
130
+ end
131
+ \`\`\`
132
+
133
+ `)
134
+ }
135
+ else if (opnames.includes('load')) {
136
+ // Every REQUIRED load-match key (id first, then parent path params like
137
+ // page_id) — the same shape the runtime resolves path params from, so
138
+ // the example always works.
139
+ const loadRequired = opRequestShape(exampleEntity, 'load').items
140
+ .filter((it: any) => !it.optional || it.name === idF)
141
+ .sort((a: any, b: any) =>
142
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
143
+ const loadArg = 0 < loadRequired.length
144
+ ? `{ ${loadRequired.map((it: any) =>
145
+ `"${it.name}" => ${rbLit(it.type,
146
+ it.name === idF ? 'example_id' : 'example_' + it.name)}`).join(', ')} }`
147
+ : ''
148
+
79
149
  Content(`### 3. Load ${article} ${eName.toLowerCase()}
80
150
 
81
151
  \`\`\`ruby
82
152
  begin
83
153
  # load returns the bare ${eName} record (raises on error).
84
- ${eName.toLowerCase()} = client.${eName}.load(${idF ? `{ "${idF}" => "example_id" }` : ''})
154
+ ${eName.toLowerCase()} = client.${eName}.load(${loadArg})
85
155
  puts ${eName.toLowerCase()}
86
156
  rescue => err
87
157
  warn "load failed: #{err}"
@@ -94,25 +164,21 @@ end
94
164
  // Model-driven example fields: derive the create/update body from the op
95
165
  // shape (opRequestShape) so the docs reference REAL writable fields, not a
96
166
  // hardcoded "name" the entity may not have. Literals are Ruby-typed by the
97
- // field's canonical type.
98
- const idField = (exampleEntity.id && exampleEntity.id.field) || 'id'
99
- const rbLit = (type: any): string => {
100
- const k = canonKey(type)
101
- if ('INTEGER' === k || 'NUMBER' === k) return '1'
102
- if ('BOOLEAN' === k) return 'true'
103
- if ('ARRAY' === k) return '[]'
104
- if ('OBJECT' === k) return '{}'
105
- return '"example"'
106
- }
167
+ // field's canonical type. ids are rendered separately as the match key for
168
+ // update/remove; a REQUIRED create id stays (the call is invalid without
169
+ // it).
107
170
  const examplePairs = (opname: string): string[] => {
108
171
  const items = opRequestShape(exampleEntity, opname).items
109
- .filter((it: any) => it.name !== idField && it.name !== 'id')
172
+ .filter((it: any) => (it.name !== idF && it.name !== 'id') ||
173
+ ('create' === opname && !it.optional))
110
174
  const required = items.filter((it: any) => !it.optional)
111
- // create needs ALL required fields; update is a patch, so a couple suffice.
175
+ const optional = items.filter((it: any) => it.optional)
176
+ // create needs ALL required fields; update is a patch, so the required
177
+ // members plus a sample optional field or two suffice.
112
178
  const chosen = 'create' === opname
113
179
  ? (required.length ? required : items.slice(0, 2))
114
- : items.slice(0, 2)
115
- return chosen.map((it: any) => `"${it.name}" => ${rbLit(it.type)}`)
180
+ : required.concat(optional).slice(0, Math.max(2, required.length))
181
+ return chosen.map((it: any) => `"${it.name}" => ${rbLit(it.type, 'example_' + it.name)}`)
116
182
  }
117
183
 
118
184
  // The id VALUE for an update/remove match: off the returned `created`
@@ -125,7 +191,7 @@ end
125
191
  }
126
192
  const idValueFor = (opname: string): string => (null != dataIdF && opnames.includes('create'))
127
193
  ? `created["${dataIdF}"]`
128
- : rbLit(idParamType(opname))
194
+ : rbLit(idParamType(opname), 'example_id')
129
195
 
130
196
  if (opnames.includes('create') || opnames.includes('update') || opnames.includes('remove')) {
131
197
  Content(`### 4. Create, update, and remove
@@ -147,8 +213,17 @@ client.${eName}.update({ ${updatePairs.join(', ')} })
147
213
  `)
148
214
  }
149
215
  if (opnames.includes('remove')) {
216
+ // Every REQUIRED remove-match key: the id (off the created record
217
+ // when possible) plus parent keys like page_id.
218
+ const removePairs = opRequestShape(exampleEntity, 'remove').items
219
+ .filter((it: any) => !it.optional || it.name === idF)
220
+ .sort((a: any, b: any) =>
221
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
222
+ .map((it: any) => it.name === idF
223
+ ? `"${it.name}" => ${idValueFor('remove')}`
224
+ : `"${it.name}" => ${rbLit(it.type, 'example_' + it.name)}`)
150
225
  Content(`# Remove
151
- client.${eName}.remove(${idF ? `{ "${idF}" => ${idValueFor('remove')} }` : ''})
226
+ client.${eName}.remove(${removePairs.length ? `{ ${removePairs.join(', ')} }` : ''})
152
227
  `)
153
228
  }
154
229
  Content(`\`\`\`
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, canonToType, canonKey, File, isAuthActive, entityIdField } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, canonKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -9,13 +9,14 @@ import {
9
9
 
10
10
  // A type-correct Ruby literal for a field's canonical type — the create body
11
11
  // is EXECUTED by the doc test, so it must carry a real value per field.
12
- function rbLit(type: any): string {
12
+ // Strings render the quoted placeholder.
13
+ function rbLit(type: any, placeholder: string = 'example'): string {
13
14
  const k = canonKey(type)
14
15
  if ('INTEGER' === k || 'NUMBER' === k) return '1'
15
16
  if ('BOOLEAN' === k) return 'true'
16
17
  if ('ARRAY' === k) return '[]'
17
18
  if ('OBJECT' === k) return '{}'
18
- return '"example"'
19
+ return `"${placeholder}"`
19
20
  }
20
21
 
21
22
 
@@ -257,8 +258,20 @@ ${info.desc}
257
258
 
258
259
  // Show example
259
260
  if ('load' === opname || 'remove' === opname) {
261
+ // The id key plus every REQUIRED match key (parent path params
262
+ // like page_id) — the same shape the runtime resolves path
263
+ // params from, so the example always works.
264
+ const matchItems = opRequestShape(ent, opname).items
265
+ .filter((it: any) => !it.optional || it.name === idF)
266
+ .sort((a: any, b: any) =>
267
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
268
+ const arg = 0 < matchItems.length
269
+ ? `{ ${matchItems.map((it: any) =>
270
+ `"${it.name}" => ${rbLit(it.type,
271
+ it.name === idF ? ent.name + '_id' : it.name)}`).join(', ')} }`
272
+ : ''
260
273
  Content(`\`\`\`ruby
261
- result = client.${ent.Name}.${opname}(${idF ? `{ "${idF}" => "${ent.name}_id" }` : ''})
274
+ result = client.${ent.Name}.${opname}(${arg})
262
275
  \`\`\`
263
276
 
264
277
  `)
@@ -271,14 +284,19 @@ results = client.${ent.Name}.list
271
284
  `)
272
285
  }
273
286
  else if ('create' === opname) {
287
+ // Members come from the SAME shape the runtime validates
288
+ // (opRequestShape): every required member must appear — including
289
+ // a required id and parent keys like page_id — with a real,
290
+ // executable literal (the doc test RUNS this block, so a comment
291
+ // placeholder would break it).
292
+ const createItems = opRequestShape(ent, 'create').items
293
+ .filter((it: any) => !it.optional)
274
294
  Content(`\`\`\`ruby
275
295
  result = client.${ent.Name}.create({
276
296
  `)
277
- each(fields, (field: any) => {
278
- if ('id' !== field.name && field.req) {
279
- Content(` "${field.name}" => ${rbLit(field.type)}, # ${canonToType(field.type, target.name)}
297
+ createItems.map((it: any) => {
298
+ Content(` "${it.name}" => ${rbLit(it.type, 'example_' + it.name)}, # ${canonToType(it.type, target.name)}
280
299
  `)
281
- }
282
300
  })
283
301
  Content(`})
284
302
  \`\`\`
@@ -286,10 +304,18 @@ result = client.${ent.Name}.create({
286
304
  `)
287
305
  }
288
306
  else if ('update' === opname) {
289
- const updateIdLine = idF ? ` "${idF}" => "${ent.name}_id",\n` : ''
307
+ // The id key plus every REQUIRED data member — the same shape the
308
+ // runtime validates — then the patch-fields note.
309
+ const updateItems = opRequestShape(ent, 'update').items
310
+ .filter((it: any) => !it.optional || it.name === idF)
311
+ .sort((a: any, b: any) =>
312
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
313
+ const updateLines = updateItems.map((it: any) =>
314
+ ` "${it.name}" => ${rbLit(it.type,
315
+ it.name === idF ? ent.name + '_id' : it.name)},\n`).join('')
290
316
  Content(`\`\`\`ruby
291
317
  result = client.${ent.Name}.update({
292
- ${updateIdLine} # Fields to update
318
+ ${updateLines} # Fields to update
293
319
  })
294
320
  \`\`\`
295
321
 
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, envName, entityIdField } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, envName, canonKey, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -8,6 +8,20 @@ import {
8
8
  } from '@voxgig/apidef'
9
9
 
10
10
 
11
+ // A type-correct, executable Ruby literal for a param: numeric/boolean/
12
+ // array/hash params render a typed literal; strings render the quoted
13
+ // placeholder (the doc test EXECUTES this block, so a comment placeholder
14
+ // would break it).
15
+ function rbLit(type: any, placeholder: string = 'example'): string {
16
+ const k = canonKey(type)
17
+ if ('INTEGER' === k || 'NUMBER' === k) return '1'
18
+ if ('BOOLEAN' === k) return 'true'
19
+ if ('ARRAY' === k) return '[]'
20
+ if ('OBJECT' === k) return '{}'
21
+ return `"${placeholder}"`
22
+ }
23
+
24
+
11
25
  const ReadmeTopQuick = cmp(function ReadmeTopQuick(props: any) {
12
26
  const { target, ctx$: { model } } = props
13
27
 
@@ -45,9 +59,21 @@ puts ${eName.toLowerCase()}s
45
59
  }
46
60
 
47
61
  if (opnames.includes('load')) {
62
+ // Every REQUIRED load-match key (id first, then parent path params like
63
+ // page_id) — the same shape the runtime resolves path params from, so
64
+ // the example always works.
65
+ const loadItems = opRequestShape(exampleEntity, 'load').items
66
+ .filter((it: any) => !it.optional || it.name === idF)
67
+ .sort((a: any, b: any) =>
68
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
69
+ const loadArg = 0 < loadItems.length
70
+ ? `{ ${loadItems.map((it: any) =>
71
+ `"${it.name}" => ${rbLit(it.type,
72
+ it.name === idF ? 'example_id' : 'example_' + it.name)}`).join(', ')} }`
73
+ : ''
48
74
  Content(`
49
75
  # Load a specific ${eName.toLowerCase()} (returns the bare record; raises on error)
50
- ${eName.toLowerCase()} = client.${eName}.load(${idF ? `{ "${idF}" => "example_id" }` : ''})
76
+ ${eName.toLowerCase()} = client.${eName}.load(${loadArg})
51
77
  puts ${eName.toLowerCase()}
52
78
  `)
53
79
  hasCall = true
@@ -39,7 +39,16 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
39
39
  const recBody = idF ? `{ "${idF}" => "test01" }` : '{}'
40
40
  let callArg = ''
41
41
  if (isMatchOp) {
42
- callArg = idF ? `{ "${idF}" => "test01" }` : ''
42
+ // Every REQUIRED match key (id first, then parent path params like
43
+ // page_id) — the same shape the runtime resolves path params from.
44
+ const items = opRequestShape(exampleEntity, primaryOp).items
45
+ .filter((it: any) => !it.optional || it.name === idF)
46
+ .sort((a: any, b: any) =>
47
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
48
+ callArg = 0 < items.length
49
+ ? `{ ${items.map((it: any) =>
50
+ `"${it.name}" => ${it.name === idF ? '"test01"' : rbLit(it.type)}`).join(', ')} }`
51
+ : ''
43
52
  } else if ('create' === primaryOp || 'update' === primaryOp) {
44
53
  const items = opRequestShape(exampleEntity, primaryOp).items
45
54
  .filter((it: any) => it.name !== idF && it.name !== 'id')
@@ -47,12 +56,14 @@ const ReadmeTopTest = cmp(function ReadmeTopTest(props: any) {
47
56
  const chosen = required.length ? required : items.slice(0, 3)
48
57
  callArg = `{ ${chosen.map((it: any) => `"${it.name}" => ${rbLit(it.type)}`).join(', ')} }`
49
58
  }
59
+ // A list result is an Array — name the variable accordingly.
60
+ const eVar = ename + ('list' === primaryOp ? 's' : '')
50
61
  Content(`\`\`\`ruby
51
62
  # Seed fixture data so offline calls resolve without a live server.
52
63
  client = ${model.const.Name}SDK.test({
53
64
  "entity" => { "${ename}" => { "test01" => ${recBody} } },
54
65
  })
55
- ${ename} = client.${eName}.${primaryOp}(${callArg})
66
+ ${eVar} = client.${eName}.${primaryOp}(${callArg})
56
67
  \`\`\`
57
68
  `)
58
69
  } else {
@@ -11,6 +11,7 @@ import {
11
11
  each,
12
12
  indent,
13
13
  isAuthActive,
14
+ resolveAuthPrefix,
14
15
  } from '@voxgig/sdkgen'
15
16
 
16
17
 
@@ -42,12 +43,8 @@ const Config = cmp(async function Config(props: any) {
42
43
  const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
43
44
 
44
45
  const authActive = isAuthActive(model)
45
- let authPrefix = 'Bearer'
46
- try {
47
- const v = getModelPath(model, `main.${KIT}.config.auth.prefix`,
48
- { only_active: false, required: false })
49
- if (null != v) authPrefix = v
50
- } catch (_e) { /* ignore */ }
46
+ // config.auth.prefix override -> spec-derived info.security.prefix -> 'Bearer'
47
+ const authPrefix = resolveAuthPrefix(model)
51
48
  const authBlock = authActive
52
49
  ? `auth: {
53
50
  prefix: '${authPrefix}',
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, canonToType, entityIdField, safeVarName } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, entityIdField, opRequestShape, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -99,10 +99,22 @@ const ReadmeEntity = cmp(function ReadmeEntity(props: any) {
99
99
  }
100
100
 
101
101
  if (opnames.includes('load')) {
102
+ // The id key plus every REQUIRED match key (parent path params like
103
+ // page_id) — the same shape that generates <Name>LoadMatch, so the
104
+ // example always type-checks.
105
+ const loadItems = opRequestShape(entity, 'load').items
106
+ .filter((it: any) => !it.optional || it.name === idF)
107
+ .sort((a: any, b: any) =>
108
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
109
+ const loadArg = 0 < loadItems.length
110
+ ? `{ ${loadItems.map((it: any) =>
111
+ `${it.name}: ${exampleValue(entity, entity.op && entity.op.load, it.name,
112
+ it.name === idF ? entity.name + '_id' : it.name)}`).join(', ')} }`
113
+ : ''
102
114
  Content(`#### Example: Load
103
115
 
104
116
  \`\`\`ts
105
- const ${eVar} = await client.${entity.Name}().load(${idF ? `{ ${idF}: ${exampleValue(entity, entity.op && entity.op.load, idF, entity.name + '_id')} }` : ''})
117
+ const ${eVar} = await client.${entity.Name}().load(${loadArg})
106
118
  \`\`\`
107
119
 
108
120
  `)
@@ -119,16 +131,20 @@ const ${eVar}s = await client.${entity.Name}().list()
119
131
  }
120
132
 
121
133
  if (opnames.includes('create')) {
134
+ // Members come from the SAME shape that generates <Name>CreateData:
135
+ // every required member must appear (the /* type */ placeholders also
136
+ // mark the block as an illustration for the doc gates); an
137
+ // all-optional create renders an empty — still assignable — literal.
138
+ const createItems = opRequestShape(entity, 'create').items
139
+ .filter((it: any) => !it.optional)
122
140
  Content(`#### Example: Create
123
141
 
124
142
  \`\`\`ts
125
143
  const ${eVar} = await client.${entity.Name}().create({
126
144
  `)
127
- each(fields, (field: any) => {
128
- if ('id' !== field.name && field.req) {
129
- Content(` ${field.name}: /* ${canonToType(field.type, target.name)} */,
145
+ createItems.map((it: any) => {
146
+ Content(` ${it.name}: /* ${canonToType(it.type, target.name)} */,
130
147
  `)
131
- }
132
148
  })
133
149
  Content(`})
134
150
  \`\`\`
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive, packageName, envName, opRequestShape, entityIdField, entityDataIdField, safeVarName } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, packageName, envName, opRequestShape, entityIdField, entityDataIdField, entityOps, safeVarName } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -18,9 +18,15 @@ const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
18
18
  // Find the first published entity for examples
19
19
  const exampleEntity = Object.values(entity).find((e: any) => e.active !== false) as any
20
20
 
21
- // Find a nested entity if available
21
+ // Find a nested entity if available: one with a parent chain
22
+ // (relations.ancestors), an active load op, and a required non-id load
23
+ // param to demonstrate (the parent key, e.g. page_id).
22
24
  const nestedEntity = Object.values(entity).find((e: any) =>
23
- e.active !== false && e.ancestors && e.ancestors.length > 0
25
+ e.active !== false &&
26
+ e.relations && e.relations.ancestors && 0 < e.relations.ancestors.length &&
27
+ entityOps(e).includes('load') &&
28
+ opRequestShape(e, 'load').items.some((it: any) =>
29
+ !it.optional && it.name !== entityIdField(e))
24
30
  ) as any
25
31
 
26
32
  const ctor = isAuthActive(model)
@@ -44,7 +50,7 @@ const client = ${ctor}
44
50
  // reserved word (`const delete = ...` is a TS1109 syntax error).
45
51
  const eVar = safeVarName(eName.toLowerCase(), 'ts')
46
52
  const article = /^[aeiou]/i.test(eName) ? 'an' : 'a'
47
- const opnames = Object.keys(exampleEntity.op || {})
53
+ const opnames = entityOps(exampleEntity)
48
54
  // Model-driven id key: `idF` is the entity's id-like MATCH field name, or
49
55
  // null when it has none (then load/remove match on no argument).
50
56
  const idF = entityIdField(exampleEntity)
@@ -74,22 +80,26 @@ for (const ${eVar} of ${eVar}s) {
74
80
  const neName = nom(nestedEntity, 'Name')
75
81
  const neVar = safeVarName(neName.toLowerCase(), 'ts')
76
82
  const neArticle = /^[aeiou]/i.test(neName) ? 'an' : 'a'
77
- const parentFields = (nestedEntity.fields || [])
78
- .filter((f: any) => f.name !== 'id' && f.name.endsWith('_id'))
79
- const parentParam = parentFields.length > 0 ? parentFields[0].name : 'parent_id'
80
83
  const loadOp = nestedEntity.op && nestedEntity.op.load
81
84
 
82
- // Model-driven id key: only emit an id match line if this nested entity
83
- // actually has an id-like key field (some response-wrapped specs do not).
85
+ // Model-driven match: every REQUIRED load-match key the same shape
86
+ // that generates <Name>LoadMatch, so the example always type-checks.
87
+ // Parent keys (e.g. page_id) first, the entity's own id last.
84
88
  const neIdF = entityIdField(nestedEntity)
85
- const neMatchLines = [` ${parentParam}: ${exampleValue(nestedEntity, loadOp, parentParam, 'example')},`]
86
- if (neIdF) {
87
- neMatchLines.push(` ${neIdF}: ${exampleValue(nestedEntity, loadOp, neIdF, 'example_id')},`)
88
- }
89
+ const neRequired = opRequestShape(nestedEntity, 'load').items
90
+ .filter((it: any) => !it.optional)
91
+ .sort((a: any, b: any) =>
92
+ (a.name === neIdF ? 1 : 0) - (b.name === neIdF ? 1 : 0))
93
+ const parentItem = neRequired.find((it: any) => it.name !== neIdF) as any
94
+ const parentParam = parentItem && parentItem.name
95
+ const parentName = parentParam ? parentParam.replace(/_id$/, '') : 'its parent'
96
+ const neMatchLines = neRequired.map((it: any) =>
97
+ ` ${it.name}: ${exampleValue(nestedEntity, loadOp, it.name,
98
+ it.name === neIdF ? 'example_id' : 'example_' + it.name)},`)
89
99
 
90
100
  Content(`### 3. Load ${neArticle} ${neName.toLowerCase()}
91
101
 
92
- ${neName} is nested under ${eName}, so provide the \`${parentParam}\`.
102
+ ${neName} is nested under ${parentName}, so provide the \`${parentParam}\`.
93
103
  \`load()\` returns the entity directly and throws on failure:
94
104
 
95
105
  \`\`\`ts
@@ -106,13 +116,25 @@ ${neMatchLines.join('\n')}
106
116
  `)
107
117
  }
108
118
  else if (opnames.includes('load')) {
119
+ // Every REQUIRED load-match key (id first) — the same shape that
120
+ // generates <Name>LoadMatch, so the example always type-checks.
121
+ const loadRequired = opRequestShape(exampleEntity, 'load').items
122
+ .filter((it: any) => !it.optional || it.name === idF)
123
+ .sort((a: any, b: any) =>
124
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
125
+ const loadArg = 0 < loadRequired.length
126
+ ? `{ ${loadRequired.map((it: any) =>
127
+ `${it.name}: ${exampleValue(exampleEntity, exampleEntity.op && exampleEntity.op.load, it.name,
128
+ it.name === idF ? 'example_id' : 'example_' + it.name)}`).join(', ')} }`
129
+ : ''
130
+
109
131
  Content(`### 3. Load ${article} ${eName.toLowerCase()}
110
132
 
111
133
  \`load()\` returns the entity directly and throws on failure:
112
134
 
113
135
  \`\`\`ts
114
136
  try {
115
- const ${eVar} = await client.${eName}().load(${idF ? `{ ${idF}: ${exampleValue(exampleEntity, exampleEntity.op && exampleEntity.op.load, idF, 'example_id')} }` : ''})
137
+ const ${eVar} = await client.${eName}().load(${loadArg})
116
138
  console.log(${eVar})
117
139
  } catch (err) {
118
140
  console.error('load failed:', err)
@@ -133,12 +155,19 @@ try {
133
155
  // <Name>CreateData (a TS2345); update is a patch, so a couple of fields
134
156
  // suffice.
135
157
  const exampleFields = (opname: string): string[] => {
158
+ // ids are rendered separately as the match key for update/remove; a
159
+ // REQUIRED id stays (dropping it makes the literal unassignable).
136
160
  const items = opRequestShape(exampleEntity, opname).items
137
- .filter((it: any) => it.name !== idF && it.name !== 'id')
161
+ .filter((it: any) => (it.name !== idF && it.name !== 'id') ||
162
+ ('create' === opname && !it.optional))
138
163
  const required = items.filter((it: any) => !it.optional)
164
+ const optional = items.filter((it: any) => it.optional)
165
+ // Required members must all appear or the literal is not assignable
166
+ // to the typed <Name>{Create,Update}Data; pad update (a patch) with a
167
+ // sample optional field or two.
139
168
  const chosen = 'create' === opname
140
169
  ? (required.length ? required : items.slice(0, 2))
141
- : items.slice(0, 2)
170
+ : required.concat(optional).slice(0, Math.max(2, required.length))
142
171
  return chosen.map((it: any) =>
143
172
  ` ${it.name}: ${exampleValue(exampleEntity, exampleEntity.op[opname], it.name, 'example_' + it.name)},`)
144
173
  }
@@ -175,8 +204,17 @@ const updated = await client.${eName}().update({${updateBody}})
175
204
  `)
176
205
  }
177
206
  if (opnames.includes('remove')) {
207
+ // Every REQUIRED remove-match key: the id (off the created record
208
+ // when possible) plus parent keys like page_id.
209
+ const removeLines = opRequestShape(exampleEntity, 'remove').items
210
+ .filter((it: any) => !it.optional || it.name === idF)
211
+ .sort((a: any, b: any) =>
212
+ (a.name === idF ? 0 : 1) - (b.name === idF ? 0 : 1))
213
+ .map((it: any) => it.name === idF
214
+ ? ` ${it.name}: ${idValueFor('remove')},`
215
+ : ` ${it.name}: ${exampleValue(exampleEntity, exampleEntity.op.remove, it.name, 'example_' + it.name)},`)
178
216
  Content(`// Remove
179
- await client.${eName}().remove(${idF ? `{\n ${idF}: ${idValueFor('remove')},\n}` : ''})
217
+ await client.${eName}().remove(${removeLines.length ? `{\n${removeLines.join('\n')}\n}` : ''})
180
218
  `)
181
219
  }
182
220
  Content(`\`\`\`