From 3f2c80bc231725c6dc53b5bf2c37f5ea8d05f2a0 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 14:51:58 -0700 Subject: [PATCH 01/37] Rename schema folder to definitions --- src/{schema => definitions}/edges.ts | 0 src/{schema => definitions}/index.ts | 0 src/{schema => definitions}/ontology.ts | 0 src/{schema => definitions}/position.ts | 0 src/{schema => definitions}/statement.ts | 0 src/{schema => definitions}/user.ts | 0 src/{schema => definitions}/util.ts | 0 src/{schema => definitions}/variant.ts | 0 src/index.ts | 2 +- 9 files changed, 1 insertion(+), 1 deletion(-) rename src/{schema => definitions}/edges.ts (100%) rename src/{schema => definitions}/index.ts (100%) rename src/{schema => definitions}/ontology.ts (100%) rename src/{schema => definitions}/position.ts (100%) rename src/{schema => definitions}/statement.ts (100%) rename src/{schema => definitions}/user.ts (100%) rename src/{schema => definitions}/util.ts (100%) rename src/{schema => definitions}/variant.ts (100%) diff --git a/src/schema/edges.ts b/src/definitions/edges.ts similarity index 100% rename from src/schema/edges.ts rename to src/definitions/edges.ts diff --git a/src/schema/index.ts b/src/definitions/index.ts similarity index 100% rename from src/schema/index.ts rename to src/definitions/index.ts diff --git a/src/schema/ontology.ts b/src/definitions/ontology.ts similarity index 100% rename from src/schema/ontology.ts rename to src/definitions/ontology.ts diff --git a/src/schema/position.ts b/src/definitions/position.ts similarity index 100% rename from src/schema/position.ts rename to src/definitions/position.ts diff --git a/src/schema/statement.ts b/src/definitions/statement.ts similarity index 100% rename from src/schema/statement.ts rename to src/definitions/statement.ts diff --git a/src/schema/user.ts b/src/definitions/user.ts similarity index 100% rename from src/schema/user.ts rename to src/definitions/user.ts diff --git a/src/schema/util.ts b/src/definitions/util.ts similarity index 100% rename from src/schema/util.ts rename to src/definitions/util.ts diff --git a/src/schema/variant.ts b/src/definitions/variant.ts similarity index 100% rename from src/schema/variant.ts rename to src/definitions/variant.ts diff --git a/src/index.ts b/src/index.ts index 97eb352..78662f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { ClassModel } from './model'; import { Property } from './property'; import { SchemaDefinitionType } from './types'; -import schema from './schema'; +import schema from './definitions'; import * as util from './util'; import * as error from './error'; import * as constants from './constants'; From c3044c116106364a13e08de370044f2d6ae48afb Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 14:54:30 -0700 Subject: [PATCH 02/37] Rename model file as class --- src/{model.ts => class.ts} | 0 src/definitions/index.ts | 2 +- src/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{model.ts => class.ts} (100%) diff --git a/src/model.ts b/src/class.ts similarity index 100% rename from src/model.ts rename to src/class.ts diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 3cc93aa..9d78d54 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -5,7 +5,7 @@ import omit from 'lodash.omit'; import { PERMISSIONS, EXPOSE_READ } from '../constants'; -import { ClassModel } from '../model'; +import { ClassModel } from '../class'; import { timeStampNow } from '../util'; import { defineSimpleIndex, BASE_PROPERTIES, activeUUID } from './util'; import edges from './edges'; diff --git a/src/index.ts b/src/index.ts index 78662f0..12e5af4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { ClassModel } from './model'; +import { ClassModel } from './class'; import { Property } from './property'; import { SchemaDefinitionType } from './types'; import schema from './definitions'; From 540e75b723461f44503f6480f5eaf11651aa1133 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:01:03 -0700 Subject: [PATCH 03/37] Rename example to examples to be more similar to openapi names --- src/definitions/edges.ts | 2 +- src/definitions/index.ts | 8 ++++---- src/definitions/ontology.ts | 26 +++++++++++++------------- src/definitions/position.ts | 20 ++++++++++---------- src/definitions/user.ts | 6 +++--- src/definitions/util.ts | 18 +++++++++--------- src/definitions/variant.ts | 4 ++-- src/property.ts | 10 +++++----- src/types.ts | 2 +- 9 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/definitions/edges.ts b/src/definitions/edges.ts index e5cdcb5..f9b5673 100644 --- a/src/definitions/edges.ts +++ b/src/definitions/edges.ts @@ -41,7 +41,7 @@ const edgeModels: Record = { { ...BASE_PROPERTIES.in }, { ...BASE_PROPERTIES.out }, { name: 'source', type: 'link', linkedClass: 'Source' }, - { name: 'actionType', description: 'The type of action between the gene and drug', example: 'inhibitor' }, + { name: 'actionType', description: 'The type of action between the gene and drug', examples: ['inhibitor'] }, ], }, }; diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 9d78d54..95ed327 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -69,7 +69,7 @@ const BASE_SCHEMA: Record = { { name: 'longName', description: 'More descriptive name if applicable. May be the expansion of the name acronym', - example: 'Disease Ontology (DO)', + examples: ['Disease Ontology (DO)'], }, { name: 'version', description: 'The source version' }, { name: 'url', type: 'string' }, @@ -85,7 +85,7 @@ const BASE_SCHEMA: Record = { { name: 'licenseType', description: 'standard license type', - example: 'MIT', + examples: ['MIT'], }, { name: 'citation', @@ -94,7 +94,7 @@ const BASE_SCHEMA: Record = { { name: 'sort', description: 'Used in ordering the sources for auto-complete on the front end. Lower numbers indicate the source should be higher in the sorting', - example: 1, + examples: [1], type: 'integer', default: 99999, }, @@ -132,7 +132,7 @@ const BASE_SCHEMA: Record = { description: 'The timestamp at which this terms of use was put into action', default: timeStampNow, generated: true, - example: 1547245339649, + examples: [1547245339649], }, { name: 'content', type: 'embeddedlist', diff --git a/src/definitions/ontology.ts b/src/definitions/ontology.ts index 4f3b799..0e3ec7f 100644 --- a/src/definitions/ontology.ts +++ b/src/definitions/ontology.ts @@ -121,7 +121,7 @@ const models: Record = { ClinicalTrial: { inherits: ['Evidence', 'Ontology'], properties: [ - { name: 'phase', type: 'string', example: '1B' }, + { name: 'phase', type: 'string', examples: ['1B'] }, { name: 'size', type: 'integer', description: 'The number of participants in the trial' }, { name: 'startDate', type: 'string', format: 'date', pattern: '^\\d{4}(-\\d{2}(-\\d{2})?)?$', @@ -157,10 +157,10 @@ const models: Record = { description: 'Abstract from a publication or conference proceeding', properties: [ { - name: 'meeting', type: 'string', mandatory: true, nullable: false, example: '2011 ASCO Annual Meeting', + name: 'meeting', type: 'string', mandatory: true, nullable: false, examples: ['2011 ASCO Annual Meeting'], }, { - name: 'abstractNumber', type: 'string', mandatory: true, nullable: false, example: '10009', + name: 'abstractNumber', type: 'string', mandatory: true, nullable: false, examples: ['10009'], }, ], indices: [ @@ -180,12 +180,12 @@ const models: Record = { { name: 'journalName', description: 'Name of the journal where the article was published', - example: 'Bioinformatics', + examples: ['Bioinformatics'], }, { - name: 'year', type: 'integer', example: 2018, description: 'The year the article was published', + name: 'year', type: 'integer', examples: [2018], description: 'The year the article was published', }, - { name: 'doi', type: 'string', example: 'doi:10.1037/rmh0000008' }, + { name: 'doi', type: 'string', examples: ['doi:10.1037/rmh0000008'] }, { name: 'content', description: 'content of the publication', @@ -195,11 +195,11 @@ const models: Record = { name: 'authors', type: 'string', description: 'list of authors involved in the publication', }, { - name: 'citation', type: 'string', description: 'citation provided by the source entity', example: 'J Clin Oncol 29: 2011 (suppl; abstr 10006)', + name: 'citation', type: 'string', description: 'citation provided by the source entity', examples: ['J Clin Oncol 29: 2011 (suppl; abstr 10006)'], }, - { name: 'issue', example: '3' }, - { name: 'volume', example: '35' }, - { name: 'pages', example: '515-517' }, + { name: 'issue', examples: ['3'] }, + { name: 'volume', examples: ['35'] }, + { name: 'pages', examples: ['515-517'] }, ], }, CuratedContent: { @@ -207,9 +207,9 @@ const models: Record = { inherits: ['Evidence', 'Ontology'], properties: [ { - name: 'year', type: 'integer', example: 2018, description: 'The year the article was published', + name: 'year', type: 'integer', examples: [2018], description: 'The year the article was published', }, - { name: 'doi', type: 'string', example: 'doi:10.1037/rmh0000008' }, + { name: 'doi', type: 'string', examples: ['doi:10.1037/rmh0000008'] }, { name: 'content', description: 'text content being referred to, stored for posterity if required', @@ -244,7 +244,7 @@ const models: Record = { nullable: false, description: 'The biological type of the feature', choices: ['gene', 'protein', 'transcript', 'exon', 'chromosome'], - example: 'gene', + examples: ['gene'], }, { ...BASE_PROPERTIES.displayName, diff --git a/src/definitions/position.ts b/src/definitions/position.ts index 1050067..68a8ee5 100644 --- a/src/definitions/position.ts +++ b/src/definitions/position.ts @@ -19,10 +19,10 @@ const models: Record = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, example: 12, nullable: true, description: 'The Amino Acid number', + name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [12], nullable: true, description: 'The Amino Acid number', }, { - name: 'refAA', type: 'string', cast: util.uppercase, example: 'G', pattern: '^[A-Z*?]$', description: 'The reference Amino Acid (single letter notation)', + name: 'refAA', type: 'string', cast: util.uppercase, examples: ['G'], pattern: '^[A-Z*?]$', description: 'The reference Amino Acid (single letter notation)', }, ], }, @@ -35,10 +35,10 @@ const models: Record = { name: 'arm', mandatory: true, nullable: false, choices: ['p', 'q'], }, { - name: 'majorBand', type: 'integer', min: 1, example: '11', + name: 'majorBand', type: 'integer', min: 1, examples: ['11'], }, { - name: 'minorBand', type: 'integer', min: 1, example: '1', + name: 'minorBand', type: 'integer', min: 1, examples: ['1'], }, ], }, @@ -73,9 +73,9 @@ const models: Record = { embedded: true, properties: [ { - name: 'pos', type: 'integer', mandatory: true, example: 55, nullable: true, + name: 'pos', type: 'integer', mandatory: true, examples: [55], nullable: true, }, - { name: 'offset', type: 'integer', example: -11 }, + { name: 'offset', type: 'integer', examples: [-11] }, ], }, NonCdsPosition: { @@ -85,10 +85,10 @@ const models: Record = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, example: 55, nullable: true, + name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [55], nullable: true, }, { - name: 'offset', type: 'integer', example: -11, description: 'distance from the nearest exon boundary (pos)', + name: 'offset', type: 'integer', examples: [-11], description: 'distance from the nearest exon boundary (pos)', }, ], @@ -100,10 +100,10 @@ const models: Record = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, example: 55, nullable: true, + name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [55], nullable: true, }, { - name: 'offset', type: 'integer', example: -11, description: 'distance from the nearest cds exon boundary', + name: 'offset', type: 'integer', examples: [-11], description: 'distance from the nearest cds exon boundary', }, ], }, diff --git a/src/definitions/user.ts b/src/definitions/user.ts index 4550220..81a5ec8 100644 --- a/src/definitions/user.ts +++ b/src/definitions/user.ts @@ -55,20 +55,20 @@ const models: Record = { type: 'long', description: 'The timestamp at which the user last logged in', nullable: true, - example: 1547245339649, + examples: [1547245339649], }, { name: 'firstLoginAt', type: 'long', description: 'The timestamp at which the user first logged in', nullable: true, - example: 1547245339649, + examples: [1547245339649], }, { name: 'loginCount', type: 'integer', description: 'The number of times this user has logged in', - example: 10, + examples: [10], nullable: true, }, ], diff --git a/src/definitions/util.ts b/src/definitions/util.ts index df48f16..e822f04 100644 --- a/src/definitions/util.ts +++ b/src/definitions/util.ts @@ -88,7 +88,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { cast: util.castUUID, default: uuidV4 as () => string, generated: true, - example: '4198e211-e761-4771-b6f8-dadbcc44e9b9', + examples: ['4198e211-e761-4771-b6f8-dadbcc44e9b9'], }, createdAt: { name: 'createdAt', @@ -98,7 +98,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { description: 'The timestamp at which the record was created', default: util.timeStampNow, generated: true, - example: 1547245339649, + examples: [1547245339649], }, updatedAt: { name: 'updatedAt', @@ -108,7 +108,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { description: 'The timestamp at which the record was last updated', default: util.timeStampNow, generated: true, - example: 1547245339649, + examples: [1547245339649], }, updatedBy: { name: 'updatedBy', @@ -118,7 +118,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { linkedClass: 'User', description: 'The user who last updated the record', generated: true, - example: '#31:1', + examples: ['#31:1'], }, deletedAt: { name: 'deletedAt', @@ -126,7 +126,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { description: 'The timestamp at which the record was deleted', nullable: false, generated: true, - example: 1547245339649, + examples: [1547245339649], }, createdBy: { name: 'createdBy', @@ -136,7 +136,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { linkedClass: 'User', description: 'The user who created the record', generated: true, - example: '#31:1', + examples: ['#31:1'], }, deletedBy: { name: 'deletedBy', @@ -145,7 +145,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { nullable: false, description: 'The user who deleted the record', generated: true, - example: '#31:1', + examples: ['#31:1'], }, history: { name: 'history', @@ -153,14 +153,14 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { nullable: false, description: 'Link to the previous version of this record', generated: true, - example: '#31:1', + examples: ['#31:1'], }, groupRestrictions: { name: 'groupRestrictions', type: 'linkset', linkedClass: 'UserGroup', description: 'user groups allowed to interact with this record', - example: ['#33:1', '#33:2'], + examples: [['#33:1', '#33:2']], }, in: { name: 'in', diff --git a/src/definitions/variant.ts b/src/definitions/variant.ts index b00d640..dd8f104 100644 --- a/src/definitions/variant.ts +++ b/src/definitions/variant.ts @@ -96,7 +96,7 @@ const models: Record = { cast: castBreakRepr, }, { - name: 'refSeq', type: 'string', cast: util.uppercase, description: 'the variants reference sequence', example: 'ATGC', + name: 'refSeq', type: 'string', cast: util.uppercase, description: 'the variants reference sequence', examples: ['ATGC'], }, { name: 'untemplatedSeq', type: 'string', cast: util.uppercase, description: 'Untemplated or alternative sequence', @@ -118,7 +118,7 @@ const models: Record = { description: 'Flag which is optionally used for genomic variants that are not linked to a fixed assembly reference', }, { - name: 'hgvsType', type: 'string', example: 'delins', description: 'the short form of this type to use in building an HGVS-like representation', + name: 'hgvsType', type: 'string', examples: ['delins'], description: 'the short form of this type to use in building an HGVS-like representation', }, ], indices: [ diff --git a/src/property.ts b/src/property.ts index dab4541..ba446c3 100644 --- a/src/property.ts +++ b/src/property.ts @@ -25,7 +25,7 @@ export class Property implements PropertyType { readonly generateDefault?: (rec?: unknown) => unknown; readonly check?: (rec?: unknown) => boolean; readonly default?: unknown; - readonly example?: unknown; + readonly examples?: unknown[]; readonly generationDependencies?: boolean; readonly nonEmpty?: boolean; readonly linkedType?: 'string'; @@ -50,7 +50,7 @@ export class Property implements PropertyType { * @param {*|Function} opt.default the default value or function for generating the default * @param {boolean} opt.generated indicates that this is a generated field and should not be input by the user * @param {boolean} opt.generationDependencies indicates that a field should be generated after all other processing is complete b/c it requires other fields - * @param {*} opt.example an example value to use for help text + * @param {*} opt.examples an example value to use for help text * @param {string} opt.pattern the regex pattern values for this property should follow (used purely in docs) * @param {boolean} opt.nullable flag to indicate if the value can be null * @param {boolean} opt.mandatory flag to indicate if this property is required @@ -83,7 +83,7 @@ export class Property implements PropertyType { this.cast = opt.cast; this.description = opt.description || ''; - this.example = opt.example; + this.examples = opt.examples; this.generated = Boolean(opt.generated); this.generationDependencies = Boolean(opt.generationDependencies); this.iterable = Boolean(/(set|list|bag|map)/ig.exec(this.type)); @@ -104,8 +104,8 @@ export class Property implements PropertyType { this.choices = opt.choices; - if (this.example === undefined && this.choices) { - [this.example] = this.choices; + if (this.examples === undefined && this.choices) { + this.examples = this.choices; } if (!this.cast) { // set the default util.cast functions diff --git a/src/types.ts b/src/types.ts index 20e61a3..d2c14bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,7 +27,7 @@ export interface PropertyTypeDefinition { choices?: unknown[]; default?: ((rec?: any) => PType) | PType; description?: string; - example?: PType; + examples?: PType[]; format?: 'date'; fulltextIndexed?: boolean; generated?: boolean; From 9abab3325d35a59ff9a4a51aebafac1ede109434 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:07:57 -0700 Subject: [PATCH 04/37] Do not alias ValidationError --- src/class.ts | 10 +++++----- src/definitions/user.ts | 4 ++-- src/definitions/util.ts | 6 +++--- src/error.ts | 5 +---- src/util.ts | 20 ++++++++++---------- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/class.ts b/src/class.ts index dcb1088..faf60ac 100644 --- a/src/class.ts +++ b/src/class.ts @@ -3,7 +3,7 @@ * @module model */ import omit from 'lodash.omit'; -import { AttributeError } from './error'; +import { ValidationError } from './error'; import { EXPOSE_ALL, EXPOSE_EDGE, EXPOSE_NONE } from './constants'; import { defaultPermissions } from './util'; import { Property, PropertyTypeInput } from './property'; @@ -326,7 +326,7 @@ export class ClassModel { continue; } if (properties[attr] === undefined) { - throw new AttributeError(`[${this.name}] unexpected attribute: ${attr}`); + throw new ValidationError(`[${this.name}] unexpected attribute: ${attr}`); } } } @@ -335,12 +335,12 @@ export class ClassModel { if (record.out) { formattedRecord.out = record.out; } else if (!ignoreMissing) { - throw new AttributeError(`[${this.name}] missing required attribute out`); + throw new ValidationError(`[${this.name}] missing required attribute out`); } if (record.in) { formattedRecord.in = record.in; } else if (!ignoreMissing) { - throw new AttributeError(`[${this.name}] missing required attribute in`); + throw new ValidationError(`[${this.name}] missing required attribute in`); } } @@ -362,7 +362,7 @@ export class ClassModel { formattedRecord[prop.name] = record[prop.name]; } if (formattedRecord[prop.name] === undefined && !ignoreMissing) { - throw new AttributeError(`[${this.name}] missing required attribute ${prop.name}`); + throw new ValidationError(`[${this.name}] missing required attribute ${prop.name}`); } } else if (record[prop.name] !== undefined) { // add any optional attributes that were specified diff --git a/src/definitions/user.ts b/src/definitions/user.ts index 81a5ec8..d989a66 100644 --- a/src/definitions/user.ts +++ b/src/definitions/user.ts @@ -1,7 +1,7 @@ import isEmail from 'isemail'; import * as util from '../util'; -import { AttributeError } from '../error'; +import { ValidationError } from '../error'; import { EXPOSE_NONE, PERMISSIONS } from '../constants'; import { BASE_PROPERTIES, activeUUID } from './util'; import { ModelTypeDefinition } from '../types'; @@ -26,7 +26,7 @@ const models: Record = { description: 'the email address to contact this user at', cast: (email) => { if (typeof email !== 'string' || !isEmail.validate(email)) { - throw new AttributeError(`Email (${email}) does not look like a valid email address`); + throw new ValidationError(`Email (${email}) does not look like a valid email address`); } return email; }, diff --git a/src/definitions/util.ts b/src/definitions/util.ts index e822f04..ce77c59 100644 --- a/src/definitions/util.ts +++ b/src/definitions/util.ts @@ -7,7 +7,7 @@ import { v4 as uuidV4 } from 'uuid'; import { position, constants } from '@bcgsc-pori/graphkb-parser'; import * as util from '../util'; -import { AttributeError } from '../error'; +import { ValidationError } from '../error'; import { IndexType, PropertyTypeDefinition } from '../types'; const CLASS_PREFIX = (() => { @@ -30,11 +30,11 @@ const generateBreakRepr = ( if (!end) { return undefined; } - throw new AttributeError('both start and end are required to define a range'); + throw new ValidationError('both start and end are required to define a range'); } if ((start && !start['@class']) || (end && !end['@class'])) { - throw new AttributeError('positions must include the @class attribute to specify the position type'); + throw new ValidationError('positions must include the @class attribute to specify the position type'); } return position.createBreakRepr( diff --git a/src/error.ts b/src/error.ts index 91709f8..0d53a91 100644 --- a/src/error.ts +++ b/src/error.ts @@ -5,7 +5,4 @@ import { ErrorMixin } from '@bcgsc-pori/graphkb-parser'; class ValidationError extends ErrorMixin {} -export { - ValidationError as AttributeError, // Old name, alias for compatibility - ValidationError, -}; +export { ValidationError }; diff --git a/src/util.ts b/src/util.ts index b14fc28..ff004c4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,7 +3,7 @@ * @module util */ import uuidValidate from 'uuid-validate'; -import { AttributeError } from './error'; +import { ValidationError } from './error'; import * as constants from './constants'; // IMPORTANT, to support for the API and GUI, must be able to patch RID import { Expose, GraphRecord } from './types'; @@ -64,7 +64,7 @@ const looksLikeRID = (rid: string, requireHash = false): boolean => { */ const castToRID = (value: constants.GraphRecordId | GraphRecord | null): constants.GraphRecordId => { if (value == null) { - throw new AttributeError('cannot cast null/undefined to RID'); + throw new ValidationError('cannot cast null/undefined to RID'); } if (value instanceof constants.RID) { return value; @@ -73,7 +73,7 @@ const castToRID = (value: constants.GraphRecordId | GraphRecord | null): constan } if (looksLikeRID(value.toString())) { return new constants.RID(`#${value.toString().replace(/^#/, '')}`); } - throw new AttributeError({ message: `not a valid RID (${value})`, value }); + throw new ValidationError({ message: `not a valid RID (${value})`, value }); }; /** @@ -86,11 +86,11 @@ const castString = (string: unknown): string => `${string}`.replace(/\s+/g, ' ') /** * @param {string} string the input string * @returns {string} a string - * @throws {AttributeError} if the input value was not a string or was null + * @throws {ValidationError} if the input value was not a string or was null */ const castLowercaseString = (string: unknown): string => { if (string === null) { - throw new AttributeError('cannot cast null to string'); + throw new ValidationError('cannot cast null to string'); } return castString(string).toLowerCase(); }; @@ -98,7 +98,7 @@ const castLowercaseString = (string: unknown): string => { /** * @param {string} string the input string * @returns {string?} a string - * @throws {AttributeError} if the input value was not a string and not null + * @throws {ValidationError} if the input value was not a string and not null */ const castNullableString = (x: unknown): string | null => (x === null ? null @@ -111,13 +111,13 @@ const castLowercaseNullableString = (x) => (x === null /** * @param {string} string the input string * @returns {string} a string - * @throws {AttributeError} if the input value was an empty string or not a string + * @throws {ValidationError} if the input value was an empty string or not a string */ const castLowercaseNonEmptyString = (x: unknown): string => { const result = castLowercaseString(x); if (result.length === 0) { - throw new AttributeError('Cannot be an empty string'); + throw new ValidationError('Cannot be an empty string'); } return result; }; @@ -125,7 +125,7 @@ const castLowercaseNonEmptyString = (x: unknown): string => { /** * @param {string} string the input string * @returns {string?} a string - * @throws {AttributeError} if the input value was an empty string or not a string and was not null + * @throws {ValidationError} if the input value was an empty string or not a string and was not null */ const castLowercaseNonEmptyNullableString = (x: unknown): string | null => (x === null ? null @@ -144,7 +144,7 @@ const castInteger = (string): number => { if (/^-?\d+$/.exec(string.toString().trim())) { return parseInt(string, 10); } - throw new AttributeError(`${string} is not a valid integer`); + throw new ValidationError(`${string} is not a valid integer`); }; const trimString = (x): string => x.toString().trim(); From 3e520ec2fd3ce87b62196e02f923b9b97d802538 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:10:37 -0700 Subject: [PATCH 05/37] Move schema definition class into sep file --- src/index.ts | 176 +----------------------------------------------- src/schema.ts | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 175 deletions(-) create mode 100644 src/schema.ts diff --git a/src/index.ts b/src/index.ts index 12e5af4..9c87cd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,186 +1,12 @@ import { ClassModel } from './class'; import { Property } from './property'; -import { SchemaDefinitionType } from './types'; import schema from './definitions'; import * as util from './util'; import * as error from './error'; import * as constants from './constants'; import * as sentenceTemplates from './sentenceTemplates'; - -class SchemaDefinition implements SchemaDefinitionType { - readonly schema: Record; - readonly normalizedModelNames: Record; - - constructor(models: Record) { - this.schema = models; - this.normalizedModelNames = {}; - Object.keys(this.schema).forEach((name) => { - const model = this.schema[name]; - this.normalizedModelNames[name.toLowerCase()] = model; - - if (model.reverseName) { - this.normalizedModelNames[model.reverseName.toLowerCase()] = model; - } - }); - } - - get models() { - return this.schema; - } - - /** - * Check that a given class/model name exists - */ - has(obj) { - try { - return Boolean(this.get(obj)); - } catch (err) { - return false; - } - } - - /** - * Returns Knowledgebase class schema. - * @param {Object|string} obj - Record to fetch schema of. - */ - get(obj) { - let cls = obj; - - if (obj && typeof obj === 'object' && obj['@class']) { - cls = obj['@class']; - } - return this.normalizedModelNames[typeof cls === 'string' - ? cls.toLowerCase() - : cls - ] || null; - } - - getFromRoute(routeName) { - for (const model of Object.values(this.schema)) { // eslint-disable-line - if (model.routeName === routeName) { - return model; - } - } - throw new Error(`Missing model corresponding to route (${routeName})`); - } - - getModels() { - return Object.values(this.schema); - } - - getEdgeModels() { - return this.getModels().filter((model) => model.isEdge); - } - - /** - * Returns preview of given record based on its '@class' value - * @param {Object} obj - Record to be parsed. - */ - getPreview(obj) { - if (obj) { - if (obj['@class'] === 'Statement') { - const { content } = sentenceTemplates.generateStatementSentence(this, obj); - return content; - } - - if (obj.displayName) { - return obj.displayName; - } - if (obj.name) { - return obj.name; - } - if (obj['@class']) { - const label = this.getPreview(this.get(obj)); - - if (label) { - return label; - } - } - if (obj['@rid']) { - return obj['@rid']; - } - if (Array.isArray(obj)) { // embedded link set - return obj.length; - } - if (obj.target) { - // preview pseudo-edge objects - return this.getPreview(obj.target); - } - } - return obj; - } - - /** - * Returns the order in which classes should be initialized so that classes which depend on - * other classes already exist. i.e the first item in the array will be classes that do not have - * any dependencies, followed by classes that only depend on those in the first item. For example if class - * Ontology inherits from class V then class V would be in an array preceding the array - * containing Ontology. - * - * Note: V, E, User, and UserGroup are special cases and always put in the first level since - * they have circular dependencies and are created in a non-standard manner - */ - splitClassLevels(): ClassModel[][] { - const adjacencyList: Record = {}; - - // initialize adjacency list - for (const model of Object.values(this.schema)) { - adjacencyList[model.name] = []; - } - - for (const model of Object.values(this.schema)) { - for (const prop of Object.values(model.properties)) { - if (prop.linkedClass) { - adjacencyList[model.name].push(prop.linkedClass.name); - } - } - - for (const parent of model.inherits) { - adjacencyList[model.name].push(parent); - } - - if (model.targetModel) { - adjacencyList[model.name].push(model.targetModel); - } - - if (model.sourceModel) { - adjacencyList[model.name].push(model.sourceModel); - } - } - - const updateAdjList = (adjList, currentLevel, removedModels) => { - currentLevel.forEach((model) => { - removedModels.add(model); - delete adjacencyList[model]; - }); - - for (const model of Object.keys(adjList)) { - adjList[model] = adjList[model].filter((d) => !removedModels.has(d)); - } - }; - - const removed = new Set(); // special cases always in the top level - const levels: string[][] = [['V', 'E', 'User', 'UserGroup']]; - - updateAdjList(adjacencyList, levels[0], removed); - - while (Object.values(adjacencyList).length > 0) { - const level: string[] = []; - - for (const [model, dependencies] of Object.entries(adjacencyList)) { - if (dependencies.length === 0) { - level.push(model); - } - } - levels.push(level); - - updateAdjList(adjacencyList, level, removed); - } - - return levels.map((level) => level.map((modelName) => this.schema[modelName])); - } -} +import { SchemaDefinition } from './schema'; const schemaDef = new SchemaDefinition(schema); diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..9d37100 --- /dev/null +++ b/src/schema.ts @@ -0,0 +1,182 @@ +import { ClassModel } from './class'; +import { SchemaDefinitionType } from './types'; + +import * as sentenceTemplates from './sentenceTemplates'; + +class SchemaDefinition implements SchemaDefinitionType { + readonly schema: Record; + readonly normalizedModelNames: Record; + + constructor(models: Record) { + this.schema = models; + this.normalizedModelNames = {}; + Object.keys(this.schema).forEach((name) => { + const model = this.schema[name]; + this.normalizedModelNames[name.toLowerCase()] = model; + + if (model.reverseName) { + this.normalizedModelNames[model.reverseName.toLowerCase()] = model; + } + }); + } + + get models() { + return this.schema; + } + + /** + * Check that a given class/model name exists + */ + has(obj) { + try { + return Boolean(this.get(obj)); + } catch (err) { + return false; + } + } + + /** + * Returns Knowledgebase class schema. + * @param {Object|string} obj - Record to fetch schema of. + */ + get(obj) { + let cls = obj; + + if (obj && typeof obj === 'object' && obj['@class']) { + cls = obj['@class']; + } + return this.normalizedModelNames[typeof cls === 'string' + ? cls.toLowerCase() + : cls + ] || null; + } + + getFromRoute(routeName) { + for (const model of Object.values(this.schema)) { // eslint-disable-line + if (model.routeName === routeName) { + return model; + } + } + throw new Error(`Missing model corresponding to route (${routeName})`); + } + + getModels() { + return Object.values(this.schema); + } + + getEdgeModels() { + return this.getModels().filter((model) => model.isEdge); + } + + /** + * Returns preview of given record based on its '@class' value + * @param {Object} obj - Record to be parsed. + */ + getPreview(obj) { + if (obj) { + if (obj['@class'] === 'Statement') { + const { content } = sentenceTemplates.generateStatementSentence(this, obj); + return content; + } + + if (obj.displayName) { + return obj.displayName; + } + if (obj.name) { + return obj.name; + } + if (obj['@class']) { + const label = this.getPreview(this.get(obj)); + + if (label) { + return label; + } + } + if (obj['@rid']) { + return obj['@rid']; + } + if (Array.isArray(obj)) { // embedded link set + return obj.length; + } + if (obj.target) { + // preview pseudo-edge objects + return this.getPreview(obj.target); + } + } + return obj; + } + + /** + * Returns the order in which classes should be initialized so that classes which depend on + * other classes already exist. i.e the first item in the array will be classes that do not have + * any dependencies, followed by classes that only depend on those in the first item. For example if class + * Ontology inherits from class V then class V would be in an array preceding the array + * containing Ontology. + * + * Note: V, E, User, and UserGroup are special cases and always put in the first level since + * they have circular dependencies and are created in a non-standard manner + */ + splitClassLevels(): ClassModel[][] { + const adjacencyList: Record = {}; + + // initialize adjacency list + for (const model of Object.values(this.schema)) { + adjacencyList[model.name] = []; + } + + for (const model of Object.values(this.schema)) { + for (const prop of Object.values(model.properties)) { + if (prop.linkedClass) { + adjacencyList[model.name].push(prop.linkedClass.name); + } + } + + for (const parent of model.inherits) { + adjacencyList[model.name].push(parent); + } + + if (model.targetModel) { + adjacencyList[model.name].push(model.targetModel); + } + + if (model.sourceModel) { + adjacencyList[model.name].push(model.sourceModel); + } + } + + const updateAdjList = (adjList, currentLevel, removedModels) => { + currentLevel.forEach((model) => { + removedModels.add(model); + delete adjacencyList[model]; + }); + + for (const model of Object.keys(adjList)) { + adjList[model] = adjList[model].filter((d) => !removedModels.has(d)); + } + }; + + const removed = new Set(); // special cases always in the top level + const levels: string[][] = [['V', 'E', 'User', 'UserGroup']]; + + updateAdjList(adjacencyList, levels[0], removed); + + while (Object.values(adjacencyList).length > 0) { + const level: string[] = []; + + for (const [model, dependencies] of Object.entries(adjacencyList)) { + if (dependencies.length === 0) { + level.push(model); + } + } + levels.push(level); + + updateAdjList(adjacencyList, level, removed); + } + + return levels.map((level) => level.map((modelName) => this.schema[modelName])); + } +} + +export { + SchemaDefinition, +}; From ef336a2a123a4537a9cde9160d9627888ab07f13 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:11:43 -0700 Subject: [PATCH 06/37] Rename test file to match module rename --- test/{model.test.ts => class.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{model.test.ts => class.test.ts} (100%) diff --git a/test/model.test.ts b/test/class.test.ts similarity index 100% rename from test/model.test.ts rename to test/class.test.ts From fc6ac07625a7cd066458fee9d34e5158c49b4eba Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:25:41 -0700 Subject: [PATCH 07/37] Do not use classes for models and properties --- src/class.ts | 464 ++++++--------------------------- src/constants.ts | 11 +- src/definitions/edges.ts | 4 +- src/definitions/index.ts | 94 +++---- src/definitions/ontology.ts | 10 +- src/definitions/position.ts | 4 +- src/definitions/statement.ts | 4 +- src/definitions/user.ts | 9 +- src/definitions/util.ts | 6 +- src/definitions/variant.ts | 4 +- src/error.ts | 4 +- src/index.ts | 41 ++- src/property.ts | 376 +++++++++++--------------- src/schema.ts | 394 ++++++++++++++++++++++++---- src/sentenceTemplates.ts | 20 +- src/types.ts | 235 +++++++++-------- src/util.ts | 30 +-- test/class.test.ts | 311 ++-------------------- test/index.test.ts | 196 -------------- test/property.test.ts | 244 ++++++++--------- test/schema.mock.test.ts | 209 +++++++++++++++ test/schema.test.ts | 439 +++++++++++++++++++++++++++++++ test/sentenceTemplates.test.ts | 70 ++--- test/util.test.ts | 15 +- 24 files changed, 1625 insertions(+), 1569 deletions(-) delete mode 100644 test/index.test.ts create mode 100644 test/schema.mock.test.ts create mode 100644 test/schema.test.ts diff --git a/src/class.ts b/src/class.ts index faf60ac..4ee7c31 100644 --- a/src/class.ts +++ b/src/class.ts @@ -1,407 +1,99 @@ -/** - * Classes for enforcing constraints on DB classes and properties - * @module model - */ -import omit from 'lodash.omit'; -import { ValidationError } from './error'; -import { EXPOSE_ALL, EXPOSE_EDGE, EXPOSE_NONE } from './constants'; -import { defaultPermissions } from './util'; -import { Property, PropertyTypeInput } from './property'; -import { IndexType, ModelTypeDefinition, GraphRecord } from './types'; - -interface ModelTypeInput extends Omit { - subclasses?: ClassModel[]; - name: string; - inherits?: ClassModel[]; - targetModel?: string; - sourceModel?: string; - properties?: PropertyTypeInput[] -} - -export class ClassModel { - readonly _properties: Record; - readonly description: string; - readonly embedded: boolean; - readonly indices: IndexType[]; - readonly isAbstract: boolean; - readonly isEdge: boolean; - readonly name: string; - readonly permissions: any; - readonly reverseName: string; - readonly routes: any; - readonly sourceModel?: string; - readonly targetModel?: string; - - // must be initialized after construction - _inherits: ClassModel[]; - subclasses: ClassModel[]; - - /** - * @param {Object} opt - * @param {string} opt.name the class name - * @param {Object.} [opt.defaults={}] the mapping of attribute names to functions producing default values - * @param {ClassModel[]} [opt.inherits=[]] the models this model inherits from - * @param {boolean} [opt.isAbstract=false] this is an abstract class - * @param {Object.} [opt.properties={}] mapping by attribute name to property objects (defined by orientjs) - * @param {Expose} [opt.routes] the routes to routes to the API for this class - * @param {boolean} [opt.embedded=false] this class owns no records and is used as part of other class records only - * @param {string} [opt.targetModel] the model edges incoming vertices are restricted to - * @param {string} [opt.source] the model edges outgoing vertices are restricted to - * @param {Array.} [opt.uniqueNonIndexedProps] the properties which in combination are expected to be unique (used in building select queries) - */ - constructor(opt: ModelTypeInput) { - this.name = opt.name; - this.description = opt.description || ''; - this._inherits = opt.inherits || []; - this.subclasses = opt.subclasses || []; - this.isEdge = Boolean(opt.isEdge); - this.targetModel = opt.targetModel; - this.sourceModel = opt.sourceModel; - - if (this.targetModel || this.sourceModel) { - this.isEdge = true; - } - this.embedded = Boolean(opt.embedded); - this.reverseName = opt.reverseName || opt.name; - this.isAbstract = Boolean(opt.isAbstract); - - if (this.isAbstract || this.embedded) { - this.routes = { ...EXPOSE_NONE, ...opt.routes }; - } else if (this.isEdge) { - this.routes = { ...EXPOSE_EDGE, ...opt.routes }; - } else { - this.routes = { ...EXPOSE_ALL, ...opt.routes }; - } - // use routing defaults to set permissions defaults - - // override defaults if specific permissions are given - this.permissions = { - ...defaultPermissions(this.routes), - ...opt.permissions, - }; - - this.indices = opt.indices || []; - - this._properties = {}; // by name - - for (const prop of (opt.properties || [])) { - this._properties[prop.name] = new Property({ ...omit(prop, ['linkedClass']) }); - } - } - - /** - * the default route name for this class - * @type {string} - */ - get routeName() { - if (this.name.length === 1) { - return `/${this.name.toLowerCase()}`; - } if (!this.isEdge && !this.name.endsWith('ary') && this.name.toLowerCase() !== 'evidence') { - if (/.*[^aeiou]y$/.exec(this.name)) { - return `/${this.name.slice(0, this.name.length - 1)}ies`.toLowerCase(); - } - return `/${this.name}s`.toLowerCase(); - } - return `/${this.name.toLowerCase()}`; +import { + ClassDefinition, PropertyDefinition, ClassDefinitionInput, Expose, ClassPermissions, GroupName, +} from './types'; +import { createPropertyDefinition } from './property'; +import { + EXPOSE_ALL, EXPOSE_EDGE, EXPOSE_NONE, PERMISSIONS, +} from './constants'; + +const getRouteName = (name: string, isEdge: boolean) => { + if (name.length === 1) { + return `/${name.toLowerCase()}`; + } if (!isEdge && !name.endsWith('ary') && name.toLowerCase() !== 'evidence') { + if (/.*[^aeiou]y$/.exec(name)) { + return `/${name.slice(0, name.length - 1)}ies`.toLowerCase(); + } + return `/${name}s`.toLowerCase(); } + return `/${name.toLowerCase()}`; +}; - /** - * the list of parent class names which this class inherits from - * @type {Array.} - */ - get inherits() { - const parents: string[] = []; +const defaultPermissions = (routes: Partial = {}): ClassPermissions => { + const permissions = { + default: PERMISSIONS.NONE, + readonly: PERMISSIONS.READ, + }; - for (const model of this._inherits) { - parents.push(model.name); - parents.push(...model.inherits); - } - return parents; + if (routes.QUERY || routes.GET) { + permissions.default |= PERMISSIONS.READ; } - - /** - * Returns the list of properties for the Class.active index. This is - * expected to represent a unique constraint on a combination of these properties - * - * @returns {Array.} list of properties which belong to the index - */ - getActiveProperties() { - for (const index of this.indices) { - if (index.name === `${this.name}.active`) { - return index.properties; - } - } - return null; + if (routes.POST) { + permissions.default |= PERMISSIONS.CREATE; } - - /** - * Given the name of a subclass, retrieve the subclass model or throw an error if it is not - * found - * - * @param {string} modelName the name of the model to find as a subclass - * @throws {Error} if the subclass was not found - */ - subClassModel(modelName) { - for (const subclass of this.subclasses) { - if (subclass.name === modelName) { - return subclass; - } - - try { - return subclass.subClassModel(modelName); - } catch (err) {} - } - throw new Error(`The subclass (${ - modelName - }) was not found as a subclass of the current model (${ - this.name - })`); + if (routes.PATCH) { + permissions.default |= PERMISSIONS.UPDATE; } - - /** - * Get a list of models (including the current model) for all descendants of the current model - * - * @param {boolean} excludeAbstract exclude abstract models - * - * @returns {Array.} the array of descendant models - */ - descendantTree(excludeAbstract = false) { - const descendants: ClassModel[] = [this]; - const queue = this.subclasses.slice(); - - while (queue.length > 0) { - const child = queue.shift() as typeof queue[number]; - - if (descendants.includes(child)) { - continue; - } - descendants.push(child); - queue.push(...child.subclasses); - } - return descendants.filter((model) => !excludeAbstract || !model.isAbstract); + if (routes.DELETE) { + permissions.default |= PERMISSIONS.DELETE; } + return permissions; +}; - /** - * Returns a set of properties from this class and all subclasses - * @type {Array.} - */ - get queryProperties() { - const queue = Array.from(this.subclasses); - const queryProps = this.properties; - - while (queue.length > 0) { - const curr = queue.shift() as typeof queue[number]; +const createClassDefinition = (opt: ClassDefinitionInput): ClassDefinition => { + const { name } = opt; + const properties: Record = {}; - for (const prop of Object.values(curr.properties)) { - if (queryProps[prop.name] === undefined) { // first model to declare is used - queryProps[prop.name] = prop; - } - } - queue.push(...curr.subclasses); - } - return queryProps; + for (const propDefn of opt.properties || []) { + properties[propDefn.name] = createPropertyDefinition(propDefn); } - /** - * a list of property names for all required properties - * @type {Array.} - */ - get required() { - const required = Array.from(Object.values(this._properties).filter( - (prop) => prop.mandatory, - ), (prop) => prop.name); + let isEdge = Boolean(opt.isEdge); - for (const parent of this._inherits) { - required.push(...parent.required); - } - return required; + if (opt.targetModel || opt.sourceModel) { + isEdge = true; } - /** - * a list of property names for all optional properties - * @type {Array.} - */ - get optional() { - const optional = Array.from( - Object.values(this._properties).filter((prop) => !prop.mandatory), - (prop) => prop.name, - ); - - for (const parent of this._inherits) { - optional.push(...parent.optional); - } - return optional; - } + let reverseName; - /** - * Check if this model inherits a property from a parent model - */ - inheritsProperty(propName) { - for (const model of this._inherits) { - if (model._properties[propName] !== undefined) { - return true; - } - if (model.inheritsProperty(propName)) { - return true; - } + if (opt.isEdge) { + if (name.endsWith('Of')) { + reverseName = `Has${name.slice(0, name.length - 2)}`; + } else if (name.endsWith('By')) { + reverseName = `${name.slice(0, name.length - 3)}s`; + } else if (name === 'Infers') { + reverseName = 'InferredBy'; + } else { + reverseName = `${name.slice(0, name.length - 1)}dBy`; } - return false; } - /** - * a list of the properties associate with this class or parents of this class - * @type {Array.} - */ - get properties() { - let properties = { ...this._properties }; + let defaultRoutes: Expose; - for (const parent of this._inherits) { - properties = { ...parent.properties, ...properties }; // properties of the same name are taken from the lowest model - } - return properties; + if (opt.isAbstract || opt.embedded) { + defaultRoutes = EXPOSE_NONE; + } else if (opt.isEdge) { + defaultRoutes = EXPOSE_EDGE; + } else { + defaultRoutes = EXPOSE_ALL; } - /** - * @returns {Object} a partial json representation of the current class model - */ - toJSON() { - const json: { - properties: Record; - inherits: string[]; - isEdge: boolean; - name: string; - isAbstract: boolean; - embedded: boolean; - reverseName?: string; - route?: string; - } = { - properties: this.properties, - inherits: this.inherits, - isEdge: !!this.isEdge, - name: this.name, - isAbstract: this.isAbstract, - embedded: this.embedded, - }; - - if (this.reverseName) { - json.reverseName = this.reverseName; - } - if (Object.values(this.routes).some((x) => x)) { - json.route = this.routeName; - } - return json; - } - - /** - * Checks a single record to ensure it matches the expected pattern for this class model - * - * @param {Object} record the record to be checked - * @param {Object} opt options - * @param {boolean} [opt.dropExtra=true] drop any record attributes that are not defined on the current class model by either required or optional - * @param {boolean} [opt.addDefaults=false] add default values for any attributes not given (where defined) - * @param {boolean} [opt.ignoreMissing=false] do not throw an error when a required attribute is missing - * @param {boolean} [opt.ignoreExtra=false] do not throw an error when an unexpected value is given - */ - formatRecord(record: GraphRecord, opt = {}): GraphRecord { - // add default options - const { - dropExtra = true, - addDefaults = true, - ignoreExtra = false, - ignoreMissing = false, - }: { - dropExtra?: boolean; - addDefaults?: boolean; - ignoreExtra?: boolean; - ignoreMissing?: boolean; - } = opt; - const formattedRecord = dropExtra - ? {} - : { ...record }; - const { properties } = this; - - if (!ignoreExtra && !dropExtra) { - for (const attr of Object.keys(record)) { - if (this.isEdge && (attr === 'out' || attr === 'in')) { - continue; - } - if (properties[attr] === undefined) { - throw new ValidationError(`[${this.name}] unexpected attribute: ${attr}`); - } - } - } - // if this is an edge class, check the to and from attributes - if (this.isEdge) { - if (record.out) { - formattedRecord.out = record.out; - } else if (!ignoreMissing) { - throw new ValidationError(`[${this.name}] missing required attribute out`); - } - if (record.in) { - formattedRecord.in = record.in; - } else if (!ignoreMissing) { - throw new ValidationError(`[${this.name}] missing required attribute in`); - } - } - - // add the non generated (from other properties) attributes - for (const prop of Object.values(properties)) { - if (addDefaults && record[prop.name] === undefined && !prop.generationDependencies) { - if (prop.default !== undefined) { - formattedRecord[prop.name] = prop.default; - } else if (prop.generateDefault) { - formattedRecord[prop.name] = prop.generateDefault(); - } - } - // check that the required attributes are there - if (prop.mandatory) { - if (record[prop.name] === undefined && ignoreMissing) { - continue; - } - if (record[prop.name] !== undefined) { - formattedRecord[prop.name] = record[prop.name]; - } - if (formattedRecord[prop.name] === undefined && !ignoreMissing) { - throw new ValidationError(`[${this.name}] missing required attribute ${prop.name}`); - } - } else if (record[prop.name] !== undefined) { - // add any optional attributes that were specified - formattedRecord[prop.name] = record[prop.name]; - } - // try the casting - if (formattedRecord[prop.name] !== undefined) { - formattedRecord[prop.name] = prop.validate(formattedRecord[prop.name]); - } - } - - // look for linked models - for (let [attr, value] of Object.entries(formattedRecord)) { - let { linkedClass, type, iterable } = properties[attr]; - - if (type.startsWith('embedded') && linkedClass !== undefined && value) { - if (value['@class'] && value['@class'] !== linkedClass.name) { - linkedClass = linkedClass.subClassModel(value['@class']); - } - if (type === 'embedded' && typeof value === 'object') { - value = linkedClass.formatRecord(value); - } else if (iterable) { - value = Array.from(value, (v) => (linkedClass as ClassModel).formatRecord(v as GraphRecord)); - } - } - formattedRecord[attr] = value; - } - - // create the generated attributes - if (addDefaults) { - for (const prop of Object.values(properties)) { - if (prop.generationDependencies - && prop.generateDefault - && (prop.generated || formattedRecord[prop.name] === undefined) - ) { - formattedRecord[prop.name] = prop.generateDefault(formattedRecord); - } - } - } - return formattedRecord; - } -} + const routes = opt.routes || defaultRoutes || {}; + + return { + ...opt, + properties, + routeName: getRouteName(opt.name, Boolean(opt.isEdge)), + isEdge, + routes, + isAbstract: Boolean(opt.isAbstract), + inherits: opt.inherits || [], + description: opt.description || '', + embedded: Boolean(opt.embedded), + indices: opt.indices || [], + permissions: { ...defaultPermissions(routes), ...(opt.permissions || {}) }, + reverseName, + + }; +}; + +export { createClassDefinition, getRouteName }; diff --git a/src/constants.ts b/src/constants.ts index f2b1158..ebd78fe 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,14 +15,9 @@ const EXPOSE_READ = { QUERY: true, PATCH: false, DELETE: false, POST: false, GET: true, }; -const FUZZY_CLASSES = ['AliasOf', 'DeprecatedBy']; - -// default separator chars for orientdb full text hash: https://github.com/orientechnologies/orientdb/blob/2.2.x/core/src/main/java/com/orientechnologies/orient/core/index/OIndexFullText.java -const INDEX_SEP_CHARS = ' \r\n\t:;,.|+*/\\=!?[]()'; - /** * @namespace - * @property {Number} CREATE permissions for create/insert/post opertations + * @property {Number} CREATE permissions for create/insert/post operations * @property {Number} READ permissions for read/get operations * @property {Number} UPDATE permissions for update/patch operations * @property {Number} DELETE permissions for delete/remove operations @@ -45,7 +40,7 @@ const PERMISSIONS = { UPDATE: 0b0010, DELETE: 0b0001, NONE: 0b0000, - ALL: 0, + ALL: 1, }; PERMISSIONS.ALL = PERMISSIONS.READ | PERMISSIONS.CREATE | PERMISSIONS.UPDATE | PERMISSIONS.DELETE; @@ -62,8 +57,6 @@ export { EXPOSE_NONE, EXPOSE_EDGE, EXPOSE_READ, - FUZZY_CLASSES, - INDEX_SEP_CHARS, PERMISSIONS, RID, // IMPORTANT: to be patched with orientjs.RID for API and not GUI }; diff --git a/src/definitions/edges.ts b/src/definitions/edges.ts index f9b5673..80ad680 100644 --- a/src/definitions/edges.ts +++ b/src/definitions/edges.ts @@ -1,8 +1,8 @@ +import { PartialSchemaDefn } from '../types'; import { EXPOSE_READ } from '../constants'; -import { ModelTypeDefinition } from '../types'; import { defineSimpleIndex, BASE_PROPERTIES, activeUUID } from './util'; -const edgeModels: Record = { +const edgeModels: PartialSchemaDefn = { E: { description: 'Edges', routes: EXPOSE_READ, diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 95ed327..0f1bd07 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -1,11 +1,7 @@ /** - * Repsonsible for defining the schema. - * @module schema + * Responsible for defining the schema. */ -import omit from 'lodash.omit'; - -import { PERMISSIONS, EXPOSE_READ } from '../constants'; -import { ClassModel } from '../class'; +import { PERMISSIONS, EXPOSE_READ, EXPOSE_NONE } from '../constants'; import { timeStampNow } from '../util'; import { defineSimpleIndex, BASE_PROPERTIES, activeUUID } from './util'; import edges from './edges'; @@ -14,9 +10,12 @@ import position from './position'; import statement from './statement'; import variant from './variant'; import user from './user'; -import { ModelTypeDefinition } from '../types'; +import { + ClassDefinitionInput, PropertyDefinitionInput, ClassDefinition, PartialSchemaDefn, +} from '../types'; +import { createClassDefinition } from '../class'; -const BASE_SCHEMA: Record = { +const BASE_SCHEMA: PartialSchemaDefn = { V: { description: 'Vertices', routes: EXPOSE_READ, @@ -148,35 +147,30 @@ const BASE_SCHEMA: Record = { * linking between classes and wrapper class/property models */ const initializeSchema = ( - inputSchema: Record, -): Record => { + inputSchema: Record>, +): Record => { // initialize the models - const schema = { ...inputSchema }; + const permissionsProperties: PropertyDefinitionInput[] = []; - for (const name of Object.keys(schema)) { - if (name !== 'Permissions' && !schema[name].embedded && schema.Permissions !== undefined) { - if (schema.Permissions.properties === undefined) { - schema.Permissions = { - ...schema.Permissions, - properties: [{ - min: PERMISSIONS.NONE, max: PERMISSIONS.ALL, type: 'integer', nullable: false, readOnly: false, name, - }], - }; - } else { - schema.Permissions.properties.push({ - min: PERMISSIONS.NONE, max: PERMISSIONS.ALL, type: 'integer', nullable: false, readOnly: false, name, - }); - } + for (const name of Object.keys(inputSchema)) { + if (name !== 'Permissions' && !inputSchema[name].embedded) { + permissionsProperties.push({ + min: PERMISSIONS.NONE, max: PERMISSIONS.ALL, type: 'integer', nullable: false, readOnly: false, name, + }); } } - const models: Record = {}; + const models: Record = { + Permissions: createClassDefinition({ + routes: EXPOSE_NONE, + properties: permissionsProperties, + name: 'Permissions', + embedded: true, + }), + }; - // build the model objects - for (const [modelName, model] of Object.entries(schema)) { - const { - inherits, properties, ...modelOptions - } = model; + // build the index/fulltext flags + for (const [modelName, model] of Object.entries(inputSchema)) { // for each fast index, mark the field as searchable const indexed = new Set(); const fulltext = new Set(); @@ -193,45 +187,21 @@ const initializeSchema = ( } } - models[modelName] = new ClassModel({ + models[modelName] = createClassDefinition({ + ...model, properties: (model.properties || []).map((prop) => ({ - ...omit(prop, ['linkedClass']), + ...prop, indexed: indexed.has(prop.name), fulltextIndexed: fulltext.has(prop.name), })), - ...modelOptions, name: modelName, }); } - - // link the inherited models and linked models - for (const [modelName, defn] of Object.entries(schema)) { - const model = models[modelName]; - - // fill the _inherits and subclasses properties - for (const parent of defn.inherits || []) { - if (models[parent] === undefined) { - throw new Error(`Schema definition error. Expected model ${parent} is not defined`); - } - models[model.name]._inherits.push(models[parent]); - models[parent].subclasses.push(models[model.name]); - } - - // resolve the linked class - for (const prop of defn.properties || []) { - if (prop.linkedClass) { - if (models[prop.linkedClass] === undefined) { - throw new Error(`Schema definition error. Expected model ${prop.linkedClass} is not defined`); - } - model._properties[prop.name].linkedClass = models[prop.linkedClass]; - } - } - } return models; }; -const mergeDefinitions = (defns: Record[]) => { - const merge: Record = {}; +const mergeDefinitions = (defns: PartialSchemaDefn[]) => { + const merge: Record> = {}; for (const defn of defns) { for (const key of Object.keys(defn)) { @@ -244,6 +214,8 @@ const mergeDefinitions = (defns: Record[]) => { return merge; }; -export default initializeSchema(mergeDefinitions([ +const definitions = initializeSchema(mergeDefinitions([ BASE_SCHEMA, edges, position, statement, variant, user, ontology, ])); + +export default definitions; diff --git a/src/definitions/ontology.ts b/src/definitions/ontology.ts index 0e3ec7f..95cf9f8 100644 --- a/src/definitions/ontology.ts +++ b/src/definitions/ontology.ts @@ -1,9 +1,9 @@ import * as util from '../util'; import { EXPOSE_READ, PERMISSIONS } from '../constants'; import { BASE_PROPERTIES } from './util'; -import { ModelTypeDefinition } from '../types'; +import { PartialSchemaDefn } from '../types'; -const models: Record = { +const models: PartialSchemaDefn = { Ontology: { routes: EXPOSE_READ, inherits: ['V', 'Biomarker'], @@ -86,9 +86,7 @@ const models: Record = { type: 'embeddedset', linkedType: 'string', description: 'A list of names of subsets this term belongs to', - cast: (item) => (typeof item === 'string' - ? item.trim().toLowerCase() - : item), + cast: (item) => `${item}`.trim().toLowerCase(), }, { name: 'deprecated', @@ -277,7 +275,7 @@ const models: Record = { admin: PERMISSIONS.ALL, manager: PERMISSIONS.ALL, }, - description: 'Curated list of terms used in clasifying variants or assigning relevance to statements', + description: 'Curated list of terms used in classifying variants or assigning relevance to statements', inherits: ['Ontology'], properties: [ { name: 'shortName', type: 'string', description: 'a shortened form of the vocabulary term. Generally this is used for variantClass type records line del for deletion' }, diff --git a/src/definitions/position.ts b/src/definitions/position.ts index 68a8ee5..23310cf 100644 --- a/src/definitions/position.ts +++ b/src/definitions/position.ts @@ -1,9 +1,9 @@ import { EXPOSE_NONE } from '../constants'; import * as util from '../util'; -import { ModelTypeDefinition } from '../types'; +import { PartialSchemaDefn } from '../types'; import { BASE_PROPERTIES } from './util'; -const models: Record = { +const models: PartialSchemaDefn = { Position: { routes: EXPOSE_NONE, properties: [ diff --git a/src/definitions/statement.ts b/src/definitions/statement.ts index 50c4e49..43868f4 100644 --- a/src/definitions/statement.ts +++ b/src/definitions/statement.ts @@ -2,9 +2,9 @@ import { REVIEW_STATUS, EXPOSE_ALL, EXPOSE_NONE } from '../constants'; import { BASE_PROPERTIES, defineSimpleIndex } from './util'; import { DEFAULT_TEMPLATE, chooseDefaultTemplate } from '../sentenceTemplates'; import * as util from '../util'; -import { ModelTypeDefinition } from '../types'; +import { PartialSchemaDefn } from '../types'; -const models: Record = { +const models: PartialSchemaDefn = { StatementReview: { description: 'Review of a statement', routes: EXPOSE_NONE, diff --git a/src/definitions/user.ts b/src/definitions/user.ts index d989a66..abd8a7d 100644 --- a/src/definitions/user.ts +++ b/src/definitions/user.ts @@ -4,9 +4,9 @@ import * as util from '../util'; import { ValidationError } from '../error'; import { EXPOSE_NONE, PERMISSIONS } from '../constants'; import { BASE_PROPERTIES, activeUUID } from './util'; -import { ModelTypeDefinition } from '../types'; +import { PartialSchemaDefn } from '../types'; -const models: Record = { +const models: PartialSchemaDefn = { User: { permissions: { default: PERMISSIONS.READ, @@ -127,11 +127,6 @@ const models: Record = { activeUUID('UserGroup'), ], }, - Permissions: { - routes: EXPOSE_NONE, - properties: [], - embedded: true, - }, }; export default models; diff --git a/src/definitions/util.ts b/src/definitions/util.ts index ce77c59..09d7080 100644 --- a/src/definitions/util.ts +++ b/src/definitions/util.ts @@ -8,7 +8,7 @@ import { position, constants } from '@bcgsc-pori/graphkb-parser'; import * as util from '../util'; import { ValidationError } from '../error'; -import { IndexType, PropertyTypeDefinition } from '../types'; +import { IndexType, PropertyDefinitionInput } from '../types'; const CLASS_PREFIX = (() => { const result = {}; @@ -64,7 +64,7 @@ const castBreakRepr = (repr: string): string => { type BasePropertyName = '@rid' | '@class' | 'uuid' | 'createdAt' | 'updatedAt' | 'updatedBy' | 'deletedAt' | 'createdBy' | 'deletedBy' | 'history' | 'groupRestrictions' | 'in' | 'out' | 'displayName'; -const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { +const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { '@rid': { name: '@rid', pattern: '^#\\d+:\\d+$', @@ -160,7 +160,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyTypeDefinition } = { type: 'linkset', linkedClass: 'UserGroup', description: 'user groups allowed to interact with this record', - examples: [['#33:1', '#33:2']], + examples: [['#33:1'], '#33:2'], }, in: { name: 'in', diff --git a/src/definitions/variant.ts b/src/definitions/variant.ts index dd8f104..472e115 100644 --- a/src/definitions/variant.ts +++ b/src/definitions/variant.ts @@ -2,9 +2,9 @@ import * as util from '../util'; import { EXPOSE_READ } from '../constants'; import { BASE_PROPERTIES, castBreakRepr, generateBreakRepr } from './util'; -import { ModelTypeDefinition } from '../types'; +import { PartialSchemaDefn } from '../types'; -const models: Record = { +const models: PartialSchemaDefn = { Variant: { description: 'Any deviation from the norm (ex. high expression) with respect to some reference object (ex. a gene)', routes: EXPOSE_READ, diff --git a/src/error.ts b/src/error.ts index 0d53a91..9a4e4d7 100644 --- a/src/error.ts +++ b/src/error.ts @@ -5,4 +5,6 @@ import { ErrorMixin } from '@bcgsc-pori/graphkb-parser'; class ValidationError extends ErrorMixin {} -export { ValidationError }; +export { + ValidationError, ErrorMixin, +}; diff --git a/src/index.ts b/src/index.ts index 9c87cd2..e06ef43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,44 @@ -import { ClassModel } from './class'; -import { Property } from './property'; -import schema from './definitions'; +import { + PropertyDefinition, + ClassDefinition, + VertexName, + EmbeddedVertexName, + EdgeName, + ClassName, + DbType, + ClassPermissions, + GraphRecord, +} from './types'; import * as util from './util'; -import * as error from './error'; +import { ValidationError, ErrorMixin } from './error'; import * as constants from './constants'; - +import { REVIEW_STATUS, PERMISSIONS } from './constants'; import * as sentenceTemplates from './sentenceTemplates'; +import definitions from './definitions'; import { SchemaDefinition } from './schema'; -const schemaDef = new SchemaDefinition(schema); +const schemaDef = new SchemaDefinition(definitions); + +export type { + ClassDefinition, + PropertyDefinition, + VertexName, + EmbeddedVertexName, + EdgeName, + ClassName, + DbType, + ClassPermissions, + GraphRecord, +}; export { - ClassModel, - Property, schemaDef as schema, util, - error, + ValidationError, + ErrorMixin, + REVIEW_STATUS, constants, + PERMISSIONS, sentenceTemplates, + SchemaDefinition, }; diff --git a/src/property.ts b/src/property.ts index ba446c3..3537b03 100644 --- a/src/property.ts +++ b/src/property.ts @@ -4,252 +4,188 @@ import { ValidationError } from './error'; import * as util from './util'; -import { - DbType, PropertyType, ModelType, PropertyTypeDefinition, -} from './types'; - -export interface PropertyTypeInput extends Omit { - linkedClass?: ModelType; -} - -export class Property implements PropertyType { - readonly name: string; - readonly cast?: (value: any) => unknown; - readonly type: DbType; - readonly pattern?: string; - readonly description?: string; - readonly generated?: boolean; - readonly mandatory?: boolean; - readonly nullable?: boolean; - readonly readOnly?: boolean; - readonly generateDefault?: (rec?: unknown) => unknown; - readonly check?: (rec?: unknown) => boolean; - readonly default?: unknown; - readonly examples?: unknown[]; - readonly generationDependencies?: boolean; - readonly nonEmpty?: boolean; - readonly linkedType?: 'string'; - readonly format?: 'date'; - readonly choices?: unknown[]; - readonly min?: number; - readonly minItems?: number; - readonly max?: number; - readonly maxItems?: number; - readonly iterable: boolean; - readonly indexed: boolean; - readonly fulltextIndexed: boolean; - - // must be initialized after creation - linkedClass?: ModelType; - - /** - * create a new property - * - * @param {Object} opt options - * @param {string} opt.name the property name - * @param {*|Function} opt.default the default value or function for generating the default - * @param {boolean} opt.generated indicates that this is a generated field and should not be input by the user - * @param {boolean} opt.generationDependencies indicates that a field should be generated after all other processing is complete b/c it requires other fields - * @param {*} opt.examples an example value to use for help text - * @param {string} opt.pattern the regex pattern values for this property should follow (used purely in docs) - * @param {boolean} opt.nullable flag to indicate if the value can be null - * @param {boolean} opt.mandatory flag to indicate if this property is required - * @param {boolean} opt.nonEmpty for string properties indicates that an empty string is invalid - * @param {string} opt.description description for the openapi spec - * @param {ClassModel} opt.linkedClass if applicable, the class this link should point to or embed - * @param {Array} opt.choices enum representing acceptable values - * @param {Number} opt.min minimum value allowed (for intger type properties) - * @param {Number} opt.max maximum value allowed (for integer type properties) - * @param {Function} opt.cast the function to be used in formatting values for this property (for list properties it is the function for elements in the list) - * @param {boolean} opt.indexed if this field is exact indexed for quick search - * @param {boolean} opt.fulltextIndexed if this field has a fulltext index - * - * @return {Property} the new property - */ - constructor(opt: PropertyTypeInput) { - this.name = opt.name; - - if (opt.default !== undefined) { - if (opt.default instanceof Function) { - this.generateDefault = opt.default; - } else { - this.default = opt.default; - } - } - const defaultType = ((opt.min !== undefined || opt.max !== undefined) && !opt.type) - ? 'integer' - : 'string'; - this.type = opt.type || defaultType; - - this.cast = opt.cast; - this.description = opt.description || ''; - this.examples = opt.examples; - this.generated = Boolean(opt.generated); - this.generationDependencies = Boolean(opt.generationDependencies); - this.iterable = Boolean(/(set|list|bag|map)/ig.exec(this.type)); - this.linkedClass = opt.linkedClass; - this.mandatory = Boolean(opt.mandatory); // default false - this.max = opt.max; - this.maxItems = opt.maxItems; - this.min = opt.min; - this.minItems = opt.minItems; - this.nonEmpty = Boolean(opt.nonEmpty); - this.nullable = opt.nullable === undefined - ? true - : opt.nullable; - this.pattern = opt.pattern; - this.check = opt.check; - this.fulltextIndexed = Boolean(opt.fulltextIndexed); - this.indexed = Boolean(opt.indexed); - - this.choices = opt.choices; - - if (this.examples === undefined && this.choices) { - this.examples = this.choices; - } +import { PropertyDefinition, DbType, PropertyDefinitionInput } from './types'; - if (!this.cast) { // set the default util.cast functions - if (this.type === 'integer') { - this.cast = util.castInteger; - } else if (this.type === 'string') { - if (!this.nullable) { - this.cast = (this.nonEmpty - ? util.castLowercaseNonEmptyString - : util.castLowercaseString); - } else { - this.cast = (this.nonEmpty - ? util.castLowercaseNonEmptyNullableString - : util.castLowercaseNullableString); - } - } else if (this.type.includes('link')) { - if (!this.nullable) { - this.cast = util.castToRID; - } else { - this.cast = util.castNullableLink; - } - } - } else if (this.choices) { - this.choices = this.choices.map( - (choice) => (this.cast as NonNullable)(choice), - ); - } +/** +* @param {PropertyDefinition} prop the property model to validate against +* @param inputValue the input value to be validated +* +* @throws {ValidationError} if the input value violates any of the property model constraints +* +* @returns the cast input value +*/ +export const validateProperty = (prop: PropertyDefinition, inputValue: unknown): unknown => { + const values = inputValue instanceof Array + ? inputValue + : [inputValue]; + + if (values.length > 1 && !prop.iterable) { + throw new ValidationError({ + message: `The ${prop.name} property is not iterable but has been given multiple values`, + field: prop.name, + }); } - /** - * @param {Property} prop the property model to validate against - * @param inputValue the input value to be validated - * - * @throws {ValidationError} if the input value violates any of the property model constraints - * - * @returns the cast input value - */ - static validateWith(prop: Property, inputValue) { - const values = inputValue instanceof Array - ? inputValue - : [inputValue]; - - if (values.length > 1 && !prop.iterable) { + const result: unknown[] = []; + + // add cast and type checking should apply to the inner elements of an iterable + for (const value of values) { + if (value === null && !prop.nullable) { throw new ValidationError({ - message: `The ${prop.name} property is not iterable but has been given multiple values`, + message: `The ${prop.name} property cannot be null`, field: prop.name, }); } + let castValue = value; - const result: unknown[] = []; - - // add cast and type checking should apply to the inner elements of an iterable - for (const value of values) { - if (value === null && !prop.nullable) { + if (prop.cast && (!prop.nullable || castValue !== null)) { + try { + castValue = prop.cast(value); + } catch (err: any) { throw new ValidationError({ - message: `The ${prop.name} property cannot be null`, + message: `Failed casting ${prop.name}: ${err.message}`, field: prop.name, + castFunction: prop.cast, }); } - let castValue = value; - - if (prop.cast && (!prop.nullable || castValue !== null)) { - try { - castValue = prop.cast(value); - } catch (err: any) { - throw new ValidationError({ - message: `Failed casting ${prop.name}: ${err.message}`, - field: prop.name, - castFunction: prop.cast, - }); - } - } - result.push(castValue); + } + result.push(castValue); - if (prop.nonEmpty && castValue === '') { + if (prop.nonEmpty && castValue === '') { + throw new ValidationError({ + message: `The ${prop.name} property cannot be an empty string`, + field: prop.name, + }); + } + if (castValue !== null) { + if (prop.min !== undefined && prop.min !== null && castValue < prop.min) { + throw new ValidationError({ + message: `Violated the minimum value constraint of ${prop.name} (${castValue} < ${prop.min})`, + field: prop.name, + }); + } + if (prop.max !== undefined && prop.max !== null && castValue > prop.max) { throw new ValidationError({ - message: `The ${prop.name} property cannot be an empty string`, + message: `Violated the maximum value constraint of ${prop.name} (${castValue} > ${prop.max})`, field: prop.name, }); } - if (castValue !== null) { - if (prop.min !== undefined && prop.min !== null && castValue < prop.min) { - throw new ValidationError({ - message: `Violated the minimum value constraint of ${prop.name} (${castValue} < ${prop.min})`, - field: prop.name, - }); - } - if (prop.max !== undefined && prop.max !== null && castValue > prop.max) { - throw new ValidationError({ - message: `Violated the maximum value constraint of ${prop.name} (${castValue} > ${prop.max})`, - field: prop.name, - }); - } - if (prop.pattern && !castValue.toString().match(prop.pattern)) { - throw new ValidationError({ - message: `Violated the pattern constraint of ${prop.name}. ${castValue} does not match the expected pattern ${prop.pattern}`, - field: prop.name, - }); - } - if (prop.choices && !prop.choices.includes(castValue)) { - throw new ValidationError({ - message: `Violated the choices constraint of ${ - prop.name - }. ${ - castValue - } is not one of the expected values [${ - prop.choices.join(', ') - }]`, - field: prop.name, - }); - } + if (prop.pattern && !castValue.toString().match(prop.pattern)) { + throw new ValidationError({ + message: `Violated the pattern constraint of ${prop.name}. ${castValue} does not match the expected pattern ${prop.pattern}`, + field: prop.name, + }); } - if (prop.check && !prop.check(castValue)) { + if (prop.choices && !prop.choices.includes(castValue)) { throw new ValidationError({ - message: `Violated check constraint of ${prop.name}${prop.check.name - ? ` (${prop.check.name})` - : '' - }`, + message: `Violated the choices constraint of ${ + prop.name + }. ${ + castValue + } is not one of the expected values [${ + prop.choices.join(', ') + }]`, field: prop.name, - value: castValue, }); } } + if (prop.check && !prop.check(castValue)) { + throw new ValidationError({ + message: `Violated check constraint of ${prop.name}${prop.check.name + ? ` (${prop.check.name})` + : '' + }`, + field: prop.name, + value: castValue, + }); + } + } - // check minItems and maxItems - if (prop.minItems && result.length < prop.minItems) { - throw new ValidationError(`Violated the minItems constraint of ${prop.name}. Less than the required number of elements (${result.length} < ${prop.minItems})`); + // check minItems and maxItems + if (prop.minItems && result.length < prop.minItems) { + throw new ValidationError(`Violated the minItems constraint of ${prop.name}. Less than the required number of elements (${result.length} < ${prop.minItems})`); + } + if ((prop.maxItems || prop.maxItems === 0) && result.length > prop.maxItems) { + throw new ValidationError(`Violated the maxItems constraint of ${prop.name}. More than the allowed number of elements (${result.length} > ${prop.maxItems})`); + } + return inputValue instanceof Array + ? result + : result[0]; +}; + +export const createPropertyDefinition = (opt: PropertyDefinitionInput): PropertyDefinition => { + const { + type: inputType, + default: inputDefault, + ...rest + } = opt; + const defaultType = ((opt.min !== undefined || opt.max !== undefined) && !opt.type) + ? 'integer' + : 'string'; + const type: DbType = inputType || defaultType; + + let defaultCast; + + if (!opt.cast) { // set the default util.cast functions + if (type === 'integer') { + defaultCast = util.castInteger; + } else if (type === 'string') { + if (!opt.nullable) { + defaultCast = (opt.nonEmpty + ? util.castLowercaseNonEmptyString + : util.castLowercaseString); + } else { + defaultCast = (opt.nonEmpty + ? util.castLowercaseNonEmptyNullableString + : util.castLowercaseNullableString); + } + } else if (type.includes('link')) { + if (!opt.nullable) { + defaultCast = util.castToRID; + } else { + defaultCast = util.castNullableLink; + } } - if ((prop.maxItems || prop.maxItems === 0) && result.length > prop.maxItems) { - throw new ValidationError(`Violated the maxItems constraint of ${prop.name}. More than the allowed number of elements (${result.length} > ${prop.maxItems})`); + } + + let generateDefault, + defaultValue; + + if (opt.default !== undefined) { + if (opt.default instanceof Function) { + generateDefault = opt.default; + } else { + defaultValue = opt.default; } - return inputValue instanceof Array - ? result - : result[0]; } - /** - * Given some value for a property, ensure that it does not violate the current model contraints - * - * @throws {ValidationError} if the input value violates any of the property model constraints - * - * @returns the cast input value - */ - validate(inputValue) { - return Property.validateWith(this, inputValue); + const castFunction = opt.cast || defaultCast; + let choices; + + if (castFunction && opt.choices) { + choices = opt.choices.map((choice) => castFunction(choice)); } -} + + const result: PropertyDefinition = { + ...rest, + type, + cast: castFunction, + default: defaultValue, + generateDefault, + description: opt.description || '', + generated: Boolean(opt.generated), + generationDependencies: Boolean(opt.generationDependencies), + iterable: Boolean(/(set|list|bag|map)/ig.exec(type)), + nullable: ( + opt.nullable === undefined + ? true + : opt.nullable + ), + fulltextIndexed: Boolean(opt.fulltextIndexed), + indexed: Boolean(opt.indexed), + choices, + examples: opt.examples || choices, + }; + + return result; +}; diff --git a/src/schema.ts b/src/schema.ts index 9d37100..ecc99dc 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,58 +1,70 @@ -import { ClassModel } from './class'; -import { SchemaDefinitionType } from './types'; - +import { + PropertyDefinition, ClassDefinition, GraphRecord, ClassMapping, +} from './types'; +import { ValidationError } from './error'; +import { validateProperty } from './property'; import * as sentenceTemplates from './sentenceTemplates'; -class SchemaDefinition implements SchemaDefinitionType { - readonly schema: Record; - readonly normalizedModelNames: Record; +class SchemaDefinition { + readonly models: Record; + readonly normalizedModelNames: Record; + readonly subclassMapping: Record; - constructor(models: Record) { - this.schema = models; + constructor(models: Record) { + this.models = models; this.normalizedModelNames = {}; - Object.keys(this.schema).forEach((name) => { - const model = this.schema[name]; + const subclassMapping: Record = {}; + + Object.keys(this.models).forEach((name) => { + const model = this.models[name]; this.normalizedModelNames[name.toLowerCase()] = model; if (model.reverseName) { this.normalizedModelNames[model.reverseName.toLowerCase()] = model; } + model.inherits.forEach((parent) => { + if (subclassMapping[parent] === undefined) { + subclassMapping[parent] = []; + } + subclassMapping[parent].push(name); + }); }); - } - - get models() { - return this.schema; + this.subclassMapping = subclassMapping; } /** * Check that a given class/model name exists */ - has(obj) { - try { - return Boolean(this.get(obj)); - } catch (err) { - return false; - } + has(obj: unknown): boolean { + return Boolean(this.get(obj, false)); } /** - * Returns Knowledgebase class schema. + * Returns Knowledge base class schema. * @param {Object|string} obj - Record to fetch schema of. */ - get(obj) { - let cls = obj; + get(obj: unknown, strict?: true): ClassDefinition; + get(obj: unknown, strict?: false): ClassDefinition | null; + get(obj: unknown, strict = true): ClassDefinition | null { + let className = obj; if (obj && typeof obj === 'object' && obj['@class']) { - cls = obj['@class']; + className = obj['@class']; } - return this.normalizedModelNames[typeof cls === 'string' - ? cls.toLowerCase() - : cls - ] || null; + let model: ClassDefinition | null = null; + + if (typeof className === 'string') { + className = className.toLowerCase(); + model = this.normalizedModelNames[className as string] || null; + } + if (!model && strict) { + throw new ValidationError(`Unable to retrieve model: ${className || obj}`); + } + return model; } - getFromRoute(routeName) { - for (const model of Object.values(this.schema)) { // eslint-disable-line + getFromRoute(routeName: string): ClassDefinition { + for (const model of Object.values(this.models)) { // eslint-disable-line if (model.routeName === routeName) { return model; } @@ -60,22 +72,25 @@ class SchemaDefinition implements SchemaDefinitionType { throw new Error(`Missing model corresponding to route (${routeName})`); } - getModels() { - return Object.values(this.schema); + getModels(): ClassDefinition[] { + return Object.values(this.models); } - getEdgeModels() { - return this.getModels().filter((model) => model.isEdge); + getEdgeModels(): ClassDefinition[] { + return Object.values(this.models).filter((model) => model.isEdge); } /** * Returns preview of given record based on its '@class' value * @param {Object} obj - Record to be parsed. */ - getPreview(obj) { - if (obj) { + getPreview(obj: GraphRecord): string { + if (obj && typeof obj === 'object') { if (obj['@class'] === 'Statement') { - const { content } = sentenceTemplates.generateStatementSentence(this, obj); + const { content } = sentenceTemplates.generateStatementSentence( + (arg0: Record): string => this.getPreview(arg0), + obj, + ); return content; } @@ -85,25 +100,18 @@ class SchemaDefinition implements SchemaDefinitionType { if (obj.name) { return obj.name; } - if (obj['@class']) { - const label = this.getPreview(this.get(obj)); - - if (label) { - return label; - } - } if (obj['@rid']) { return obj['@rid']; } if (Array.isArray(obj)) { // embedded link set - return obj.length; + return `${obj.length}`; } - if (obj.target) { + if (obj.target && typeof obj.target === 'object') { // preview pseudo-edge objects - return this.getPreview(obj.target); + return this.getPreview(obj.target as Record); } } - return obj; + return `${obj}`; } /** @@ -116,22 +124,22 @@ class SchemaDefinition implements SchemaDefinitionType { * Note: V, E, User, and UserGroup are special cases and always put in the first level since * they have circular dependencies and are created in a non-standard manner */ - splitClassLevels(): ClassModel[][] { - const adjacencyList: Record = {}; + splitClassLevels(): Array[] { + const adjacencyList: ClassMapping = {}; // initialize adjacency list - for (const model of Object.values(this.schema)) { + for (const model of Object.values(this.models)) { adjacencyList[model.name] = []; } - for (const model of Object.values(this.schema)) { - for (const prop of Object.values(model.properties)) { + for (const model of Object.values(this.models)) { + for (const prop of Object.values(this.getProperties(model.name))) { if (prop.linkedClass) { - adjacencyList[model.name].push(prop.linkedClass.name); + adjacencyList[model.name].push(prop.linkedClass); } } - for (const parent of model.inherits) { + for (const parent of this.ancestors(model.name)) { adjacencyList[model.name].push(parent); } @@ -173,7 +181,281 @@ class SchemaDefinition implements SchemaDefinitionType { updateAdjList(adjacencyList, level, removed); } - return levels.map((level) => level.map((modelName) => this.schema[modelName])); + return levels; + } + + /** + * a list of the properties associate with this class or parents of this class + * @type {Array.} + */ + getProperties(modelName: string): Record { + const model = this.get(modelName); + let properties: Record = { ...model.properties }; + + for (const parent of this.ancestors(modelName)) { + properties = { ...this.getProperties(parent), ...properties }; // properties of the same name are taken from the lowest model + } + return properties; + } + + requiredProperties(modelName: string): string[] { + return Object.values(this.getProperties(modelName)) + .filter((propDefn) => propDefn.mandatory) + .map((propDefn) => propDefn.name); + } + + optionalProperties(modelName: string): string[] { + return Object.values(this.getProperties(modelName)) + .filter((propDefn) => !propDefn.mandatory) + .map((propDefn) => propDefn.name); + } + + getProperty(modelName: string, property: string): PropertyDefinition { + return this.getProperties(modelName)[property]; + } + + hasProperty(modelName: string, property: string): boolean { + return Boolean(this.getProperty(modelName, property)); + } + + inheritsFrom(modelName: string, parent: string): boolean { + return this.ancestors(modelName).includes(parent); + } + + /** + * Returns the list of properties for the Class.active index. This is + * expected to represent a unique constraint on a combination of these properties + * + * @returns list of properties which belong to the index + */ + activeProperties(modelName: string): string[] | null { + const model = this.get(modelName); + + for (const index of model.indices) { + if (index.name === `${model.name}.active`) { + return index.properties; + } + } + return null; + } + + /** + * the list of parent class names which this class inherits from + */ + ancestors(modelName: string): string[] { + const model = this.get(modelName); + const parents: string[] = []; + + for (const parent of model.inherits) { + parents.push(parent); + parents.push(...this.ancestors(parent)); + } + return parents; + } + + children(modelName: string): string[] { + const model = this.get(modelName); + return this.subclassMapping[model.name] || []; + } + + /** + * Get a list of models (including the current model) for all descendants of the current model + * + * @param excludeAbstract exclude abstract models + * + * @returns the array of descendant models + */ + descendants( + modelName: string, + opt: { excludeAbstract?: boolean; includeSelf?: boolean } = {}, + ): string[] { + const { excludeAbstract = false, includeSelf = false } = opt; + const descendants: string[] = includeSelf + ? [modelName] + : []; + const queue: string[] = this.children(modelName).slice(); + + while (queue.length > 0) { + const child = queue.shift() as typeof queue[number]; + + if (descendants.includes(child)) { + continue; + } + descendants.push(child); + queue.push(...this.children(child)); + } + return descendants.filter((child) => { + const descendantModel = this.get(child, true); + return (!excludeAbstract || !descendantModel.isAbstract); + }); + } + + /** + * Returns a set of properties from this class and all subclasses + * @type {Array.} + */ + queryableProperties(modelName: string) { + const model = this.get(modelName); + const queue = Array.from(this.children(modelName)); + const queryProps = this.getProperties(model.name); + + while (queue.length > 0) { + const curr = queue.shift() as typeof queue[number]; + const currModel = this.get(curr); + + for (const prop of Object.values(this.getProperties(currModel.name))) { + if (queryProps[prop.name] === undefined) { // first model to declare is used + queryProps[prop.name] = prop; + } + } + queue.push(...this.children(curr)); + } + return queryProps; + } + + /** + * Check if this model inherits a property from a parent model + */ + inheritsProperty(modelName: string, propName: string) { + const model = this.get(modelName); + + for (const parent of model.inherits) { + const parentModel = this.get(parent); + + if (parentModel.properties[propName] !== undefined) { + return true; + } + if (this.inheritsProperty(parent, propName)) { + return true; + } + } + return false; + } + + /** + * Checks a single record to ensure it matches the expected pattern for this class model + * + * @param {Object} record the record to be checked + * @param {Object} opt options + * @param {boolean} [opt.dropExtra=true] drop any record attributes that are not defined on the current class model by either required or optional + * @param {boolean} [opt.addDefaults=false] add default values for any attributes not given (where defined) + * @param {boolean} [opt.ignoreMissing=false] do not throw an error when a required attribute is missing + * @param {boolean} [opt.ignoreExtra=false] do not throw an error when an unexpected value is given + */ + formatRecord( + modelName: unknown, + record: GraphRecord, + opt: { + dropExtra?: boolean; + addDefaults?: boolean; + ignoreExtra?: boolean; + ignoreMissing?: boolean; + } = {}, + ): GraphRecord { + const model = this.get(modelName); + // add default options + const { + dropExtra = true, + addDefaults = true, + ignoreExtra = false, + ignoreMissing = false, + } = opt; + const formattedRecord = dropExtra + ? {} + : { ...record }; + const properties = this.getProperties(model.name); + + if (!ignoreExtra && !dropExtra) { + for (const attr of Object.keys(record)) { + if (model.isEdge && (attr === 'out' || attr === 'in')) { + continue; + } + if (properties[attr] === undefined) { + throw new ValidationError(`[${model.name}] unexpected attribute: ${attr}`); + } + } + } + // if this is an edge class, check the to and from attributes + if (model.isEdge) { + if (record.out) { + formattedRecord.out = record.out; + } else if (!ignoreMissing) { + throw new ValidationError(`[${model.name}] missing required attribute out`); + } + if (record.in) { + formattedRecord.in = record.in; + } else if (!ignoreMissing) { + throw new ValidationError(`[${model.name}] missing required attribute in`); + } + } + + // add the non generated (from other properties) attributes + for (const prop of Object.values(properties)) { + if (addDefaults && record[prop.name] === undefined && !prop.generationDependencies) { + if (prop.default !== undefined) { + formattedRecord[prop.name] = prop.default; + } else if (prop.generateDefault) { + formattedRecord[prop.name] = prop.generateDefault(); + } + } + // check that the required attributes are there + if (prop.mandatory) { + if (record[prop.name] === undefined && ignoreMissing) { + continue; + } + if (record[prop.name] !== undefined) { + formattedRecord[prop.name] = record[prop.name]; + } + if (formattedRecord[prop.name] === undefined && !ignoreMissing) { + throw new ValidationError(`[${model.name}] missing required attribute ${prop.name}`); + } + } else if (record[prop.name] !== undefined) { + // add any optional attributes that were specified + formattedRecord[prop.name] = record[prop.name]; + } + // try the casting + if (formattedRecord[prop.name] !== undefined) { + formattedRecord[prop.name] = validateProperty(prop, formattedRecord[prop.name]); + } + } + + // look for linked models + for (let [attr, value] of Object.entries(formattedRecord)) { + let { linkedClass, type, iterable } = properties[attr]; + + if (type.startsWith('embedded') && linkedClass !== undefined && value) { + if (value['@class'] && value['@class'] !== linkedClass) { + // record has a class type that doesn't match the expected linkedClass, is it a subclass of it? + if (this.ancestors(value['@class']).includes(linkedClass)) { + linkedClass = value['@class']; + } else { + throw new ValidationError(`A linked class was defined (${linkedClass}) but the record is not of that class or its descendants: ${value['@class']}`); + } + } + if (type === 'embedded' && typeof value === 'object') { + value = this.formatRecord(linkedClass, value); + } else if (iterable) { + value = Array.from( + value, + (v) => this.formatRecord(linkedClass, v as GraphRecord), + ); + } + } + formattedRecord[attr] = value; + } + + // create the generated attributes + if (addDefaults) { + for (const prop of Object.values(properties)) { + if (prop.generationDependencies + && prop.generateDefault + && (prop.generated || formattedRecord[prop.name] === undefined) + ) { + formattedRecord[prop.name] = prop.generateDefault(formattedRecord); + } + } + } + return formattedRecord; } } diff --git a/src/sentenceTemplates.ts b/src/sentenceTemplates.ts index d569920..ce2319d 100644 --- a/src/sentenceTemplates.ts +++ b/src/sentenceTemplates.ts @@ -1,5 +1,5 @@ import { naturalListJoin } from './util'; -import { SchemaDefinitionType, GraphRecord } from './types'; +import { GraphRecord } from './types'; const keys = { disease: '{conditions:disease}', @@ -129,10 +129,12 @@ const chooseDefaultTemplate = (record: GraphRecord) => { /** * builds the sentence representing the preview of a statement record - * @param {SchemaDefn} schemaDefn the schema definition class instance * @param {object} record the statement record to build the sentence for */ -const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: GraphRecord) => { +const generateStatementSentence = ( + previewFunc: (arg0: Record)=> string, + record: GraphRecord, +) => { let template; try { @@ -157,7 +159,7 @@ const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: Gra // don't re-use the subject if it is placed elsewhere if (replacementsFound.includes(keys.subject) && record.subject) { conditionsUsed.push(record.subject['@rid']); - substitutions[keys.subject] = schemaDefn.getPreview(record.subject); + substitutions[keys.subject] = previewFunc(record.subject); highlighted.push(substitutions[keys.subject]); } @@ -172,7 +174,7 @@ const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: Gra } if (variants.length) { - const words = variants.map((c) => schemaDefn.getPreview(c)); + const words = variants.map((c) => previewFunc(c)); highlighted.push(...words); substitutions[keys.variant] = naturalListJoin(words); } @@ -189,7 +191,7 @@ const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: Gra } if (diseases.length) { - const words = diseases.map((c) => schemaDefn.getPreview(c)); + const words = diseases.map((c) => previewFunc(c)); highlighted.push(...words); substitutions[keys.disease] = naturalListJoin(words); } @@ -200,7 +202,7 @@ const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: Gra const rest = conditions.filter((c) => !conditionsUsed.includes(c['@rid'])); if (rest.length) { - const words = rest.map((c) => schemaDefn.getPreview(c)); + const words = rest.map((c) => previewFunc(c)); highlighted.push(...words); substitutions[keys.conditions] = naturalListJoin(words); } @@ -208,13 +210,13 @@ const generateStatementSentence = (schemaDefn: SchemaDefinitionType, record: Gra // add the relevance if (replacementsFound.includes(keys.relevance) && record.relevance) { - substitutions[keys.relevance] = schemaDefn.getPreview(record.relevance); + substitutions[keys.relevance] = previewFunc(record.relevance); highlighted.push(substitutions[keys.relevance]); } // add the evidence if (replacementsFound.includes(keys.evidence) && record.evidence && record.evidence.length) { - const words = record.evidence.map((e) => schemaDefn.getPreview(e)); + const words = record.evidence.map((e) => previewFunc(e)); highlighted.push(...words); substitutions[keys.evidence] = naturalListJoin(words); } diff --git a/src/types.ts b/src/types.ts index d2c14bb..9754c87 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,133 +21,130 @@ export interface IndexType { export type DbType = 'string' | 'long' | 'link' | 'linkset' | 'integer' | 'embeddedlist' | 'embeddedset' | 'boolean' | 'embedded'; -export interface PropertyTypeDefinition { - cast?: (value: any) => PType; - check?: (value: any) => boolean; - choices?: unknown[]; - default?: ((rec?: any) => PType) | PType; - description?: string; - examples?: PType[]; - format?: 'date'; - fulltextIndexed?: boolean; - generated?: boolean; - generationDependencies?: boolean; - indexed?: boolean; - linkedClass?: string; - linkedType?: 'string'; - mandatory?: boolean; - max?: number; - maxItems?: number; - min?: number; - minItems?: number; - name: string; - nonEmpty?: boolean; - nullable?: boolean; - pattern?: string; - readOnly?: boolean; - type?: DbType; -} +export type EdgeName = ( + 'E' + | 'AliasOf' + | 'Cites' + | 'CrossReferenceOf' + | 'DeprecatedBy' + | 'ElementOf' + | 'GeneralizationOf' + | 'Infers' + | 'SubClassOf' + | 'TargetOf' +); -export interface ModelTypeDefinition { - description?: string; - embedded?: boolean; - indices?: IndexType[]; - inherits?: string[]; - isAbstract?: boolean; - isEdge?: boolean; - permissions?: Record; - properties?: PropertyTypeDefinition[]; - reverseName?: string; - routes?: Expose; - sourceModel?: string; - targetModel?: string; - - // added during initialization - name?: string; -} +export type VertexName = ( + 'V' + | 'Abstract' + | 'AnatomicalEntity' + | 'Biomarker' + | 'CatalogueVariant' + | 'CategoryVariant' + | 'ClinicalTrial' + | 'CuratedContent' + | 'Disease' + | 'Evidence' + | 'EvidenceLevel' + | 'Feature' + | 'LicenseAgreement' + | 'Ontology' + | 'Pathway' + | 'PositionalVariant' + | 'Publication' + | 'Signature' + | 'Source' + | 'Statement' + | 'Therapy' + | 'User' + | 'UserGroup' + | 'Variant' + | 'Vocabulary' +); -export interface ModelType { - _inherits: ModelType[]; - _properties: Record; - description: string; - embedded: boolean; - indices: IndexType[]; - isAbstract: boolean; - isEdge: boolean; - name: string; - permissions: any; - reverseName: string; - routes: any; - sourceModel?: string; - subclasses: ModelType[]; - targetModel?: string; - - get inherits(): string[]; - get optional(): string[]; - get properties(): Record; - get queryProperties(): Record; - get required(): string[]; - get routeName(): string; - - descendantTree(excludeAbstract: boolean): ModelType[]; - formatRecord(record: Record, opt?); - getActiveProperties(): string[] | null; - inheritsProperty(propName: string): boolean; - inheritsProperty(propName: string): boolean; - subClassModel(modelName: string): ModelType; - toJSON(): { - properties: Record; - inherits: string[]; - isEdge: boolean; - name: string; - isAbstract: boolean; - embedded: boolean; - reverseName?: string; - route?: string; - }; +export type EmbeddedVertexName = ( + 'Position' + | 'CdsPosition' + | 'CytobandPosition' + | 'ExonicPosition' + | 'GenomicPosition' + | 'IntronicPosition' + | 'NonCdsPosition' + | 'Permissions' + | 'ProteinPosition' + | 'RnaPosition' + | 'StatementReview' +); + +export type ClassName = VertexName | EdgeName | EmbeddedVertexName; + +export type GroupName = 'readonly' | 'regular' | 'manager' | 'admin'; + +export type ClassPermissions = Partial > & { default?: number }; + +export type UserGroupPermissions = Record>>; + +export interface PropertyDefinition { + readonly name: string; + readonly cast?: (value: any) => unknown; + readonly type: DbType; + readonly pattern?: string; + readonly description?: string; + readonly generated?: boolean; + readonly mandatory?: boolean; + readonly nullable?: boolean; + readonly readOnly?: boolean; + readonly generateDefault?: (rec?: unknown) => unknown; + readonly check?: (rec?: unknown) => boolean; + readonly default?: unknown; + readonly examples?: unknown[]; + readonly generationDependencies?: boolean; + readonly nonEmpty?: boolean; + readonly linkedType?: 'string'; + readonly format?: 'date'; + readonly choices?: unknown[]; + readonly min?: number; + readonly minItems?: number; + readonly max?: number; + readonly maxItems?: number; + readonly iterable: boolean; + readonly indexed: boolean; + readonly fulltextIndexed: boolean; + readonly linkedClass?: string; } -export interface PropertyType { - cast?: (value: any) => unknown; - check?: (rec?: unknown) => boolean; - choices?: unknown[]; - default?: unknown; - description?: string; - example?: unknown; - format?: 'date'; - fulltextIndexed: boolean; - generated?: boolean; - generateDefault?: (rec?: any) => any; - generationDependencies?: boolean; - indexed: boolean; - iterable: boolean; - linkedClass?: ModelType; - linkedType?: DbType; - mandatory?: boolean; - max?: number; - maxItems?: number; - min?: number; - minItems?: number; - name: string; - nonEmpty?: boolean; - nullable?: boolean; - pattern?: string; - readOnly?: boolean; - type: DbType; +export interface ClassDefinition { + readonly routeName: string; + readonly inherits: string[]; + readonly properties: Record; + readonly description: string; + readonly embedded: boolean; + readonly indices: IndexType[]; + readonly isAbstract: boolean; + readonly isEdge: boolean; + readonly name: string; + readonly permissions: ClassPermissions; + readonly reverseName?: string; + readonly routes: any; + readonly sourceModel?: VertexName; + readonly targetModel?: VertexName; } export interface GraphRecord { [key: string]: any; } -export interface SchemaDefinitionType { - readonly schema: Record; - readonly normalizedModelNames: Record; - get models(): Record - has(obj: GraphRecord | string): boolean; - get(obj: GraphRecord | string): ModelType | null; - getFromRoute(routeName: string): ModelType; - getModels(): ModelType[]; - getEdgeModels(): ModelType[]; - getPreview(GraphRecord): string; +export interface PropertyDefinitionInput extends Partial> { + generated?: unknown; + default?: unknown; + name: PropertyDefinition['name']; +} + +export interface ClassDefinitionInput extends Partial> { + properties?: PropertyDefinitionInput[]; + name: ClassDefinition['name']; } + +export type ClassMapping = Partial>; + +export type PartialSchemaDefn = ClassMapping>; diff --git a/src/util.ts b/src/util.ts index ff004c4..6f6e57b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,7 +5,7 @@ import uuidValidate from 'uuid-validate'; import { ValidationError } from './error'; import * as constants from './constants'; // IMPORTANT, to support for the API and GUI, must be able to patch RID -import { Expose, GraphRecord } from './types'; +import { GraphRecord } from './types'; const castUUID = (uuid: string) => { if (uuidValidate(uuid, 4)) { @@ -186,33 +186,6 @@ const displayFeature = ({ return sourceId || name; }; -const defaultPermissions = (routes: Partial = {}) => { - const { - PERMISSIONS: { - CREATE, READ, UPDATE, NONE, DELETE, - }, - } = constants; - - const permissions = { - default: NONE, - readonly: READ, - }; - - if (routes.QUERY || routes.GET) { - permissions.default |= READ; - } - if (routes.POST) { - permissions.default |= CREATE; - } - if (routes.PATCH) { - permissions.default |= UPDATE; - } - if (routes.DELETE) { - permissions.default |= DELETE; - } - return permissions; -}; - const naturalListJoin = (words: string[]): string => { if (words.length > 1) { return `${words.slice(0, words.length - 1).join(', ')}, and ${words[words.length - 1]}`; @@ -231,7 +204,6 @@ export { castString, castToRID, castUUID, - defaultPermissions, displayFeature, displayOntology, looksLikeRID, diff --git a/test/class.test.ts b/test/class.test.ts index 3d1e1c3..a23084e 100644 --- a/test/class.test.ts +++ b/test/class.test.ts @@ -1,320 +1,45 @@ -import { ClassModel, schema } from '../src'; +import { createClassDefinition } from '../src/class'; -const { schema: SCHEMA_DEFN } = schema; - -describe('ClassModel', () => { - describe('descendantTree', () => { - test('is an single element list for terminal models', () => { - expect(SCHEMA_DEFN.ProteinPosition.descendantTree()).toEqual([SCHEMA_DEFN.ProteinPosition]); - }); - - test('Includes child models and self', () => { - const tree = SCHEMA_DEFN.Position.descendantTree().map((model) => model.name); - expect(tree).toEqual(expect.arrayContaining(['ProteinPosition'])); - expect(tree).toEqual(expect.arrayContaining(['Position'])); - }); - - test('On flag excludes abstract models', () => { - const tree = SCHEMA_DEFN.Position.descendantTree(true).map((model) => model.name); - expect(tree).toEqual(expect.arrayContaining(['ProteinPosition'])); - expect(tree).toEqual(expect.not.arrayContaining(['Position'])); - }); - - test('fetches grandchild models', () => { - const tree = SCHEMA_DEFN.V.descendantTree().map((model) => model.name); - expect(tree).toEqual(expect.arrayContaining(['Publication'])); - expect(tree).toEqual(expect.arrayContaining(['V'])); - expect(tree).toEqual(expect.arrayContaining(['Ontology'])); - }); - }); +describe('createClassDefinition', () => { describe('routeName', () => { test('does not alter ary suffix', () => { - const model = new ClassModel({ name: 'vocabulary' }); + const model =createClassDefinition({ name: 'vocabulary' }); expect(model.routeName).toBe('/vocabulary'); }); test('does not alter edge class names', () => { - const model = new ClassModel({ name: 'edge', isEdge: true }); + const model =createClassDefinition({ name: 'edge', isEdge: true }); expect(model.routeName).toBe('/edge'); }); test('changes ys to ies', () => { - const model = new ClassModel({ name: 'ontology' }); + const model =createClassDefinition({ name: 'ontology' }); expect(model.routeName).toBe('/ontologies'); }); test('adds s to regular class names', () => { - const model = new ClassModel({ name: 'statement' }); + const model =createClassDefinition({ name: 'statement' }); expect(model.routeName).toBe('/statements'); }); - }); - - describe('subclassModel', () => { - const child = new ClassModel({ name: 'child' }); - const parent = new ClassModel({ name: 'parent', subclasses: [child] }); - const grandparent = new ClassModel({ name: 'grandparent', subclasses: [parent] }); - - test('errors when the class does not exist', () => { - expect(() => { - grandparent.subClassModel('badName'); - }).toThrowError('was not found as a subclass'); - }); - - test('returns an immeadiate subclass', () => { - expect(parent.subClassModel('child')).toEqual(child); - }); - - test('returns a subclass of a subclass recursively', () => { - expect(grandparent.subClassModel('child')).toEqual(child); - }); - }); - - describe('queryProperties', () => { - const child = new ClassModel({ - name: 'child', - properties: [{ name: 'childProp' }], - }); - const parent = new ClassModel({ name: 'parent', subclasses: [child], properties: [] }); - const grandparent = new ClassModel({ - name: 'grandparent', - subclasses: [parent], - properties: [{ name: 'grandProp' }], - }); - - test('fetches grandfathered properties', () => { - const queryProp = grandparent.queryProperties; - expect(queryProp).toHaveProperty('childProp'); - expect(queryProp).toHaveProperty('grandProp'); - }); - - test('ok when no subclasses', () => { - const queryProp = child.queryProperties; - expect(Object.keys(queryProp)).toEqual(['childProp']); - }); - }); - - describe('inheritance', () => { - const person = new ClassModel({ - name: 'person', - properties: [ - { name: 'gender', default: 'not specified' }, - { name: 'name', mandatory: true }, - ], - }); - const child = new ClassModel({ - name: 'child', - properties: [ - { name: 'mom', mandatory: true, cast: (x) => x.toLowerCase() }, - { name: 'age' }, - ], - inherits: [person], - targetModel: 'OtherClass', - }); - - test('child required returns person attr', () => { - expect(person.required).toEqual(['name']); - expect(child.required).toEqual(['mom', 'name']); - }); - - test('child optional returns person attr', () => { - expect(person.optional).toEqual(['gender']); - expect(child.optional).toEqual(['age', 'gender']); - }); - - test('inherits to return list of strings', () => { - expect(person.inherits).toEqual([]); - expect(child.inherits).toEqual([person.name]); - }); - - test('is not an edge', () => { - expect(person.isEdge).toBe(false); - expect(child.isEdge).toBe(true); - }); - }); - - describe('formatRecord', () => { - let model; - - beforeEach(() => { - const childModel = new ClassModel({ - name: 'child', - properties: [ - { name: 'name', type: 'string' }, - ], - }); - model = new ClassModel({ - name: 'example', - properties: [ - { - name: 'req1', mandatory: true, nonEmpty: true, type: 'string', - }, - { - name: 'req2', mandatory: true, default: 1, type: 'integer', - }, - { name: 'opt1' }, - { - name: 'opt2', choices: [2, 3], nullable: true, default: 2, type: 'integer', - }, - { - name: 'opt3', type: 'embedded', linkedClass: childModel, - }, - { - name: 'opt4', type: 'embeddedset', linkedClass: childModel, - }, - ], - }); - }); - - test('casts an embedded list/set', () => { - const record = model.formatRecord({ - req1: 'term1', opt3: { name: 'bob' }, - }, { dropExtra: false, addDefaults: true }); - expect(record).toHaveProperty('opt3'); - expect(record.opt3).toEqual({ name: 'bob' }); - }); - - test('casts an embedded property', () => { - const record = model.formatRecord({ - req1: 'term1', opt4: [{ name: 'bob' }, { name: 'alice' }], - }, { dropExtra: false, addDefaults: true }); - expect(record).toHaveProperty('opt4'); - expect(record.opt4).toEqual([{ name: 'bob' }, { name: 'alice' }]); - }); - - test('error on empty string', () => { - expect(() => { - model.formatRecord({ - req1: '', - }, { dropExtra: false, addDefaults: true }); - }).toThrowError(); - }); - - test('errors on un-cast-able input', () => { - expect(() => { - model.formatRecord({ - req1: 2, - req2: 'f45', - }, { dropExtra: false, addDefaults: true }); - }).toThrowError(); - }); - - test('errors on un-expected attr', () => { - expect(() => { - model.formatRecord({ - req1: 2, - req2: 1, - badAttr: 3, - }, { dropExtra: false, ignoreExtra: false, addDefaults: false }); - }).toThrowError(); - }); - - test('adds defaults', () => { - const record = model.formatRecord({ - req1: 'term1', - }, { dropExtra: false, addDefaults: true }); - expect(record).toHaveProperty('req1', 'term1'); - expect(record).toHaveProperty('req2', 1); - expect(record).toHaveProperty('opt2', 2); - expect(record).not.toHaveProperty('opt1'); - }); - - test('cast embedded types', () => { - model = new ClassModel({ - name: 'example', - properties: [{ - name: 'thing', - type: 'embeddedset', - cast: (x) => x.toLowerCase().trim(), - }], - - }); - const record = model.formatRecord({ - thing: ['aThinNG', 'another THING'], - }, { dropExtra: false, addDefaults: true }); - expect(record).toHaveProperty('thing'); - expect(record.thing).toEqual(['athinng', 'another thing']); - }); - - test('cast inheritied embedded types', () => { - model = new ClassModel({ - name: 'example', - properties: [{ - name: 'thing', - type: 'embeddedset', - cast: (x) => x.toLowerCase().trim(), - }], - - }); - const childModel = new ClassModel({ - name: 'child', - inherits: [model], - }); - const record = childModel.formatRecord({ - thing: ['aThinNG', 'another THING'], - }, { dropExtra: false, addDefaults: true }); - expect(record).toHaveProperty('thing'); - expect(record.thing).toEqual(['athinng', 'another thing']); - }); - - test('does not add defaults', () => { - expect(() => { - model.formatRecord({ - req1: 'term1', - }, { dropExtra: false, addDefaults: false }); - }).toThrowError(); - - const record = model.formatRecord({ - req1: 'term1', req2: '4', - }, { dropExtra: false, addDefaults: false }); - expect(record).toHaveProperty('req1', 'term1'); - expect(record).toHaveProperty('req2', 4); - expect(record).not.toHaveProperty('opt2'); - expect(record).not.toHaveProperty('opt1'); - }); - - test('allows optional parameters', () => { - const record = model.formatRecord({ - req1: 'term1', req2: '2', opt1: '2', - }, { dropExtra: false, addDefaults: false }); - expect(record).toHaveProperty('req1', 'term1'); - expect(record).toHaveProperty('req2', 2); - expect(record).toHaveProperty('opt1', '2'); - expect(record).not.toHaveProperty('opt2'); - }); - - test('error on invalid enum choice', () => { - expect(() => { - model.formatRecord({ - req1: 'term1', opt2: 4, req2: 1, - }, { dropExtra: false, addDefaults: false }); - }).toThrowError('Violated the choices constraint'); + test('does not alter ary suffix', () => { + const model = createClassDefinition({ name: 'vocabulary' }); + expect(model.routeName).toBe('/vocabulary'); }); - test('allow nullable enum', () => { - const record = model.formatRecord({ - req1: 'term1', opt2: null, req2: 1, - }, { dropExtra: false, addDefaults: false }); - expect(record).toHaveProperty('req1', 'term1'); - expect(record).toHaveProperty('opt2', null); + test('does not alter edge class names', () => { + const model = createClassDefinition({ isEdge: true, name: 'edge' }); + expect(model.routeName).toBe('/edge'); }); - }); - - describe('inheritance tests', () => { - const greatGrandParentA = new ClassModel({ name: 'monkey madness', properties: [{ name: 'name' }] }); - const greatGrandParentB = new ClassModel({ name: 'blargh' }); - const grandParentA = new ClassModel({ name: 'grandparent', inherits: [greatGrandParentA, greatGrandParentB] }); - const grandParentB = new ClassModel({ name: 'other grandparent' }); - const parentA = new ClassModel({ name: 'parentA', inherits: [grandParentA] }); - const parentB = new ClassModel({ name: 'parentB', inherits: [grandParentB] }); - const root = new ClassModel({ inherits: [parentA, parentB], name: 'root', properties: [{ name: 'directName' }, { name: 'name' }] }); - test('inheritsProperty is true for parent properties', () => { - expect(root.inheritsProperty('name')).toBe(true); + test('changes ys to ies', () => { + const model = createClassDefinition({ name: 'ontology' }); + expect(model.routeName).toBe('/ontologies'); }); - test('false for direct only properties', () => { - expect(root.inheritsProperty('directName')).toBe(false); + test('adds s to regular class names', () => { + const model = createClassDefinition({ name: 'statement' }); + expect(model.routeName).toBe('/statements'); }); }); }); diff --git a/test/index.test.ts b/test/index.test.ts deleted file mode 100644 index 567ce89..0000000 --- a/test/index.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { schema as SCHEMA_DEFN, ClassModel } from '../src'; - -describe('SchemaDefinition', () => { - describe('get', () => { - test('edge by reverse name', () => { - expect(SCHEMA_DEFN.get('hasAlias')).toEqual(SCHEMA_DEFN.schema.AliasOf); - }); - - test('vertex by wrong case', () => { - expect(SCHEMA_DEFN.get('diseasE')).toEqual(SCHEMA_DEFN.schema.Disease); - }); - - test('null for bad class name', () => { - expect(SCHEMA_DEFN.get('blarghBmojhsgjhs')).toBeNull(); - }); - }); - - describe('has', () => { - test('returns true for valid class', () => { - expect(SCHEMA_DEFN.has('hasAlias')).toBe(true); - }); - - test('false for missing class', () => { - expect(SCHEMA_DEFN.has('blarghBmojhsgjhs')).toBe(false); - }); - - test('false for bad object', () => { - expect(SCHEMA_DEFN.has({ '@class': 1 })).toBe(false); - }); - }); - - describe('getFromRoute', () => { - test('returns the model for a valid route', () => { - expect(SCHEMA_DEFN.getFromRoute('/diseases')).toEqual(SCHEMA_DEFN.schema.Disease); - }); - - test('error on a non-existant route', () => { - expect(() => SCHEMA_DEFN.getFromRoute('/blarghBmojhsgjhs')).toThrowError('Missing model'); - }); - }); - - test('getModels', () => { - const models = SCHEMA_DEFN.getModels(); - expect(Array.isArray(models)).toBe(true); - expect(models[0]).toBeInstanceOf(ClassModel); - }); - - test('getEdgeModels', () => { - const models = SCHEMA_DEFN.getEdgeModels(); - expect(Array.isArray(models)).toBe(true); - expect(models[0]).toBeInstanceOf(ClassModel); - const names = models.map((m) => m.name); - expect(names).toContain('E'); - expect(names).not.toContain('V'); - }); - - test('splitClassLevels', () => { - const levels = SCHEMA_DEFN.splitClassLevels() - .map((level) => level.map((model) => model.name).sort()); - - expect(levels).toEqual([ - [ - 'E', - 'User', - 'UserGroup', - 'V', - ], - [ - 'Biomarker', - 'Evidence', - 'LicenseAgreement', - 'Permissions', - 'Position', - 'StatementReview', - ], - [ - 'CdsPosition', - 'CytobandPosition', - 'ExonicPosition', - 'GenomicPosition', - 'IntronicPosition', - 'NonCdsPosition', - 'ProteinPosition', - 'RnaPosition', - 'Source', - ], - ['Ontology'], - [ - 'AliasOf', - 'AnatomicalEntity', - 'CatalogueVariant', - 'Cites', - 'ClinicalTrial', - 'CrossReferenceOf', - 'CuratedContent', - 'DeprecatedBy', - 'Disease', - 'ElementOf', - 'EvidenceLevel', - 'Feature', - 'GeneralizationOf', - 'OppositeOf', - 'Pathway', - 'Publication', - 'Signature', - 'SubClassOf', - 'TargetOf', - 'Therapy', - 'Vocabulary', - ], - ['Abstract', 'Statement', 'Variant'], - ['CategoryVariant', 'Infers', 'PositionalVariant'], - ]); - }); -}); - -describe('SCHEMA', () => { - describe('PositionalVariant.formatRecord', () => { - test('error on missing reference1', () => { - expect(() => { - SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference2: '#33:1', - break1Start: { '@class': 'ProteinPosition', pos: 1 }, - type: '#33:2', - createdBy: '#44:1', - updatedBy: '#44:1', - }, { addDefaults: true }); - }).toThrowError('missing required attribute'); - }); - - test('error on missing break1Start', () => { - expect(() => { - SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference1: '#33:1', - break2Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, - type: '#33:2', - createdBy: '#44:1', - updatedBy: '#44:1', - }, { addDefaults: true }); - }).toThrowError('missing required attribute'); - }); - - test('error on position without @class attribute', () => { - expect(() => { - SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference1: '#33:1', - break1Start: { pos: 1, refAA: 'A' }, - type: '#33:2', - createdBy: '#44:1', - updatedBy: '#44:1', - }, { addDefaults: true }); - }).toThrowError('positions must include the @class attribute'); - }); - - test('error on break2End without break2Start', () => { - expect(() => { - SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference1: '#33:1', - break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, - type: '#33:2', - break2End: { '@class': 'ProteinPosition', pos: 10, refAA: 'B' }, - createdBy: '#44:1', - updatedBy: '#44:1', - }, { addDefaults: true }); - }).toThrowError('both start and end'); - }); - - test('auto generates the breakRepr', () => { - const formatted = SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference1: '#33:1', - type: '#33:2', - createdBy: '#44:1', - updatedBy: '#44:1', - break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, - break2Start: { '@class': 'ExonicPosition', pos: 1 }, - break2End: { '@class': 'ExonicPosition', pos: 3 }, - }, { addDefaults: true }); - expect(formatted).toHaveProperty('break1Repr', 'p.A1'); - expect(formatted).toHaveProperty('break2Repr', 'e.(1_3)'); - }); - - test('generated attr overwrites input if given', () => { - const formatted = SCHEMA_DEFN.schema.PositionalVariant.formatRecord({ - reference1: '#33:1', - type: '#33:2', - createdBy: '#44:1', - updatedBy: '#44:1', - break1Start: { - '@class': 'ProteinPosition', pos: 1, refAA: 'A', prefix: 'p', - }, - break1Repr: 'bad', - }, { addDefaults: true }); - expect(formatted).toHaveProperty('break1Repr', 'p.A1'); - }); - }); -}); diff --git a/test/property.test.ts b/test/property.test.ts index 4bf12a0..1a1d1cd 100644 --- a/test/property.test.ts +++ b/test/property.test.ts @@ -1,149 +1,151 @@ -import { Property } from '../src'; +import { validateProperty, createPropertyDefinition } from '../src/property'; -describe('Property', () => { - test('cast choices if given', () => { - const prop = new Property({ name: 'name', choices: ['Stuff', 'OtherStuff', 'morestuff'], cast: (x) => x.toLowerCase() }); - expect(prop.choices).toEqual(['stuff', 'otherstuff', 'morestuff']); +test('cast choices if given', () => { + const prop = createPropertyDefinition({ + name: 'name', + choices: ['Stuff', 'OtherStuff', 'morestuff'], + cast: (x) => x.toLowerCase() }); + expect(prop.choices).toEqual(['stuff', 'otherstuff', 'morestuff']); +}); - describe('validate', () => { - test('nullable', () => { - const prop = new Property({ - name: 'example', - nullable: false, - }); - expect(() => prop.validate(null)).toThrowError('cannot be null'); - expect(prop.validate('')).toBe(''); - expect(prop.validate('blargh')).toBe('blargh'); +describe('validate', () => { + test('nullable', () => { + const prop = createPropertyDefinition({ + name: 'example', + nullable: false, + }); + expect(() => validateProperty(prop, null)).toThrowError('cannot be null'); + expect(validateProperty(prop, '')).toBe(''); + expect(validateProperty(prop, 'blargh')).toBe('blargh'); - const nullableProp = new Property({ - name: 'example', - nullable: true, - }); - expect(nullableProp.validate(null)).toBeNull(); + const nullableProp = createPropertyDefinition({ + name: 'example', + nullable: true, }); + expect(validateProperty(nullableProp, null)).toBeNull(); + }); - test('nonEmpty', () => { - const prop = new Property({ - name: 'example', - nonEmpty: false, - }); - expect(prop.validate('')).toBe(''); - expect(prop.validate(null)).toBeNull(); - const prop1 = new Property({ - name: 'example', - nonEmpty: true, - }); - expect(() => prop1.validate('')).toThrowError('Cannot be an empty string'); - expect(prop1.validate('blargh')).toBe('blargh'); - expect(prop1.validate(null)).toBeNull(); + test('nonEmpty', () => { + const prop = createPropertyDefinition({ + name: 'example', + nonEmpty: false, + }); + expect(validateProperty(prop, '')).toBe(''); + expect(validateProperty(prop, null)).toBeNull(); + const prop1 = createPropertyDefinition({ + name: 'example', + nonEmpty: true, + }); + expect(() => validateProperty(prop1, '')).toThrowError('Cannot be an empty string'); + expect(validateProperty(prop1, 'blargh')).toBe('blargh'); + expect(validateProperty(prop1, null)).toBeNull(); - const prop2 = new Property({ - name: 'example', - nonEmpty: true, - cast: (x) => x, - }); - expect(() => prop2.validate('')).toThrowError('cannot be an empty string'); - expect(prop2.validate(null)).toBeNull(); - expect(prop2.validate('blargh')).toBe('blargh'); + const prop2 = createPropertyDefinition({ + name: 'example', + nonEmpty: true, + cast: (x) => x, }); + expect(() => validateProperty(prop2, '')).toThrowError('cannot be an empty string'); + expect(validateProperty(prop2, null)).toBeNull(); + expect(validateProperty(prop2, 'blargh')).toBe('blargh'); + }); - test('min', () => { - const prop = new Property({ - name: 'example', - min: -1, - type: 'integer', - }); - expect(prop.validate('1')).toBe(1); - expect(prop.validate(null)).toBeNull(); - expect(() => prop.validate(-2)).toThrowError('Violated the minimum value constraint'); - expect(prop.validate('-1')).toBe(-1); + test('min', () => { + const prop = createPropertyDefinition({ + name: 'example', + min: -1, + type: 'integer', }); + expect(validateProperty(prop, '1')).toBe(1); + expect(validateProperty(prop, null)).toBeNull(); + expect(() => validateProperty(prop, -2)).toThrowError('Violated the minimum value constraint'); + expect(validateProperty(prop, '-1')).toBe(-1); + }); - test('minItems', () => { - const prop = new Property({ - name: 'example', - minItems: 1, - type: 'embeddedlist', - }); - expect(prop.validate([1, 2])).toEqual([1, 2]); - expect(() => prop.validate([])).toThrowError('Less than the required number of elements (0 < 1)'); + test('minItems', () => { + const prop = createPropertyDefinition({ + name: 'example', + minItems: 1, + type: 'embeddedlist', }); + expect(validateProperty(prop, [1, 2])).toEqual([1, 2]); + expect(() => validateProperty(prop, [])).toThrowError('Less than the required number of elements (0 < 1)'); + }); - test('maxItems', () => { - const prop = new Property({ - name: 'example', - maxItems: 0, - type: 'embeddedlist', - }); - expect(prop.validate([])).toEqual([]); - expect(() => prop.validate([1])).toThrowError('More than the allowed number of elements (1 > 0)'); + test('maxItems', () => { + const prop = createPropertyDefinition({ + name: 'example', + maxItems: 0, + type: 'embeddedlist', }); + expect(validateProperty(prop, [])).toEqual([]); + expect(() => validateProperty(prop, [1])).toThrowError('More than the allowed number of elements (1 > 0)'); + }); - test('check', () => { - const prop = new Property({ - name: 'example', - check: (input) => input === '1', - type: 'string', - }); - expect(prop.validate('1')).toBe('1'); - expect(() => prop.validate('2')).toThrowError('Violated check constraint'); + test('check', () => { + const prop = createPropertyDefinition({ + name: 'example', + check: (input) => input === '1', + type: 'string', }); + expect(validateProperty(prop, '1')).toBe('1'); + expect(() => validateProperty(prop, '2')).toThrowError('Violated check constraint'); + }); - test('named check', () => { - const checkIsOne = (input) => input === '1'; - const prop = new Property({ - name: 'example', - check: checkIsOne, - type: 'string', - }); - expect(prop.validate('1')).toBe('1'); - expect(() => prop.validate('2')).toThrowError('Violated check constraint of example (checkIsOne)'); + test('named check', () => { + const checkIsOne = (input) => input === '1'; + const prop = createPropertyDefinition({ + name: 'example', + check: checkIsOne, + type: 'string', }); + expect(validateProperty(prop, '1')).toBe('1'); + expect(() => validateProperty(prop, '2')).toThrowError('Violated check constraint of example (checkIsOne)'); + }); - test('max', () => { - const prop = new Property({ - name: 'example', - max: 10, - type: 'integer', - }); - expect(prop.validate('1')).toBe(1); - expect(prop.validate(null)).toBeNull(); - expect(() => prop.validate('100')).toThrowError('Violated the maximum value constraint'); + test('max', () => { + const prop = createPropertyDefinition({ + name: 'example', + max: 10, + type: 'integer', }); + expect(validateProperty(prop, '1')).toBe(1); + expect(validateProperty(prop, null)).toBeNull(); + expect(() => validateProperty(prop, '100')).toThrowError('Violated the maximum value constraint'); + }); - test('pattern', () => { - const stringRegexProp = new Property({ - name: 'example', - pattern: '^\\d+$', - }); - expect(stringRegexProp.validate('1')).toBe('1'); - expect(() => stringRegexProp.validate('100d')).toThrowError('Violated the pattern constraint'); - expect(stringRegexProp.validate(null)).toBeNull(); + test('pattern', () => { + const stringRegexProp = createPropertyDefinition({ + name: 'example', + pattern: '^\\d+$', }); + expect(validateProperty(stringRegexProp, '1')).toBe('1'); + expect(() => validateProperty(stringRegexProp, '100d')).toThrowError('Violated the pattern constraint'); + expect(validateProperty(stringRegexProp, null)).toBeNull(); + }); - test('choices && !nullable', () => { - const prop = new Property({ - name: 'example', - choices: [1, 2, 3], - cast: Number, - nullable: false, - }); - expect(prop.validate('1')).toBe(1); - expect(prop.validate(3)).toBe(3); - expect(() => prop.validate('100')).toThrowError('Violated the choices constraint'); + test('choices && !nullable', () => { + const prop = createPropertyDefinition({ + name: 'example', + choices: [1, 2, 3], + cast: Number, + nullable: false, }); + expect(validateProperty(prop, '1')).toBe(1); + expect(validateProperty(prop, 3)).toBe(3); + expect(() => validateProperty(prop, '100')).toThrowError('Violated the choices constraint'); + }); - test('choices', () => { - const prop = new Property({ - name: 'example', - choices: [1, 2, 3], - cast: Number, - }); - expect(prop.validate('1')).toBe(1); - expect(prop.validate(null)).toBeNull(); - expect(prop.validate(3)).toBe(3); - expect(() => prop.validate('100')).toThrowError('Violated the choices constraint'); + test('choices', () => { + const prop = createPropertyDefinition({ + name: 'example', + choices: [1, 2, 3], + cast: Number, }); + expect(validateProperty(prop, '1')).toBe(1); + expect(validateProperty(prop, null)).toBeNull(); + expect(validateProperty(prop, 3)).toBe(3); + expect(() => validateProperty(prop, '100')).toThrowError('Violated the choices constraint'); }); }); diff --git a/test/schema.mock.test.ts b/test/schema.mock.test.ts new file mode 100644 index 0000000..d91b52f --- /dev/null +++ b/test/schema.mock.test.ts @@ -0,0 +1,209 @@ +import { createClassDefinition } from "../src/class"; +import { SchemaDefinition } from '../src/schema'; + + +describe('queryableProperties', () => { + const schema = new SchemaDefinition({ + child: createClassDefinition({ + name: 'child', + properties: [{ name: 'childProp' }], + inherits: ['parent'] + }), + parent: createClassDefinition({ name: 'parent', properties: [], inherits: ['grandparent'] }), + grandparent: createClassDefinition({ + name: 'grandparent', + properties: [{ name: 'grandProp' }], + }), + aunt: createClassDefinition({ + name: 'aunt', + inherits: ['grandparent'], + properties: [{name: 'auntProp'}] + }) + }) + + test('fetches grandfathered properties', () => { + const queryProp = schema.queryableProperties('grandparent'); + expect(Object.keys(queryProp).sort()).toEqual(['childProp', 'grandProp', 'auntProp'].sort()); + }); + + test('ok when no subclasses', () => { + const queryProp = schema.queryableProperties('child'); + expect(Object.keys(queryProp).sort()).toEqual(['childProp', 'grandProp']); + }); +}); + +describe('inheritance', () => { + const schema = new SchemaDefinition({ + person: createClassDefinition({ + name: 'person', + properties: [ + { default: 'not specified', name: 'gender' }, + { mandatory: true, name: 'name' }, + ], + }), + child: createClassDefinition({ + inherits: ['person'], + name: 'child', + properties: [ + { name: 'age' }, + { cast: (x) => x.toLowerCase(), mandatory: true, name: 'mom' }, + ], + }) + }); + + test('child required returns person attr', () => { + expect(schema.requiredProperties('person')).toEqual(['name']); + expect(schema.requiredProperties('child').sort()).toEqual(['mom', 'name'].sort()); + }); + + test('child optional returns person attr', () => { + expect(schema.optionalProperties('person')).toEqual(['gender']); + expect(schema.optionalProperties('child').sort()).toEqual(['age', 'gender'].sort()); + }); + + test('inherits to return list of strings', () => { + expect(schema.models.person.inherits).toEqual([]); + expect(schema.models.child.inherits).toEqual([schema.models.person.name]); + }); + + test('is not an edge', () => { + expect(schema.models.person.isEdge).toBe(false); + expect(schema.models.child.isEdge).toBe(false); + }); +}); + +describe('formatRecord', () => { + let schema; + + beforeEach(() => { + schema = new SchemaDefinition({ + example: createClassDefinition({ + name: 'example', + properties: [ + { + cast: (x) => x.toLowerCase().trim(), + name: 'thing', + type: 'embeddedset', + }, + { name: 'opt1' }, + { + choices: [2, 3], default: 2, name: 'opt2', nullable: true, type: 'integer', + }, + { + mandatory: true, name: 'req1', nonEmpty: true, type: 'string', + }, + { + default: 1, mandatory: true, name: 'req2', type: 'integer', + }, + ], + }), + child: createClassDefinition({ + inherits: ['example'], + name: 'child', + }), + other: createClassDefinition({ + name: 'other', + properties: [{ + cast: (x) => x.toLowerCase().trim(), + name: 'thing', + type: 'embeddedset', + }] + }) + }); + }); + + test('error on empty string', () => { + expect(() => { + schema.formatRecord('example', { + req1: '', + }, { addDefaults: true, dropExtra: false }); + }).toThrow(); + }); + + test('errors on un-cast-able input', () => { + expect(() => { + schema.formatRecord('example', { + req1: 2, + req2: 'f45', + }, { addDefaults: true, dropExtra: false }); + }).toThrow(); + }); + + test('errors on un-expected attr', () => { + expect(() => { + schema.formatRecord('example', { + badAttr: 3, + req1: 2, + req2: 1, + }, { addDefaults: false, dropExtra: false, ignoreExtra: false }); + }).toThrow(); + }); + + test('adds defaults', () => { + const record = schema.formatRecord('example', { + req1: 'term1', + }, { addDefaults: true, dropExtra: false }); + expect(record).toHaveProperty('req1', 'term1'); + expect(record).toHaveProperty('req2', 1); + expect(record).toHaveProperty('opt2', 2); + expect(record).not.toHaveProperty('opt1'); + }); + + test('cast embedded types', () => { + const record = schema.formatRecord('other', { + thing: ['aThinNG', 'another THING'], + }, { addDefaults: true, dropExtra: false }); + expect(record).toHaveProperty('thing'); + expect(record.thing).toEqual(['athinng', 'another thing']); + }); + + test('cast inherited embedded types', () => { + const record = schema.formatRecord('child', { + thing: ['aThinNG', 'another THING'], req1: 't', req2: '3' + }, { addDefaults: true, dropExtra: false }); + expect(record).toHaveProperty('thing'); + expect(record.thing).toEqual(['athinng', 'another thing']); + }); + + test('does not add defaults', () => { + expect(() => { + schema.formatRecord('child', { + req1: 'term1', + }, { addDefaults: false, dropExtra: false }); + }).toThrow(); + + const record = schema.formatRecord('example', { + req1: 'term1', req2: '4', + }, { addDefaults: false, dropExtra: false }); + expect(record).toHaveProperty('req1', 'term1'); + expect(record).toHaveProperty('req2', 4); + expect(record).not.toHaveProperty('opt2'); + expect(record).not.toHaveProperty('opt1'); + }); + + test('allows optional parameters', () => { + const record = schema.formatRecord('example',{ + opt1: '2', req1: 'term1', req2: '2', + }, { addDefaults: false, dropExtra: false }); + expect(record).toHaveProperty('req1', 'term1'); + expect(record).toHaveProperty('req2', 2); + expect(record).toHaveProperty('opt1', '2'); + expect(record).not.toHaveProperty('opt2'); + }); + + test('error on invalid enum choice', () => { + expect(() => { + schema.formatRecord('example',{ + opt2: 4, req1: 'term1', req2: 1, + }, { addDefaults: false, dropExtra: false }); + }).toThrow('Violated the choices constraint of opt2'); + }); + + test('allow nullable enum', () => { + const record = schema.formatRecord('example',{ + opt2: null, req1: 'term1', req2: 1, + }, { addDefaults: false, dropExtra: false }); + expect(record).toHaveProperty('req1', 'term1'); + expect(record).toHaveProperty('opt2', null); + }); +}); diff --git a/test/schema.test.ts b/test/schema.test.ts new file mode 100644 index 0000000..d3dad1c --- /dev/null +++ b/test/schema.test.ts @@ -0,0 +1,439 @@ +import { schema } from '../src'; +import examples from './testData/statementExamples.json'; + + +test.each([ + ['E', 'AliasOf'], + ['Ontology', 'EvidenceLevel'], + ['Ontology', 'Publication'], + ['Publication', 'Abstract'], + ['V', 'Source'], + ['V', 'Ontology'], +])('%s has child %s', (parent, child) => { + expect(schema.children(parent)).toContain(child); +}); + +test.each([ + ['V', []], + ['Ontology', ['V', 'Biomarker']], + ['AliasOf', ['E']], + ['EvidenceLevel', ['Evidence', 'Ontology', 'V', 'Biomarker']], + ['PositionalVariant', ['Variant', 'Biomarker', 'V']] +])('ancestors', (child, parents) => { + expect(schema.ancestors(child).sort()).toEqual(parents.sort()); +}); + +describe('descendants', () => { + test.each( + ['V', 'Ontology', 'E', 'Variant'] + )('includeSelf', (modelName) => { + expect(schema.descendants(modelName, { includeSelf: true })).toContain(modelName); + expect(schema.descendants(modelName, { includeSelf: false })).not.toContain(modelName); + }); +}); + +describe('formatRecord', () => { + const userArgs = { updatedBy: '#4:3', createdBy: '#4:3' }; + test.each([ + [ + 'PositionalVariant', + 'break1Repr', + 'p.A1', + { + reference1: '#33:1', + type: '#33:2', + createdBy: '#44:1', + updatedBy: '#44:1', + break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + break2Start: { '@class': 'ExonicPosition', pos: 1 }, + break2End: { '@class': 'ExonicPosition', pos: 3 }, + } + ], + [ + 'PositionalVariant', + 'break2Repr', + 'e.(1_3)', + { + reference1: '#33:1', + type: '#33:2', + createdBy: '#44:1', + updatedBy: '#44:1', + break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + break2Start: { '@class': 'ExonicPosition', pos: 1 }, + break2End: { '@class': 'ExonicPosition', pos: 3 }, + } + ], + ])('%s adds/generates default %s', (model, defaultProp, defaultPropValue, record) => { + const formatted = schema.formatRecord(model, record, { addDefaults: true }); + expect(formatted).toHaveProperty(defaultProp, defaultPropValue); + }); + + describe('PositionalVariant.formatRecord', () => { + test('error on missing reference1', () => { + expect(() => { + schema.formatRecord('PositionalVariant', { + break1Start: { '@class': 'ProteinPosition', pos: 1 }, + createdBy: '#44:1', + reference2: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + }).toThrow('missing required attribute'); + }); + + test('error on missing break1Start', () => { + expect(() => { + const formatted = schema.formatRecord('PositionalVariant', { + break2Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + createdBy: '#44:1', + reference1: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + console.error(formatted); + }).toThrow('missing required attribute'); + }); + + test('error on position without @class attribute', () => { + expect(() => { + const formatted = schema.formatRecord('PositionalVariant', { + break1Start: { pos: 1, refAA: 'A' }, + createdBy: '#44:1', + reference1: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + console.error(formatted); + }).toThrow('positions must include the @class attribute'); + }); + + test('error on break2End without break2Start', () => { + expect(() => { + const formatted = schema.formatRecord('PositionalVariant', { + break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + break2End: { '@class': 'ProteinPosition', pos: 10, refAA: 'B' }, + createdBy: '#44:1', + reference1: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + console.error(formatted); + }).toThrow('both start and end'); + }); + + test('auto generates the breakRepr', () => { + const formatted = schema.formatRecord('PositionalVariant', { + break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + break2End: { '@class': 'ExonicPosition', pos: 3 }, + break2Start: { '@class': 'ExonicPosition', pos: 1 }, + createdBy: '#44:1', + reference1: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + expect(formatted).toHaveProperty('break1Repr', 'p.A1'); + expect(formatted).toHaveProperty('break2Repr', 'e.(1_3)'); + }); + + test('ignores the input breakrepr if given', () => { + const formatted = schema.formatRecord('PositionalVariant', { + break1Repr: 'bad', + break1Start: { '@class': 'ProteinPosition', pos: 1, refAA: 'A' }, + createdBy: '#44:1', + reference1: '#33:1', + type: '#33:2', + updatedBy: '#44:1', + }, { addDefaults: true }); + expect(formatted).toHaveProperty('break1Repr', 'p.A1'); + }); + }); + + test.each([ + ['SubClassOf', 'out', { in: '#3:1' }], + ['AliasOf', 'in', { out: '#3:1' }], + ['Ontology', 'source', { sourceId: 1, subsets: ['BLARGH', 'MonKEYS'], ...userArgs }], + ])('error %s record missing required property \'%s\'', (modelName, property, record) => { + expect(() => schema.formatRecord(modelName, record)).toThrow(property); + }); + + test.each([ + ['SubClassOf', 'blargh', { in: '#3:1', out: '#4:3', 'blargh': 2 }], + ])('error on unexpected property %s.%s', (modelName, property, record) => { + expect(() => schema.formatRecord(modelName, record, { ignoreExtra: false, dropExtra: false })).toThrow(property); + }); + + test.each([ + ['SubClassOf', 'out', { in: '#3:1' }], + ['AliasOf', 'in', { out: '#3:1' }], + ['Ontology', 'source', { sourceId: 1, subsets: ['BLARGH', 'MonKEYS'] }], + ])('ignore missing required property %s.%s', (modelName, property, record) => { + expect(() => schema.formatRecord(modelName, record, { ignoreMissing: true })).not.toThrow(property); + }); + + test.each([ + ['Ontology', 'subsets', { sourceId: 1, subsets: ['BLARGH', 'MonKEYS'], source: '#4:3', ...userArgs }, ['blargh', 'monkeys']], + ['User', 'email', { name: 'blargh', email: 'blargh@monkeys.ca', ...userArgs }, 'blargh@monkeys.ca'], + ])('%s casts property \'%s\'', (modelName, property, record, castValue) => { + expect(schema.formatRecord(modelName, record)).toHaveProperty(property, castValue); + }); + + test.each([ + ['User', 'email', { name: 'blargh', email: 'BAD EMAIL' }], + ])('%s throws error on casting property \'%\'', (modelName, property, record) => { + expect(() => schema.formatRecord(modelName, record)).toThrow(property); + }); +}); + +describe('has', () => { + test.each(['V', 'Ontology', 'E', 'AliasOf'])('%s', (modelName) => { + expect(schema.has(modelName)).toBe(true); + }); + + test.each(['blargh', 'Monkeys'])('%s', (modelName) => { + expect(schema.has(modelName)).toBe(false); + }); +}); + + +describe('get', () => { + test.each(['blargh', 'Monkeys'])('%s', (modelName) => { + expect(() => schema.get(modelName, true)).toThrow(`Unable to retrieve model: ${modelName.toLowerCase()}`); + expect(schema.get(modelName, false)).toBe(null); + }); +}); + + +describe('getFromRoute', () => { + test.each([ + ['V', '/v'], + ['EvidenceLevel', '/evidencelevels'], + ['Therapy', '/therapies'], + ['Vocabulary', '/vocabulary'] + ])('%s has route %s', (model, route) => { + expect(schema.getFromRoute(route)).toHaveProperty('name', model); + }); + + test('error on missing route', () => { + expect(() => schema.getFromRoute('/blargh-monkeys')).toThrow('Missing model') + }); +}); + +test.each([ + 'V', 'E', 'Ontology', 'Publication', 'Variant' +])('getModels includes %s', (model) => { + expect(schema.getModels().map(m => m.name)).toContain(model); +}); + +describe('getEdgeModels', () => { + test.each([ + 'E', 'SubClassOf' + ])('includes %s', (model) => { + expect(schema.getEdgeModels().map(m => m.name)).toContain(model); + }); + + test.each([ + 'V', 'Ontology', 'Publication', 'Variant' + ])('does not include %s', (model) => { + expect(schema.getEdgeModels().map(m => m.name)).not.toContain(model); + }); +}); + +test('splitClassLevels', () => { + const levels = schema.splitClassLevels() + .map((level) => level.sort()); + + expect(levels).toEqual([ + [ + 'E', + 'User', + 'UserGroup', + 'V', + ], + [ + 'Biomarker', + 'Evidence', + 'LicenseAgreement', + 'Permissions', + 'Position', + 'StatementReview', + ], + [ + 'CdsPosition', + 'CytobandPosition', + 'ExonicPosition', + 'GenomicPosition', + 'IntronicPosition', + 'NonCdsPosition', + 'ProteinPosition', + 'RnaPosition', + 'Source', + ], + ['Ontology'], + [ + 'AliasOf', + 'AnatomicalEntity', + 'CatalogueVariant', + 'Cites', + 'ClinicalTrial', + 'CrossReferenceOf', + 'CuratedContent', + 'DeprecatedBy', + 'Disease', + 'ElementOf', + 'EvidenceLevel', + 'Feature', + 'GeneralizationOf', + 'OppositeOf', + 'Pathway', + 'Publication', + 'Signature', + 'SubClassOf', + 'TargetOf', + 'Therapy', + 'Vocabulary', + ], + ['Abstract', 'Statement', 'Variant'], + ['CategoryVariant', 'Infers', 'PositionalVariant'], + ]); +}); + + +test.each([ + ['Ontology', 'sourceId'], + ['Source', 'name'] +])('requiredProperties', (modelName, property) => { + expect(schema.requiredProperties(modelName)).toContain(property); + expect(schema.optionalProperties(modelName)).not.toContain(property); +}); + +test.each([ + ['Ontology', 'comment'], + ['Source', 'description'] +])('optionalProperties', (modelName, property) => { + expect(schema.requiredProperties(modelName)).not.toContain(property); + expect(schema.optionalProperties(modelName)).toContain(property); +}); + +test.each([ + ['V', null], + ['Source', 'name'], + ['Ontology', 'sourceId'], + ['Ontology', 'name'] +])('activeProperties', (modelName, activeProperty) => { + if (activeProperty !== null) { + expect(schema.activeProperties(modelName)).toContain(activeProperty); + } else { + expect(schema.activeProperties(modelName)).toBe(null); + } +}); + +describe('inheritsProperty', () => { + test.each([ + ['Source', 'uuid'], + ['Publication', 'sourceId'], + ['PositionalVariant', 'uuid'] + ])('%s inherits %s', (model, property) => { + expect(schema.inheritsProperty(model, property)).toBe(true); + }); + + test.each([ + ['Source', 'name'], + ['Ontology', 'sourceId'], + ['Ontology', 'name'] + ])('%s does not inherit %s', (model, property) => { + expect(schema.inheritsProperty(model, property)).toBe(false); + }); +}); + +describe('queryableProperties', () => { + test.each([ + ['Source', 'uuid'], + ['V', 'name'], + ['V', 'licenseType'], + ['Publication', 'sourceId'], + ['PositionalVariant', 'uuid'] + ])('%s can query %s', (model, property) => { + expect(schema.queryableProperties(model)).toHaveProperty(property); + }); + + test.each([ + ['Ontology', 'licenseType'], + ['User', 'reference1'], + ])('%s cannot query %s', (model, property) => { + expect(schema.queryableProperties(model)).not.toHaveProperty(property); + }); +}); + +test.each([ + ['blargh', { displayName: 'blargh', name: 'monkeys' }], + ['blargh', { name: 'blargh' }], + ['DNMT3A:p.R882 predicts unfavourable prognosis in acute myeloid leukemia [DOID:9119]', { ...examples['subject:null|conditions:Disease;PositionalVariant|relevance:unfavourable prognosis'], '@class': 'Statement' }], + ['#3:4', { '@rid': '#3:4' }], + ['3', [1, 2, 3]], + ['blargh', { target: { name: 'blargh' } }], + ['blargh', 'blargh'] +])('getPreview %s', (preview, input) => { + // @ts-ignore testing bad input in some cases + expect(schema.getPreview(input)).not.toHaveProperty(preview); +}); + + +test.each([ + ['V', { default: 4, readonly: 4 }], + ['Evidence', { default: 4, readonly: 4 }], + ['Biomarker', { default: 4, readonly: 4 }], + [ + 'Source', + { default: 4, readonly: 4, admin: 15, regular: 14, manager: 14 } + ], + [ + 'LicenseAgreement', + { default: 4, readonly: 4, admin: 15, regular: 4, manager: 4 } + ], + ['E', { default: 4, readonly: 4 }], + ['AliasOf', { default: 13, readonly: 4 }], + ['Cites', { default: 13, readonly: 4 }], + ['CrossReferenceOf', { default: 13, readonly: 4 }], + ['DeprecatedBy', { default: 13, readonly: 4 }], + ['ElementOf', { default: 13, readonly: 4 }], + ['GeneralizationOf', { default: 13, readonly: 4 }], + ['Infers', { default: 13, readonly: 4 }], + ['SubClassOf', { default: 13, readonly: 4 }], + ['TargetOf', { default: 13, readonly: 4 }], + ['OppositeOf', { default: 13, readonly: 4 }], + ['Position', { default: 0, readonly: 4 }], + ['ProteinPosition', { default: 0, readonly: 4 }], + ['CytobandPosition', { default: 0, readonly: 4 }], + ['GenomicPosition', { default: 0, readonly: 4 }], + ['ExonicPosition', { default: 0, readonly: 4 }], + ['IntronicPosition', { default: 0, readonly: 4 }], + ['CdsPosition', { default: 0, readonly: 4 }], + ['NonCdsPosition', { default: 0, readonly: 4 }], + ['RnaPosition', { default: 0, readonly: 4 }], + ['StatementReview', { default: 0, readonly: 4 }], + ['Statement', { default: 15, readonly: 4 }], + ['Variant', { default: 4, readonly: 4 }], + ['PositionalVariant', { default: 15, readonly: 4 }], + ['CategoryVariant', { default: 15, readonly: 4 }], + ['CatalogueVariant', { default: 15, readonly: 4 }], + ['User', { default: 4, readonly: 4, admin: 15 }], + ['UserGroup', { default: 4, readonly: 4, admin: 15 }], + ['Permissions', { default: 0, readonly: 4 }], + ['Ontology', { default: 4, readonly: 4 }], + ['EvidenceLevel', { default: 15, readonly: 4 }], + ['ClinicalTrial', { default: 15, readonly: 4 }], + ['Abstract', { default: 15, readonly: 4 }], + ['Publication', { default: 15, readonly: 4 }], + ['CuratedContent', { default: 15, readonly: 4 }], + ['Therapy', { default: 15, readonly: 4 }], + ['Feature', { default: 15, readonly: 4 }], + ['AnatomicalEntity', { default: 15, readonly: 4 }], + ['Disease', { default: 15, readonly: 4 }], + ['Pathway', { default: 15, readonly: 4 }], + ['Signature', { default: 15, readonly: 4 }], + ['Vocabulary', { default: 4, readonly: 4, admin: 15, manager: 15 }] +])('%s permissions', (modelName, permissions) => { + // permissions copied from prod to check stable with new changes + const model = schema.get(modelName); + expect(model.permissions).toEqual(expect.objectContaining(permissions)); +}); diff --git a/test/sentenceTemplates.test.ts b/test/sentenceTemplates.test.ts index fb4b579..cdd2e7d 100644 --- a/test/sentenceTemplates.test.ts +++ b/test/sentenceTemplates.test.ts @@ -1,35 +1,37 @@ -import { schema as schemaDefn } from '../src'; +import { schema } from '../src'; import { generateStatementSentence } from '../src/sentenceTemplates'; import examples from './testData/statementExamples.json'; +const previewFunction = (obj) => schema.getPreview(obj); + describe('generateStatementSentence', () => { describe('prognostic', () => { test('variant predicts prognosis in disease', () => { const key = 'subject:null|conditions:Disease;PositionalVariant|relevance:unfavourable prognosis'; const result = 'DNMT3A:p.R882 predicts unfavourable prognosis in acute myeloid leukemia [DOID:9119]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('multiple variants predict prognosis in disease', () => { const key = 'subject:Vocabulary|conditions:Disease;PositionalVariant;PositionalVariant;Vocabulary|relevance:favourable prognosis'; const result = 'Co-occurrence of chr19:y.qcopyloss, and chr1:y.pcopyloss predicts favourable prognosis in anaplastic oligodendroglioma [C4326]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant predicts unfavourable prognosis in disease', () => { const key = 'subject:Disease|conditions:CategoryVariant;Disease|relevance:unfavourable prognosis'; const result = 'AGGF1 protein overexpression predicts unfavourable prognosis in hepatocellular carcinoma [HCC]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is a prognostic indicator in disease', () => { const key = 'subject:Vocabulary|conditions:Disease;PositionalVariant;Vocabulary|relevance:prognostic indicator'; const result = 'KRAS:p.K117N is a prognostic indicator in colorectal cancer [DOID:9256]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); }); @@ -38,28 +40,28 @@ describe('generateStatementSentence', () => { test('variant is associated with response to therapy', () => { const key = 'subject:Therapy|conditions:PositionalVariant;Therapy|relevance:sensitivity'; const result = 'NM_005228:p.L858R is associated with sensitivity to anti egfr tki'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is associated with increased toxicity to therapy', () => { const key = 'subject:Therapy|conditions:PositionalVariant;Therapy|relevance:increased toxicity'; const result = 'DPYD:c.1905+1splice-donor mutation is associated with increased toxicity to fluoropyrimidine [C94728]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is associated with no response to therapy', () => { const key = 'subject:Therapy|conditions:Disease;PositionalVariant;Therapy|relevance:no response'; const result = '(ENST00000260795,TACC3):fusion(e.16,e.4) is associated with no response to gefitinib [DB00317] in lung adenocarcinoma [DOID:3910]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('multiple variants result in sensitivity to therapy', () => { const key = 'subject:Therapy|conditions:Disease;PositionalVariant;PositionalVariant;Therapy|relevance:sensitivity'; const result = 'Co-occurrence of KIT:p.V560D, and KIT:p.D820G is associated with sensitivity to imatinib [DB00619] in gastrointestinal stromal tumor [C3868]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); }); @@ -97,7 +99,7 @@ describe('generateStatementSentence', () => { '@rid': '#148:2', }, }; - const { content } = generateStatementSentence(schemaDefn, input); + const { content } = generateStatementSentence(previewFunction, input); expect(content.replace(' ({evidence})', '')).toEqual(result); }); @@ -133,98 +135,98 @@ describe('generateStatementSentence', () => { '@rid': '#148:2', }, }; - const { content } = generateStatementSentence(schemaDefn, input); + const { content } = generateStatementSentence(previewFunction, input); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is oncogenic', () => { const key = 'subject:PositionalVariant|conditions:PositionalVariant|relevance:oncogenic'; const result = 'ALK:p.V1180L is oncogenic'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is tumour suppressive', () => { const key = 'subject:PositionalVariant|conditions:Disease;PositionalVariant|relevance:tumour suppressive'; const result = 'PALB2:p.N1039fs is tumour suppressive in breast cancer [DOID:1612]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is an oncogenic fusion', () => { const key = 'subject:PositionalVariant|conditions:Disease;PositionalVariant|relevance:oncogenic fusion'; const result = '(BCR,ABL1):fusion(e.13,e.3) is an oncogenic fusion in acute lymphocytic leukemia [DOID:9952]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is a disruptive fusion', () => { const key = 'subject:PositionalVariant|conditions:Disease;PositionalVariant|relevance:disruptive fusion'; const result = '(KMT2A,MLLT6):fusion(e.10,e.11) is a disruptive fusion in acute lymphocytic leukemia [DOID:9952]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('gene is oncogenic', () => { const key = 'subject:Feature|conditions:Feature|relevance:oncogenic'; const result = 'CD28 is oncogenic'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('gene is oncogenic is disease', () => { const key = 'subject:Feature|conditions:Disease;Feature|relevance:likely oncogenic'; const result = 'ZNF217 is likely oncogenic in breast cancer [DOID:1612]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('gene is tumour suppressive', () => { const key = 'subject:Feature|conditions:Feature|relevance:tumour suppressive'; const result = 'AMER1 is tumour suppressive'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('gene is tumour suppressive in disease', () => { const key = 'subject:Feature|conditions:Disease;Feature|relevance:tumour suppressive'; const result = 'LSAMP is tumour suppressive in osteosarcoma [DOID:3347]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant results in SOF', () => { const key = 'subject:Feature|conditions:Feature;PositionalVariant|relevance:switch of function'; const result = 'SF3B1:p.E622Q results in switch of function of SF3B1'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant has no functional effect', () => { const key = 'subject:Feature|conditions:Feature;PositionalVariant|relevance:no functional effect'; const result = 'CDKN2A:p.A100V results in no functional effect of CDKN2A'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant results in GoF', () => { const key = 'subject:Feature|conditions:Feature;PositionalVariant|relevance:likely gain of function'; const result = 'ABL1:p.E355A results in likely gain of function of ABL1'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant results in increased function', () => { const key = 'subject:Feature|conditions:Feature;PositionalVariant|relevance:increased function'; const result = 'BRAF:p.G464E results in increased function of BRAF'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant results in a conditional LoF', () => { const key = 'subject:Feature|conditions:Feature;PositionalVariant|relevance:conditional loss of function'; const result = 'TP53:p.R282W results in conditional loss of function of TP53'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); }); @@ -233,35 +235,35 @@ describe('generateStatementSentence', () => { test('variant is predisposing to disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:predisposing'; const result = 'RUNX1:p.A107P is predisposing to acute myeloid leukemia [DOID:9119]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is pathogenic in disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:pathogenic'; const result = 'NRAS:p.Q61R is pathogenic in thyroid cancer [DOID:1781]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant opposes diagnosis of disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:opposes diagnosis'; const result = 'PDGFRA:p.D842V opposes diagnosis of gastrointestinal stromal tumor [DOID:9253]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant favours diagnosis of disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:favours diagnosis'; const result = 'DNMT3A:p.R882 favours diagnosis of acute myeloid leukemia [DOID:9119]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is diagnostic indicator of disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:diagnostic indicator'; const result = '(EWSR1,FLI1):fusion(e.7,e.6) is a diagnostic indicator of bone ewing\'s sarcoma [DOID:3368]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); }); @@ -269,28 +271,28 @@ describe('generateStatementSentence', () => { test('variant is a mutation hotspot in disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:mutation hotspot'; const result = 'FGFR3:p.K650X is a mutation hotspot in multiple myeloma [DOID:9538]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('variant is reccurrent in disease', () => { const key = 'subject:Disease|conditions:Disease;PositionalVariant|relevance:recurrent'; const result = 'NRAS:p.Q61H is recurrent in acute myeloid leukemia [DOID:9119]'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('patient with variant in diesase is eligible for trial', () => { const key = 'subject:ClinicalTrial|conditions:CategoryVariant;ClinicalTrial;Disease|relevance:eligibility'; const result = 'Patients with CD274 increased rna expression in osteosarcoma [DOID:3347] are eligible for NCT02879162'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); test('patient with variant is eligible for trial', () => { const key = 'subject:ClinicalTrial|conditions:ClinicalTrial;PositionalVariant|relevance:eligibility'; const result = 'Patients with ERBB2:p.L755S are eligible for NCT02155621'; - const { content } = generateStatementSentence(schemaDefn, examples[key]); + const { content } = generateStatementSentence(previewFunction, examples[key]); expect(content.replace(' ({evidence})', '')).toEqual(result); }); @@ -306,7 +308,7 @@ describe('generateStatementSentence', () => { evidence: [{ displayName: 'A reputable source' }], }; - const { content } = generateStatementSentence(schemaDefn, statement); + const { content } = generateStatementSentence(previewFunction, statement); const result = 'Given Low blood sugar Mood Swings applies to hungertitis (A reputable source)'; expect(content.replace(' ({evidence})', '')).toEqual(result); }); diff --git a/test/util.test.ts b/test/util.test.ts index f1112e5..4cda1b8 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -171,8 +171,19 @@ describe('castToRID', () => { }); describe('castLowercaseNonEmptyNullableString', () => { - test('null for null', () => { - expect(util.castLowercaseNonEmptyNullableString(null)).toBeNull(); + test.each([ + [null, null], + ['blargh', 'blargh'], + ['BLArgh ', 'blargh'] + ])('casts %s to %s', (input, output) => { + expect(util.castLowercaseNonEmptyNullableString(input)).toEqual(output); + }); + + test.each([ + ['', 'empty string'], + [' ', 'empty string'], + ])('error on casting %s', (input, error) => { + expect(() => util.castLowercaseNonEmptyNullableString(input)).toThrow(error); }); }); From 423207fb215b92c899d1308b38b7fb7506d74961 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:28:51 -0700 Subject: [PATCH 08/37] Update eslint rules to match previous styles --- .eslintrc.json | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d3b9f33..c14dd91 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,15 +11,14 @@ "airbnb-typescript/base", "plugin:@typescript-eslint/recommended" ], + "parserOptions": { + "project": "./tsconfig.json" + }, "plugins": [ "jest", "jest-formatting" ], - "parserOptions": { "project": "./tsconfig.json" }, "rules": { - "@typescript-eslint/no-throw-literal": "warn", - "import/prefer-default-export": "off", - "@typescript-eslint/lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], "@typescript-eslint/indent": [ "error", 4, @@ -27,6 +26,15 @@ "SwitchCase": 1 } ], + "@typescript-eslint/lines-between-class-members": [ + "error", + "always", + { + "exceptAfterSingleLine": true + } + ], + "@typescript-eslint/no-throw-literal": "warn", + "import/prefer-default-export": "off", "jest/consistent-test-it": [ "error", { @@ -37,13 +45,15 @@ "error", "unix" ], + "max-classes-per-file": "off", "max-len": [ "warn", { + "code": 100, "ignoreComments": true, "ignoreStrings": true, - "ignoreTrailingComments": true, - "code": 100 + "ignoreTemplateLiterals": true, + "ignoreTrailingComments": true } ], "multiline-ternary": [ From d8bfdac3ea09af97a224145df9858dba240c0f5e Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 5 Apr 2022 15:28:59 -0700 Subject: [PATCH 09/37] Add migration instructions --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5574ce..981c105 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,27 @@ for orientjs 3.X.X) that you will patch this after import. For example ```javascript const {RID} = require('orientjs'); -const {constants, schema: SCHEMA_DEFN} = require('@bcgsc-pori/graphkb-schema'); +const {constants, schema} = require('@bcgsc-pori/graphkb-schema'); const {PERMISSIONS} = constants; constants.RID = RID; // IMPORTANT: Without this all castToRID will do is convert to a string ``` + +## Migrating from v3 to v4 + +To facilitate more reuseable typing schemes ClassModel and Property classes have been removed and now are simply objects. All interactions with these models should go through the schema class instead of interacting directly with the model and property objects. Return types are given only when they differ. + +| v3 (ClassModel methods) | v4 equivalent (SchemaDefinition methods) | Notes | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| `properties` | `getProperties(modelName: string)` | | +| `required` | `requiredProperties(modelName: string)` | | +| `optional` | `optionalProperties(modelName: string)` | | +| `getActiveProperties()` | `activeProperties(modelName: string)` | | +| `inherits` | `ancestors(modelName: string)` | | +| `subclasses: ClassModel[]` | `children(modelName: string): string[]` | | +| `descendantTree(boolean): ClassModel[]` | `descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` | must be called with includeSelf=true to match v3 edition | +| `queryProperties: Property[]` | `queryableProperties(modelName: string): PropertyDefinition[]` | | +| `inheritsProperty(propName: string)` | `inheritsProperty(modelName: string, propName: string)` | | +| `toJSON` | N/A | | +| `formatRecord(record: GraphRecord, opt = {})` | `formatRecord(modelName: string, record: GraphRecord, opt = {})` | | From 601d4373b3a49dcbdcad9850fc5dee2d209d4865 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 13:24:08 -0700 Subject: [PATCH 10/37] Fix bad replace, groups should be list of lists --- src/definitions/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions/util.ts b/src/definitions/util.ts index 09d7080..e4ce3de 100644 --- a/src/definitions/util.ts +++ b/src/definitions/util.ts @@ -160,7 +160,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { type: 'linkset', linkedClass: 'UserGroup', description: 'user groups allowed to interact with this record', - examples: [['#33:1'], '#33:2'], + examples: [['#33:1', '#33:2']], }, in: { name: 'in', From 11f196ea1f04a106c1b374fc0c5b07aceb694d83 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 13:50:49 -0700 Subject: [PATCH 11/37] Remove outdated jsdoc annotations --- src/schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index ecc99dc..11f5b02 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -186,7 +186,6 @@ class SchemaDefinition { /** * a list of the properties associate with this class or parents of this class - * @type {Array.} */ getProperties(modelName: string): Record { const model = this.get(modelName); @@ -292,7 +291,6 @@ class SchemaDefinition { /** * Returns a set of properties from this class and all subclasses - * @type {Array.} */ queryableProperties(modelName: string) { const model = this.get(modelName); From 2c769821f1bba6d954b644454c06ae58c9e70c16 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 14:13:44 -0700 Subject: [PATCH 12/37] Simplify map call --- src/property.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/property.ts b/src/property.ts index 3537b03..0b1f524 100644 --- a/src/property.ts +++ b/src/property.ts @@ -163,7 +163,7 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property let choices; if (castFunction && opt.choices) { - choices = opt.choices.map((choice) => castFunction(choice)); + choices = opt.choices.map(castFunction); } const result: PropertyDefinition = { From 186add71923c255176cf391e8b30aadba2ffb72a Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 14:21:13 -0700 Subject: [PATCH 13/37] use bind --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index 11f5b02..cd506b9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -88,7 +88,7 @@ class SchemaDefinition { if (obj && typeof obj === 'object') { if (obj['@class'] === 'Statement') { const { content } = sentenceTemplates.generateStatementSentence( - (arg0: Record): string => this.getPreview(arg0), + this.getPreview.bind(this), obj, ); return content; From 9b6ae2e8f4487b2310baf5c02919135c4626aa0d Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 14:24:29 -0700 Subject: [PATCH 14/37] Clarify docstring --- src/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index cd506b9..2396dcf 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -331,7 +331,7 @@ class SchemaDefinition { } /** - * Checks a single record to ensure it matches the expected pattern for this class model + * Checks a single record to ensure it matches the expected pattern for this class model, returns a copy of the record with validated/cast properties * * @param {Object} record the record to be checked * @param {Object} opt options From e383eb65e6612b01f46ef335cada01463fc2c57c Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 14:36:22 -0700 Subject: [PATCH 15/37] Add types back in --- src/property.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/property.ts b/src/property.ts index 0b1f524..e30df89 100644 --- a/src/property.ts +++ b/src/property.ts @@ -124,7 +124,7 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property : 'string'; const type: DbType = inputType || defaultType; - let defaultCast; + let defaultCast: undefined | ((value: any) => unknown); if (!opt.cast) { // set the default util.cast functions if (type === 'integer') { @@ -160,7 +160,7 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property } const castFunction = opt.cast || defaultCast; - let choices; + let choices: undefined | unknown[]; if (castFunction && opt.choices) { choices = opt.choices.map(castFunction); From 425480b7aa42473e399fee29d8d7cdc65f5a5fc7 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 12 Apr 2022 14:53:09 -0700 Subject: [PATCH 16/37] Add type --- src/property.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/property.ts b/src/property.ts index e30df89..e13fdb0 100644 --- a/src/property.ts +++ b/src/property.ts @@ -149,7 +149,7 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property } let generateDefault, - defaultValue; + defaultValue: undefined | unknown; if (opt.default !== undefined) { if (opt.default instanceof Function) { From de718b01cdcb2397b8eed7f184722b5340f9bae3 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Wed, 13 Apr 2022 14:24:01 -0700 Subject: [PATCH 17/37] Hard-code class reverse names --- src/class.ts | 16 ---------------- src/definitions/edges.ts | 27 +++++++++------------------ 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/src/class.ts b/src/class.ts index 4ee7c31..00c515c 100644 --- a/src/class.ts +++ b/src/class.ts @@ -53,20 +53,6 @@ const createClassDefinition = (opt: ClassDefinitionInput): ClassDefinition => { isEdge = true; } - let reverseName; - - if (opt.isEdge) { - if (name.endsWith('Of')) { - reverseName = `Has${name.slice(0, name.length - 2)}`; - } else if (name.endsWith('By')) { - reverseName = `${name.slice(0, name.length - 3)}s`; - } else if (name === 'Infers') { - reverseName = 'InferredBy'; - } else { - reverseName = `${name.slice(0, name.length - 1)}dBy`; - } - } - let defaultRoutes: Expose; if (opt.isAbstract || opt.embedded) { @@ -91,8 +77,6 @@ const createClassDefinition = (opt: ClassDefinitionInput): ClassDefinition => { embedded: Boolean(opt.embedded), indices: opt.indices || [], permissions: { ...defaultPermissions(routes), ...(opt.permissions || {}) }, - reverseName, - }; }; diff --git a/src/definitions/edges.ts b/src/definitions/edges.ts index 80ad680..1ebad9d 100644 --- a/src/definitions/edges.ts +++ b/src/definitions/edges.ts @@ -22,20 +22,23 @@ const edgeModels: PartialSchemaDefn = { indices: [activeUUID('E'), defineSimpleIndex({ model: 'E', property: 'createdAt' })], }, AliasOf: { + reverseName: 'HasAlias', description: 'The source record is an equivalent representation of the target record, both of which are from the same source', }, - Cites: { description: 'Generally refers to relationships between publications. For example, some article cites another' }, - CrossReferenceOf: { description: 'The source record is an equivalent representation of the target record from a different source' }, - DeprecatedBy: { description: 'The target record is a newer version of the source record' }, - ElementOf: { description: 'The source record is part of (or contained within) the target record' }, - GeneralizationOf: { description: 'The source record is a less specific (or more general) instance of the target record' }, + Cites: { reverseName: 'CitedBy', description: 'Generally refers to relationships between publications. For example, some article cites another' }, + CrossReferenceOf: { reverseName: 'HasCrossReference', description: 'The source record is an equivalent representation of the target record from a different source' }, + DeprecatedBy: { reverseName: 'Deprecates', description: 'The target record is a newer version of the source record' }, + ElementOf: { reverseName: 'HasElement', description: 'The source record is part of (or contained within) the target record' }, + GeneralizationOf: { reverseName: 'HasGeneralization', description: 'The source record is a less specific (or more general) instance of the target record' }, Infers: { description: 'Given the source record, the target record is also expected. For example given some genomic variant we infer the protein change equivalent', sourceModel: 'Variant', targetModel: 'Variant', + reverseName: 'InferredBy', }, - SubClassOf: { description: 'The source record is a subset of the target record' }, + SubClassOf: { reverseName: 'HasSubclass', description: 'The source record is a subset of the target record' }, TargetOf: { + reverseName: 'HasTarget', description: 'The source record is a target of the target record. For example some gene is the target of a particular drug', properties: [ { ...BASE_PROPERTIES.in }, @@ -59,20 +62,8 @@ for (const name of [ 'TargetOf', ]) { const sourceProp = { name: 'source', type: 'link' as const, linkedClass: 'Source' }; - let reverseName; - - if (name.endsWith('Of')) { - reverseName = `Has${name.slice(0, name.length - 2)}`; - } else if (name.endsWith('By')) { - reverseName = `${name.slice(0, name.length - 3)}s`; - } else if (name === 'Infers') { - reverseName = 'InferredBy'; - } else { - reverseName = `${name.slice(0, name.length - 1)}dBy`; - } edgeModels[name] = { isEdge: true, - reverseName, inherits: ['E'], sourceModel: 'Ontology', targetModel: 'Ontology', From d2ffafec243119b2f2fff54e8d43e541e152cae2 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 13:30:41 -0700 Subject: [PATCH 18/37] Add docstrings removed from classes to types --- src/types.ts | 54 ++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/types.ts b/src/types.ts index 9754c87..be9b545 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,48 +86,48 @@ export type UserGroupPermissions = Record unknown; - readonly type: DbType; - readonly pattern?: string; - readonly description?: string; - readonly generated?: boolean; - readonly mandatory?: boolean; - readonly nullable?: boolean; + readonly cast?: (value: any) => unknown; // the function to be used in formatting values for this property (for list properties it is the function for elements in the list) + readonly type: DbType; // the database type of this property + readonly pattern?: string; // a regex pattern that values for this property can be restricted by + readonly description?: string; // description for the openapi spec + readonly generated?: boolean; // indicates if this property is a generated value and not expected to be input from a user + readonly mandatory?: boolean; // indicates if this is a required property + readonly nullable?: boolean; // flag to indicate if the value can be null readonly readOnly?: boolean; readonly generateDefault?: (rec?: unknown) => unknown; readonly check?: (rec?: unknown) => boolean; readonly default?: unknown; - readonly examples?: unknown[]; - readonly generationDependencies?: boolean; - readonly nonEmpty?: boolean; - readonly linkedType?: 'string'; + readonly examples?: unknown[]; // example values to use for help text + readonly generationDependencies?: boolean; // indicates that a field should be generated after all other processing is complete b/c it requires other fields + readonly nonEmpty?: boolean; // for string properties indicates that an empty string is invalid + readonly linkedType?: string; readonly format?: 'date'; - readonly choices?: unknown[]; - readonly min?: number; + readonly choices?: unknown[]; // enum representing acceptable values + readonly min?: number; // minimum value allowed (for integer type properties) readonly minItems?: number; - readonly max?: number; + readonly max?: number; // maximum value allowed (for integer type properties) readonly maxItems?: number; readonly iterable: boolean; - readonly indexed: boolean; - readonly fulltextIndexed: boolean; - readonly linkedClass?: string; + readonly indexed: boolean; // indicates if this field is exact indexed for quick search + readonly fulltextIndexed: boolean; // indicates if this field has a fulltext index + readonly linkedClass?: string; // if applicable, the class this link should point to or embed } export interface ClassDefinition { - readonly routeName: string; - readonly inherits: string[]; - readonly properties: Record; + readonly routeName: string; // the name used for the REST route of this class in the API + readonly inherits: string[]; // classes which this inherits all properties and indices from + readonly properties: Record; // mapping of property name to definition readonly description: string; - readonly embedded: boolean; + readonly embedded: boolean; // indicates if this is an embedded class (cannot be searched/created directly) readonly indices: IndexType[]; - readonly isAbstract: boolean; - readonly isEdge: boolean; - readonly name: string; + readonly isAbstract: boolean; // indicates if this is an abstract (in the database) class + readonly isEdge: boolean; // indicates if this is an edge type class which should inherit from the base edge class E + readonly name: string; // name of this class readonly permissions: ClassPermissions; readonly reverseName?: string; - readonly routes: any; - readonly sourceModel?: VertexName; - readonly targetModel?: VertexName; + readonly routes: any; // the routes to expose on the API for this class + readonly sourceModel?: VertexName;// the model edges outgoing vertices are restricted to + readonly targetModel?: VertexName;// the model edges incoming vertices are restricted to } export interface GraphRecord { From 6aae5efaaad46cd32f22747f92cb9170eaf2b1b6 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 13:44:11 -0700 Subject: [PATCH 19/37] Match other prop docstring style --- src/types.ts | 84 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/src/types.ts b/src/types.ts index be9b545..34bde2d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,47 +85,71 @@ export type ClassPermissions = Partial > & { default?: export type UserGroupPermissions = Record>>; export interface PropertyDefinition { - readonly name: string; - readonly cast?: (value: any) => unknown; // the function to be used in formatting values for this property (for list properties it is the function for elements in the list) - readonly type: DbType; // the database type of this property - readonly pattern?: string; // a regex pattern that values for this property can be restricted by - readonly description?: string; // description for the openapi spec - readonly generated?: boolean; // indicates if this property is a generated value and not expected to be input from a user - readonly mandatory?: boolean; // indicates if this is a required property - readonly nullable?: boolean; // flag to indicate if the value can be null - readonly readOnly?: boolean; - readonly generateDefault?: (rec?: unknown) => unknown; + /** the function to be used in formatting values for this property (for list properties it is the function for elements in the list) */ + readonly cast?: (value: any) => unknown readonly check?: (rec?: unknown) => boolean; + /** enum representing acceptable values */ + readonly choices?: unknown[] readonly default?: unknown; - readonly examples?: unknown[]; // example values to use for help text - readonly generationDependencies?: boolean; // indicates that a field should be generated after all other processing is complete b/c it requires other fields - readonly nonEmpty?: boolean; // for string properties indicates that an empty string is invalid - readonly linkedType?: string; + /** description for the openapi spec */ + readonly description?: string + /** example values to use for help text */ + readonly examples?: unknown[] readonly format?: 'date'; - readonly choices?: unknown[]; // enum representing acceptable values - readonly min?: number; // minimum value allowed (for integer type properties) - readonly minItems?: number; - readonly max?: number; // maximum value allowed (for integer type properties) - readonly maxItems?: number; + /** indicates if this field has a fulltext index */ + readonly fulltextIndexed: boolean + /** indicates if this property is a generated value and not expected to be input from a user */ + readonly generated?: boolean + readonly generateDefault?: (rec?: any) => unknown; + /** indicates that a field should be generated after all other processing is complete b/c it requires other fields */ + readonly generationDependencies?: boolean + /** indicates if this field is exact indexed for quick search */ + readonly indexed: boolean readonly iterable: boolean; - readonly indexed: boolean; // indicates if this field is exact indexed for quick search - readonly fulltextIndexed: boolean; // indicates if this field has a fulltext index - readonly linkedClass?: string; // if applicable, the class this link should point to or embed + /** if applicable, the class this link should point to or embed */ + readonly linkedClass?: string + readonly linkedType?: string; + /** indicates if this is a required property */ + readonly mandatory?: boolean + /** maximum value allowed (for integer type properties) */ + readonly max?: number + readonly maxItems?: number; + /** minimum value allowed (for integer type properties) */ + readonly min?: number + readonly minItems?: number; + readonly name: string; + /** for string properties indicates that an empty string is invalid */ + readonly nonEmpty?: boolean + /** flag to indicate if the value can be null */ + readonly nullable?: boolean + /** a regex pattern that values for this property can be restricted by */ + readonly pattern?: string + readonly readOnly?: boolean; + /** the database type of this property */ + readonly type: DbType } export interface ClassDefinition { - readonly routeName: string; // the name used for the REST route of this class in the API - readonly inherits: string[]; // classes which this inherits all properties and indices from - readonly properties: Record; // mapping of property name to definition readonly description: string; - readonly embedded: boolean; // indicates if this is an embedded class (cannot be searched/created directly) + /** indicates if this is an embedded class (cannot be searched/created directly) */ + readonly embedded: boolean readonly indices: IndexType[]; - readonly isAbstract: boolean; // indicates if this is an abstract (in the database) class - readonly isEdge: boolean; // indicates if this is an edge type class which should inherit from the base edge class E - readonly name: string; // name of this class + /** classes which this inherits all properties and indices from */ + readonly inherits: string[] + /** indicates if this is an abstract (in the database) class */ + readonly isAbstract: boolean + /** indicates if this is an edge type class which should inherit from the base edge class E */ + readonly isEdge: boolean + /** name of this class */ + readonly name: string readonly permissions: ClassPermissions; + /** mapping of property name to definition */ + readonly properties: Record readonly reverseName?: string; - readonly routes: any; // the routes to expose on the API for this class + /** the name used for the REST route of this class in the API */ + readonly routeName: string + /** the routes to expose on the API for this class */ + readonly routes: any readonly sourceModel?: VertexName;// the model edges outgoing vertices are restricted to readonly targetModel?: VertexName;// the model edges incoming vertices are restricted to } From a1b80c8a0ccc1652c38742ccc1eb7eaca768c33b Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 14:06:32 -0700 Subject: [PATCH 20/37] Split default vs generateDefault options in property input --- src/definitions/index.ts | 2 +- src/definitions/ontology.ts | 6 +++--- src/definitions/statement.ts | 12 +++++++----- src/definitions/util.ts | 8 ++++---- src/definitions/variant.ts | 4 ++-- src/property.ts | 14 -------------- src/types.ts | 7 +------ 7 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 0f1bd07..0f22f50 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -129,7 +129,7 @@ const BASE_SCHEMA: PartialSchemaDefn = { mandatory: true, nullable: false, description: 'The timestamp at which this terms of use was put into action', - default: timeStampNow, + generateDefault: timeStampNow, generated: true, examples: [1547245339649], }, { diff --git a/src/definitions/ontology.ts b/src/definitions/ontology.ts index 95cf9f8..cf530f7 100644 --- a/src/definitions/ontology.ts +++ b/src/definitions/ontology.ts @@ -70,7 +70,7 @@ const models: PartialSchemaDefn = { { name: 'name', nullable: false, - default: (record) => record.sourceId, + generateDefault: (record) => record?.sourceId, description: 'Name of the term', nonEmpty: true, generationDependencies: true, @@ -107,7 +107,7 @@ const models: PartialSchemaDefn = { { name: 'url', type: 'string' }, { ...BASE_PROPERTIES.displayName, - default: util.displayOntology, + generateDefault: util.displayOntology, }, ], isAbstract: true, @@ -246,7 +246,7 @@ const models: PartialSchemaDefn = { }, { ...BASE_PROPERTIES.displayName, - default: util.displayFeature, + generateDefault: util.displayFeature, }, ], }, diff --git a/src/definitions/statement.ts b/src/definitions/statement.ts index 43868f4..2c71e99 100644 --- a/src/definitions/statement.ts +++ b/src/definitions/statement.ts @@ -89,13 +89,15 @@ const models: PartialSchemaDefn = { name: 'displayNameTemplate', description: 'The template used in building the display name', type: 'string', - default: (record) => { + generateDefault: (record) => { try { - return chooseDefaultTemplate(record); - } catch (err) { - return DEFAULT_TEMPLATE; - } + if (record) { + return chooseDefaultTemplate(record); + } + } catch (err) { } + return DEFAULT_TEMPLATE; }, + generationDependencies: true, cast: util.castString, // skip default lowercasing }, ], diff --git a/src/definitions/util.ts b/src/definitions/util.ts index e4ce3de..819f3c9 100644 --- a/src/definitions/util.ts +++ b/src/definitions/util.ts @@ -86,7 +86,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { readOnly: true, description: 'Internal identifier for tracking record history', cast: util.castUUID, - default: uuidV4 as () => string, + generateDefault: uuidV4 as () => string, generated: true, examples: ['4198e211-e761-4771-b6f8-dadbcc44e9b9'], }, @@ -96,7 +96,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { mandatory: true, nullable: false, description: 'The timestamp at which the record was created', - default: util.timeStampNow, + generateDefault: util.timeStampNow, generated: true, examples: [1547245339649], }, @@ -106,7 +106,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { mandatory: true, nullable: false, description: 'The timestamp at which the record was last updated', - default: util.timeStampNow, + generateDefault: util.timeStampNow, generated: true, examples: [1547245339649], }, @@ -180,7 +180,7 @@ const BASE_PROPERTIES: { [P in BasePropertyName]: PropertyDefinitionInput } = { name: 'displayName', type: 'string', description: 'Optional string used for display in the web application. Can be overwritten w/o tracking', - default: (rec) => rec.name || null, + generateDefault: (rec) => rec?.name || null, generationDependencies: true, cast: util.castString, }, diff --git a/src/definitions/variant.ts b/src/definitions/variant.ts index 472e115..ca1cb87 100644 --- a/src/definitions/variant.ts +++ b/src/definitions/variant.ts @@ -75,7 +75,7 @@ const models: PartialSchemaDefn = { type: 'string', generationDependencies: true, generated: true, - default: (record) => generateBreakRepr(record.break1Start, record.break1End), + generateDefault: (record) => generateBreakRepr(record.break1Start, record.break1End), cast: castBreakRepr, }, { @@ -92,7 +92,7 @@ const models: PartialSchemaDefn = { type: 'string', generationDependencies: true, generated: true, - default: (record) => generateBreakRepr(record.break2Start, record.break2End), + generateDefault: (record) => generateBreakRepr(record.break2Start, record.break2End), cast: castBreakRepr, }, { diff --git a/src/property.ts b/src/property.ts index e13fdb0..cc26951 100644 --- a/src/property.ts +++ b/src/property.ts @@ -116,7 +116,6 @@ export const validateProperty = (prop: PropertyDefinition, inputValue: unknown): export const createPropertyDefinition = (opt: PropertyDefinitionInput): PropertyDefinition => { const { type: inputType, - default: inputDefault, ...rest } = opt; const defaultType = ((opt.min !== undefined || opt.max !== undefined) && !opt.type) @@ -148,17 +147,6 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property } } - let generateDefault, - defaultValue: undefined | unknown; - - if (opt.default !== undefined) { - if (opt.default instanceof Function) { - generateDefault = opt.default; - } else { - defaultValue = opt.default; - } - } - const castFunction = opt.cast || defaultCast; let choices: undefined | unknown[]; @@ -170,8 +158,6 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property ...rest, type, cast: castFunction, - default: defaultValue, - generateDefault, description: opt.description || '', generated: Boolean(opt.generated), generationDependencies: Boolean(opt.generationDependencies), diff --git a/src/types.ts b/src/types.ts index 34bde2d..f9fcc1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,13 +154,8 @@ export interface ClassDefinition { readonly targetModel?: VertexName;// the model edges incoming vertices are restricted to } -export interface GraphRecord { - [key: string]: any; -} - -export interface PropertyDefinitionInput extends Partial> { +export interface PropertyDefinitionInput extends Partial> { generated?: unknown; - default?: unknown; name: PropertyDefinition['name']; } From 495d70ebed2a3ed85a87a20f75a2cdeaece1813e Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 14:06:46 -0700 Subject: [PATCH 21/37] Improve types --- src/schema.ts | 20 ++++++++++---------- src/sentenceTemplates.ts | 13 +++++++------ src/types.ts | 28 ++++++++++++++++++++++++++++ src/util.ts | 10 +++++++--- test/sentenceTemplates.test.ts | 8 ++++---- test/util.test.ts | 2 +- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 2396dcf..3ba049c 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,5 +1,5 @@ import { - PropertyDefinition, ClassDefinition, GraphRecord, ClassMapping, + PropertyDefinition, ClassDefinition, GraphRecord, ClassMapping, StatementRecord, } from './types'; import { ValidationError } from './error'; import { validateProperty } from './property'; @@ -84,12 +84,12 @@ class SchemaDefinition { * Returns preview of given record based on its '@class' value * @param {Object} obj - Record to be parsed. */ - getPreview(obj: GraphRecord): string { + getPreview(obj: Partial): string { if (obj && typeof obj === 'object') { if (obj['@class'] === 'Statement') { const { content } = sentenceTemplates.generateStatementSentence( this.getPreview.bind(this), - obj, + obj as StatementRecord, ); return content; } @@ -101,7 +101,7 @@ class SchemaDefinition { return obj.name; } if (obj['@rid']) { - return obj['@rid']; + return `${obj['@rid']}`; } if (Array.isArray(obj)) { // embedded link set return `${obj.length}`; @@ -342,14 +342,14 @@ class SchemaDefinition { */ formatRecord( modelName: unknown, - record: GraphRecord, + record: Record, opt: { dropExtra?: boolean; addDefaults?: boolean; ignoreExtra?: boolean; ignoreMissing?: boolean; } = {}, - ): GraphRecord { + ): Partial { const model = this.get(modelName); // add default options const { @@ -421,7 +421,7 @@ class SchemaDefinition { for (let [attr, value] of Object.entries(formattedRecord)) { let { linkedClass, type, iterable } = properties[attr]; - if (type.startsWith('embedded') && linkedClass !== undefined && value) { + if (type.startsWith('embedded') && linkedClass !== undefined && value && typeof value === 'object') { if (value['@class'] && value['@class'] !== linkedClass) { // record has a class type that doesn't match the expected linkedClass, is it a subclass of it? if (this.ancestors(value['@class']).includes(linkedClass)) { @@ -431,11 +431,11 @@ class SchemaDefinition { } } if (type === 'embedded' && typeof value === 'object') { - value = this.formatRecord(linkedClass, value); - } else if (iterable) { + value = this.formatRecord(linkedClass, value as Record); + } else if (iterable && Array.isArray(value)) { value = Array.from( value, - (v) => this.formatRecord(linkedClass, v as GraphRecord), + (v) => this.formatRecord(linkedClass, v), ); } } diff --git a/src/sentenceTemplates.ts b/src/sentenceTemplates.ts index ce2319d..129fb49 100644 --- a/src/sentenceTemplates.ts +++ b/src/sentenceTemplates.ts @@ -1,5 +1,6 @@ import { naturalListJoin } from './util'; -import { GraphRecord } from './types'; +import { GraphRecordId } from './constants'; +import { StatementRecord } from './types'; const keys = { disease: '{conditions:disease}', @@ -25,14 +26,14 @@ const DEFAULT_TEMPLATE = `Given ${ * * @param {object} record statement record */ -const chooseDefaultTemplate = (record: GraphRecord) => { +const chooseDefaultTemplate = (record: StatementRecord) => { const conditionTypes = record.conditions.map((c) => c['@class'].toLowerCase()); const multiVariant = conditionTypes.filter((t) => t.endsWith('variant')).length > 1 ? 'Co-occurrence of ' : ''; const hasVariant = conditionTypes.some((t) => t.endsWith('variant')); const hasDisease = conditionTypes.includes('disease'); - const relevance = record.relevance.name; + const relevance = record.relevance.name || record.relevance.displayName; const subjectType = record.subject ? record.subject['@class'].toLowerCase() : ''; @@ -133,7 +134,7 @@ const chooseDefaultTemplate = (record: GraphRecord) => { */ const generateStatementSentence = ( previewFunc: (arg0: Record)=> string, - record: GraphRecord, + record: StatementRecord, ) => { let template; @@ -145,10 +146,10 @@ const generateStatementSentence = ( // detect the condition substitutions that are present const replacementsFound: string[] = []; - const conditionsUsed: string[] = []; + const conditionsUsed: GraphRecordId[] = []; const substitutions: Record = {}; const highlighted: string[] = []; - const conditions = (record.conditions || []).map((rec, index) => ({ '@rid': `${index}`, ...rec })); + const conditions = (record.conditions || []).map((rec) => ({ ...rec })); for (const key of Object.values(keys)) { if (template.includes(key)) { diff --git a/src/types.ts b/src/types.ts index f9fcc1e..4e1da7e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { GraphRecordId } from './constants'; + export interface Expose { /** create the GET route */ QUERY: boolean; @@ -84,6 +86,32 @@ export type ClassPermissions = Partial > & { default?: export type UserGroupPermissions = Record>>; +export interface GraphRecord { + [key: string]: unknown; + sourceId?: string; + name?: string; + source?: string | GraphRecord; + displayName?: string; + '@rid': GraphRecordId; + '@class'?: string; +} + +interface OntologyRecord extends GraphRecord { + displayName: string; + '@class': string; +} + +interface PartialOntologyRecord extends GraphRecord { + displayName: string; +} + +export interface StatementRecord extends Partial { + conditions: OntologyRecord[]; + evidence?: PartialOntologyRecord[]; + subject: OntologyRecord; + relevance: PartialOntologyRecord; +} + export interface PropertyDefinition { /** the function to be used in formatting values for this property (for list properties it is the function for elements in the list) */ readonly cast?: (value: any) => unknown diff --git a/src/util.ts b/src/util.ts index 6f6e57b..c59e06c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -62,7 +62,9 @@ const looksLikeRID = (rid: string, requireHash = false): boolean => { * @param string the input object * @returns {String} the record ID */ -const castToRID = (value: constants.GraphRecordId | GraphRecord | null): constants.GraphRecordId => { +const castToRID = ( + value: constants.GraphRecordId | Partial | null, +): constants.GraphRecordId => { if (value == null) { throw new ValidationError('cannot cast null/undefined to RID'); } @@ -153,13 +155,15 @@ const uppercase = (x): string => x.toString().trim().toUpperCase(); const displayOntology = ({ name = '', sourceId = '', source = '', -}: GraphRecord) => { +}: Partial): string => { if (!sourceId) { return name; } if (!name && /^\d+$/.exec(sourceId)) { - return `${source?.displayName || source}:${sourceId}`; + return `${typeof source !== 'string' + ? source.displayName + : source}:${sourceId}`; } if (sourceId === name) { return sourceId; diff --git a/test/sentenceTemplates.test.ts b/test/sentenceTemplates.test.ts index cdd2e7d..e0fbaa9 100644 --- a/test/sentenceTemplates.test.ts +++ b/test/sentenceTemplates.test.ts @@ -302,10 +302,10 @@ describe('generateStatementSentence', () => { '@class': 'Statement', '@rid': '22:0', displayNameTemplate: 'Given {conditions} {relevance} applies to {subject} ({evidence})', - relevance: { displayName: 'Mood Swings' }, - conditions: [{ displayName: 'Low blood sugar', class: 'Disease' }], - subject: { displayName: 'hungertitis' }, - evidence: [{ displayName: 'A reputable source' }], + relevance: { displayName: 'Mood Swings', '@rid': '1' }, + conditions: [{ displayName: 'Low blood sugar', '@class': 'Disease', '@rid': '2' }], + subject: { displayName: 'hungertitis', '@rid': '3', '@class': 'Disease' }, + evidence: [{ displayName: 'A reputable source', '@rid': '4' }], }; const { content } = generateStatementSentence(previewFunction, statement); diff --git a/test/util.test.ts b/test/util.test.ts index 4cda1b8..35b7744 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -119,7 +119,7 @@ describe('displayOntology', () => { }); test('uses source if given with sourceId number', () => { - expect(util.displayOntology({ sourceId: '1234', source: { displayName: 'pmid' } })).toBe('pmid:1234'); + expect(util.displayOntology({ sourceId: '1234', source: { displayName: 'pmid', '@rid': '1' } })).toBe('pmid:1234'); }); }); From a819ac85d6fe59c39677c225345baf0cce44ac8f Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 14:11:57 -0700 Subject: [PATCH 22/37] Rename min/max properties to match json-schema/openapi-spec names --- src/definitions/index.ts | 2 +- src/definitions/position.ts | 16 ++++++++-------- src/property.ts | 10 +++++----- src/types.ts | 4 ++-- test/property.test.ts | 8 ++++---- test/util.test.ts | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 0f22f50..e7d30b4 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -155,7 +155,7 @@ const initializeSchema = ( for (const name of Object.keys(inputSchema)) { if (name !== 'Permissions' && !inputSchema[name].embedded) { permissionsProperties.push({ - min: PERMISSIONS.NONE, max: PERMISSIONS.ALL, type: 'integer', nullable: false, readOnly: false, name, + minimum: PERMISSIONS.NONE, maximum: PERMISSIONS.ALL, type: 'integer', nullable: false, readOnly: false, name, }); } } diff --git a/src/definitions/position.ts b/src/definitions/position.ts index 23310cf..58a285f 100644 --- a/src/definitions/position.ts +++ b/src/definitions/position.ts @@ -19,7 +19,7 @@ const models: PartialSchemaDefn = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [12], nullable: true, description: 'The Amino Acid number', + name: 'pos', type: 'integer', minimum: 1, mandatory: true, examples: [12], nullable: true, description: 'The Amino Acid number', }, { name: 'refAA', type: 'string', cast: util.uppercase, examples: ['G'], pattern: '^[A-Z*?]$', description: 'The reference Amino Acid (single letter notation)', @@ -35,10 +35,10 @@ const models: PartialSchemaDefn = { name: 'arm', mandatory: true, nullable: false, choices: ['p', 'q'], }, { - name: 'majorBand', type: 'integer', min: 1, examples: ['11'], + name: 'majorBand', type: 'integer', minimum: 1, examples: ['11'], }, { - name: 'minorBand', type: 'integer', min: 1, examples: ['1'], + name: 'minorBand', type: 'integer', minimum: 1, examples: ['1'], }, ], }, @@ -47,7 +47,7 @@ const models: PartialSchemaDefn = { inherits: ['Position'], embedded: true, properties: [{ - name: 'pos', type: 'integer', min: 1, mandatory: true, nullable: true, description: 'The genomic/nucleotide number', + name: 'pos', type: 'integer', minimum: 1, mandatory: true, nullable: true, description: 'The genomic/nucleotide number', }], }, ExonicPosition: { @@ -55,7 +55,7 @@ const models: PartialSchemaDefn = { inherits: ['Position'], embedded: true, properties: [{ - name: 'pos', type: 'integer', min: 1, mandatory: true, nullable: true, description: 'The exon number', + name: 'pos', type: 'integer', minimum: 1, mandatory: true, nullable: true, description: 'The exon number', }], }, IntronicPosition: { @@ -63,7 +63,7 @@ const models: PartialSchemaDefn = { inherits: ['Position'], embedded: true, properties: [{ - name: 'pos', type: 'integer', min: 1, mandatory: true, nullable: true, + name: 'pos', type: 'integer', minimum: 1, mandatory: true, nullable: true, }], }, CdsPosition: { @@ -85,7 +85,7 @@ const models: PartialSchemaDefn = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [55], nullable: true, + name: 'pos', type: 'integer', minimum: 1, mandatory: true, examples: [55], nullable: true, }, { name: 'offset', type: 'integer', examples: [-11], description: 'distance from the nearest exon boundary (pos)', @@ -100,7 +100,7 @@ const models: PartialSchemaDefn = { embedded: true, properties: [ { - name: 'pos', type: 'integer', min: 1, mandatory: true, examples: [55], nullable: true, + name: 'pos', type: 'integer', minimum: 1, mandatory: true, examples: [55], nullable: true, }, { name: 'offset', type: 'integer', examples: [-11], description: 'distance from the nearest cds exon boundary', diff --git a/src/property.ts b/src/property.ts index cc26951..30f682f 100644 --- a/src/property.ts +++ b/src/property.ts @@ -58,15 +58,15 @@ export const validateProperty = (prop: PropertyDefinition, inputValue: unknown): }); } if (castValue !== null) { - if (prop.min !== undefined && prop.min !== null && castValue < prop.min) { + if (prop.minimum !== undefined && prop.minimum !== null && castValue < prop.minimum) { throw new ValidationError({ - message: `Violated the minimum value constraint of ${prop.name} (${castValue} < ${prop.min})`, + message: `Violated the minimum value constraint of ${prop.name} (${castValue} < ${prop.minimum})`, field: prop.name, }); } - if (prop.max !== undefined && prop.max !== null && castValue > prop.max) { + if (prop.maximum !== undefined && prop.maximum !== null && castValue > prop.maximum) { throw new ValidationError({ - message: `Violated the maximum value constraint of ${prop.name} (${castValue} > ${prop.max})`, + message: `Violated the maximum value constraint of ${prop.name} (${castValue} > ${prop.maximum})`, field: prop.name, }); } @@ -118,7 +118,7 @@ export const createPropertyDefinition = (opt: PropertyDefinitionInput): Property type: inputType, ...rest } = opt; - const defaultType = ((opt.min !== undefined || opt.max !== undefined) && !opt.type) + const defaultType = ((opt.minimum !== undefined || opt.maximum !== undefined) && !opt.type) ? 'integer' : 'string'; const type: DbType = inputType || defaultType; diff --git a/src/types.ts b/src/types.ts index 4e1da7e..22810ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,10 +140,10 @@ export interface PropertyDefinition { /** indicates if this is a required property */ readonly mandatory?: boolean /** maximum value allowed (for integer type properties) */ - readonly max?: number + readonly maximum?: number readonly maxItems?: number; /** minimum value allowed (for integer type properties) */ - readonly min?: number + readonly minimum?: number readonly minItems?: number; readonly name: string; /** for string properties indicates that an empty string is invalid */ diff --git a/test/property.test.ts b/test/property.test.ts index 1a1d1cd..f2a82ff 100644 --- a/test/property.test.ts +++ b/test/property.test.ts @@ -51,10 +51,10 @@ describe('validate', () => { expect(validateProperty(prop2, 'blargh')).toBe('blargh'); }); - test('min', () => { + test('minimum', () => { const prop = createPropertyDefinition({ name: 'example', - min: -1, + minimum: -1, type: 'integer', }); expect(validateProperty(prop, '1')).toBe(1); @@ -104,10 +104,10 @@ describe('validate', () => { expect(() => validateProperty(prop, '2')).toThrowError('Violated check constraint of example (checkIsOne)'); }); - test('max', () => { + test('maximum', () => { const prop = createPropertyDefinition({ name: 'example', - max: 10, + maximum: 10, type: 'integer', }); expect(validateProperty(prop, '1')).toBe(1); diff --git a/test/util.test.ts b/test/util.test.ts index 35b7744..90b24c5 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -99,7 +99,7 @@ describe('looksLikeRID', () => { expect(util.looksLikeRID('#-4:0')).toBe(true); }); - test('enforces max cluster value', () => { + test('enforces maximum cluster value', () => { expect(util.looksLikeRID('#32767:-0')).toBe(true); expect(util.looksLikeRID('#32768:-0')).toBe(false); }); From a564ee1ccccf8e4388e656638ff01d3f1d14a25e Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 14 Apr 2022 14:26:23 -0700 Subject: [PATCH 23/37] Add missing semi-colons --- src/types.ts | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/types.ts b/src/types.ts index 22810ca..160c6a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -114,72 +114,74 @@ export interface StatementRecord extends Partial { export interface PropertyDefinition { /** the function to be used in formatting values for this property (for list properties it is the function for elements in the list) */ - readonly cast?: (value: any) => unknown + readonly cast?: (value: any) => unknown; readonly check?: (rec?: unknown) => boolean; /** enum representing acceptable values */ - readonly choices?: unknown[] + readonly choices?: unknown[]; readonly default?: unknown; /** description for the openapi spec */ - readonly description?: string + readonly description?: string; /** example values to use for help text */ - readonly examples?: unknown[] + readonly examples?: unknown[]; readonly format?: 'date'; /** indicates if this field has a fulltext index */ - readonly fulltextIndexed: boolean + readonly fulltextIndexed: boolean; /** indicates if this property is a generated value and not expected to be input from a user */ - readonly generated?: boolean + readonly generated?: boolean; readonly generateDefault?: (rec?: any) => unknown; /** indicates that a field should be generated after all other processing is complete b/c it requires other fields */ - readonly generationDependencies?: boolean + readonly generationDependencies?: boolean; /** indicates if this field is exact indexed for quick search */ - readonly indexed: boolean + readonly indexed: boolean; readonly iterable: boolean; /** if applicable, the class this link should point to or embed */ - readonly linkedClass?: string + readonly linkedClass?: string; readonly linkedType?: string; /** indicates if this is a required property */ - readonly mandatory?: boolean + readonly mandatory?: boolean; /** maximum value allowed (for integer type properties) */ - readonly maximum?: number + readonly maximum?: number; readonly maxItems?: number; /** minimum value allowed (for integer type properties) */ - readonly minimum?: number + readonly minimum?: number; readonly minItems?: number; readonly name: string; /** for string properties indicates that an empty string is invalid */ - readonly nonEmpty?: boolean + readonly nonEmpty?: boolean; /** flag to indicate if the value can be null */ - readonly nullable?: boolean + readonly nullable?: boolean; /** a regex pattern that values for this property can be restricted by */ - readonly pattern?: string + readonly pattern?: string; readonly readOnly?: boolean; /** the database type of this property */ - readonly type: DbType + readonly type: DbType; } export interface ClassDefinition { readonly description: string; /** indicates if this is an embedded class (cannot be searched/created directly) */ - readonly embedded: boolean + readonly embedded: boolean; readonly indices: IndexType[]; /** classes which this inherits all properties and indices from */ - readonly inherits: string[] + readonly inherits: string[]; /** indicates if this is an abstract (in the database) class */ - readonly isAbstract: boolean + readonly isAbstract: boolean; /** indicates if this is an edge type class which should inherit from the base edge class E */ - readonly isEdge: boolean + readonly isEdge: boolean; /** name of this class */ - readonly name: string + readonly name: string; readonly permissions: ClassPermissions; /** mapping of property name to definition */ - readonly properties: Record + readonly properties: Record; readonly reverseName?: string; /** the name used for the REST route of this class in the API */ - readonly routeName: string + readonly routeName: string; /** the routes to expose on the API for this class */ readonly routes: any - readonly sourceModel?: VertexName;// the model edges outgoing vertices are restricted to - readonly targetModel?: VertexName;// the model edges incoming vertices are restricted to + /** the model edges outgoing vertices are restricted to */ + readonly sourceModel?: VertexName; + /** the model edges incoming vertices are restricted to */ + readonly targetModel?: VertexName; } export interface PropertyDefinitionInput extends Partial> { From 3c4c88da54ff3b9d6e0e13efc2fb993314c49e55 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Mon, 25 Apr 2022 13:54:19 -0700 Subject: [PATCH 24/37] Be more explicit about routes type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 160c6a3..bd6d3ce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -177,7 +177,7 @@ export interface ClassDefinition { /** the name used for the REST route of this class in the API */ readonly routeName: string; /** the routes to expose on the API for this class */ - readonly routes: any + readonly routes: Expose /** the model edges outgoing vertices are restricted to */ readonly sourceModel?: VertexName; /** the model edges incoming vertices are restricted to */ From 6d038b766864609913deeceb61f25d1520fd6c67 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 3 May 2022 15:15:59 -0700 Subject: [PATCH 25/37] Bump version to 4.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e830cb..cecaed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bcgsc-pori/graphkb-schema", - "version": "3.16.0", + "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index b5ef174..aa4bd3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bcgsc-pori/graphkb-schema", - "version": "3.16.0", + "version": "4.0.0", "description": "Shared package between the API and GUI for GraphKB which holds the schema definitions and schema-related functions", "bugs": { "email": "graphkb@bcgsc.ca" From 997b1703cbab949156f5d7f369b9496b3894cb4b Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 3 May 2022 15:17:51 -0700 Subject: [PATCH 26/37] Add another note on migrating --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 981c105..0cc1686 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,5 @@ To facilitate more reuseable typing schemes ClassModel and Property classes have | `inheritsProperty(propName: string)` | `inheritsProperty(modelName: string, propName: string)` | | | `toJSON` | N/A | | | `formatRecord(record: GraphRecord, opt = {})` | `formatRecord(modelName: string, record: GraphRecord, opt = {})` | | + +PropertyDefinition objects also now store the class name under linkedClass as a string rather than a reference to the actual ClassDefinition instance From 79e59d74b0e0181184081d9670d100ae9cc96e55 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 3 May 2022 17:47:21 -0700 Subject: [PATCH 27/37] Add method for validating property values --- src/schema.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/schema.ts b/src/schema.ts index 3ba049c..80428d8 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -330,6 +330,11 @@ class SchemaDefinition { return false; } + validate(modelName: string, propName: string, inputValue: unknown): unknown { + const prop = this.getProperty(modelName, propName); + return validateProperty(prop, inputValue); + } + /** * Checks a single record to ensure it matches the expected pattern for this class model, returns a copy of the record with validated/cast properties * From cb32de2b546c3341d0df3c070d123d794b09d2e2 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Tue, 3 May 2022 17:53:53 -0700 Subject: [PATCH 28/37] Add docstring --- src/schema.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/schema.ts b/src/schema.ts index 80428d8..bd834ad 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -330,6 +330,15 @@ class SchemaDefinition { return false; } + /** + * cast/format a value based on a property definition + * + * @param modelName The name of the class/model + * @param propName the name of the property on this class/model + * @param inputValue the raw unprocessed value + * @returns the value re-formatted based on the property definition and cast to an appropriate form where possible. + * @throws error on uncastable or invalid value for this property type + */ validate(modelName: string, propName: string, inputValue: unknown): unknown { const prop = this.getProperty(modelName, propName); return validateProperty(prop, inputValue); From 2b047ba09ba7e4cc6bf352b20ab35317f2b52d54 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Wed, 4 May 2022 15:17:29 -0700 Subject: [PATCH 29/37] Add IndexType to exports --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index e06ef43..5a492f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { DbType, ClassPermissions, GraphRecord, + IndexType, } from './types'; import * as util from './util'; import { ValidationError, ErrorMixin } from './error'; @@ -29,6 +30,7 @@ export type { DbType, ClassPermissions, GraphRecord, + IndexType, }; export { From 73f50b7dc83ff0ab1f2e7b049c51b91014979732 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Wed, 4 May 2022 17:22:19 -0700 Subject: [PATCH 30/37] Add validateProperty export --- README.md | 29 ++++++++++++++--------------- src/index.ts | 2 ++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0cc1686..a9dcde3 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,17 @@ constants.RID = RID; // IMPORTANT: Without this all castToRID will do is convert To facilitate more reuseable typing schemes ClassModel and Property classes have been removed and now are simply objects. All interactions with these models should go through the schema class instead of interacting directly with the model and property objects. Return types are given only when they differ. -| v3 (ClassModel methods) | v4 equivalent (SchemaDefinition methods) | Notes | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| `properties` | `getProperties(modelName: string)` | | -| `required` | `requiredProperties(modelName: string)` | | -| `optional` | `optionalProperties(modelName: string)` | | -| `getActiveProperties()` | `activeProperties(modelName: string)` | | -| `inherits` | `ancestors(modelName: string)` | | -| `subclasses: ClassModel[]` | `children(modelName: string): string[]` | | -| `descendantTree(boolean): ClassModel[]` | `descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` | must be called with includeSelf=true to match v3 edition | -| `queryProperties: Property[]` | `queryableProperties(modelName: string): PropertyDefinition[]` | | -| `inheritsProperty(propName: string)` | `inheritsProperty(modelName: string, propName: string)` | | -| `toJSON` | N/A | | -| `formatRecord(record: GraphRecord, opt = {})` | `formatRecord(modelName: string, record: GraphRecord, opt = {})` | | - -PropertyDefinition objects also now store the class name under linkedClass as a string rather than a reference to the actual ClassDefinition instance +| v3 | v4 equivalent | Notes | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| `ClassModel.properties` | `SchemaDefinition.getProperties(modelName: string)` | | +| `ClassModel.required` | `SchemaDefinition.requiredProperties(modelName: string)` | | +| `ClassModel.optional` | `SchemaDefinition.optionalProperties(modelName: string)` | | +| `ClassModel.getActiveProperties()` | `SchemaDefinition.activeProperties(modelName: string)` | | +| `ClassModel.inherits` | `SchemaDefinition.ancestors(modelName: string)` | | +| `ClassModel.subclasses: ClassModel[]` | `SchemaDefinition.children(modelName: string): string[]` | | +| `ClassModel.descendantTree(boolean): ClassModel[]` | `SchemaDefinition.descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` | must be called with includeSelf=true to match v3 edition | +| `ClassModel.queryProperties: Property[]` | `SchemaDefinition.queryableProperties(modelName: string): PropertyDefinition[]` | | +| `ClassModel.inheritsProperty(propName: string)` | `SchemaDefinition.inheritsProperty(modelName: string, propName: string)` | | +| `ClassModel.toJSON` | N/A | | +| `ClassModel.formatRecord(record: GraphRecord, opt = {})` | `SchemaDefinition.formatRecord(modelName: string, record: GraphRecord, opt = {})` | | +| `Property.validate(inputValue: unknown): unknown` | `validateProperty = (prop: PropertyDefinition, inputValue: unknown): unknown` | | diff --git a/src/index.ts b/src/index.ts index 5a492f6..9d1a56b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { GraphRecord, IndexType, } from './types'; +import { validateProperty } from './property'; import * as util from './util'; import { ValidationError, ErrorMixin } from './error'; import * as constants from './constants'; @@ -43,4 +44,5 @@ export { PERMISSIONS, sentenceTemplates, SchemaDefinition, + validateProperty, }; From 83f6a380f6a53ec6d780d541a494174b49eebf28 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Wed, 4 May 2022 17:23:24 -0700 Subject: [PATCH 31/37] Fix migration guide typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9dcde3..75e9e23 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ To facilitate more reuseable typing schemes ClassModel and Property classes have | `ClassModel.inherits` | `SchemaDefinition.ancestors(modelName: string)` | | | `ClassModel.subclasses: ClassModel[]` | `SchemaDefinition.children(modelName: string): string[]` | | | `ClassModel.descendantTree(boolean): ClassModel[]` | `SchemaDefinition.descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` | must be called with includeSelf=true to match v3 edition | -| `ClassModel.queryProperties: Property[]` | `SchemaDefinition.queryableProperties(modelName: string): PropertyDefinition[]` | | +| `ClassModel.queryProperties: Property[]` | `SchemaDefinition.queryableProperties(modelName: string): Record` | | | `ClassModel.inheritsProperty(propName: string)` | `SchemaDefinition.inheritsProperty(modelName: string, propName: string)` | | | `ClassModel.toJSON` | N/A | | | `ClassModel.formatRecord(record: GraphRecord, opt = {})` | `SchemaDefinition.formatRecord(modelName: string, record: GraphRecord, opt = {})` | | From a710e41f1b6bc05522535b5508461941e6185769 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Thu, 5 May 2022 11:31:26 -0700 Subject: [PATCH 32/37] Update migration instructions --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 75e9e23..a88de62 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,21 @@ constants.RID = RID; // IMPORTANT: Without this all castToRID will do is convert To facilitate more reuseable typing schemes ClassModel and Property classes have been removed and now are simply objects. All interactions with these models should go through the schema class instead of interacting directly with the model and property objects. Return types are given only when they differ. -| v3 | v4 equivalent | Notes | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| `ClassModel.properties` | `SchemaDefinition.getProperties(modelName: string)` | | -| `ClassModel.required` | `SchemaDefinition.requiredProperties(modelName: string)` | | -| `ClassModel.optional` | `SchemaDefinition.optionalProperties(modelName: string)` | | -| `ClassModel.getActiveProperties()` | `SchemaDefinition.activeProperties(modelName: string)` | | -| `ClassModel.inherits` | `SchemaDefinition.ancestors(modelName: string)` | | -| `ClassModel.subclasses: ClassModel[]` | `SchemaDefinition.children(modelName: string): string[]` | | -| `ClassModel.descendantTree(boolean): ClassModel[]` | `SchemaDefinition.descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` | must be called with includeSelf=true to match v3 edition | -| `ClassModel.queryProperties: Property[]` | `SchemaDefinition.queryableProperties(modelName: string): Record` | | -| `ClassModel.inheritsProperty(propName: string)` | `SchemaDefinition.inheritsProperty(modelName: string, propName: string)` | | -| `ClassModel.toJSON` | N/A | | -| `ClassModel.formatRecord(record: GraphRecord, opt = {})` | `SchemaDefinition.formatRecord(modelName: string, record: GraphRecord, opt = {})` | | -| `Property.validate(inputValue: unknown): unknown` | `validateProperty = (prop: PropertyDefinition, inputValue: unknown): unknown` | | +| v3 | v4 equivalent | +| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `ClassModel._properties` | `ClassDefinition.properties` | +| `ClassModel.properties` | `SchemaDefinition.getProperties(modelName: string)` | +| `ClassModel.required` | `SchemaDefinition.requiredProperties(modelName: string)` | +| `ClassModel.optional` | `SchemaDefinition.optionalProperties(modelName: string)` | +| `ClassModel.getActiveProperties()` | `SchemaDefinition.activeProperties(modelName: string)` | +| `ClassModel.inherits` | `SchemaDefinition.ancestors(modelName: string)` | +| `ClassModel.subclasses: ClassModel[]` | `SchemaDefinition.children(modelName: string): string[]` | +| `ClassModel.descendantTree(excludeAbstract: boolean): ClassModel[]` | `SchemaDefinition.descendants(modelName: string, opt: { excludeAbstract?: boolean, includeSelf?: boolean }): string[]` [^1] | +| `ClassModel.queryProperties: Record` | `SchemaDefinition.queryableProperties(modelName: string): Record` | +| `ClassModel.inheritsProperty(propName: string)` | `SchemaDefinition.inheritsProperty(modelName: string, propName: string)` | +| `ClassModel.toJSON` | N/A [^2] | +| `ClassModel.formatRecord(record: GraphRecord, opt = {})` | `SchemaDefinition.formatRecord(modelName: string, record: GraphRecord, opt = {})` | +| `Property.validate(inputValue: unknown): unknown` | `validateProperty = (prop: PropertyDefinition, inputValue: unknown): unknown` | + +[^1]: must be called with includeSelf=true to match v3 edition +[^2]: There is no need for this function now since the ClassDefinition object is effectively already a JSON object From 9a71d780c26a5f4dfc93f7158f515ab2fb8ee078 Mon Sep 17 00:00:00 2001 From: Mathieu Lemieux Date: Thu, 5 May 2022 13:32:20 -0700 Subject: [PATCH 33/37] Remove isemail package --- package-lock.json | 26 ++++---------------------- package.json | 1 - 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index cecaed6..37120e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,9 @@ "packages": { "": { "name": "@bcgsc-pori/graphkb-schema", - "version": "3.16.0", + "version": "4.0.0", "license": "GPL-3.0", "dependencies": { - "isemail": "^3.2.0", "lodash.omit": "4.5.0", "typescript": "^4.5.5", "uuid": "3.3.2", @@ -7187,17 +7186,6 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "node_modules/isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "dependencies": { - "punycode": "2.x.x" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -15041,6 +15029,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } @@ -22501,14 +22490,6 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "isemail": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", - "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", - "requires": { - "punycode": "2.x.x" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -28611,7 +28592,8 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "qs": { "version": "6.5.2", diff --git a/package.json b/package.json index aa4bd3d..29c3bab 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@bcgsc-pori/graphkb-parser": ">=2.0.0 <3.0.0" }, "dependencies": { - "isemail": "^3.2.0", "lodash.omit": "4.5.0", "typescript": "^4.5.5", "uuid": "3.3.2", From 6668956ae8cf6d3b13b1e509d2ddd4d78310c6cd Mon Sep 17 00:00:00 2001 From: Mathieu Lemieux Date: Thu, 5 May 2022 13:33:15 -0700 Subject: [PATCH 34/37] Install email validator package --- package-lock.json | 16 +++++++++++++++- package.json | 3 ++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37120e2..d25a1c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "lodash.omit": "4.5.0", "typescript": "^4.5.5", "uuid": "3.3.2", - "uuid-validate": "0.0.3" + "uuid-validate": "0.0.3", + "validator": "^13.7.0" }, "devDependencies": { "@types/jest": "^27.4.0", @@ -16701,6 +16702,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -29879,6 +29888,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index 29c3bab..d032fd6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "lodash.omit": "4.5.0", "typescript": "^4.5.5", "uuid": "3.3.2", - "uuid-validate": "0.0.3" + "uuid-validate": "0.0.3", + "validator": "^13.7.0" }, "devDependencies": { "@types/jest": "^27.4.0", From 35cb3cc40d3b3c5c2ec1e407290c924d48b41dd0 Mon Sep 17 00:00:00 2001 From: Mathieu Lemieux Date: Thu, 5 May 2022 13:42:25 -0700 Subject: [PATCH 35/37] Use new email validator --- src/definitions/user.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/definitions/user.ts b/src/definitions/user.ts index abd8a7d..507b2a9 100644 --- a/src/definitions/user.ts +++ b/src/definitions/user.ts @@ -1,4 +1,4 @@ -import isEmail from 'isemail'; +import isEmail from 'validator/lib/isEmail'; import * as util from '../util'; import { ValidationError } from '../error'; @@ -25,7 +25,7 @@ const models: PartialSchemaDefn = { name: 'email', description: 'the email address to contact this user at', cast: (email) => { - if (typeof email !== 'string' || !isEmail.validate(email)) { + if (typeof email !== 'string' || !isEmail(email)) { throw new ValidationError(`Email (${email}) does not look like a valid email address`); } return email; From 0c9873bb351d152dfdf0d7ef37a0c177dad121d7 Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Sat, 7 May 2022 13:49:50 -0700 Subject: [PATCH 36/37] Fix: revert changes to getPreview function --- src/schema.ts | 3 +++ test/schema.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index bd834ad..2c45981 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -100,6 +100,9 @@ class SchemaDefinition { if (obj.name) { return obj.name; } + if (obj['@class']) { + return obj['@class']; + } if (obj['@rid']) { return `${obj['@rid']}`; } diff --git a/test/schema.test.ts b/test/schema.test.ts index d3dad1c..554e6fb 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -367,14 +367,51 @@ describe('queryableProperties', () => { test.each([ ['blargh', { displayName: 'blargh', name: 'monkeys' }], ['blargh', { name: 'blargh' }], - ['DNMT3A:p.R882 predicts unfavourable prognosis in acute myeloid leukemia [DOID:9119]', { ...examples['subject:null|conditions:Disease;PositionalVariant|relevance:unfavourable prognosis'], '@class': 'Statement' }], + ['DNMT3A:p.R882 predicts unfavourable prognosis in acute myeloid leukemia [DOID:9119] ({evidence})', { ...examples['subject:null|conditions:Disease;PositionalVariant|relevance:unfavourable prognosis'], '@class': 'Statement' }], ['#3:4', { '@rid': '#3:4' }], ['3', [1, 2, 3]], ['blargh', { target: { name: 'blargh' } }], - ['blargh', 'blargh'] + ['blargh', 'blargh'], + [ + 'User', + { + '@rid': '20:0', + '@class': 'User', + createdBy: 'Mom', + deletedBy: 'Mom', + }, + ], + [ + '22:0', + { + '@rid': '22:0', + }, + ], + [ + 'Disease', + { + '@class': 'Disease', + }, + ], + [ + 'Given Low blood sugar Mood Swings applies to hungertitis (A reputable source)', + { + displayName: 'displayName', + '@class': 'Statement', + '@rid': '22:0', + displayNameTemplate: 'Given {conditions} {relevance} applies to {subject} ({evidence})', + relevance: { displayName: 'Mood Swings', '@rid': '1:2' }, + conditions: [{ displayName: 'Low blood sugar', '@rid': '1:3' }], + subject: { displayName: 'hungertitis', '@rid': '1:4' }, + evidence: [{ displayName: 'A reputable source', '@rid': '1:5' }], + }, + ], + [ + '#19:0', '#19:0', + ], ])('getPreview %s', (preview, input) => { // @ts-ignore testing bad input in some cases - expect(schema.getPreview(input)).not.toHaveProperty(preview); + expect(schema.getPreview(input)).toEqual(preview); }); From 6c2a2988ebfe19cd3ce98b4e39579a92c14f123a Mon Sep 17 00:00:00 2001 From: Caralyn Reisle Date: Mon, 9 May 2022 12:43:34 -0700 Subject: [PATCH 37/37] Do not use readonly on properties directly --- src/schema.ts | 13 +++++---- src/types.ts | 80 +++++++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 2c45981..aff6e85 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -6,21 +6,21 @@ import { validateProperty } from './property'; import * as sentenceTemplates from './sentenceTemplates'; class SchemaDefinition { - readonly models: Record; - readonly normalizedModelNames: Record; - readonly subclassMapping: Record; + readonly models: Readonly>>; + readonly normalizedModelNames: Readonly>>; + readonly subclassMapping: Readonly>; constructor(models: Record) { this.models = models; - this.normalizedModelNames = {}; + const normalizedModelNames = {}; const subclassMapping: Record = {}; Object.keys(this.models).forEach((name) => { const model = this.models[name]; - this.normalizedModelNames[name.toLowerCase()] = model; + normalizedModelNames[name.toLowerCase()] = model; if (model.reverseName) { - this.normalizedModelNames[model.reverseName.toLowerCase()] = model; + normalizedModelNames[model.reverseName.toLowerCase()] = model; } model.inherits.forEach((parent) => { if (subclassMapping[parent] === undefined) { @@ -30,6 +30,7 @@ class SchemaDefinition { }); }); this.subclassMapping = subclassMapping; + this.normalizedModelNames = normalizedModelNames; } /** diff --git a/src/types.ts b/src/types.ts index bd6d3ce..72ac1de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -114,74 +114,74 @@ export interface StatementRecord extends Partial { export interface PropertyDefinition { /** the function to be used in formatting values for this property (for list properties it is the function for elements in the list) */ - readonly cast?: (value: any) => unknown; - readonly check?: (rec?: unknown) => boolean; + cast?: (value: any) => unknown; + check?: (rec?: unknown) => boolean; /** enum representing acceptable values */ - readonly choices?: unknown[]; - readonly default?: unknown; + choices?: unknown[]; + default?: unknown; /** description for the openapi spec */ - readonly description?: string; + description?: string; /** example values to use for help text */ - readonly examples?: unknown[]; - readonly format?: 'date'; + examples?: unknown[]; + format?: 'date'; /** indicates if this field has a fulltext index */ - readonly fulltextIndexed: boolean; + fulltextIndexed: boolean; /** indicates if this property is a generated value and not expected to be input from a user */ - readonly generated?: boolean; - readonly generateDefault?: (rec?: any) => unknown; + generated?: boolean; + generateDefault?: (rec?: any) => unknown; /** indicates that a field should be generated after all other processing is complete b/c it requires other fields */ - readonly generationDependencies?: boolean; + generationDependencies?: boolean; /** indicates if this field is exact indexed for quick search */ - readonly indexed: boolean; - readonly iterable: boolean; + indexed: boolean; + iterable: boolean; /** if applicable, the class this link should point to or embed */ - readonly linkedClass?: string; - readonly linkedType?: string; + linkedClass?: string; + linkedType?: string; /** indicates if this is a required property */ - readonly mandatory?: boolean; + mandatory?: boolean; /** maximum value allowed (for integer type properties) */ - readonly maximum?: number; - readonly maxItems?: number; + maximum?: number; + maxItems?: number; /** minimum value allowed (for integer type properties) */ - readonly minimum?: number; - readonly minItems?: number; - readonly name: string; + minimum?: number; + minItems?: number; + name: string; /** for string properties indicates that an empty string is invalid */ - readonly nonEmpty?: boolean; + nonEmpty?: boolean; /** flag to indicate if the value can be null */ - readonly nullable?: boolean; + nullable?: boolean; /** a regex pattern that values for this property can be restricted by */ - readonly pattern?: string; - readonly readOnly?: boolean; + pattern?: string; + readOnly?: boolean; /** the database type of this property */ - readonly type: DbType; + type: DbType; } export interface ClassDefinition { - readonly description: string; + description: string; /** indicates if this is an embedded class (cannot be searched/created directly) */ - readonly embedded: boolean; - readonly indices: IndexType[]; + embedded: boolean; + indices: IndexType[]; /** classes which this inherits all properties and indices from */ - readonly inherits: string[]; + inherits: string[]; /** indicates if this is an abstract (in the database) class */ - readonly isAbstract: boolean; + isAbstract: boolean; /** indicates if this is an edge type class which should inherit from the base edge class E */ - readonly isEdge: boolean; + isEdge: boolean; /** name of this class */ - readonly name: string; - readonly permissions: ClassPermissions; + name: string; + permissions: ClassPermissions; /** mapping of property name to definition */ - readonly properties: Record; - readonly reverseName?: string; + properties: Readonly>>; + reverseName?: string; /** the name used for the REST route of this class in the API */ - readonly routeName: string; + routeName: string; /** the routes to expose on the API for this class */ - readonly routes: Expose + routes: Expose /** the model edges outgoing vertices are restricted to */ - readonly sourceModel?: VertexName; + sourceModel?: VertexName; /** the model edges incoming vertices are restricted to */ - readonly targetModel?: VertexName; + targetModel?: VertexName; } export interface PropertyDefinitionInput extends Partial> {