common-schema
Advanced tools
Comparing version 4.2.0 to 4.3.0
@@ -18,2 +18,3 @@ import { SchemaType } from './schema-type.js'; | ||
toJSONSchema(subschema: SubschemaType, schema: Schema): any; | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any; | ||
} | ||
@@ -35,3 +36,37 @@ export declare class SchemaTypeArray extends SchemaType { | ||
toJSONSchema(subschema: SubschemaType, schema: Schema): any; | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any; | ||
} | ||
/** | ||
* This schema type is a variant of an array that actually represents a set/map of unique items. | ||
* In paths, this is represented as a string key (matching the value) instead of an array index. | ||
* | ||
* Schemas look like this: | ||
* { | ||
* type: 'arrayset', | ||
* elements: <Subschema for Elements>, | ||
* keyField: <Subfield within elements to get unique key; defaults to whole element> | ||
* } | ||
*/ | ||
export declare class SchemaTypeArraySet extends SchemaType { | ||
constructor(name?: string); | ||
matchShorthandType(subschema: any): boolean; | ||
getFieldSubschemaPath(subschema: SubschemaType, field: string, schema: Schema): string; | ||
listSchemaSubfields(subschema: SubschemaType, schema: Schema): string[]; | ||
getFieldSubschema(subschema: SubschemaType, pathComponent: string, schema: Schema): any | undefined; | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined; | ||
_getElementKey(elementValue: any, subschema: SubschemaType, schema: Schema, throwOnUndefined?: boolean): string | undefined; | ||
listValueSubfields(value: any, subschema: SubschemaType, schema: Schema): string[]; | ||
listValueSubfieldEntries(value: any, subschema: SubschemaType, schema: Schema): [string, any][]; | ||
setValueSubfieldBatch(value: any, subschema: SubschemaType, newValues: { | ||
[subfield: string]: any; | ||
}, schema: Schema): void; | ||
getValueSubfield(value: any, subschema: SubschemaType, field: string, schema: Schema): any; | ||
setValueSubfield(value: any, subschema: SubschemaType, field: string, fieldValue: any, schema: Schema): void; | ||
normalizeSchema(subschema: any, schema: Schema): SubschemaType; | ||
validate(value: any, subschema: SubschemaType, field: string, options: ValidateOptions, schema: Schema): void; | ||
normalize(value: any, subschema: SubschemaType, field: string, options: NormalizeOptions, schema: Schema): any; | ||
checkTypeMatch(value: any, subschema: SubschemaType, schema: Schema): 0 | 1 | 2 | 3; | ||
toJSONSchema(subschema: SubschemaType, schema: Schema): any; | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any; | ||
} | ||
export declare class SchemaTypeMap extends SchemaType { | ||
@@ -41,2 +76,3 @@ constructor(name?: string); | ||
getFieldSubschema(subschema: SubschemaType, pathComponent: string, schema: Schema): any | undefined; | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined; | ||
getFieldSubschemaPath(subschema: SubschemaType, field: string, schema: Schema): string; | ||
@@ -52,2 +88,3 @@ listSchemaSubfields(subschema: SubschemaType, schema: Schema): string[]; | ||
toJSONSchema(subschema: SubschemaType, schema: Schema): any; | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any; | ||
} | ||
@@ -54,0 +91,0 @@ export declare class SchemaTypeOr extends SchemaType { |
@@ -176,2 +176,5 @@ import { SchemaType } from './schema-type.js'; | ||
} | ||
newEmptyContainer(valueTemplate, subschema, schema) { | ||
return {}; | ||
} | ||
} | ||
@@ -324,3 +327,188 @@ export class SchemaTypeArray extends SchemaType { | ||
} | ||
newEmptyContainer(valueTemplate, subschema, schema) { | ||
return []; | ||
} | ||
} | ||
/** | ||
* This schema type is a variant of an array that actually represents a set/map of unique items. | ||
* In paths, this is represented as a string key (matching the value) instead of an array index. | ||
* | ||
* Schemas look like this: | ||
* { | ||
* type: 'arrayset', | ||
* elements: <Subschema for Elements>, | ||
* keyField: <Subfield within elements to get unique key; defaults to whole element> | ||
* } | ||
*/ | ||
export class SchemaTypeArraySet extends SchemaType { | ||
constructor(name = 'arrayset') { | ||
super(name, true, false); | ||
} | ||
matchShorthandType(subschema) { | ||
return false; | ||
} | ||
getFieldSubschemaPath(subschema, field, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return 'keySchemas.' + field; | ||
} | ||
return 'elements'; | ||
} | ||
listSchemaSubfields(subschema, schema) { | ||
let r = ['$']; | ||
if (subschema.keySchemas) { | ||
r.push(...(Object.keys(subschema.keySchemas))); | ||
} | ||
return r; | ||
} | ||
getFieldSubschema(subschema, pathComponent, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[pathComponent]) { | ||
return subschema.keySchemas[pathComponent]; | ||
} | ||
return subschema.elements; | ||
} | ||
getFieldSubschemaForModify(subschema, field, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return subschema.keySchemas[field]; | ||
} | ||
else if (field === '$') { // generic array placeholder always returns generic elements schema | ||
return subschema.elements; | ||
} | ||
else { | ||
if (!subschema.keySchemas) { | ||
subschema.keySchemas = {}; | ||
} | ||
subschema.keySchemas[field] = objtools.deepCopy(subschema.elements); | ||
return subschema.keySchemas[field]; | ||
} | ||
} | ||
// Return a unique key string used to index the element based on keyField | ||
_getElementKey(elementValue, subschema, schema, throwOnUndefined = false) { | ||
const encodeValue = (v) => { | ||
if (v === undefined) | ||
return undefined; | ||
if (Array.isArray(v)) { | ||
return v.map(encodeValue).join('$'); | ||
} | ||
else if (objtools.isScalar(v)) { | ||
return String(v); | ||
} | ||
else { | ||
return objtools.objectHash(v); | ||
} | ||
}; | ||
let r; | ||
if (subschema.keyField && Array.isArray(subschema.keyField)) { | ||
r = encodeValue(subschema.keyField.map((f) => objtools.getPath(elementValue, f))); | ||
} | ||
else if (subschema.keyField) { | ||
r = encodeValue(objtools.getPath(elementValue, subschema.keyField)); | ||
} | ||
else { | ||
r = encodeValue(elementValue); | ||
} | ||
if (r === undefined && throwOnUndefined) { | ||
throw new SchemaError('ArraySet element key does not exist'); | ||
} | ||
return r; | ||
} | ||
listValueSubfields(value, subschema, schema) { | ||
let ret = []; | ||
if (Array.isArray(value)) { | ||
for (let el of value) { | ||
ret.push(this._getElementKey(el, subschema, schema, true)); | ||
} | ||
return _.uniq(ret); | ||
} | ||
return ret; | ||
} | ||
listValueSubfieldEntries(value, subschema, schema) { | ||
let retMap = new Map(); | ||
if (Array.isArray(value)) { | ||
for (let el of value) { | ||
retMap.set(this._getElementKey(el, subschema, schema, true), el); | ||
} | ||
let retAr = []; | ||
for (let [k, v] of retMap.entries()) { | ||
retAr.push([k, v]); | ||
} | ||
return retAr; | ||
} | ||
return []; | ||
} | ||
setValueSubfieldBatch(value, subschema, newValues, schema) { | ||
let keyToIndexMap = new Map(); | ||
for (let i = 0; i < value.length; i++) { | ||
let key = this._getElementKey(value[i], subschema, schema, true); | ||
keyToIndexMap.set(key, i); | ||
} | ||
for (let newValue of newValues.values()) { | ||
// note: ignore given key for value and recalculate it ourselves | ||
let key = this._getElementKey(newValue, subschema, schema, true); | ||
if (keyToIndexMap.has(key)) { | ||
value[keyToIndexMap.get(key)] = newValue; | ||
} | ||
else { | ||
value.push(newValue); | ||
} | ||
} | ||
} | ||
getValueSubfield(value, subschema, field, schema) { | ||
for (let el of value) { | ||
if (this._getElementKey(el, subschema, schema) === field) { | ||
return el; | ||
} | ||
} | ||
return undefined; | ||
} | ||
setValueSubfield(value, subschema, field, fieldValue, schema) { | ||
for (let i = 0; i < value.length; i++) { | ||
if (this._getElementKey(value[i], subschema, schema) === field) { | ||
value[i] = fieldValue; | ||
return; | ||
} | ||
} | ||
value.push(fieldValue); | ||
} | ||
normalizeSchema(subschema, schema) { | ||
if (!subschema.elements) { | ||
throw new SchemaError('ArraySet schema must have elements field'); | ||
} | ||
subschema.elements = schema._normalizeSubschema(subschema.elements); | ||
if (subschema.keySchemas) { | ||
for (let k in subschema.keySchemas) { | ||
subschema.keySchemas[k] = schema._normalizeSubschema(subschema.keySchemas[k]); | ||
} | ||
} | ||
return subschema; | ||
} | ||
validate(value, subschema, field, options, schema) { | ||
if (!_.isArray(value)) { | ||
throw new FieldError('invalid_type', 'Must be an array'); | ||
} | ||
for (let elem of value) { | ||
if (elem === undefined) { | ||
throw new FieldError('invalid', 'Arrays may not contain undefined elements'); | ||
} | ||
if (this._getElementKey(elem, subschema, schema) === undefined) { | ||
throw new FieldError('invalid', 'ArraySet element has no key'); | ||
} | ||
} | ||
} | ||
normalize(value, subschema, field, options, schema) { | ||
this.validate(value, subschema, field, options, schema); | ||
return value; | ||
} | ||
checkTypeMatch(value, subschema, schema) { | ||
return (_.isArray(value) ? 1 : 0); | ||
} | ||
toJSONSchema(subschema, schema) { | ||
return { | ||
type: 'array', | ||
items: schema._subschemaToJSONSchema(subschema.elements) | ||
}; | ||
} | ||
newEmptyContainer(valueTemplate, subschema, schema) { | ||
return []; | ||
} | ||
} | ||
export class SchemaTypeMap extends SchemaType { | ||
@@ -350,9 +538,34 @@ constructor(name = 'map') { | ||
getFieldSubschema(subschema, pathComponent, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[pathComponent]) { | ||
return subschema.keySchemas[pathComponent]; | ||
} | ||
return subschema.values; | ||
} | ||
getFieldSubschemaForModify(subschema, field, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return subschema.keySchemas[field]; | ||
} | ||
else if (field === '$') { | ||
return subschema.values; | ||
} | ||
else { | ||
if (!subschema.keySchemas) { | ||
subschema.keySchemas = {}; | ||
} | ||
subschema.keySchemas[field] = objtools.deepCopy(subschema.values); | ||
return subschema.keySchemas[field]; | ||
} | ||
} | ||
getFieldSubschemaPath(subschema, field, schema) { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return 'keySchemas.' + field; | ||
} | ||
return 'values'; | ||
} | ||
listSchemaSubfields(subschema, schema) { | ||
return ['$']; | ||
let r = ['$']; | ||
if (subschema.keySchemas) { | ||
r.push(...(Object.keys(subschema.keySchemas))); | ||
} | ||
return r; | ||
} | ||
@@ -378,2 +591,7 @@ listValueSubfields(value, subschema, schema) { | ||
subschema.values = schema._normalizeSubschema(subschema.values); | ||
if (subschema.keySchemas) { | ||
for (let k in subschema.keySchemas) { | ||
subschema.keySchemas[k] = schema._normalizeSubschema(subschema.keySchemas[k]); | ||
} | ||
} | ||
return subschema; | ||
@@ -445,2 +663,5 @@ } | ||
} | ||
newEmptyContainer(valueTemplate, subschema, schema) { | ||
return {}; | ||
} | ||
} | ||
@@ -447,0 +668,0 @@ export class SchemaTypeOr extends SchemaType { |
@@ -12,2 +12,3 @@ export * from './schema.js'; | ||
export { map } from './map.js'; | ||
export * from './core-schema-types.js'; | ||
import { SchemaFactory } from './schema-factory.js'; | ||
@@ -14,0 +15,0 @@ import { SchemaOptions, Schema } from './schema.js'; |
@@ -12,2 +12,3 @@ export * from './schema.js'; | ||
export { map } from './map.js'; | ||
export * from './core-schema-types.js'; | ||
import { SchemaFactory } from './schema-factory.js'; | ||
@@ -14,0 +15,0 @@ export const defaultSchemaFactory = new SchemaFactory(); |
@@ -62,2 +62,3 @@ import { Schema, SchemaOptions } from './schema.js'; | ||
getRegisteredSchema(name: string): Schema; | ||
getType(name: string): SchemaType; | ||
} |
import { Schema } from './schema.js'; | ||
import { SchemaError } from './schema-error.js'; | ||
import * as coreSchemaTypes from './core-schema-types.js'; | ||
@@ -76,3 +77,10 @@ import * as geoSchemaTypes from './geo-schema-types.js'; | ||
} | ||
getType(name) { | ||
let schemaType = this._schemaTypes[name]; | ||
if (!schemaType) { | ||
throw new SchemaError('Unknown schema type: ' + name); | ||
} | ||
return schemaType; | ||
} | ||
} | ||
//# sourceMappingURL=schema-factory.js.map |
@@ -15,3 +15,3 @@ import { Schema, SchemaTraverseHandlers, SchemaTraverseOptions, TraverseHandlers, SubschemaType, TransformHandlers, TransformAsyncHandlers, ValidateOptions, NormalizeOptions } from './schema.js'; | ||
_typeName: string; | ||
isContainer: boolean; | ||
_defaultIsContainer: boolean; | ||
containerSchemaKeysMatchValueKeys: boolean; | ||
@@ -27,2 +27,23 @@ constructor(name: string, isContainer: boolean, containerSchemaKeysMatchValueKeys?: boolean); | ||
/** | ||
* Returns whether or not this value is a container type. Usually this will just return whether | ||
* or not the schema itself represents a container type, but can be overridden for complex types | ||
* where whether or not the value is a container depends on the value. | ||
* | ||
* If undefined is passed for the value, return the default for the schema. | ||
* | ||
* @method isContainer | ||
*/ | ||
isContainer(value: any, subschema: SubschemaType, schema: Schema): boolean; | ||
/** | ||
* For container types, constructs and returns a new value representing an empty container | ||
* of the same parameters as valueTemplate (if provided). | ||
* | ||
* @method newEmptyContainer | ||
* @param {Mixed} valueTemplate - A template value to use for container parameters, if applicable. | ||
* @param {Object} subschema | ||
* @param {Schema} schema | ||
* @return {Mixed} - The empty container | ||
*/ | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any; | ||
/** | ||
* Determines whether this subschema handles a shorthand type, ex. `Number` . | ||
@@ -70,2 +91,17 @@ * | ||
/** | ||
* Returns the subschema of this schema that corresponds to the given field. The returned subschema | ||
* can be modified and modifications should apply to only the requested field. | ||
* | ||
* Often this can be the same as getFieldSubschema() but differs in two ways: | ||
* 1. For container schema types that support subfields with a default subschema but also support specific | ||
* subschemas for different fields (eg. SchemaTypeMap supports the default 'values' subschemas as well | ||
* as key-specific subschemas with 'keySchemas'): If the specified field does not correspond to an | ||
* already-defined key-specific subschema, a new key-specific subschema should be created for this field, | ||
* copied from the default. | ||
* 2. For schema types that return a static subschema, this method must return a modifiable version. | ||
* | ||
* @method getFieldSubschemaForModify | ||
*/ | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined; | ||
/** | ||
* Returns the field in the schema that corresponds to the given field path in a corresponding value. | ||
@@ -72,0 +108,0 @@ */ |
@@ -14,7 +14,7 @@ /** | ||
_typeName; | ||
isContainer; | ||
_defaultIsContainer; | ||
containerSchemaKeysMatchValueKeys; | ||
constructor(name, isContainer, containerSchemaKeysMatchValueKeys = false) { | ||
this._typeName = name; | ||
this.isContainer = isContainer; | ||
this._defaultIsContainer = isContainer; | ||
this.containerSchemaKeysMatchValueKeys = containerSchemaKeysMatchValueKeys; | ||
@@ -32,2 +32,27 @@ } | ||
/** | ||
* Returns whether or not this value is a container type. Usually this will just return whether | ||
* or not the schema itself represents a container type, but can be overridden for complex types | ||
* where whether or not the value is a container depends on the value. | ||
* | ||
* If undefined is passed for the value, return the default for the schema. | ||
* | ||
* @method isContainer | ||
*/ | ||
isContainer(value, subschema, schema) { | ||
return this._defaultIsContainer; | ||
} | ||
/** | ||
* For container types, constructs and returns a new value representing an empty container | ||
* of the same parameters as valueTemplate (if provided). | ||
* | ||
* @method newEmptyContainer | ||
* @param {Mixed} valueTemplate - A template value to use for container parameters, if applicable. | ||
* @param {Object} subschema | ||
* @param {Schema} schema | ||
* @return {Mixed} - The empty container | ||
*/ | ||
newEmptyContainer(valueTemplate, subschema, schema) { | ||
throw new Error('Unsupported operation'); | ||
} | ||
/** | ||
* Determines whether this subschema handles a shorthand type, ex. `Number` . | ||
@@ -54,3 +79,3 @@ * | ||
traverseSchema(subschema, path, rawPath, handlers, schema, options) { | ||
if (this.isContainer) { | ||
if (this.isContainer(undefined, subschema, schema)) { | ||
for (let subfield of this.listSchemaSubfields(subschema, schema)) { | ||
@@ -89,2 +114,19 @@ let subschemaPathPart = this.getFieldSubschemaPath(subschema, subfield, schema); | ||
/** | ||
* Returns the subschema of this schema that corresponds to the given field. The returned subschema | ||
* can be modified and modifications should apply to only the requested field. | ||
* | ||
* Often this can be the same as getFieldSubschema() but differs in two ways: | ||
* 1. For container schema types that support subfields with a default subschema but also support specific | ||
* subschemas for different fields (eg. SchemaTypeMap supports the default 'values' subschemas as well | ||
* as key-specific subschemas with 'keySchemas'): If the specified field does not correspond to an | ||
* already-defined key-specific subschema, a new key-specific subschema should be created for this field, | ||
* copied from the default. | ||
* 2. For schema types that return a static subschema, this method must return a modifiable version. | ||
* | ||
* @method getFieldSubschemaForModify | ||
*/ | ||
getFieldSubschemaForModify(subschema, field, schema) { | ||
return this.getFieldSubschema(subschema, field, schema); | ||
} | ||
/** | ||
* Returns the field in the schema that corresponds to the given field path in a corresponding value. | ||
@@ -174,3 +216,3 @@ */ | ||
traverse(value, subschema, field, handlers, schema) { | ||
if (this.isContainer) { | ||
if (this.isContainer(value, subschema, schema)) { | ||
let subfieldSet = new Set(); | ||
@@ -209,3 +251,3 @@ for (let [subfield, subvalue] of this.listValueSubfieldEntries(value, subschema, schema)) { | ||
transform(value, subschema, field, handlers, schema) { | ||
if (this.isContainer && value) { | ||
if (this.isContainer(value, subschema, schema) && value) { | ||
let subfieldSet = new Set(); | ||
@@ -219,2 +261,4 @@ for (let [subfield, subvalue] of this.listValueSubfieldEntries(value, subschema, schema)) { | ||
if (this.containerSchemaKeysMatchValueKeys) { | ||
let newValueBatch = {}; | ||
let changeCount = 0; | ||
for (let subfield of this.listSchemaSubfields(subschema, schema)) { | ||
@@ -227,5 +271,10 @@ if (!subfieldSet.has(subfield)) { | ||
subfieldSet.add(subfield); | ||
this.setValueSubfield(value, subschema, subfield, newValue, schema); | ||
//this.setValueSubfield(value, subschema, subfield, newValue, schema); | ||
newValueBatch[subfield] = newValue; | ||
changeCount++; | ||
} | ||
} | ||
if (changeCount > 0) { | ||
this.setValueSubfieldBatch(value, subschema, newValueBatch, schema); | ||
} | ||
} | ||
@@ -250,3 +299,3 @@ return value; | ||
async transformAsync(value, subschema, field, handlers, schema) { | ||
if (this.isContainer && value) { | ||
if (this.isContainer(value, subschema, schema) && value) { | ||
let subfieldSet = new Set(); | ||
@@ -253,0 +302,0 @@ for (let [subfield, subvalue] of this.listValueSubfieldEntries(value, subschema, schema)) { |
@@ -355,2 +355,3 @@ import { Normalizer } from './normalizer.js'; | ||
_getType(name: string): SchemaType; | ||
getType(name: string): SchemaType; | ||
/** | ||
@@ -376,2 +377,13 @@ * Returns the SchemaType corresponding to a subschema. | ||
/** | ||
* Returns the subschema data at the path in such a way that the subschema can be modified | ||
* and it will apply to the specific named field. | ||
* | ||
* @method getSubschemaDataForModify | ||
*/ | ||
getSubschemaDataForModify(path: string): SubschemaType; | ||
/** | ||
* Sets a schema option at the given path. | ||
*/ | ||
setSubschemaOption(subschemaPath: string, optionName: string, optionValue: any): void; | ||
/** | ||
* Creates a schema that represents a subcomponent of this schema. | ||
@@ -378,0 +390,0 @@ * |
@@ -513,8 +513,7 @@ import { SchemaError } from './schema-error.js'; | ||
_getType(name) { | ||
let schemaType = this._schemaFactory._schemaTypes[name]; | ||
if (!schemaType) { | ||
throw new SchemaError('Unknown schema type: ' + name); | ||
} | ||
return schemaType; | ||
return this._schemaFactory.getType(name); | ||
} | ||
getType(name) { | ||
return this._getType(name); | ||
} | ||
/** | ||
@@ -557,2 +556,33 @@ * Returns the SchemaType corresponding to a subschema. | ||
/** | ||
* Returns the subschema data at the path in such a way that the subschema can be modified | ||
* and it will apply to the specific named field. | ||
* | ||
* @method getSubschemaDataForModify | ||
*/ | ||
getSubschemaDataForModify(path) { | ||
// Edge case for root path | ||
if (!path) | ||
return this.getData(); | ||
// Traverse schema along path | ||
let pathComponents = path.split('.'); | ||
let currentSubschema = this.getData(); | ||
for (let pathComponent of pathComponents) { | ||
if (!currentSubschema) | ||
return undefined; | ||
currentSubschema = this | ||
.getSchemaType(currentSubschema) | ||
.getFieldSubschemaForModify(currentSubschema, pathComponent, this); | ||
} | ||
return currentSubschema; | ||
} | ||
/** | ||
* Sets a schema option at the given path. | ||
*/ | ||
setSubschemaOption(subschemaPath, optionName, optionValue) { | ||
let subschema = this.getSubschemaDataForModify(subschemaPath); | ||
if (!subschema) | ||
throw new Error('Subschema path ' + subschemaPath + ' does not exist on schema'); | ||
objtools.setPath(subschema, optionName, optionValue); | ||
} | ||
/** | ||
* Creates a schema that represents a subcomponent of this schema. | ||
@@ -559,0 +589,0 @@ * |
@@ -52,3 +52,85 @@ import { expect } from 'chai'; | ||
}); | ||
it('#setSubschemaOption', function () { | ||
let schema = createSchema({ | ||
scalarTest: String, | ||
nestedObjectTest: { | ||
foo: String | ||
}, | ||
arrayTest: [String], | ||
arraySetTest: { | ||
type: 'arrayset', | ||
elements: String | ||
}, | ||
mapTest: { | ||
type: 'map', | ||
values: String | ||
} | ||
}); | ||
schema.setSubschemaOption('', 'testOpt', true); | ||
schema.setSubschemaOption('scalarTest', 'testOpt', true); | ||
schema.setSubschemaOption('nestedObjectTest', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('nestedObjectTest.foo', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('arrayTest', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('arrayTest.$', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('arraySetTest.$', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('arraySetTest.el', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('mapTest.$', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('mapTest.el', 'testOpt', 'bar'); | ||
let expected = { | ||
type: 'object', | ||
testOpt: true, | ||
properties: { | ||
scalarTest: { | ||
type: 'string', | ||
testOpt: true | ||
}, | ||
nestedObjectTest: { | ||
type: 'object', | ||
testOpt: 'foo', | ||
properties: { | ||
foo: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
}, | ||
arrayTest: { | ||
type: 'array', | ||
testOpt: 'foo', | ||
elements: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
}, | ||
arraySetTest: { | ||
type: 'arrayset', | ||
elements: { | ||
type: 'string', | ||
testOpt: 'foo' | ||
}, | ||
keySchemas: { | ||
el: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
}, | ||
mapTest: { | ||
type: 'map', | ||
values: { | ||
type: 'string', | ||
testOpt: 'foo' | ||
}, | ||
keySchemas: { | ||
el: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
expect(schema.getData()).to.deep.equal(expected); | ||
}); | ||
}); | ||
//# sourceMappingURL=schema.js.map |
@@ -196,2 +196,6 @@ import { SchemaType } from './schema-type.js'; | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any { | ||
return {}; | ||
} | ||
} | ||
@@ -369,4 +373,204 @@ | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any { | ||
return []; | ||
} | ||
} | ||
/** | ||
* This schema type is a variant of an array that actually represents a set/map of unique items. | ||
* In paths, this is represented as a string key (matching the value) instead of an array index. | ||
* | ||
* Schemas look like this: | ||
* { | ||
* type: 'arrayset', | ||
* elements: <Subschema for Elements>, | ||
* keyField: <Subfield within elements to get unique key; defaults to whole element> | ||
* } | ||
*/ | ||
export class SchemaTypeArraySet extends SchemaType { | ||
constructor(name: string = 'arrayset') { | ||
super(name, true, false); | ||
} | ||
matchShorthandType(subschema: any): boolean { | ||
return false; | ||
} | ||
getFieldSubschemaPath(subschema: SubschemaType, field: string, schema: Schema): string { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return 'keySchemas.' + field; | ||
} | ||
return 'elements'; | ||
} | ||
listSchemaSubfields(subschema: SubschemaType, schema: Schema): string[] { | ||
let r: string[] = [ '$' ]; | ||
if (subschema.keySchemas) { | ||
r.push(...(Object.keys(subschema.keySchemas))); | ||
} | ||
return r; | ||
} | ||
getFieldSubschema(subschema: SubschemaType, pathComponent: string, schema: Schema): any | undefined { | ||
if (subschema.keySchemas && subschema.keySchemas[pathComponent]) { | ||
return subschema.keySchemas[pathComponent]; | ||
} | ||
return subschema.elements; | ||
} | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return subschema.keySchemas[field]; | ||
} else if (field === '$') { // generic array placeholder always returns generic elements schema | ||
return subschema.elements; | ||
} else { | ||
if (!subschema.keySchemas) { | ||
subschema.keySchemas = {}; | ||
} | ||
subschema.keySchemas[field] = objtools.deepCopy(subschema.elements); | ||
return subschema.keySchemas[field]; | ||
} | ||
} | ||
// Return a unique key string used to index the element based on keyField | ||
_getElementKey(elementValue: any, subschema: SubschemaType, schema: Schema, throwOnUndefined: boolean = false): string | undefined { | ||
const encodeValue = (v: any): string | undefined => { | ||
if (v === undefined) return undefined; | ||
if (Array.isArray(v)) { | ||
return v.map(encodeValue).join('$'); | ||
} else if (objtools.isScalar(v)) { | ||
return String(v); | ||
} else { | ||
return objtools.objectHash(v); | ||
} | ||
}; | ||
let r: any; | ||
if (subschema.keyField && Array.isArray(subschema.keyField)) { | ||
r = encodeValue(subschema.keyField.map((f: string) => objtools.getPath(elementValue, f))); | ||
} else if (subschema.keyField) { | ||
r = encodeValue(objtools.getPath(elementValue, subschema.keyField)); | ||
} else { | ||
r = encodeValue(elementValue); | ||
} | ||
if (r === undefined && throwOnUndefined) { | ||
throw new SchemaError('ArraySet element key does not exist'); | ||
} | ||
return r; | ||
} | ||
listValueSubfields(value: any, subschema: SubschemaType, schema: Schema): string[] { | ||
let ret: string[] = []; | ||
if (Array.isArray(value)) { | ||
for (let el of value) { | ||
ret.push(this._getElementKey(el, subschema, schema, true)); | ||
} | ||
return _.uniq(ret); | ||
} | ||
return ret; | ||
} | ||
listValueSubfieldEntries(value: any, subschema: SubschemaType, schema: Schema): [ string, any ][] { | ||
let retMap: Map<string, any> = new Map(); | ||
if (Array.isArray(value)) { | ||
for (let el of value) { | ||
retMap.set(this._getElementKey(el, subschema, schema, true), el); | ||
} | ||
let retAr: [ string, any ][] = []; | ||
for (let [ k, v ] of retMap.entries()) { | ||
retAr.push([ k, v ]); | ||
} | ||
return retAr; | ||
} | ||
return []; | ||
} | ||
setValueSubfieldBatch(value: any, subschema: SubschemaType, newValues: { [subfield: string]: any }, schema: Schema): void { | ||
let keyToIndexMap: Map<string, number> = new Map(); | ||
for (let i = 0; i < value.length; i++) { | ||
let key: string = this._getElementKey(value[i], subschema, schema, true); | ||
keyToIndexMap.set(key, i); | ||
} | ||
for (let newValue of newValues.values()) { | ||
// note: ignore given key for value and recalculate it ourselves | ||
let key: string = this._getElementKey(newValue, subschema, schema, true); | ||
if (keyToIndexMap.has(key)) { | ||
value[keyToIndexMap.get(key)] = newValue; | ||
} else { | ||
value.push(newValue); | ||
} | ||
} | ||
} | ||
getValueSubfield(value: any, subschema: SubschemaType, field: string, schema: Schema): any { | ||
for (let el of value) { | ||
if (this._getElementKey(el, subschema, schema) === field) { | ||
return el; | ||
} | ||
} | ||
return undefined; | ||
} | ||
setValueSubfield(value: any, subschema: SubschemaType, field: string, fieldValue: any, schema: Schema): void { | ||
for (let i = 0; i < value.length; i++) { | ||
if (this._getElementKey(value[i], subschema, schema) === field) { | ||
value[i] = fieldValue; | ||
return; | ||
} | ||
} | ||
value.push(fieldValue); | ||
} | ||
normalizeSchema(subschema: any, schema: Schema): SubschemaType { | ||
if (!subschema.elements) { | ||
throw new SchemaError('ArraySet schema must have elements field'); | ||
} | ||
subschema.elements = schema._normalizeSubschema(subschema.elements); | ||
if (subschema.keySchemas) { | ||
for (let k in subschema.keySchemas) { | ||
subschema.keySchemas[k] = schema._normalizeSubschema(subschema.keySchemas[k]); | ||
} | ||
} | ||
return subschema; | ||
} | ||
validate(value: any, subschema: SubschemaType, field: string, options: ValidateOptions, schema: Schema): void { | ||
if (!_.isArray(value)) { | ||
throw new FieldError('invalid_type', 'Must be an array'); | ||
} | ||
for (let elem of value) { | ||
if (elem === undefined) { | ||
throw new FieldError('invalid', 'Arrays may not contain undefined elements'); | ||
} | ||
if (this._getElementKey(elem, subschema, schema) === undefined) { | ||
throw new FieldError('invalid', 'ArraySet element has no key'); | ||
} | ||
} | ||
} | ||
normalize(value: any, subschema: SubschemaType, field: string, options: NormalizeOptions, schema: Schema): any { | ||
this.validate(value, subschema, field, options, schema); | ||
return value; | ||
} | ||
checkTypeMatch(value: any, subschema: SubschemaType, schema: Schema): 0 | 1 | 2 | 3 { | ||
return (_.isArray(value) ? 1 : 0); | ||
} | ||
toJSONSchema(subschema: SubschemaType, schema: Schema): any { | ||
return { | ||
type: 'array', | ||
items: schema._subschemaToJSONSchema(subschema.elements) | ||
}; | ||
} | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any { | ||
return []; | ||
} | ||
} | ||
export class SchemaTypeMap extends SchemaType { | ||
@@ -400,6 +604,26 @@ | ||
getFieldSubschema(subschema: SubschemaType, pathComponent: string, schema: Schema): any | undefined { | ||
if (subschema.keySchemas && subschema.keySchemas[pathComponent]) { | ||
return subschema.keySchemas[pathComponent]; | ||
} | ||
return subschema.values; | ||
} | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return subschema.keySchemas[field]; | ||
} else if (field === '$') { | ||
return subschema.values; | ||
} else { | ||
if (!subschema.keySchemas) { | ||
subschema.keySchemas = {}; | ||
} | ||
subschema.keySchemas[field] = objtools.deepCopy(subschema.values); | ||
return subschema.keySchemas[field]; | ||
} | ||
} | ||
getFieldSubschemaPath(subschema: SubschemaType, field: string, schema: Schema): string { | ||
if (subschema.keySchemas && subschema.keySchemas[field]) { | ||
return 'keySchemas.' + field; | ||
} | ||
return 'values'; | ||
@@ -409,3 +633,7 @@ } | ||
listSchemaSubfields(subschema: SubschemaType, schema: Schema): string[] { | ||
return [ '$' ]; | ||
let r: string[] = [ '$' ]; | ||
if (subschema.keySchemas) { | ||
r.push(...(Object.keys(subschema.keySchemas))); | ||
} | ||
return r; | ||
} | ||
@@ -434,2 +662,7 @@ | ||
subschema.values = schema._normalizeSubschema(subschema.values); | ||
if (subschema.keySchemas) { | ||
for (let k in subschema.keySchemas) { | ||
subschema.keySchemas[k] = schema._normalizeSubschema(subschema.keySchemas[k]); | ||
} | ||
} | ||
return subschema; | ||
@@ -506,2 +739,6 @@ } | ||
} | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any { | ||
return {}; | ||
} | ||
} | ||
@@ -508,0 +745,0 @@ |
@@ -12,2 +12,3 @@ export * from './schema.js'; | ||
export { map } from './map.js'; | ||
export * from './core-schema-types.js'; | ||
@@ -14,0 +15,0 @@ import { SchemaFactory } from './schema-factory.js'; |
import { Schema, SchemaOptions } from './schema.js'; | ||
import { SchemaType } from './schema-type.js'; | ||
import { SchemaError } from './schema-error.js'; | ||
import * as coreSchemaTypes from './core-schema-types.js'; | ||
@@ -86,3 +87,11 @@ import * as geoSchemaTypes from './geo-schema-types.js'; | ||
} | ||
getType(name: string): SchemaType { | ||
let schemaType: SchemaType = this._schemaTypes[name]; | ||
if (!schemaType) { | ||
throw new SchemaError('Unknown schema type: ' + name); | ||
} | ||
return schemaType; | ||
} | ||
} | ||
@@ -16,3 +16,3 @@ import { Schema, SchemaTraverseHandlers, SchemaTraverseOptions, TraverseHandlers, SubschemaType, TransformHandlers, TransformAsyncHandlers, ValidateOptions, NormalizeOptions } from './schema.js'; | ||
_typeName: string; | ||
isContainer: boolean; | ||
_defaultIsContainer: boolean; | ||
containerSchemaKeysMatchValueKeys: boolean; | ||
@@ -22,3 +22,3 @@ | ||
this._typeName = name; | ||
this.isContainer = isContainer; | ||
this._defaultIsContainer = isContainer; | ||
this.containerSchemaKeysMatchValueKeys = containerSchemaKeysMatchValueKeys; | ||
@@ -38,2 +38,29 @@ } | ||
/** | ||
* Returns whether or not this value is a container type. Usually this will just return whether | ||
* or not the schema itself represents a container type, but can be overridden for complex types | ||
* where whether or not the value is a container depends on the value. | ||
* | ||
* If undefined is passed for the value, return the default for the schema. | ||
* | ||
* @method isContainer | ||
*/ | ||
isContainer(value: any, subschema: SubschemaType, schema: Schema): boolean { | ||
return this._defaultIsContainer; | ||
} | ||
/** | ||
* For container types, constructs and returns a new value representing an empty container | ||
* of the same parameters as valueTemplate (if provided). | ||
* | ||
* @method newEmptyContainer | ||
* @param {Mixed} valueTemplate - A template value to use for container parameters, if applicable. | ||
* @param {Object} subschema | ||
* @param {Schema} schema | ||
* @return {Mixed} - The empty container | ||
*/ | ||
newEmptyContainer(valueTemplate: any, subschema: SubschemaType, schema: Schema): any { | ||
throw new Error('Unsupported operation'); | ||
} | ||
/** | ||
* Determines whether this subschema handles a shorthand type, ex. `Number` . | ||
@@ -61,3 +88,3 @@ * | ||
traverseSchema(subschema: SubschemaType, path: string, rawPath: string, handlers: SchemaTraverseHandlers, schema: Schema, options: SchemaTraverseOptions): void { | ||
if (this.isContainer) { | ||
if (this.isContainer(undefined, subschema, schema)) { | ||
for (let subfield of this.listSchemaSubfields(subschema, schema)) { | ||
@@ -105,2 +132,20 @@ let subschemaPathPart = this.getFieldSubschemaPath(subschema, subfield, schema); | ||
/** | ||
* Returns the subschema of this schema that corresponds to the given field. The returned subschema | ||
* can be modified and modifications should apply to only the requested field. | ||
* | ||
* Often this can be the same as getFieldSubschema() but differs in two ways: | ||
* 1. For container schema types that support subfields with a default subschema but also support specific | ||
* subschemas for different fields (eg. SchemaTypeMap supports the default 'values' subschemas as well | ||
* as key-specific subschemas with 'keySchemas'): If the specified field does not correspond to an | ||
* already-defined key-specific subschema, a new key-specific subschema should be created for this field, | ||
* copied from the default. | ||
* 2. For schema types that return a static subschema, this method must return a modifiable version. | ||
* | ||
* @method getFieldSubschemaForModify | ||
*/ | ||
getFieldSubschemaForModify(subschema: SubschemaType, field: string, schema: Schema): any | undefined { | ||
return this.getFieldSubschema(subschema, field, schema); | ||
} | ||
/** | ||
* Returns the field in the schema that corresponds to the given field path in a corresponding value. | ||
@@ -198,3 +243,3 @@ */ | ||
traverse(value: any, subschema: SubschemaType, field: string, handlers: TraverseHandlers, schema: Schema): void { | ||
if (this.isContainer) { | ||
if (this.isContainer(value, subschema, schema)) { | ||
let subfieldSet: Set<string> = new Set(); | ||
@@ -244,3 +289,3 @@ for (let [ subfield, subvalue ] of this.listValueSubfieldEntries(value, subschema, schema)) { | ||
transform(value: any, subschema: SubschemaType, field: string, handlers: TransformHandlers, schema: Schema): any { | ||
if (this.isContainer && value) { | ||
if (this.isContainer(value, subschema, schema) && value) { | ||
let subfieldSet: Set<string> = new Set(); | ||
@@ -259,2 +304,4 @@ for (let [ subfield, subvalue ] of this.listValueSubfieldEntries(value, subschema, schema)) { | ||
if (this.containerSchemaKeysMatchValueKeys) { | ||
let newValueBatch = {}; | ||
let changeCount = 0; | ||
for (let subfield of this.listSchemaSubfields(subschema, schema)) { | ||
@@ -272,5 +319,10 @@ if (!subfieldSet.has(subfield)) { | ||
subfieldSet.add(subfield); | ||
this.setValueSubfield(value, subschema, subfield, newValue, schema); | ||
//this.setValueSubfield(value, subschema, subfield, newValue, schema); | ||
newValueBatch[subfield] = newValue; | ||
changeCount++; | ||
} | ||
} | ||
if (changeCount > 0) { | ||
this.setValueSubfieldBatch(value, subschema, newValueBatch, schema); | ||
} | ||
} | ||
@@ -295,3 +347,3 @@ return value; | ||
async transformAsync(value: any, subschema: SubschemaType, field: string, handlers: TransformAsyncHandlers, schema: Schema): Promise<any> { | ||
if (this.isContainer && value) { | ||
if (this.isContainer(value, subschema, schema) && value) { | ||
let subfieldSet: Set<string> = new Set(); | ||
@@ -298,0 +350,0 @@ for (let [ subfield, subvalue ] of this.listValueSubfieldEntries(value, subschema, schema)) { |
@@ -592,9 +592,9 @@ import { SchemaError } from './schema-error.js'; | ||
_getType(name: string): SchemaType { | ||
let schemaType: SchemaType = this._schemaFactory._schemaTypes[name]; | ||
if (!schemaType) { | ||
throw new SchemaError('Unknown schema type: ' + name); | ||
} | ||
return schemaType; | ||
return this._schemaFactory.getType(name); | ||
} | ||
getType(name: string): SchemaType { | ||
return this._getType(name); | ||
} | ||
/** | ||
@@ -638,2 +638,33 @@ * Returns the SchemaType corresponding to a subschema. | ||
/** | ||
* Returns the subschema data at the path in such a way that the subschema can be modified | ||
* and it will apply to the specific named field. | ||
* | ||
* @method getSubschemaDataForModify | ||
*/ | ||
getSubschemaDataForModify(path: string): SubschemaType { | ||
// Edge case for root path | ||
if (!path) return this.getData(); | ||
// Traverse schema along path | ||
let pathComponents: string[] = path.split('.'); | ||
let currentSubschema: SubschemaType = this.getData(); | ||
for (let pathComponent of pathComponents) { | ||
if (!currentSubschema) return undefined; | ||
currentSubschema = this | ||
.getSchemaType(currentSubschema) | ||
.getFieldSubschemaForModify(currentSubschema, pathComponent, this); | ||
} | ||
return currentSubschema; | ||
} | ||
/** | ||
* Sets a schema option at the given path. | ||
*/ | ||
setSubschemaOption(subschemaPath: string, optionName: string, optionValue: any): void { | ||
let subschema = this.getSubschemaDataForModify(subschemaPath); | ||
if (!subschema) throw new Error('Subschema path ' + subschemaPath + ' does not exist on schema'); | ||
objtools.setPath(subschema, optionName, optionValue); | ||
} | ||
/** | ||
* Creates a schema that represents a subcomponent of this schema. | ||
@@ -640,0 +671,0 @@ * |
{ | ||
"name": "common-schema", | ||
"version": "4.2.0", | ||
"version": "4.3.0", | ||
"description": "Schema-handling utilities, including schema validators and checkers.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/lib/index.js", |
@@ -61,3 +61,88 @@ import { expect } from 'chai'; | ||
it('#setSubschemaOption', function() { | ||
let schema = createSchema({ | ||
scalarTest: String, | ||
nestedObjectTest: { | ||
foo: String | ||
}, | ||
arrayTest: [ String ], | ||
arraySetTest: { | ||
type: 'arrayset', | ||
elements: String | ||
}, | ||
mapTest: { | ||
type: 'map', | ||
values: String | ||
} | ||
}); | ||
schema.setSubschemaOption('', 'testOpt', true); | ||
schema.setSubschemaOption('scalarTest', 'testOpt', true); | ||
schema.setSubschemaOption('nestedObjectTest', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('nestedObjectTest.foo', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('arrayTest', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('arrayTest.$', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('arraySetTest.$', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('arraySetTest.el', 'testOpt', 'bar'); | ||
schema.setSubschemaOption('mapTest.$', 'testOpt', 'foo'); | ||
schema.setSubschemaOption('mapTest.el', 'testOpt', 'bar'); | ||
let expected = { | ||
type: 'object', | ||
testOpt: true, | ||
properties: { | ||
scalarTest: { | ||
type: 'string', | ||
testOpt: true | ||
}, | ||
nestedObjectTest: { | ||
type: 'object', | ||
testOpt: 'foo', | ||
properties: { | ||
foo: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
}, | ||
arrayTest: { | ||
type: 'array', | ||
testOpt: 'foo', | ||
elements: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
}, | ||
arraySetTest: { | ||
type: 'arrayset', | ||
elements: { | ||
type: 'string', | ||
testOpt: 'foo' | ||
}, | ||
keySchemas: { | ||
el: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
}, | ||
mapTest: { | ||
type: 'map', | ||
values: { | ||
type: 'string', | ||
testOpt: 'foo' | ||
}, | ||
keySchemas: { | ||
el: { | ||
type: 'string', | ||
testOpt: 'bar' | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
expect(schema.getData()).to.deep.equal(expected); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
549185
11510