@financial-times/biz-ops-schema
Advanced tools
Comparing version 0.0.3 to 0.1.0
@@ -25,3 +25,3 @@ # Contributing to the biz-ops model | ||
code: // required. Defines the code for the type | ||
type: String // Any graphql type (see below) | ||
type: Paragraph // Any primitive type or enum (see below) | ||
required: true // whether or not the field is required | ||
@@ -34,12 +34,26 @@ canIdentify: true // whether the field can be used to identify a single record | ||
Graphql types are `String`, `Int`, `Float` and `Boolean`. | ||
### Primitive types | ||
In addition to these, the name of any [enum](#enums) can be used as the type of a property | ||
Each property should have a type chosen from the following | ||
- Word - for ids and other very short strings, generally not allowing whitespace | ||
- Sentence - for short pieces of text | ||
- Paragraph - for longer pieces of text | ||
- Document - for arbitrarily long pieces of text | ||
- Url - Urls | ||
- Date - Dates, which should generally be input as ISO strings | ||
- Int - Integers | ||
- Float - Decimal numbers | ||
- Boolean - True or False | ||
Most of the above will be mapped to Strings in the data layer, and do not have any strict conditions attached. They are intended as hints for the underlying systems storing the data, and any systems displaying it. | ||
In addition to the above, the name of any [enum](#enums) can be used as the type of a property | ||
Note that yaml files are indented with two spaces | ||
## Attributes | ||
To add attributes to any existing type, add them in the `properties` section of the type's `.yaml` file. Property names must be camelCased. | ||
## Relationships | ||
@@ -79,6 +93,6 @@ | ||
## String validation rules | ||
These are expressed as regular expressions and are used to (optionally) validate values. Define a pattern in `schema/string-patterns.yaml` by adding a property to the yaml file abserving the following rules: | ||
- The property name must be in CONSTANT_CASE | ||
@@ -105,5 +119,1 @@ - The value must be either | ||
``` | ||
@@ -8,5 +8,6 @@ module.exports = Object.assign( | ||
getGraphqlDefs: require('./methods/get-graphql-defs').method, | ||
normalizeTypeName: name => name | ||
normalizeTypeName: name => name, | ||
primitiveTypesMap: require('./lib/primitive-types-map') | ||
}, | ||
require('./lib/validate') | ||
); |
const getEnums = require('../methods/get-enums'); | ||
const getType = require('../methods/get-type'); | ||
const attributeNameRegex = /^[a-z][a-zA-Z\d]+$/; | ||
const primitiveTypesMap = require('./primitive-types-map'); | ||
const { stripIndents } = require('common-tags'); | ||
@@ -67,3 +67,3 @@ | ||
const val = attributes[propName]; | ||
type = primitiveTypesMap[type] || type; | ||
if (type === 'Boolean') { | ||
@@ -70,0 +70,0 @@ if (typeof val !== 'boolean') { |
@@ -1,26 +0,4 @@ | ||
const { stripIndent } = require('common-tags'); | ||
const getTypes = require('../methods/get-types').method; | ||
const getEnums = require('../methods/get-enums').method; | ||
// Just here to ensure 100% backwards compatibility with previous beginnings | ||
// of graphql mutations | ||
const customGraphql = ` | ||
input SystemInput { | ||
serviceTier: ServiceTier | ||
name: String | ||
supported: Boolean | ||
primaryURL: String | ||
systemType: String | ||
serviceTier: ServiceTier | ||
serviceType: String | ||
hostPlatform: String | ||
personalData: Boolean | ||
sensitiveData: Boolean | ||
lifecycleStage: SystemLifecycle | ||
} | ||
type Mutation { | ||
System(code: String, params: SystemInput): System! | ||
}`; | ||
const stripEmptyFirstLine = (hardCoded, ...vars) => { | ||
@@ -76,3 +54,3 @@ hardCoded[0] = hardCoded[0].replace(/^\n+(.*)$/, ($0, $1) => $1); | ||
const generatePropertyFields = properties => { | ||
const defineProperties = properties => { | ||
return properties | ||
@@ -91,3 +69,3 @@ .map( | ||
const PAGINATE = indentMultiline( | ||
generatePropertyFields( | ||
defineProperties( | ||
Object.entries({ | ||
@@ -115,18 +93,15 @@ offset: { | ||
const generateQuery = ({ name, type, properties, paginate }) => { | ||
const defineQuery = ({ name, type, properties, paginate }) => { | ||
return ` | ||
${name}( | ||
${paginate ? PAGINATE : ''} | ||
${indentMultiline(generatePropertyFields(properties), 4, true)} | ||
${indentMultiline(defineProperties(properties), 4, true)} | ||
): ${type}`; | ||
}; | ||
module.exports.method = () => { | ||
const typeDefinitions = getTypes({ relationshipStructure: 'graphql' }).map( | ||
config => { | ||
return ` | ||
const defineType = config => ` | ||
# ${config.description} | ||
type ${config.name} { | ||
${indentMultiline( | ||
generatePropertyFields(Object.entries(config.properties)), | ||
defineProperties(Object.entries(config.properties)), | ||
2, | ||
@@ -136,28 +111,18 @@ true | ||
}`; | ||
} | ||
); | ||
const queries = getTypes().map(config => { | ||
return stripIndent` | ||
${generateQuery({ | ||
name: config.name, | ||
type: config.name, | ||
properties: getIdentifyingFields(config) | ||
})} | ||
${generateQuery({ | ||
name: config.pluralName, | ||
type: `[${config.name}]`, | ||
properties: getFilteringFields(config), | ||
paginate: true | ||
})}`; | ||
}); | ||
const defineQueries = config => [ | ||
defineQuery({ | ||
name: config.name, | ||
type: config.name, | ||
properties: getIdentifyingFields(config) | ||
}), | ||
defineQuery({ | ||
name: config.pluralName, | ||
type: `[${config.name}]`, | ||
properties: getFilteringFields(config), | ||
paginate: true | ||
}) | ||
]; | ||
const queryDefinitions = stripIndent` | ||
type Query { | ||
${indentMultiline(queries.join('\n\n'), 2)} | ||
}`; | ||
const enumDefinitions = Object.entries(getEnums({ withMeta: true })).map( | ||
([name, { description, options }]) => { | ||
return ` | ||
const defineEnum = ([name, { description, options }]) => ` | ||
# ${description} | ||
@@ -167,8 +132,16 @@ enum ${name} { | ||
}`; | ||
} | ||
module.exports.method = () => { | ||
const typesFromSchema = getTypes({ | ||
primitiveTypes: 'graphql', | ||
relationshipStructure: 'graphql' | ||
}); | ||
return [].concat( | ||
typesFromSchema.map(defineType), | ||
'type Query {\n', | ||
...typesFromSchema.map(defineQueries), | ||
'}', | ||
Object.entries(getEnums({ withMeta: true })).map(defineEnum) | ||
); | ||
return typeDefinitions.concat([queryDefinitions], enumDefinitions, [ | ||
customGraphql | ||
]); | ||
}; |
@@ -7,2 +7,3 @@ const rawData = require('../lib/raw-data'); | ||
const getStringValidator = require('../lib/get-string-validator'); | ||
const primitiveTypesMap = require('../lib/primitive-types-map'); | ||
@@ -12,2 +13,3 @@ const getType = ( | ||
{ | ||
primitiveTypes = 'biz-ops', // graphql | ||
relationshipStructure = false // flat, rest, graphql | ||
@@ -30,7 +32,19 @@ // groupProperties = false | ||
Object.values(type.properties).forEach(prop => { | ||
if (prop.pattern) { | ||
prop.validator = getStringValidator(prop.pattern); | ||
} | ||
}); | ||
type.properties = Object.entries(type.properties) | ||
.map(([name, def]) => { | ||
if (primitiveTypes === 'graphql') { | ||
if (def.type === 'Document') { | ||
// documents are too big to be served by graphql | ||
return | ||
} | ||
// If not a primitive type we assume it's an enum and leave it unaltered | ||
def.type = primitiveTypesMap[def.type] || def.type; | ||
} | ||
if (def.pattern) { | ||
def.validator = getStringValidator(def.pattern); | ||
} | ||
return [name, def] | ||
}) | ||
.filter(entry => !!entry) | ||
.reduce((obj, [name, def])=> Object.assign(obj, {[name]: def}), {}); | ||
@@ -37,0 +51,0 @@ if (relationshipStructure) { |
{ | ||
"name": "@financial-times/biz-ops-schema", | ||
"version": "0.0.3", | ||
"version": "0.1.0", | ||
"description": "Schema for biz-ops data store and api. It provides two things: - yaml files which define which types, properties and relationships are allowed - a nodejs library for extracting subsets of this information", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -9,3 +9,3 @@ const rawData = require('../../lib/raw-data'); | ||
const readYaml = require('../../lib/read-yaml'); | ||
const primitiveTypesMap = require('../../lib/primitive-types-map'); | ||
const fs = require('fs'); | ||
@@ -17,8 +17,4 @@ const path = require('path'); | ||
const validStringPatterns = Object.keys(stringPatterns); | ||
const validPropTypes = validEnums.concat([ | ||
'String', | ||
'Int', | ||
'Float', | ||
'Boolean' | ||
]); | ||
const validPropTypes = validEnums.concat(Object.keys(primitiveTypesMap)); | ||
fs.readdirSync(path.join(process.cwd(), 'schema/types')) | ||
@@ -25,0 +21,0 @@ .filter(fileName => /\.yaml$/.test(fileName)) |
@@ -6,6 +6,14 @@ const { expect } = require('chai'); | ||
const cache = require('../../lib/cache'); | ||
const primitiveTypesMap = require('../../lib/primitive-types-map'); | ||
const explodeString = str => | ||
str | ||
.split('\n') | ||
// exclude strings which are just whitespace or empty | ||
.filter(str => !/^[\s]*$/.test(str)) | ||
.map(str => str.trim()); | ||
describe('graphql def creation', () => { | ||
let sandbox; | ||
before(() => { | ||
beforeEach(() => { | ||
cache.clear(); | ||
@@ -15,6 +23,7 @@ sandbox = sinon.createSandbox(); | ||
after(() => { | ||
afterEach(() => { | ||
sandbox.restore(); | ||
cache.clear(); | ||
}); | ||
it('generates expected graphql def given schema', () => { | ||
@@ -27,3 +36,3 @@ sandbox.stub(rawData, 'getTypes').returns([ | ||
code: { | ||
type: 'String', | ||
type: 'Word', | ||
required: true, | ||
@@ -36,3 +45,3 @@ unique: true, | ||
name: { | ||
type: 'String', | ||
type: 'Word', | ||
canIdentify: true, | ||
@@ -49,3 +58,3 @@ description: 'The name of the cost centre' | ||
code: { | ||
type: 'String', | ||
type: 'Word', | ||
required: true, | ||
@@ -58,3 +67,3 @@ unique: true, | ||
name: { | ||
type: 'String', | ||
type: 'Word', | ||
canIdentify: true, | ||
@@ -101,8 +110,2 @@ description: 'The name of the group' | ||
const explodeString = str => | ||
str | ||
.split('\n') | ||
.filter(str => !/^[\s]*$/.test(str)) | ||
.map(str => str.trim()); | ||
const generated = [].concat(...generateGraphqlDefs().map(explodeString)); | ||
@@ -189,20 +192,2 @@ | ||
Sunset | ||
} | ||
input SystemInput { | ||
serviceTier: ServiceTier | ||
name: String | ||
supported: Boolean | ||
primaryURL: String | ||
systemType: String | ||
serviceTier: ServiceTier | ||
serviceType: String | ||
hostPlatform: String | ||
personalData: Boolean | ||
sensitiveData: Boolean | ||
lifecycleStage: SystemLifecycle | ||
} | ||
type Mutation { | ||
System(code: String, params: SystemInput): System! | ||
}` | ||
@@ -212,2 +197,48 @@ ) | ||
}); | ||
describe('converting types', () => { | ||
Object.entries(primitiveTypesMap).forEach(([bizopsType, graphqlType]) => { | ||
if (bizopsType === 'Document') { | ||
it(`Does not expose Document properties`, () => { | ||
sandbox.stub(rawData, 'getTypes').returns([ | ||
{ | ||
name: 'Dummy', | ||
description: 'dummy type description', | ||
properties: { | ||
prop: { | ||
type: 'Document', | ||
description: 'a description' | ||
} | ||
} | ||
} | ||
]); | ||
sandbox.stub(rawData, 'getRelationships').returns({}); | ||
sandbox.stub(rawData, 'getEnums').returns({}); | ||
const generated = [].concat(...generateGraphqlDefs()).join(''); | ||
expect(generated).not.to.match(new RegExp(`prop: String`)); | ||
}); | ||
} else { | ||
it(`Outputs correct type for properties using ${bizopsType}`, () => { | ||
sandbox.stub(rawData, 'getTypes').returns([ | ||
{ | ||
name: 'Dummy', | ||
description: 'dummy type description', | ||
properties: { | ||
prop: { | ||
type: bizopsType, | ||
description: 'a description' | ||
} | ||
} | ||
} | ||
]); | ||
sandbox.stub(rawData, 'getRelationships').returns({}); | ||
sandbox.stub(rawData, 'getEnums').returns({}); | ||
const generated = [].concat(...generateGraphqlDefs()).join(''); | ||
expect(generated).to.match(new RegExp(`prop: ${graphqlType}`)); | ||
}); | ||
} | ||
}); | ||
}); | ||
}); |
@@ -105,2 +105,28 @@ const { getType } = require('../../'); | ||
it('it maps types to graphql properties', async () => { | ||
sandbox.stub(getRelationships, 'method'); | ||
rawData.getTypes.returns([ | ||
{ | ||
name: 'Type1', | ||
properties: { | ||
primitiveProp: { | ||
type: 'Word' | ||
}, | ||
documentProp: { | ||
type: 'Document' | ||
}, | ||
enumProp: { | ||
type: 'SomeEnum' | ||
} | ||
} | ||
} | ||
]); | ||
const type = getType('Type1', { primitiveTypes: 'graphql' }); | ||
expect(type.properties.primitiveProp).to.eql({ type: 'String' }); | ||
expect(type.properties.documentProp).to.not.exist; | ||
expect(type.properties.enumProp).to.eql({ type: 'SomeEnum' }); | ||
}); | ||
describe('relationships', () => { | ||
@@ -107,0 +133,0 @@ it('it includes rest api relationship definitions', async () => { |
@@ -5,3 +5,3 @@ const sinon = require('sinon'); | ||
const { validateAttributes } = require('../../'); | ||
const primitiveTypesMap = require('../../lib/primitive-types-map') | ||
describe('validateAttributes', () => { | ||
@@ -15,41 +15,46 @@ const sandbox = sinon.createSandbox(); | ||
describe('validating strings', () => { | ||
beforeEach(() => { | ||
getType.method.returns({ | ||
name: 'Thing', | ||
properties: { | ||
prop: { | ||
type: 'String', | ||
validator: /^[^z]+$/ //exclude the letter z | ||
} | ||
} | ||
}); | ||
}); | ||
it('accept strings', () => { | ||
expect(() => | ||
validateAttributes('Thing', { prop: 'I am Tracy Beaker' }) | ||
).not.to.throw(); | ||
}); | ||
it('not accept booleans', () => { | ||
expect(() => validateAttributes('Thing', { prop: true })).to.throw( | ||
/Must be a string/ | ||
); | ||
expect(() => validateAttributes('Thing', { prop: false })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('not accept floats', () => { | ||
expect(() => validateAttributes('Thing', { prop: 1.34 })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('not accept integers', () => { | ||
expect(() => validateAttributes('Thing', { prop: 134 })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('apply string patterns', () => { | ||
expect(() => | ||
validateAttributes('Thing', { prop: 'I am zebbedee' }) | ||
).to.throw(/Must match pattern/); | ||
}); | ||
Object.entries(primitiveTypesMap).forEach(([bizOpsType, graphqlType]) => { | ||
if (graphqlType === 'String') { | ||
beforeEach(() => { | ||
getType.method.returns({ | ||
name: 'Thing', | ||
properties: { | ||
prop: { | ||
type: bizOpsType, | ||
validator: /^[^z]+$/ //exclude the letter z | ||
} | ||
} | ||
}); | ||
}); | ||
it('accept strings', () => { | ||
expect(() => | ||
validateAttributes('Thing', { prop: 'I am Tracy Beaker' }) | ||
).not.to.throw(); | ||
}); | ||
it('not accept booleans', () => { | ||
expect(() => validateAttributes('Thing', { prop: true })).to.throw( | ||
/Must be a string/ | ||
); | ||
expect(() => validateAttributes('Thing', { prop: false })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('not accept floats', () => { | ||
expect(() => validateAttributes('Thing', { prop: 1.34 })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('not accept integers', () => { | ||
expect(() => validateAttributes('Thing', { prop: 134 })).to.throw( | ||
/Must be a string/ | ||
); | ||
}); | ||
it('apply string patterns', () => { | ||
expect(() => | ||
validateAttributes('Thing', { prop: 'I am zebbedee' }) | ||
).to.throw(/Must match pattern/); | ||
}); | ||
} | ||
}) | ||
}); | ||
@@ -56,0 +61,0 @@ describe('validating booleans', () => { |
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
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
88220
49
2237