api 4.4.0 → 5.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -5
  3. package/bin/api +2 -0
  4. package/dist/bin.d.ts +1 -0
  5. package/dist/bin.js +91 -0
  6. package/dist/cache.d.ts +30 -0
  7. package/dist/cache.js +217 -0
  8. package/dist/cli/codegen/index.d.ts +4 -0
  9. package/dist/cli/codegen/index.js +23 -0
  10. package/dist/cli/codegen/language.d.ts +27 -0
  11. package/dist/cli/codegen/language.js +19 -0
  12. package/dist/cli/codegen/languages/typescript.d.ts +99 -0
  13. package/dist/cli/codegen/languages/typescript.js +762 -0
  14. package/dist/cli/commands/index.d.ts +4 -0
  15. package/dist/cli/commands/index.js +9 -0
  16. package/dist/cli/commands/install.d.ts +3 -0
  17. package/dist/cli/commands/install.js +230 -0
  18. package/dist/cli/lib/prompt.d.ts +9 -0
  19. package/dist/cli/lib/prompt.js +81 -0
  20. package/dist/cli/logger.d.ts +1 -0
  21. package/dist/cli/logger.js +16 -0
  22. package/dist/cli/storage.d.ts +105 -0
  23. package/dist/cli/storage.js +264 -0
  24. package/dist/core/getJSONSchemaDefaults.d.ts +15 -0
  25. package/dist/core/getJSONSchemaDefaults.js +62 -0
  26. package/dist/core/index.d.ts +32 -0
  27. package/dist/core/index.js +143 -0
  28. package/dist/core/parseResponse.d.ts +1 -0
  29. package/dist/core/parseResponse.js +65 -0
  30. package/dist/core/prepareAuth.d.ts +5 -0
  31. package/dist/core/prepareAuth.js +55 -0
  32. package/dist/core/prepareParams.d.ts +24 -0
  33. package/dist/core/prepareParams.js +351 -0
  34. package/dist/core/prepareServer.d.ts +13 -0
  35. package/dist/core/prepareServer.js +50 -0
  36. package/dist/fetcher.d.ts +53 -0
  37. package/dist/fetcher.js +149 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +276 -0
  40. package/dist/packageInfo.d.ts +2 -0
  41. package/dist/packageInfo.js +6 -0
  42. package/package.json +65 -25
  43. package/src/.sink.d.ts +1 -0
  44. package/src/bin.ts +20 -0
  45. package/src/cache.ts +212 -0
  46. package/src/cli/codegen/index.ts +31 -0
  47. package/src/cli/codegen/language.ts +47 -0
  48. package/src/cli/codegen/languages/typescript.ts +798 -0
  49. package/src/cli/commands/index.ts +5 -0
  50. package/src/cli/commands/install.ts +196 -0
  51. package/src/cli/lib/prompt.ts +29 -0
  52. package/src/cli/logger.ts +10 -0
  53. package/src/cli/storage.ts +297 -0
  54. package/src/core/getJSONSchemaDefaults.ts +74 -0
  55. package/src/core/index.ts +108 -0
  56. package/src/{lib/parseResponse.js → core/parseResponse.ts} +5 -7
  57. package/src/core/prepareAuth.ts +85 -0
  58. package/src/core/prepareParams.ts +338 -0
  59. package/src/{lib/prepareServer.js → core/prepareServer.ts} +13 -12
  60. package/src/fetcher.ts +126 -0
  61. package/src/index.ts +212 -0
  62. package/src/packageInfo.ts +3 -0
  63. package/src/typings.d.ts +3 -0
  64. package/tsconfig.json +24 -0
  65. package/src/cache.js +0 -209
  66. package/src/index.js +0 -175
  67. package/src/lib/getSchema.js +0 -34
  68. package/src/lib/index.js +0 -11
  69. package/src/lib/prepareAuth.js +0 -69
  70. package/src/lib/prepareParams.js +0 -198
@@ -0,0 +1,5 @@
1
+ import installCommand from './install';
2
+
3
+ export default {
4
+ install: installCommand,
5
+ };
@@ -0,0 +1,196 @@
1
+ import type { SupportedLanguages } from '../codegen';
2
+
3
+ import { Command, Option } from 'commander';
4
+ import ora from 'ora';
5
+ import Oas from 'oas';
6
+
7
+ import codegen from '../codegen';
8
+ import Fetcher from '../../fetcher';
9
+ import Storage from '../storage';
10
+ import logger from '../logger';
11
+
12
+ import promptTerminal from '../lib/prompt';
13
+
14
+ import figures from 'figures';
15
+ import validateNPMPackageName from 'validate-npm-package-name';
16
+
17
+ // @todo log logs to `.api/.logs` and have `.logs` ignored
18
+ const cmd = new Command();
19
+ cmd
20
+ .name('install')
21
+ .description('install an API SDK into your codebase')
22
+ .argument('<uri>', 'an API to install')
23
+ .addOption(
24
+ new Option('-l, --lang <language>', 'SDK language').choices([
25
+ 'js', // User generally wants JS, we'll prompt if they want CJS or ESM files.
26
+ 'js-cjs',
27
+ 'js-esm',
28
+ 'ts',
29
+ ])
30
+ )
31
+ .action(async (uri: string, options: { lang: string }) => {
32
+ let language: SupportedLanguages;
33
+ if (options.lang) {
34
+ language = options.lang as SupportedLanguages;
35
+ } else {
36
+ ({ value: language } = await promptTerminal({
37
+ type: 'select',
38
+ name: 'value',
39
+ message: 'What language would you like to generate an SDK for?',
40
+ choices: [
41
+ { title: 'TypeScript', value: 'ts' },
42
+ { title: 'JavaScript', value: 'js' },
43
+ ],
44
+ initial: 1,
45
+ }));
46
+ }
47
+
48
+ // Because our TS generation outputs raw TS source files we don't need to worry about CJS/ESM.
49
+ if (language === 'js') {
50
+ ({ value: language } = await promptTerminal({
51
+ type: 'select',
52
+ name: 'value',
53
+ message: 'How are your project imports and exports structured?',
54
+ choices: [
55
+ { title: 'CommonJS', description: 'require/exports', value: 'cjs' },
56
+ { title: 'ECMAScript Modules', description: 'import/export', value: 'esm' },
57
+ ],
58
+ initial: 0,
59
+ format: sel => (sel === 'cjs' ? 'js-cjs' : 'js-esm'),
60
+ }));
61
+ }
62
+
63
+ // @todo let them know that we're going to be creating a `.api/ directory
64
+ // @todo detect if they have a gitigore and .npmignore and if .api woudl be ignored by that
65
+ // @todo don't support swagger files without upconverting them
66
+
67
+ if (Storage.isInLockFile({ source: uri })) {
68
+ // @todo
69
+ // logger(`It looks like you already have this API installed. Would you like to update it?`);
70
+ }
71
+
72
+ let identifier;
73
+ if (Fetcher.isAPIRegistryUUID(uri)) {
74
+ identifier = Fetcher.getProjectPrefixFromRegistryUUID(uri);
75
+ } else {
76
+ ({ value: identifier } = await promptTerminal({
77
+ type: 'text',
78
+ name: 'value',
79
+ message:
80
+ 'What would you like to identify this API as? This will be how you import the SDK. (e.g. entering `petstore` would result in `@api/petstore`)',
81
+ validate: value => {
82
+ if (!value) {
83
+ return false;
84
+ }
85
+
86
+ // Is this identifier already in storage?
87
+ if (Storage.isInLockFile({ identifier: value })) {
88
+ return `"${value}" is already taken in your \`.api/\` directory. Please enter another identifier.`;
89
+ }
90
+
91
+ const isValidForNPM = validateNPMPackageName(`@api/${value}`);
92
+ if (!isValidForNPM.validForNewPackages) {
93
+ // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can
94
+ // only surface the first to the user.
95
+ return isValidForNPM.errors[0];
96
+ }
97
+
98
+ return true;
99
+ },
100
+ }));
101
+ }
102
+
103
+ if (!identifier) {
104
+ logger('You must tell us what you would like to identify this API as in order to install it.', true);
105
+ process.exit(1);
106
+ }
107
+
108
+ let spinner = ora('Fetching your API').start();
109
+ const storage = new Storage(uri, identifier);
110
+
111
+ const oas = await storage
112
+ .load()
113
+ .then(res => {
114
+ spinner.succeed(spinner.text);
115
+ return res;
116
+ })
117
+ .then(Oas.init)
118
+ .catch(err => {
119
+ // @todo cleanup installed files
120
+ spinner.fail(spinner.text);
121
+ logger(err.message, true);
122
+ process.exit(1);
123
+ });
124
+
125
+ // @todo look for a prettier config and if we find one ask them if we should use it
126
+ spinner = ora('Generating your SDK').start();
127
+ const generator = codegen(language, oas, './openapi.json', identifier);
128
+ const sdkSource = await generator
129
+ .generator()
130
+ .then(res => {
131
+ spinner.succeed(spinner.text);
132
+ return res;
133
+ })
134
+ .catch(err => {
135
+ // @todo cleanup installed files
136
+ spinner.fail(spinner.text);
137
+ logger(err.message, true);
138
+ process.exit(1);
139
+ });
140
+
141
+ spinner = ora('Saving your SDK into your codebase').start();
142
+ await storage
143
+ .saveSourceFiles(sdkSource)
144
+ .then(() => {
145
+ spinner.succeed(spinner.text);
146
+ })
147
+ .catch(err => {
148
+ // @todo cleanup installed files
149
+ spinner.fail(spinner.text);
150
+ logger(err.message, true);
151
+ process.exit(1);
152
+ });
153
+
154
+ if (generator.hasRequiredPackages()) {
155
+ logger(`${figures.warning} This generator requires some packages to be installed alongside it:`);
156
+ Object.entries(generator.requiredPackages).forEach(([pkg, pkgInfo]) => {
157
+ logger(` ${figures.pointerSmall} ${pkg}: ${pkgInfo.reason} ${pkgInfo.url}`);
158
+ });
159
+
160
+ await promptTerminal({
161
+ type: 'confirm',
162
+ name: 'value',
163
+ message: 'OK to proceed with package installation?',
164
+ initial: true,
165
+ }).then(({ value }) => {
166
+ if (!value) {
167
+ // @todo cleanup installed files
168
+ logger('Installation cancelled.', true);
169
+ process.exit(1);
170
+ }
171
+ });
172
+
173
+ spinner = ora('Installing required packages').start();
174
+ try {
175
+ await generator.installer(storage);
176
+ spinner.succeed(spinner.text);
177
+ } catch (err) {
178
+ // @todo cleanup installed files
179
+ spinner.fail(spinner.text);
180
+ logger(err.message, true);
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ logger('🚀 All done!');
186
+ })
187
+ .addHelpText(
188
+ 'after',
189
+ `
190
+ Examples:
191
+ $ api install @developers/v2.0#nysezql0wwo236
192
+ $ api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
193
+ $ api install ./petstore.json`
194
+ );
195
+
196
+ export default cmd;
@@ -0,0 +1,29 @@
1
+ import prompts from 'prompts';
2
+
3
+ /**
4
+ * The `prompts` library doesn't always interpret CTRL+C and release the terminal back to the user
5
+ * so we need handle this ourselves. This function is just a simple overload of the main `prompts`
6
+ * import that we use.
7
+ *
8
+ * @see {@link https://github.com/terkelg/prompts/issues/252}
9
+ */
10
+ export default async function promptTerminal<T extends string = string>(
11
+ question: prompts.PromptObject<T>,
12
+ options?: prompts.Options
13
+ ) {
14
+ const enableTerminalCursor = () => {
15
+ process.stdout.write('\x1B[?25h');
16
+ };
17
+
18
+ const onState = (state: { aborted: boolean }) => {
19
+ if (state.aborted) {
20
+ // If we don't re-enable the terminal cursor before exiting the program, the cursor will
21
+ // remain hidden.
22
+ enableTerminalCursor();
23
+ process.stdout.write('\n');
24
+ process.exit(1);
25
+ }
26
+ };
27
+
28
+ return prompts({ ...question, onState }, options);
29
+ }
@@ -0,0 +1,10 @@
1
+ /* eslint-disable no-console */
2
+ import chalk from 'chalk';
3
+
4
+ export default function logger(log: string, error?: boolean) {
5
+ if (error) {
6
+ console.error(chalk.red(log));
7
+ } else {
8
+ console.log(log);
9
+ }
10
+ }
@@ -0,0 +1,297 @@
1
+ import type { OASDocument } from 'oas/@types/rmoas.types';
2
+
3
+ import ssri from 'ssri';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import makeDir from 'make-dir';
7
+
8
+ import { PACKAGE_VERSION } from '../packageInfo';
9
+
10
+ import Fetcher from '../fetcher';
11
+
12
+ export default class Storage {
13
+ static dir: string;
14
+
15
+ static lockfile: false | Lockfile;
16
+
17
+ /**
18
+ * This is the original source that the file came from (relative/absolute file path, URL, ReadMe
19
+ * registry UUID, etc.).
20
+ */
21
+ source: string;
22
+
23
+ identifier: string;
24
+
25
+ fetcher: Fetcher;
26
+
27
+ constructor(source: string, identifier?: string) {
28
+ Storage.setStorageDir();
29
+
30
+ this.fetcher = new Fetcher(source);
31
+
32
+ this.source = source;
33
+ this.identifier = identifier;
34
+
35
+ // This should default to false so we have awareness if we've looked at the lockfile yet.
36
+ Storage.lockfile = false;
37
+ }
38
+
39
+ static getLockfilePath() {
40
+ return path.join(Storage.dir, 'api.json');
41
+ }
42
+
43
+ static getAPIsDir() {
44
+ return path.join(Storage.dir, 'apis');
45
+ }
46
+
47
+ static setStorageDir(dir?: string) {
48
+ if (dir) {
49
+ Storage.dir = dir;
50
+ return;
51
+ } else if (Storage.dir) {
52
+ // If we already have a storage dir set and aren't explicitly it to something new then we
53
+ // shouldn't overwrite what we've already got.
54
+ return;
55
+ }
56
+
57
+ Storage.dir = makeDir.sync(path.join(process.cwd(), '.api'));
58
+
59
+ makeDir.sync(Storage.getAPIsDir());
60
+ }
61
+
62
+ /**
63
+ * Reset the state of the entire storage system.
64
+ *
65
+ * This will completely destroy the contents of the `.api/` directory!
66
+ */
67
+ static async reset() {
68
+ if (Storage.getLockfilePath()) {
69
+ await fs.promises.writeFile(Storage.getLockfilePath(), JSON.stringify(Storage.getDefaultLockfile(), null, 2));
70
+ }
71
+
72
+ if (Storage.getAPIsDir()) {
73
+ await fs.promises.rm(Storage.getAPIsDir(), { recursive: true });
74
+ await fs.promises.mkdir(Storage.getAPIsDir(), { recursive: true });
75
+ }
76
+ }
77
+
78
+ static getDefaultLockfile(): Lockfile {
79
+ return {
80
+ version: '1.0',
81
+ apis: [],
82
+ };
83
+ }
84
+
85
+ static generateIntegrityHash(definition: OASDocument) {
86
+ return ssri
87
+ .fromData(JSON.stringify(definition), {
88
+ algorithms: ['sha512'],
89
+ })
90
+ .toString();
91
+ }
92
+
93
+ static getLockfile() {
94
+ if (typeof Storage.lockfile === 'object') {
95
+ return Storage.lockfile;
96
+ }
97
+
98
+ if (fs.existsSync(Storage.getLockfilePath())) {
99
+ const file = fs.readFileSync(Storage.getLockfilePath(), 'utf8');
100
+ Storage.lockfile = JSON.parse(file) as Lockfile;
101
+ } else {
102
+ Storage.lockfile = Storage.getDefaultLockfile();
103
+ }
104
+
105
+ return Storage.lockfile;
106
+ }
107
+
108
+ static isInLockFile(search: { identifier?: string; source?: string }) {
109
+ // Because this method may run before we initialize a new storage object we should make sure
110
+ // that we have a storage directory present.
111
+ Storage.setStorageDir();
112
+
113
+ if (!search.identifier && !search.source) {
114
+ throw new TypeError('An `identifier` or `source` must be supplied to this method to search in the lockfile.');
115
+ }
116
+
117
+ const lockfile = Storage.getLockfile();
118
+ if (typeof lockfile !== 'object' || lockfile === null || !lockfile.apis) {
119
+ return false;
120
+ }
121
+
122
+ const res = lockfile.apis.find(a => {
123
+ if (search.identifier) {
124
+ return a.identifier === search.identifier;
125
+ }
126
+
127
+ return a.source === search.source;
128
+ });
129
+
130
+ return res === undefined ? false : res;
131
+ }
132
+
133
+ setIdentifier(identifier: string) {
134
+ this.identifier = identifier;
135
+ }
136
+
137
+ /**
138
+ * Determine if the current spec + identifier we're working with is already in the lockfile.
139
+ */
140
+ isInLockfile() {
141
+ return Boolean(this.getFromLockfile());
142
+ }
143
+
144
+ /**
145
+ * Retrieve the lockfile record for the current spec + identifier if it exists in the lockfile.
146
+ */
147
+ getFromLockfile() {
148
+ const lockfile = Storage.getLockfile();
149
+ return lockfile.apis.find(a => a.identifier === this.identifier);
150
+ }
151
+
152
+ getIdentifierStorageDir() {
153
+ if (!this.isInLockfile()) {
154
+ throw new Error(`${this.source} has not been saved to storage yet and must do so before being retrieved.`);
155
+ }
156
+
157
+ return path.join(Storage.getAPIsDir(), this.identifier);
158
+ }
159
+
160
+ getAPIDefinition() {
161
+ const file = fs.readFileSync(path.join(this.getIdentifierStorageDir(), 'openapi.json'), 'utf8');
162
+
163
+ return JSON.parse(file);
164
+ }
165
+
166
+ saveSourceFiles(files: Record<string, string>) {
167
+ if (!this.isInLockfile()) {
168
+ throw new Error(`${this.source} has not been saved to storage yet and must do so before being retrieved.`);
169
+ }
170
+
171
+ return new Promise(resolve => {
172
+ const savedSource: string[] = [];
173
+ Object.entries(files).forEach(([fileName, contents]) => {
174
+ const sourceFilePath = path.join(this.getIdentifierStorageDir(), fileName);
175
+
176
+ fs.writeFileSync(sourceFilePath, contents);
177
+
178
+ savedSource.push(sourceFilePath);
179
+ });
180
+
181
+ resolve(savedSource);
182
+ });
183
+ }
184
+
185
+ async load() {
186
+ return this.fetcher.load().then(async spec => this.save(spec));
187
+ }
188
+
189
+ /**
190
+ * @example <caption>Storage directory structure</caption>
191
+ * .api/
192
+ * ├── api.json // The `package-lock.json` equivalent that records everything that's
193
+ * | // installed, when it was installed, what the original source was,
194
+ * | // and what version of `api` was used.
195
+ * └── apis/
196
+ * ├── readme/
197
+ * | ├── node_modules/
198
+ * │ ├── index.js // We may offer the option to export a raw TS file for folks who want
199
+ * | | // that, but for now it'll be a compiled JS file.
200
+ * │ ├── index.d.ts // All types for their SDK, ready to use in an IDE.
201
+ * │ |── openapi.json
202
+ * │ └── package.json
203
+ * └── petstore/
204
+ * ├── node_modules/
205
+ * ├── index.js
206
+ * ├── index.d.ts
207
+ * ├── openapi.json
208
+ * └── package.json
209
+ *
210
+ * @param spec
211
+ */
212
+ save(spec: OASDocument) {
213
+ if (!this.identifier) {
214
+ throw new TypeError('An identifier must be set before saving the API definition into storage.');
215
+ }
216
+
217
+ // Create our main `.api/` directory.
218
+ if (!fs.existsSync(Storage.dir)) {
219
+ fs.mkdirSync(Storage.dir, { recursive: true });
220
+ }
221
+
222
+ // Create the `.api/apis/` diretory where we'll be storing API definitions.
223
+ if (!fs.existsSync(Storage.getAPIsDir())) {
224
+ fs.mkdirSync(Storage.getAPIsDir(), { recursive: true });
225
+ }
226
+
227
+ if (!this.isInLockfile()) {
228
+ // This API doesn't exist within our storage system yet so we need to record it in the
229
+ // lockfile.
230
+ const identifierStorageDir = path.join(Storage.getAPIsDir(), this.identifier);
231
+ const saved = JSON.stringify(spec, null, 2);
232
+
233
+ // Create the `.api/apis/<identifier>` directory where we'll be storing this API definition
234
+ // and eventually its codegen'd SDK.
235
+ if (!fs.existsSync(identifierStorageDir)) {
236
+ fs.mkdirSync(identifierStorageDir, { recursive: true });
237
+ }
238
+
239
+ (Storage.lockfile as Lockfile).apis.push({
240
+ identifier: this.identifier,
241
+ source: this.source,
242
+ integrity: Storage.generateIntegrityHash(spec),
243
+ installerVersion: PACKAGE_VERSION,
244
+ } as LockfileAPI);
245
+
246
+ fs.writeFileSync(path.join(identifierStorageDir, 'openapi.json'), saved);
247
+ fs.writeFileSync(Storage.getLockfilePath(), JSON.stringify(Storage.lockfile, null, 2));
248
+ } else {
249
+ // Is this the same spec that we already have? Should we update it? // @todo
250
+ }
251
+
252
+ return spec;
253
+ }
254
+ }
255
+
256
+ export type Lockfile = {
257
+ /**
258
+ * The `api.json` schema version. This will only ever change if we introduce breaking changes to
259
+ * this store.
260
+ */
261
+ version: '1.0';
262
+ apis: LockfileAPI[];
263
+ };
264
+
265
+ export type LockfileAPI = {
266
+ /**
267
+ * A unique identifier of the API. This'll be used to do requires on `@api/<identifier>` and also
268
+ * where the SDK code will be located in `.api/apis/<identifier>`.
269
+ *
270
+ * @example petstore
271
+ */
272
+ identifier: string;
273
+
274
+ /**
275
+ * The original source that was used to generate the SDK with.
276
+ *
277
+ * @example https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
278
+ * @example ./petstore.json
279
+ * @example @developers/v2.0#nysezql0wwo236
280
+ */
281
+ source: string;
282
+
283
+ /**
284
+ * An integrity hash that will be used to determine on `npx api update` calls if the API has
285
+ * changed since the SDK was last generated.
286
+ *
287
+ * @example sha512-ld+djZk8uRWmzXC+JYla1PTBScg0NjP/8x9vOOKRW+DuJ3NNMRjrpfbY7T77Jgnc87dZZsU49robbQfYe3ukug==
288
+ */
289
+ integrity: string;
290
+
291
+ /**
292
+ * The version of `api` that was used to install this SDK.
293
+ *
294
+ * @example 5.0.0
295
+ */
296
+ installerVersion: string;
297
+ };
@@ -0,0 +1,74 @@
1
+ import type { SchemaObject } from 'oas/@types/rmoas.types';
2
+ import type { SchemaWrapper } from 'oas/@types/operation/get-parameters-as-json-schema';
3
+ import traverse from 'json-schema-traverse';
4
+
5
+ /**
6
+ * Run through a JSON Schema object and compose up an object containing default data for any schema
7
+ * property that is required and also has a defined default.
8
+ *
9
+ * Code partially adapted from the `json-schema-default` package but modified to only return
10
+ * defaults of required properties.
11
+ *
12
+ * @todo This is a good candidate to be moved into a core `oas` library method.
13
+ * @see {@link https://github.com/mdornseif/json-schema-default}
14
+ * @param jsonSchemas
15
+ */
16
+ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
17
+ return jsonSchemas
18
+ .map(({ type: payloadType, schema: jsonSchema }) => {
19
+ const defaults: Record<string, unknown> = {};
20
+ traverse(
21
+ jsonSchema,
22
+ (
23
+ schema: SchemaObject,
24
+ pointer: string,
25
+ rootSchema: SchemaObject,
26
+ parentPointer: string,
27
+ parentKeyword: string,
28
+ parentSchema: SchemaObject,
29
+ indexProperty: string
30
+ ) => {
31
+ if (!pointer.startsWith('/properties/')) {
32
+ return;
33
+ }
34
+
35
+ if (Array.isArray(parentSchema?.required) && parentSchema.required.includes(indexProperty)) {
36
+ if (schema.type === 'object' && indexProperty) {
37
+ defaults[indexProperty] = {};
38
+ }
39
+
40
+ let destination = defaults;
41
+ if (parentPointer) {
42
+ // To map nested objects correct we need to pick apart the parent pointer.
43
+ parentPointer
44
+ .replace(/\/properties/g, '')
45
+ .split('/')
46
+ .forEach((subSchema: string) => {
47
+ if (subSchema === '') {
48
+ return;
49
+ }
50
+
51
+ destination = (destination?.[subSchema] as Record<string, unknown>) || {};
52
+ });
53
+ }
54
+
55
+ if (schema.default !== undefined) {
56
+ if (indexProperty !== undefined) {
57
+ destination[indexProperty] = schema.default;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ );
63
+
64
+ if (!Object.keys(defaults).length) {
65
+ return {};
66
+ }
67
+
68
+ return {
69
+ // @todo should we filter out empty and undefined objects from here with `remove-undefined-objects`?
70
+ [payloadType]: defaults,
71
+ };
72
+ })
73
+ .reduce((prev, next) => Object.assign(prev, next));
74
+ }