@friggframework/devtools 2.0.0--canary.666.81647c4.0 → 2.0.0--canary.668.8e53aa9.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 (26) hide show
  1. package/frigg-cli/README.md +1 -38
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -3
  3. package/frigg-cli/__tests__/unit/commands/deploy.test.js +0 -3
  4. package/frigg-cli/build-command/index.js +0 -6
  5. package/frigg-cli/deploy-command/index.js +0 -6
  6. package/frigg-cli/index.js +1 -13
  7. package/frigg-cli/init-command/backend-first-handler.js +46 -25
  8. package/frigg-cli/start-command/index.js +0 -4
  9. package/frigg-cli/start-command/start-command.test.js +0 -3
  10. package/infrastructure/create-frigg-infrastructure.js +1 -4
  11. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
  12. package/package.json +7 -7
  13. package/frigg-cli/__tests__/unit/commands/validate-wiring.test.js +0 -92
  14. package/frigg-cli/validate-command/__tests__/init-validation.test.js +0 -63
  15. package/frigg-cli/validate-command/__tests__/integrations-checks.test.js +0 -133
  16. package/frigg-cli/validate-command/__tests__/validate-app-definition.test.js +0 -326
  17. package/frigg-cli/validate-command/__tests__/validate-command.test.js +0 -241
  18. package/frigg-cli/validate-command/checks/config.js +0 -468
  19. package/frigg-cli/validate-command/checks/integrations.js +0 -274
  20. package/frigg-cli/validate-command/format-report.js +0 -58
  21. package/frigg-cli/validate-command/index.js +0 -107
  22. package/frigg-cli/validate-command/load-app-definition.js +0 -93
  23. package/frigg-cli/validate-command/schema-issues.js +0 -154
  24. package/frigg-cli/validate-command/validate-app-definition.js +0 -60
  25. package/infrastructure/app-definition-schema-contract.test.js +0 -607
  26. package/infrastructure/create-frigg-infrastructure.test.js +0 -51
@@ -1,274 +0,0 @@
1
- /**
2
- * Structural checks of the integration classes listed in
3
- * `Definition.integrations`. JSON Schema cannot describe a class, so these
4
- * checks mirror what core does with each class at runtime
5
- * (IntegrationBase, Module.validateDefinition, the integration builder).
6
- */
7
-
8
- const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
9
-
10
- function issue(severity, code, pointer, message, hint) {
11
- return { severity, code, pointer, message, hint };
12
- }
13
-
14
- function describe(value) {
15
- if (value === null) return 'null';
16
- if (Array.isArray(value)) return 'an array';
17
- return typeof value === 'object' ? 'a plain object' : `a ${typeof value}`;
18
- }
19
-
20
- /**
21
- * Mirrors Module.validateDefinition (packages/core/modules/module.js): an
22
- * API module definition that fails these checks throws when the integration
23
- * is instantiated.
24
- */
25
- function checkModuleDefinition(definition, pointer) {
26
- const issues = [];
27
- const err = (code, path, message, hint) =>
28
- issues.push(issue('error', code, `${pointer}${path}`, message, hint));
29
-
30
- if (!definition || typeof definition !== 'object') {
31
- err(
32
- 'module-definition-missing',
33
- '',
34
- 'The module entry has no `definition` (the API module definition).',
35
- "Use `{ definition: require('@friggframework/api-module-<name>').Definition }`."
36
- );
37
- return issues;
38
- }
39
- if (typeof definition.moduleName !== 'string' || !definition.moduleName) {
40
- err(
41
- 'module-name-missing',
42
- '/moduleName',
43
- 'The API module definition has no moduleName.',
44
- 'Set moduleName to a string.'
45
- );
46
- }
47
- if (typeof definition.API !== 'function') {
48
- err(
49
- 'module-api-missing',
50
- '/API',
51
- 'The API module definition has no API class.',
52
- "Set API to the module's requester class."
53
- );
54
- }
55
- const methods = definition.requiredAuthMethods;
56
- if (!methods || typeof methods !== 'object') {
57
- err(
58
- 'module-auth-methods-missing',
59
- '/requiredAuthMethods',
60
- 'The API module definition has no requiredAuthMethods.',
61
- 'Define getToken (OAuth2), getEntityDetails, getCredentialDetails, apiPropertiesToPersist and testAuthRequest.'
62
- );
63
- return issues;
64
- }
65
- const required = [
66
- 'getEntityDetails',
67
- 'getCredentialDetails',
68
- 'testAuthRequest',
69
- ];
70
- if (definition.API && definition.API.requesterType === 'oauth2') {
71
- required.unshift('getToken');
72
- }
73
- for (const name of required) {
74
- if (typeof methods[name] !== 'function') {
75
- err(
76
- 'module-auth-method-missing',
77
- `/requiredAuthMethods/${name}`,
78
- `requiredAuthMethods.${name} is missing or not a function.`,
79
- `Add an async ${name}() to the module definition.`
80
- );
81
- }
82
- }
83
- if (
84
- !methods.apiPropertiesToPersist ||
85
- typeof methods.apiPropertiesToPersist !== 'object'
86
- ) {
87
- err(
88
- 'module-auth-method-missing',
89
- '/requiredAuthMethods/apiPropertiesToPersist',
90
- 'requiredAuthMethods.apiPropertiesToPersist is missing.',
91
- 'Set it to `{ credential: [...], entity: [...] }`.'
92
- );
93
- }
94
- return issues;
95
- }
96
-
97
- function checkIntegration(integration, index) {
98
- const pointer = `/integrations/${index}`;
99
- const issues = [];
100
-
101
- if (typeof integration !== 'function') {
102
- issues.push(
103
- issue(
104
- 'error',
105
- 'integration-not-a-class',
106
- pointer,
107
- `Integration ${index} is ${describe(
108
- integration
109
- )}, not a class. Core instantiates each entry with \`new\`.`,
110
- 'List the IntegrationBase subclass itself, e.g. `integrations: [HubSpotIntegration]`.'
111
- )
112
- );
113
- return issues;
114
- }
115
-
116
- const label = integration.name || `Integration ${index}`;
117
- const Definition = integration.Definition;
118
- if (!Definition || typeof Definition !== 'object') {
119
- issues.push(
120
- issue(
121
- 'error',
122
- 'integration-definition-missing',
123
- `${pointer}/Definition`,
124
- `${label} has no static Definition.`,
125
- 'Add `static Definition = { name, version, modules: { ... } }` to the class.'
126
- )
127
- );
128
- return issues;
129
- }
130
-
131
- if (typeof Definition.name !== 'string' || !Definition.name) {
132
- issues.push(
133
- issue(
134
- 'error',
135
- 'integration-name-missing',
136
- `${pointer}/Definition/name`,
137
- `${label}.Definition has no name.`,
138
- "Set Definition.name; it names the integration's routes, queue and Lambda functions."
139
- )
140
- );
141
- } else if (!NAME_PATTERN.test(Definition.name)) {
142
- issues.push(
143
- issue(
144
- 'error',
145
- 'integration-name-invalid',
146
- `${pointer}/Definition/name`,
147
- `${label}.Definition.name "${Definition.name}" is not a valid name.`,
148
- 'Use letters, digits, "-" and "_", starting with a letter: it becomes part of AWS resource names.'
149
- )
150
- );
151
- }
152
-
153
- if (
154
- Definition.version === undefined ||
155
- Definition.version === null ||
156
- Definition.version === ''
157
- ) {
158
- issues.push(
159
- issue(
160
- 'warning',
161
- 'integration-version-missing',
162
- `${pointer}/Definition/version`,
163
- `${label}.Definition has no version; integration records are stored without one.`,
164
- "Set Definition.version, e.g. '1.0.0'."
165
- )
166
- );
167
- }
168
-
169
- const modules = Definition.modules;
170
- if (!modules || typeof modules !== 'object' || Array.isArray(modules)) {
171
- issues.push(
172
- issue(
173
- 'error',
174
- 'integration-modules-missing',
175
- `${pointer}/Definition/modules`,
176
- `${label}.Definition.modules is ${
177
- modules === undefined ? 'missing' : describe(modules)
178
- }.`,
179
- 'Set modules to an object, e.g. `modules: { hubspot: { definition: hubspot.Definition } }`.'
180
- )
181
- );
182
- return issues;
183
- }
184
-
185
- for (const [key, entry] of Object.entries(modules)) {
186
- const modulePointer = `${pointer}/Definition/modules/${key}`;
187
- if (!entry || typeof entry !== 'object') {
188
- issues.push(
189
- issue(
190
- 'error',
191
- 'module-entry-invalid',
192
- modulePointer,
193
- `Module "${key}" is ${describe(entry)}.`,
194
- 'Use `{ definition: <API module Definition> }`.'
195
- )
196
- );
197
- continue;
198
- }
199
- issues.push(
200
- ...checkModuleDefinition(
201
- entry.definition,
202
- `${modulePointer}/definition`
203
- )
204
- );
205
- const rateLimit =
206
- entry.definition?.rateLimit ?? entry.definition?.config?.rateLimit;
207
- if (rateLimit !== undefined) {
208
- issues.push(
209
- issue(
210
- 'warning',
211
- 'module-rate-limit-ignored',
212
- `${modulePointer}/definition/${
213
- entry.definition?.rateLimit !== undefined
214
- ? 'rateLimit'
215
- : 'config/rateLimit'
216
- }`,
217
- `Module "${key}" declares rateLimit, but the Frigg runtime does not read it: no rate limit is applied.`,
218
- 'Throttle in the API class (or with the queue concurrency) until module rate limits are supported.'
219
- )
220
- );
221
- }
222
- }
223
-
224
- return issues;
225
- }
226
-
227
- /**
228
- * @param {object} definition the app definition
229
- * @returns {object[]} issues
230
- */
231
- function checkIntegrations(definition) {
232
- const integrations = definition.integrations;
233
- if (!Array.isArray(integrations)) return []; // the schema reports this
234
- const issues = [];
235
- const seen = new Map();
236
- integrations.forEach((integration, index) => {
237
- issues.push(...checkIntegration(integration, index));
238
- const name =
239
- typeof integration === 'function'
240
- ? integration.Definition?.name
241
- : undefined;
242
- if (typeof name === 'string' && name) {
243
- if (seen.has(name)) {
244
- issues.push(
245
- issue(
246
- 'error',
247
- 'integration-name-duplicate',
248
- `/integrations/${index}/Definition/name`,
249
- `Integrations ${seen.get(
250
- name
251
- )} and ${index} are both named "${name}".`,
252
- 'Give each integration a unique Definition.name; routes and queues are keyed by it.'
253
- )
254
- );
255
- } else {
256
- seen.set(name, index);
257
- }
258
- }
259
- });
260
- if (integrations.length === 0) {
261
- issues.push(
262
- issue(
263
- 'warning',
264
- 'no-integrations',
265
- '/integrations',
266
- 'The app lists no integrations.',
267
- 'Add one with `frigg install <module>`.'
268
- )
269
- );
270
- }
271
- return issues;
272
- }
273
-
274
- module.exports = { checkIntegrations, checkModuleDefinition };
@@ -1,58 +0,0 @@
1
- const chalk = require('chalk');
2
-
3
- function formatIssue(issue) {
4
- const label =
5
- issue.severity === 'error'
6
- ? chalk.red('error')
7
- : chalk.yellow('warning');
8
- const where = issue.pointer
9
- ? chalk.cyan(issue.pointer)
10
- : chalk.cyan('(definition)');
11
- const lines = [` ${label} ${where} ${issue.message}`];
12
- if (issue.hint) {
13
- lines.push(chalk.gray(` fix: ${issue.hint}`));
14
- }
15
- return lines.join('\n');
16
- }
17
-
18
- /**
19
- * Human-readable report.
20
- * @param {{ errors: object[], warnings: object[], source?: string, stage?: string }} report
21
- */
22
- function formatReport(report) {
23
- const { errors, warnings, source, stage } = report;
24
- const lines = [];
25
- const header = `Validating ${source || 'the app definition'}${
26
- stage ? ` (stage: ${stage})` : ''
27
- }`;
28
- lines.push(chalk.bold(header));
29
- for (const issue of errors) lines.push(formatIssue(issue));
30
- for (const issue of warnings) lines.push(formatIssue(issue));
31
- if (errors.length === 0 && warnings.length === 0) {
32
- lines.push(chalk.green(' ✓ The app definition is valid.'));
33
- } else {
34
- const summary = `${errors.length} error(s), ${warnings.length} warning(s)`;
35
- lines.push(
36
- errors.length
37
- ? chalk.red(` ✗ ${summary}`)
38
- : chalk.yellow(` ✓ valid, ${summary}`)
39
- );
40
- }
41
- return lines.join('\n');
42
- }
43
-
44
- /**
45
- * Machine-readable report (`frigg validate --json`).
46
- */
47
- function toJsonReport(report) {
48
- const strip = ({ stack, ...issue }) => issue;
49
- return {
50
- valid: report.errors.length === 0,
51
- source: report.source || null,
52
- stage: report.stage || null,
53
- errors: report.errors.map(strip),
54
- warnings: report.warnings.map(strip),
55
- };
56
- }
57
-
58
- module.exports = { formatReport, toJsonReport };
@@ -1,107 +0,0 @@
1
- /**
2
- * `frigg validate` and the validation step that `frigg build`, `frigg deploy`,
3
- * `frigg start` and `frigg init` run first (ADR-051).
4
- */
5
-
6
- const chalk = require('chalk');
7
- const { loadAppDefinition } = require('./load-app-definition');
8
- const { validateDefinition } = require('./validate-app-definition');
9
- const { formatReport, toJsonReport } = require('./format-report');
10
-
11
- /**
12
- * Load the backend's app definition the way build does and validate it.
13
- *
14
- * @param {object} [params]
15
- * @param {string} [params.cwd]
16
- * @param {string} [params.stage]
17
- * @param {object} [params.env]
18
- * @returns {{ errors: object[], warnings: object[], source: string, stage?: string }}
19
- */
20
- function runValidation({ cwd = process.cwd(), stage, env = process.env } = {}) {
21
- const loaded = loadAppDefinition({ cwd });
22
- if (!loaded.ok) {
23
- return { errors: [loaded.issue], warnings: [], source: cwd, stage };
24
- }
25
- const { errors, warnings } = validateDefinition({
26
- definition: loaded.definition,
27
- stage,
28
- env,
29
- });
30
- return { errors, warnings, source: loaded.indexPath, stage };
31
- }
32
-
33
- /**
34
- * `frigg validate [--json] [--stage <stage>]`. Exits non-zero on errors.
35
- */
36
- async function validateCommand(options = {}) {
37
- const report = runValidation({ stage: options.stage });
38
- if (options.json) {
39
- console.log(JSON.stringify(toJsonReport(report), null, 2));
40
- } else {
41
- console.log(formatReport(report));
42
- const stack = report.errors.find((issue) => issue.stack)?.stack;
43
- if (stack && options.verbose) console.log(chalk.gray(stack));
44
- }
45
- if (report.errors.length > 0) {
46
- process.exitCode = 1;
47
- }
48
- return report;
49
- }
50
-
51
- /**
52
- * Validation step run before another command.
53
- *
54
- * @param {object} params
55
- * @param {string} params.command the command name, for messages
56
- * @param {object} params.options the command's options (stage, skipValidate)
57
- * @param {boolean} params.failOnErrors exit 1 on errors (build, deploy);
58
- * false prints errors as warnings (start)
59
- * @returns {boolean} true when the command may continue
60
- */
61
- function preflightValidation({ command, options = {}, failOnErrors }) {
62
- if (options.skipValidate) {
63
- console.warn(
64
- chalk.bgYellow.black(' VALIDATION SKIPPED ') +
65
- chalk.yellow(
66
- ` frigg ${command} --skip-validate: the app definition was not validated. Run \`frigg validate\` to see what was skipped.`
67
- )
68
- );
69
- return true;
70
- }
71
-
72
- const report = runValidation({ stage: options.stage });
73
- const { errors, warnings } = report;
74
- if (errors.length === 0 && warnings.length === 0) {
75
- console.log(chalk.green('✓ App definition is valid'));
76
- return true;
77
- }
78
-
79
- if (failOnErrors || errors.length === 0) {
80
- console.log(formatReport(report));
81
- } else {
82
- // frigg start: report errors without stopping local development.
83
- console.log(
84
- formatReport({
85
- ...report,
86
- errors: [],
87
- warnings: [
88
- ...errors.map((e) => ({ ...e, severity: 'warning' })),
89
- ...warnings,
90
- ],
91
- })
92
- );
93
- }
94
-
95
- if (errors.length > 0 && failOnErrors) {
96
- console.error(
97
- chalk.red(
98
- `\n✗ frigg ${command} stopped: the app definition has ${errors.length} error(s). Fix them, or rerun with --skip-validate to bypass.`
99
- )
100
- );
101
- process.exit(1);
102
- return false;
103
- }
104
- return true;
105
- }
106
-
107
- module.exports = { validateCommand, runValidation, preflightValidation };
@@ -1,93 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- /**
5
- * Find the backend directory the way `frigg build` and core's
6
- * findNearestBackendPackageJson do: the current directory when it holds
7
- * package.json and index.js, else the nearest ancestor's `backend/`.
8
- *
9
- * @param {string} cwd
10
- * @returns {string|null}
11
- */
12
- function resolveBackendDir(cwd = process.cwd()) {
13
- if (
14
- fs.existsSync(path.join(cwd, 'package.json')) &&
15
- fs.existsSync(path.join(cwd, 'index.js'))
16
- ) {
17
- return cwd;
18
- }
19
- let dir = cwd;
20
- while (dir !== path.parse(dir).root) {
21
- const candidate = path.join(dir, 'backend');
22
- if (
23
- fs.existsSync(path.join(candidate, 'package.json')) &&
24
- fs.existsSync(path.join(candidate, 'index.js'))
25
- ) {
26
- return candidate;
27
- }
28
- dir = path.dirname(dir);
29
- }
30
- return null;
31
- }
32
-
33
- /**
34
- * Load the `Definition` exported by the backend's index.js, the same module
35
- * that infrastructure.js (createFriggInfrastructure) and core's
36
- * loadAppDefinition load.
37
- *
38
- * @param {object} [params]
39
- * @param {string} [params.cwd]
40
- * @returns {{ ok: true, definition: object, backendDir: string, indexPath: string }
41
- * | { ok: false, issue: object }}
42
- */
43
- function loadAppDefinition({ cwd = process.cwd() } = {}) {
44
- const backendDir = resolveBackendDir(cwd);
45
- if (!backendDir) {
46
- return {
47
- ok: false,
48
- issue: {
49
- severity: 'error',
50
- code: 'backend-not-found',
51
- pointer: '',
52
- message: `No Frigg backend found from ${cwd}: expected package.json and index.js here, or a backend/ directory above.`,
53
- hint: 'Run the command in your backend directory (the one with index.js and infrastructure.js).',
54
- },
55
- };
56
- }
57
-
58
- const indexPath = path.join(backendDir, 'index.js');
59
- let backend;
60
- try {
61
- backend = require(indexPath);
62
- } catch (error) {
63
- return {
64
- ok: false,
65
- issue: {
66
- severity: 'error',
67
- code: 'load-failed',
68
- pointer: '',
69
- message: `${indexPath} failed to load: ${error.message}`,
70
- hint: 'Fix the error above; build and deploy load the same file.',
71
- stack: error.stack,
72
- },
73
- };
74
- }
75
-
76
- const definition = backend && backend.Definition;
77
- if (!definition || typeof definition !== 'object') {
78
- return {
79
- ok: false,
80
- issue: {
81
- severity: 'error',
82
- code: 'definition-not-exported',
83
- pointer: '',
84
- message: `${indexPath} does not export a Definition object.`,
85
- hint: 'End index.js with `module.exports = { Definition: appDefinition };`.',
86
- },
87
- };
88
- }
89
-
90
- return { ok: true, definition, backendDir, indexPath };
91
- }
92
-
93
- module.exports = { loadAppDefinition, resolveBackendDir };
@@ -1,154 +0,0 @@
1
- /**
2
- * Turn Ajv errors from the app-definition schema into validation issues:
3
- * { severity, code, pointer, message, hint }. The pointer is a JSON pointer
4
- * into the app definition (RFC 6901).
5
- */
6
-
7
- function appendPointer(pointer, key) {
8
- const escaped = String(key).replace(/~/g, '~0').replace(/\//g, '~1');
9
- return `${pointer}/${escaped}`;
10
- }
11
-
12
- function schemaNodeAt(schema, schemaPath) {
13
- // schemaPath looks like "#/properties/vpc/properties/management/enum"
14
- const parts = schemaPath.replace(/^#\/?/, '').split('/').filter(Boolean);
15
- parts.pop(); // drop the keyword
16
- let node = schema;
17
- for (const part of parts) {
18
- if (!node) return undefined;
19
- node = node[part.replace(/~1/g, '/').replace(/~0/g, '~')];
20
- }
21
- return node;
22
- }
23
-
24
- function knownKeys(schemaNode) {
25
- return Object.keys(schemaNode?.properties || {});
26
- }
27
-
28
- function closest(word, candidates) {
29
- const distance = (a, b) => {
30
- const dp = Array.from({ length: a.length + 1 }, (_, i) => [i]);
31
- for (let j = 1; j <= b.length; j++) dp[0][j] = j;
32
- for (let i = 1; i <= a.length; i++) {
33
- for (let j = 1; j <= b.length; j++) {
34
- dp[i][j] = Math.min(
35
- dp[i - 1][j] + 1,
36
- dp[i][j - 1] + 1,
37
- dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
38
- );
39
- }
40
- }
41
- return dp[a.length][b.length];
42
- };
43
- let best = null;
44
- for (const candidate of candidates) {
45
- const d = distance(word.toLowerCase(), candidate.toLowerCase());
46
- if (
47
- d <= Math.max(2, Math.floor(candidate.length / 3)) &&
48
- (!best || d < best.d)
49
- ) {
50
- best = { candidate, d };
51
- }
52
- }
53
- return best ? best.candidate : null;
54
- }
55
-
56
- function toIssue(error, schema) {
57
- const pointer = error.instancePath || '';
58
- const node = schemaNodeAt(schema, error.schemaPath || '');
59
-
60
- switch (error.keyword) {
61
- case 'additionalProperties': {
62
- const key = error.params.additionalProperty;
63
- const keys = knownKeys(node);
64
- const suggestion = closest(key, keys);
65
- return {
66
- severity: 'error',
67
- code: 'unknown-key',
68
- pointer: appendPointer(pointer, key),
69
- message: `Unknown key "${key}"${
70
- pointer ? ` in ${pointer}` : ''
71
- }: nothing in Frigg reads it.`,
72
- hint: suggestion
73
- ? `Did you mean "${suggestion}"?`
74
- : keys.length
75
- ? `Known keys: ${keys.join(', ')}.`
76
- : 'Remove it.',
77
- };
78
- }
79
- case 'required':
80
- return {
81
- severity: 'error',
82
- code: 'missing-key',
83
- pointer: appendPointer(pointer, error.params.missingProperty),
84
- message: `Missing required key "${error.params.missingProperty}".`,
85
- hint: node?.properties?.[error.params.missingProperty]
86
- ?.description,
87
- };
88
- case 'enum':
89
- return {
90
- severity: 'error',
91
- code: 'invalid-value',
92
- pointer,
93
- message: `${pointer || 'Value'} is ${JSON.stringify(
94
- error.data
95
- )}, which is not an allowed value.`,
96
- hint: `Use one of: ${error.params.allowedValues
97
- .map((v) => JSON.stringify(v))
98
- .join(', ')}.`,
99
- };
100
- case 'type':
101
- return {
102
- severity: 'error',
103
- code: 'invalid-type',
104
- pointer,
105
- message: `${pointer || 'Value'} must be ${
106
- error.params.type
107
- }, got ${error.data === null ? 'null' : typeof error.data}.`,
108
- hint: node?.description,
109
- };
110
- default:
111
- return {
112
- severity: 'error',
113
- code: `schema-${error.keyword}`,
114
- pointer,
115
- message: `${pointer || 'The app definition'} ${error.message}.`,
116
- hint: node?.description,
117
- };
118
- }
119
- }
120
-
121
- /**
122
- * @param {Array<object>|null} errors Ajv errors
123
- * @param {object} schema the app-definition schema
124
- * @returns {Array<object>} issues, one per distinct pointer+code
125
- */
126
- function schemaErrorsToIssues(errors, schema) {
127
- if (!errors) return [];
128
- // oneOf/anyOf failures repeat as one error per branch plus a summary:
129
- // keep the summary only.
130
- const oneOfPointers = new Set(
131
- errors
132
- .filter((e) => e.keyword === 'oneOf' || e.keyword === 'anyOf')
133
- .map((e) => e.instancePath)
134
- );
135
- const seen = new Set();
136
- const issues = [];
137
- for (const error of errors) {
138
- if (
139
- error.keyword !== 'oneOf' &&
140
- error.keyword !== 'anyOf' &&
141
- oneOfPointers.has(error.instancePath)
142
- ) {
143
- continue;
144
- }
145
- const issue = toIssue(error, schema);
146
- const key = `${issue.code}:${issue.pointer}`;
147
- if (seen.has(key)) continue;
148
- seen.add(key);
149
- issues.push(issue);
150
- }
151
- return issues;
152
- }
153
-
154
- module.exports = { schemaErrorsToIssues, appendPointer };