diff --git a/packages/plugin-jsonschema/LICENSE.md b/packages/plugin-jsonschema/LICENSE.md new file mode 100644 index 000000000..573596b32 --- /dev/null +++ b/packages/plugin-jsonschema/LICENSE.md @@ -0,0 +1,13 @@ +Copyright 2023 OramaSearch Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/plugin-jsonschema/README.md b/packages/plugin-jsonschema/README.md new file mode 100644 index 000000000..f6a758328 --- /dev/null +++ b/packages/plugin-jsonschema/README.md @@ -0,0 +1,13 @@ +# Nextra Plugin + +[![Tests](https://github.com/oramasearch/orama/actions/workflows/turbo.yml/badge.svg)](https://github.com/oramasearch/orama/actions/workflows/turbo.yml) + +Official plugin to convert schemas in the jsonschema format to work with Orama. + +# Usage + +For the complete usage guide, please refer to the [official plugin documentation](https://docs.oramasearch.com/plugins/plugin-nextra). + +# License + +[Apache-2.0](/LICENSE.md) diff --git a/packages/plugin-jsonschema/package.json b/packages/plugin-jsonschema/package.json new file mode 100644 index 000000000..6cdd0fee7 --- /dev/null +++ b/packages/plugin-jsonschema/package.json @@ -0,0 +1,50 @@ +{ + "name": "@orama/plugin-jsonschema", + "version": "1.1.0", + "description": "Orama plugin to convert from jsonschema to Oramas schema format", + "keywords": [ + "orama", + "jsonschema" + ], + "license": "Apache-2.0", + "main": "./dist/index.js", + "type": "module", + "bugs": { + "url": "https://github.com/oramasearch/orama/issues" + }, + "homepage": "https://github.com/oramasearch/orama#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/oramasearch/orama.git" + }, + "sideEffects": false, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "swc --delete-dir-on-start --extensions .ts,.cts -d dist src", + "postbuild": "tsc -p . --emitDeclarationOnly", + "lint": "eslint src --ext .js,.ts,.cts", + "test": "c8 -c test/config/c8.json tap --rcfile=test/config/tap.yml test/*.test.ts" + }, + "dependencies": { + "@orama/orama": "workspace:*" + }, + "publishConfig": { + "access": "public" + }, + "lint-staged": { + "*.{ts, tsx}": "eslint ./src --cache --fix" + }, + "devDependencies": { + "@swc/cli": "^0.1.59", + "@swc/core": "^1.3.27", + "@types/tap": "^15.0.7", + "c8": "^7.12.0", + "tap": "^16.3.0", + "tap-mocha-reporter": "^5.0.3", + "tsx": "^3.12.2", + "typescript": "^5.0.3" + } +} diff --git a/packages/plugin-jsonschema/src/index.ts b/packages/plugin-jsonschema/src/index.ts new file mode 100644 index 000000000..5f50ce6ec --- /dev/null +++ b/packages/plugin-jsonschema/src/index.ts @@ -0,0 +1,38 @@ +type ScalarType = 'string' | 'number' | 'boolean' +type ValueType = ScalarType | `${ScalarType}[]` | TransformedSchema +interface TransformedSchema { + [key: string]: ValueType +} + +function getOramaType(jsonSchema): ValueType { + switch (jsonSchema.type) { + case 'string': + case 'date': + return 'string' + case 'number': + case 'integer': + return 'number' + case 'boolean': + return 'boolean' + case 'array': + if (jsonSchema.items.type === 'object') throw new Error("Can't convert arrays of objects") + if (jsonSchema.items.type === 'array') throw new Error("Can't convert arrays of arrays") + return `${getOramaType(jsonSchema.items)}[]` as `${ScalarType}[]` + case 'object': { + const transformedSchema: TransformedSchema = {} + for (const key in jsonSchema.properties) { + transformedSchema[key] = getOramaType(jsonSchema.properties[key]) + } + return transformedSchema + } + default: + throw new Error("Can't convert type " + jsonSchema.type) + } +} + +export function fromJsonSchema(schema): TransformedSchema { + if (!schema || typeof schema !== 'object' || schema.type !== 'object') { + throw new Error('JSON schema must have top level type object') + } + return getOramaType(schema) as TransformedSchema +} diff --git a/packages/plugin-jsonschema/test/config/c8.json b/packages/plugin-jsonschema/test/config/c8.json new file mode 100644 index 000000000..14bc5e00e --- /dev/null +++ b/packages/plugin-jsonschema/test/config/c8.json @@ -0,0 +1,8 @@ +{ + "check-coverage": true, + "reporter": ["text", "json"], + "branches": 80, + "functions": 80, + "lines": 80, + "statements": 80 +} diff --git a/packages/plugin-jsonschema/test/config/tap.yml b/packages/plugin-jsonschema/test/config/tap.yml new file mode 100644 index 000000000..ee28002fa --- /dev/null +++ b/packages/plugin-jsonschema/test/config/tap.yml @@ -0,0 +1,8 @@ +--- +jobs: 5 +timeout: 120 +reporter: spec +coverage: false +node-arg: + - --loader=tsx + - --no-warnings=loader diff --git a/packages/plugin-jsonschema/test/index.test.ts b/packages/plugin-jsonschema/test/index.test.ts new file mode 100644 index 000000000..6ac916a02 --- /dev/null +++ b/packages/plugin-jsonschema/test/index.test.ts @@ -0,0 +1,131 @@ +import * as t from 'tap' +import { fromJsonSchema } from '../src/index.js' + +t.test('convert sample schema', t => { + t.plan(1) + const jsonschema = { + type: 'object', + properties: { + resource_kind: { type: 'string' }, + resource_uri: { type: 'string' }, + resource_id: { type: 'string' }, + data: { + type: 'object', + properties: { + filed: { type: 'boolean' }, + barcode: { type: 'string' }, + category: { type: 'string' }, + date: { type: 'string' }, + description: { type: 'string' }, + description_values: { + type: 'object', + properties: { + made_up_date: { type: 'string' }, + }, + }, + links: { + type: 'object', + properties: { + document_metadata: { type: 'string' }, + self: { type: 'string' }, + }, + }, + pages: { type: 'number' }, + transaction_id: { type: 'string' }, + type: { type: 'string' }, + }, + }, + event: { + type: 'object', + properties: { + fields_changed: { + type: 'array', + items: { type: 'string' }, + }, + published_at: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + } + + const oramaSchema = fromJsonSchema(jsonschema) + + const expected = { + resource_kind: 'string', + resource_uri: 'string', + resource_id: 'string', + data: { + filed: 'boolean', + barcode: 'string', + category: 'string', + date: 'string', + description: 'string', + description_values: { + made_up_date: 'string', + }, + links: { + document_metadata: 'string', + self: 'string', + }, + pages: 'number', + transaction_id: 'string', + type: 'string', + }, + event: { + fields_changed: 'string[]', + published_at: 'string', + type: 'string', + }, + } + + t.same(oramaSchema, expected) +}) + +t.test('should throw errors on invalid schemas', t => { + t.plan(4) + + t.test('throw error for nested arrays', t => { + t.plan(1) + const jsonschema = { + type: 'object', + properties: { + coOrdinates: { + type: 'array', + items: { type: 'array', items: { type: 'number' } }, + }, + }, + } + t.throws(() => fromJsonSchema(jsonschema), Error) + }) + t.test('throw error for array of objects', t => { + t.plan(1) + const jsonschema = { + type: 'object', + properties: { + coOrdinates: { + type: 'array', + items: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } }, + }, + }, + } + t.throws(() => fromJsonSchema(jsonschema), Error) + }) + t.test('throw error for non-object at top level', t => { + t.plan(1) + const jsonschema = { + type: 'string', + } + t.throws(() => fromJsonSchema(jsonschema), Error) + }) + t.test('throw error for unknown type', t => { + t.plan(1) + const jsonschema = { + type: 'object', + properties: { + value: { type: 'unknown' }, + }, + } + t.throws(() => fromJsonSchema(jsonschema), Error) + }) +}) diff --git a/packages/plugin-jsonschema/tsconfig.json b/packages/plugin-jsonschema/tsconfig.json new file mode 100644 index 000000000..be25918e0 --- /dev/null +++ b/packages/plugin-jsonschema/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "ES5", + "module": "ESNext", + "outDir": "dist", + "jsx": "react", + "noImplicitAny": false, + "esModuleInterop": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true, + "moduleResolution": "nodenext" + }, + "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 850c9ee6c..4719a9c98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -407,6 +407,37 @@ importers: specifier: ^5.75.0 version: 5.75.0(@swc/core@1.3.27) + packages/plugin-jsonschema: + dependencies: + '@orama/orama': + specifier: workspace:* + version: link:../orama + devDependencies: + '@swc/cli': + specifier: ^0.1.59 + version: 0.1.59(@swc/core@1.3.27) + '@swc/core': + specifier: ^1.3.27 + version: 1.3.27 + '@types/tap': + specifier: ^15.0.7 + version: 15.0.7 + c8: + specifier: ^7.12.0 + version: 7.12.0 + tap: + specifier: ^16.3.0 + version: 16.3.4(typescript@5.0.3) + tap-mocha-reporter: + specifier: ^5.0.3 + version: 5.0.3 + tsx: + specifier: ^3.12.2 + version: 3.12.2 + typescript: + specifier: ^5.0.3 + version: 5.0.3 + packages/plugin-match-highlight: dependencies: '@orama/orama': @@ -2603,7 +2634,7 @@ packages: react-router-config: 5.1.1(react-router@5.3.4)(react@17.0.2) react-router-dom: 5.3.4(react@17.0.2) rtl-detect: 1.0.4 - semver: 7.3.8 + semver: 7.5.0 serve-handler: 6.1.5 shelljs: 0.8.5 terser-webpack-plugin: 5.3.9(@swc/core@1.3.27)(webpack@5.75.0) @@ -4746,7 +4777,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4767,7 +4798,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4788,7 +4819,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4812,7 +4843,7 @@ packages: eslint: 8.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@8.32.0) - semver: 7.3.8 + semver: 7.5.0 transitivePeerDependencies: - supports-color - typescript @@ -4835,7 +4866,7 @@ packages: '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.4) eslint: 8.32.0 eslint-scope: 5.1.1 - semver: 7.3.8 + semver: 7.5.0 transitivePeerDependencies: - supports-color - typescript @@ -5797,7 +5828,7 @@ packages: /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: - semver: 7.3.8 + semver: 7.5.0 dev: true /bundle-name@3.0.0: @@ -6342,7 +6373,7 @@ packages: dev: false /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -6585,7 +6616,7 @@ packages: postcss-modules-scope: 3.0.0(postcss@8.4.24) postcss-modules-values: 4.0.0(postcss@8.4.24) postcss-value-parser: 4.2.0 - semver: 7.3.8 + semver: 7.5.0 webpack: 5.75.0(@swc/core@1.3.27) dev: false @@ -8492,7 +8523,7 @@ packages: memfs: 3.5.3 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.8 + semver: 7.5.0 tapable: 1.1.3 typescript: 4.9.4 webpack: 5.75.0(@swc/core@1.3.27) @@ -11781,7 +11812,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.12.1 - semver: 7.3.8 + semver: 7.5.0 validate-npm-package-license: 3.0.4 dev: true @@ -12546,7 +12577,7 @@ packages: jiti: 1.18.2 klona: 2.0.6 postcss: 8.4.24 - semver: 7.3.8 + semver: 7.5.0 webpack: 5.75.0(@swc/core@1.3.27) dev: false @@ -14219,7 +14250,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -15002,6 +15032,57 @@ packages: - '@isaacs/import-jsx' - react + /tap@16.3.4(typescript@5.0.3): + resolution: {integrity: sha512-SAexdt2ZF4XBgye6TPucFI2y7VE0qeFXlXucJIV1XDPCs+iJodk0MYacr1zR6Ycltzz7PYg8zrblDXKbAZM2LQ==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + coveralls: ^3.1.1 + flow-remove-types: '>=2.112.0' + ts-node: '>=8.5.2' + typescript: '>=3.7.2' + peerDependenciesMeta: + coveralls: + optional: true + flow-remove-types: + optional: true + ts-node: + optional: true + typescript: + optional: true + dependencies: + chokidar: 3.5.3 + findit: 2.0.0 + foreground-child: 2.0.0 + fs-exists-cached: 1.0.0 + glob: 7.2.3 + isexe: 2.0.0 + istanbul-lib-processinfo: 2.0.3 + jackspeak: 1.4.2 + libtap: 1.4.1 + minipass: 3.3.6 + mkdirp: 1.0.4 + nyc: 15.1.0 + opener: 1.5.2 + rimraf: 3.0.2 + signal-exit: 3.0.7 + source-map-support: 0.5.21 + tap-mocha-reporter: 5.0.3 + tap-parser: 11.0.2 + tap-yaml: 1.0.2 + tcompare: 5.0.7 + typescript: 5.0.3 + which: 2.0.2 + transitivePeerDependencies: + - supports-color + dev: true + bundledDependencies: + - ink + - treport + - '@types/react' + - '@isaacs/import-jsx' + - react + /tapable@1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} @@ -15784,7 +15865,7 @@ packages: is-yarn-global: 0.3.0 latest-version: 5.1.0 pupa: 2.1.1 - semver: 7.3.8 + semver: 7.5.0 semver-diff: 3.1.1 xdg-basedir: 4.0.0 dev: false