Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
- name: Setup pnpm
run: pnpm install --frozen-lockfile

- name: Validate configuration schemas
run: pnpm validate:schemas
- name: Build
run: pnpm run build
- name: Git Status
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
node-version: '22.20.0'
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- name: Validate configuration schemas
run: pnpm validate:schemas
- run: pnpm build
- run: pnpm test:git
- run: pnpm test
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,22 @@ to help you build your own adaptor.
3. [Implement your adaptor](https://github.com/OpenFn/adaptors/wiki/Adaptor-Writing-Best-Practice-&-Common-Patterns)
in `packages/<adaptor-name>/src/Adaptor.js`

4. Test your adaptor:
4. Update `packages/<adaptor-name>/configuration-schema.json` and validate it
against
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes):

```bash
pnpm validate:schemas
```

5. Test your adaptor:
[See unit test guideline](https://github.com/OpenFn/adaptors/wiki/Unit-Testing-Guide)

```bash
pnpm test
```

5. Create a test job in `tmp/job.js` and initial state in `tmp/state.json` then
6. Create a test job in `tmp/job.js` and initial state in `tmp/state.json` then
run:

```bash
Expand All @@ -193,6 +201,7 @@ to help you build your own adaptor.
### Best Practices

- Update the adaptor's README
- Keep `configuration-schema.json` valid against JSON Schema draft-07
- Include comprehensive [JSDoc](https://jsdoc.app/) comments for all functions
- [Write unit tests for your adaptor functions](https://github.com/OpenFn/adaptors/wiki/Unit-Testing-Guide)
- [Follow the existing code style and patterns](https://github.com/OpenFn/adaptors/wiki/Adaptor-Writing-Best-Practice-&-Common-Patterns)
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test:imports": "cd tools/import-tests && pnpm test",
"test": "pnpm lint && pnpm --filter \"./packages/**\" test && pnpm test:imports",
"test:git": "pnpm exec scripts/status-check.sh",
"validate:schemas": "node scripts/validate-configuration-schemas.mjs",
"version": "pnpm changeset version && pnpm run changelog"
},
"author": "Open Function Group",
Expand All @@ -36,6 +37,7 @@
"@openfn/parse-jsdoc": "workspace:^1.0.0",
"@types/chai": "^5.2.2",
"@types/mocha": "^10.0.10",
"ajv": "^8.18.0",
"chokidar-cli": "^3.0.0",
"eslint": "10.2.0",
"globals": "^17.2.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

215 changes: 215 additions & 0 deletions scripts/validate-configuration-schemas.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';

import Ajv from 'ajv';

const require = createRequire(import.meta.url);
const draft07MetaSchema = require('ajv/dist/refs/json-schema-draft-07.json');
const rootDir = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const packagesDir = path.join(rootDir, 'packages');
const args = new Set(process.argv.slice(2));
const markdown = args.has('--markdown');
const help = args.has('--help') || args.has('-h');

const pointerFor = ({ instancePath }) =>
instancePath ? `#${instancePath}` : '#';

const relativePath = file =>
path.relative(rootDir, file).split(path.sep).join('/');

const escapeMarkdown = value =>
String(value)
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|')
.replace(/\n/g, ' ');

const formatReason = error => {
if (!error) return 'Schema validation failed.';

const details = [];
if (error.keyword === 'additionalProperties') {
details.push(`additional property "${error.params.additionalProperty}"`);
}
if (error.keyword === 'enum' && error.params?.allowedValues) {
details.push(`allowed values: ${error.params.allowedValues.join(', ')}`);
}

const message = error.message ?? 'Schema validation failed.';
return details.length ? `${message} (${details.join('; ')})` : message;
};

const getUsefulErrors = errors =>
errors.filter(error => {
const samePointerErrors = errors.filter(
other => other.instancePath === error.instancePath
);

if (
['anyOf', 'oneOf', 'allOf'].includes(error.keyword) &&
samePointerErrors.length > 1
) {
return false;
}

if (
error.keyword === 'type' &&
samePointerErrors.some(other => other.keyword === 'enum')
) {
return false;
}

return true;
});

const findSchemaFiles = async () => {
const adaptors = await fs.readdir(packagesDir, { withFileTypes: true });
const schemaFiles = [];

for (const adaptor of adaptors) {
if (!adaptor.isDirectory()) continue;

const adaptorDir = path.join(packagesDir, adaptor.name);
const files = await fs.readdir(adaptorDir, { withFileTypes: true });
for (const file of files) {
if (file.isFile() && file.name.endsWith('configuration-schema.json')) {
schemaFiles.push(path.join(adaptorDir, file.name));
}
}
}

return schemaFiles.sort((a, b) =>
relativePath(a).localeCompare(relativePath(b))
);
};

const validateSchemaFile = async (file, ajv) => {
const adaptor = path.basename(path.dirname(file));
const filePath = relativePath(file);
let schema;

try {
schema = JSON.parse(await fs.readFile(file, 'utf8'));
} catch (error) {
return [
{
adaptor,
filePath,
pointer: '#',
reason: `Invalid JSON: ${error.message}`,
},
];
}

try {
if (ajv.validateSchema(schema)) return [];
} catch (error) {
return [
{
adaptor,
filePath,
pointer: '#',
reason: error.message,
},
];
}

return getUsefulErrors(ajv.errors ?? []).map(error => ({
adaptor,
filePath,
pointer: pointerFor(error),
reason: formatReason(error),
}));
};

const printMarkdown = (failures, checked) => {
console.log('| Adaptor | File | JSON pointer | Failure reason |');
console.log('| --- | --- | --- | --- |');

if (!failures.length) {
console.log(
`| - | - | - | No schema validation failures (${checked} files checked). |`
);
return;
}

for (const failure of failures) {
console.log(
`| ${escapeMarkdown(failure.adaptor)} | ${escapeMarkdown(
failure.filePath
)} | \`${escapeMarkdown(failure.pointer)}\` | ${escapeMarkdown(
failure.reason
)} |`
);
}
};

const printText = (failures, checked) => {
if (!failures.length) {
console.log(`Validated ${checked} configuration schema files.`);
return;
}

console.error('Configuration schema validation failed:');
console.error();

for (const failure of failures) {
console.error(`- ${failure.filePath}`);
console.error(` adaptor: ${failure.adaptor}`);
console.error(` pointer: ${failure.pointer}`);
console.error(` reason: ${failure.reason}`);
}
};

const main = async () => {
if (help) {
console.log(`Usage: pnpm validate:schemas [-- --markdown]

Validates every packages/*/*configuration-schema.json file against the JSON
Schema draft-07 meta-schema. Use --markdown to print the one-shot audit table.`);
return;
}

const ajv = new Ajv({
allErrors: true,
strict: false,
validateSchema: true,
});
ajv.addMetaSchema(
draft07MetaSchema,
'https://json-schema.org/draft-07/schema#'
);

const schemaFiles = await findSchemaFiles();

if (!schemaFiles.length) {
console.error('No configuration schema files found.');
process.exitCode = 1;
return;
}

const failures = (
await Promise.all(schemaFiles.map(file => validateSchemaFile(file, ajv)))
).flat();

if (markdown) {
printMarkdown(failures, schemaFiles.length);
} else {
printText(failures, schemaFiles.length);
}

if (failures.length) {
process.exitCode = 1;
}
};

main().catch(error => {
console.error(error);
process.exitCode = 1;
});
4 changes: 4 additions & 0 deletions tools/generate-fhir/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ View the
[configuration-schema](https://docs.openfn.org/adaptors/packages/{{NAME}}-configuration-schema/)
for required and optional `configuration` properties.

The configuration schema uses
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes).
Run `pnpm validate:schemas` from the adaptors repo root after editing it.

## Development

Clone the [adaptors monorepo](https://github.com/OpenFn/adaptors). Follow the
Expand Down
4 changes: 4 additions & 0 deletions tools/generate/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ View the
[configuration-schema](https://docs.openfn.org/adaptors/packages/{{TEMPLATE}}-configuration-schema/)
for required and optional `configuration` properties.

The configuration schema uses
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes).
Run `pnpm validate:schemas` from the adaptors repo root after editing it.

## Development

Clone the [adaptors monorepo](https://github.com/OpenFn/adaptors). Follow the
Expand Down
4 changes: 3 additions & 1 deletion wiki/build-a-new-adaptor.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ pnpm generate <adaptor-name>
adhere to the size specifications mentioned in the requirements section.
- Ensure the images have a transparent background. Navigate to
configuration-schema.json, and change any configs that do not align with the
adaptor
adaptor. This file must be valid
[JSON Schema draft-07](https://json-schema.org/draft-07/json-schema-release-notes);
run `pnpm validate:schemas` from the repo root after editing it.
- Go to `/src/Adaptor.js` and create the adaptor’s Operations - the functions
used in job code. You may want to set up `POST, GET,` to fit the current
adaptor’s requirements
Expand Down
Loading