diff --git a/package.json b/package.json index c5bfd4f6..8b4fb65e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@babel/preset-react": "7.16.7", "@babel/register": "^7.16.0", "@babel/runtime-corejs3": "7.16.8", - "@exabyte-io/esse.js": "2022.10.10-0", + "@exabyte-io/esse.js": "2022.10.21-0", "crypto-js": "^4.1.1", "json-schema-merge-allof": "^0.8.1", "lodash": "^4.17.21", diff --git a/src/JSONSchemasInterface.js b/src/JSONSchemasInterface.js index b45f3730..e1abe9c2 100644 --- a/src/JSONSchemasInterface.js +++ b/src/JSONSchemasInterface.js @@ -2,6 +2,43 @@ import { schemas } from "@exabyte-io/esse.js/schemas"; import mergeAllOf from "json-schema-merge-allof"; const schemasCache = new Map(); + +/** + * We assume that each schema in the application has its own unique schemaId + * Unfortunately, mergeAllOf keeps schemaId after merging, and this results in multiple different schemas with the same schemaId + * Hence this function + */ +function removeSchemaIdsAfterAllOf(schema, clean = false) { + if (clean) { + const { schemaId, ...restSchema } = schema; + + return restSchema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => removeSchemaIdsAfterAllOf(item)); + } + + if (typeof schema !== "object") { + return schema; + } + + if (schema.allOf) { + const { allOf, ...restSchema } = schema; + + return { + allOf: allOf.map((innerSchema) => removeSchemaIdsAfterAllOf(innerSchema, true)), + ...restSchema, + }; + } + + return Object.fromEntries( + Object.entries(schema).map(([key, value]) => { + return [key, removeSchemaIdsAfterAllOf(value)]; + }), + ); +} + export class JSONSchemasInterface { /** * @@ -16,13 +53,7 @@ export class JSONSchemasInterface { throw new Error(`Schema not found: ${schemaId}`); } - const schema = mergeAllOf(originalSchema, { - resolvers: { - defaultResolver: mergeAllOf.options.resolvers.title, - }, - }); - - schemasCache.set(schemaId, schema); + this.registerSchema(originalSchema); } return schemasCache.get(schemaId); @@ -32,8 +63,16 @@ export class JSONSchemasInterface { * * @param {Object} - external schema */ - static registerSchema(schema) { + static registerSchema(originalSchema) { + const schema = mergeAllOf(removeSchemaIdsAfterAllOf(originalSchema), { + resolvers: { + defaultResolver: mergeAllOf.options.resolvers.title, + }, + }); + schemasCache.set(schema.schemaId, schema); + + return schema; } /** diff --git a/src/entity/in_memory.js b/src/entity/in_memory.js index 9f2fdc66..3e4b5930 100644 --- a/src/entity/in_memory.js +++ b/src/entity/in_memory.js @@ -1,9 +1,8 @@ -import mergeAllOf from "json-schema-merge-allof"; import lodash from "lodash"; // import { ESSE } from "@exabyte-io/esse.js"; import { deepClone } from "../utils/clone"; -import { getMixSchemasByClassName, getSchemaByClassName } from "../utils/schemas"; +import { getSchemaByClassName } from "../utils/schemas"; // TODO: https://exabyte.atlassian.net/browse/SOF-5946 // const schemas = new ESSE().schemas; @@ -195,42 +194,24 @@ export class InMemoryEntity { } /** - * Returns original ESSE schema with nested properties from customJsonSchemaProperties - * @see customJsonSchemaProperties - * @returns {Object} schema - */ - static get baseJSONSchema() { - if (!this.customJsonSchemaProperties) { - return getSchemaByClassName(this.name); - } - - const { properties, ...schema } = getSchemaByClassName(this.name); - - return { - ...schema, - properties: { - ...properties, - ...this.customJsonSchemaProperties, - }, - }; - } - - /** - * Returns resolved JSON schema with custom properties and all mixes from schemas.js + * Returns class JSON schema * @returns {Object} schema */ static get jsonSchema() { try { - return mergeAllOf( - { - allOf: [this.baseJSONSchema, ...getMixSchemasByClassName(this.name)], - }, - { - resolvers: { - defaultResolver: mergeAllOf.options.resolvers.title, - }, + if (!this.customJsonSchemaProperties) { + return getSchemaByClassName(this.name); + } + + const { properties, ...schema } = getSchemaByClassName(this.name); + + return { + ...schema, + properties: { + ...properties, + ...this.customJsonSchemaProperties, }, - ); + }; } catch (e) { console.error(e.stack); throw e; diff --git a/src/utils/schemas.js b/src/utils/schemas.js index a8b01c76..bb6c409d 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -1,104 +1,21 @@ import { JSONSchemasInterface } from "../JSONSchemasInterface"; -export const baseSchemas = { - Material: "material", - Entity: "system/entity", - BankMaterial: "material", - Workflow: "workflow", - Subworkflow: "workflow/subworkflow", - BankWorkflow: "workflow", - Job: "job", - Application: "software/application", - Executable: "software/executable", - Flavor: "software/flavor", - Template: "software/template", - AssertionUnit: "workflow/unit/assertion", - AssignmentUnit: "workflow/unit/assignment", - ConditionUnit: "workflow/unit/condition", - ExecutionUnit: "workflow/unit/execution", - IOUnit: "workflow/unit/io", - MapUnit: "workflow/unit/map", - ProcessingUnit: "workflow/unit/processing", - ReduceUnit: "workflow/unit/reduce", - SubworkflowUnit: "workflow/unit", - Unit: "workflow/unit", - Project: "project", -}; - -export const entityMix = [ - "system/description-object", - "system/base-entity-set", - "system/sharing", - "system/metadata", - "system/defaultable", -]; - -export const subWorkflowMix = ["system/system-name", "system/is-multi-material"]; - -export const workflowMix = ["workflow/base-flow", "system/history", "system/is-outdated"]; - -export const bankMaterialMix = ["material/conventional", "system/creator-account"]; - -export const bankWorkflowMix = ["system/creator-account"]; - -export const jobMix = ["system/status", "system/job-extended"]; - -export const unitMix = [ - "system/unit-extended", - "system/status", - "workflow/unit/runtime/runtime-items", -]; - -export const assignmentUnitMix = ["system/scope"]; - -export const flavorMix = ["system/is-multi-material"]; - -export const systemEntityMix = ["system/entity"]; - -export const projectMix = ["system/status"]; - -export const mixSchemas = { - Entity: [...entityMix], - Material: [...entityMix], - BankMaterial: [...entityMix, ...bankMaterialMix], - Workflow: [...entityMix, ...subWorkflowMix, ...workflowMix], - Subworkflow: [...subWorkflowMix], - BankWorkflow: [...entityMix, ...subWorkflowMix, ...workflowMix, ...bankWorkflowMix], - Job: [...entityMix, ...jobMix], - Application: [...entityMix, ...systemEntityMix], - Executable: [...entityMix, ...systemEntityMix], - Flavor: [...entityMix, ...flavorMix, ...systemEntityMix], - Template: [...entityMix, ...systemEntityMix], - AssertionUnit: [...unitMix], - AssignmentUnit: [...unitMix, ...assignmentUnitMix], - ConditionUnit: [...unitMix], - ExecutionUnit: [...unitMix], - IOUnit: [...unitMix], - MapUnit: [...unitMix], - ProcessingUnit: [...unitMix], - ReduceUnit: [...unitMix], - SubworkflowUnit: [...unitMix], - Unit: [...unitMix], - Project: [...entityMix, ...systemEntityMix, ...projectMix], -}; +export const schemas = {}; +/** + * Returns previously registered schema for InMemoryEntity + * @param {*} className + * @returns + */ export function getSchemaByClassName(className) { - return baseSchemas[className] ? JSONSchemasInterface.schemaById(baseSchemas[className]) : null; -} - -export function getMixSchemasByClassName(className) { - return mixSchemas[className] - ? mixSchemas[className].map((schemaId) => JSONSchemasInterface.schemaById(schemaId)) - : []; + return schemas[className] ? JSONSchemasInterface.schemaById(schemas[className]) : null; } /** * Register additional Entity classes to be resolved with jsonSchema property * @param {String} className - class name derived from InMemoryEntity - * @param {String} classBaseSchema - base schemaId - * @param {Array} classMixSchemas - array of schemaId to mix + * @param {String} schemaId - class schemaId */ -export function registerClassName(className, classBaseSchema, classMixSchemas) { - baseSchemas[className] = classBaseSchema; - mixSchemas[className] = classMixSchemas; +export function registerClassName(className, schemaId) { + schemas[className] = schemaId; } diff --git a/tests/JSONSchemasInterface.tests.js b/tests/JSONSchemasInterface.tests.js index 29d4a82d..3066c2c7 100644 --- a/tests/JSONSchemasInterface.tests.js +++ b/tests/JSONSchemasInterface.tests.js @@ -1,47 +1,91 @@ -import { expect } from "chai"; +import { assert, expect } from "chai"; import { JSONSchemasInterface } from "../src/JSONSchemasInterface"; -import { baseSchemas, mixSchemas } from "../src/utils/schemas"; describe("JSONSchemasInterface", () => { - it("can find main schema", () => { - Object.values(baseSchemas).forEach((schemaId) => { - const schema = JSONSchemasInterface.schemaById(schemaId); - expect(schema).to.be.an("object"); - }); - }); - - it("can find mix schemas", () => { - Object.values(mixSchemas).forEach((schemaIds) => { - schemaIds.forEach((schemaId) => { - const schema = JSONSchemasInterface.schemaById(schemaId); - expect(schema).to.be.an("object"); - }); - }); + it("can find schema", () => { + const schema = JSONSchemasInterface.schemaById("workflow"); + expect(schema).to.be.an("object"); }); it("can match schemas", () => { - const schemaId = Object.values(baseSchemas)[0]; const schema = JSONSchemasInterface.matchSchema({ schemaId: { - $regex: schemaId, + $regex: "workflow", }, }); expect(schema).to.be.an("object"); }); - it("can find registered schemas", () => { + it("can find registered schemas; the schema is merged and clean", () => { JSONSchemasInterface.registerSchema({ - schemaId: "test-schema-id", + schemaId: "system/in-set", + $schema: "http://json-schema.org/draft-04/schema#", + title: "System in-set schema", properties: { - testProp: { + inSet: { + type: "array", + items: { + allOf: [ + { + schemaId: "system/entity-reference", + $schema: "http://json-schema.org/draft-04/schema#", + title: "entity reference schema", + properties: { + _id: { + description: "entity identity", + type: "string", + }, + cls: { + description: "entity class", + type: "string", + }, + slug: { + description: "entity slug", + type: "string", + }, + }, + }, + { + type: "object", + properties: { + type: { + type: "string", + }, + index: { + type: "number", + }, + }, + }, + ], + }, + }, + valueMapFunction: { + description: "Specifies the function to convert the currentValue in UI.", type: "string", + enum: [ + "toString", + "toContactUs", + "toPlusMinusSign", + "toUnlimited", + "toSupportSeverity", + ], + default: "toString", }, }, }); - const schema = JSONSchemasInterface.schemaById("test-schema-id"); + const schema = JSONSchemasInterface.schemaById("system/in-set"); + expect(schema).to.be.an("object"); + assert(schema.schemaId, "system/in-set"); + expect(schema.properties.inSet.items.schemaId).to.be.an("undefined"); + expect(schema.properties.inSet.items.properties).to.be.an("object"); + expect(schema.properties.valueMapFunction.enum[0]).to.be.an("string"); + expect(schema.properties.valueMapFunction.enum[1]).to.be.an("string"); + expect(schema.properties.valueMapFunction.enum[2]).to.be.an("string"); + expect(schema.properties.valueMapFunction.enum[3]).to.be.an("string"); + expect(schema.properties.valueMapFunction.enum[4]).to.be.an("string"); }); }); diff --git a/tests/in_memory.tests.js b/tests/in_memory.tests.js index e97ad36d..4ed053d5 100644 --- a/tests/in_memory.tests.js +++ b/tests/in_memory.tests.js @@ -2,7 +2,7 @@ import { expect } from "chai"; import { InMemoryEntity } from "../src/entity/in_memory"; -import { entityMix, registerClassName } from "../src/utils/schemas"; +import { registerClassName } from "../src/utils/schemas"; describe("InMemoryEntity", () => { const obj = { @@ -48,21 +48,6 @@ describe("InMemoryEntity", () => { expect(JSON.stringify(entity.toJSON())).to.be.equal(JSON.stringify(obj)); }); - it("jsonSchema returns correct schema", () => { - class Entity extends InMemoryEntity { - static get customJsonSchemaProperties() { - return { - nested: { - type: "string", - }, - }; - } - } - expect(Entity.jsonSchema).to.be.an("object"); - expect(Entity.jsonSchema).to.have.nested.property("properties.isDefault"); // check mix schemas - expect(Entity.jsonSchema).to.have.nested.property("properties.nested.type"); // check custom properties - }); - it("jsonSchema returns correct registered schema", () => { class RegisteredEntity extends InMemoryEntity { static get customJsonSchemaProperties() { @@ -74,10 +59,10 @@ describe("InMemoryEntity", () => { } } - registerClassName(RegisteredEntity.name, "system/entity", entityMix); + registerClassName(RegisteredEntity.name, "in-memory-entity/base"); expect(RegisteredEntity.jsonSchema).to.be.an("object"); - expect(RegisteredEntity.jsonSchema).to.have.nested.property("properties.isDefault"); // check mix schemas + expect(RegisteredEntity.jsonSchema).to.have.nested.property("properties._id"); // check mix schemas expect(RegisteredEntity.jsonSchema).to.have.nested.property("properties.nested.type"); // check custom properties }); });