@resin/pinejs
Version:
Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make
186 lines (173 loc) • 5.31 kB
text/typescript
import type {
AbstractSqlModel,
AbstractSqlTable,
} from '@resin/abstract-sql-compiler';
import * as sbvrTypes from '@resin/sbvr-types';
// tslint:disable-next-line:no-var-requires
const { version }: { version: string } = require('../../package.json');
const getResourceName = (resourceName: string): string =>
resourceName
.split('-')
.map((namePart) => namePart.split(' ').join('_'))
.join('__');
const forEachUniqueTable = <T>(
model: AbstractSqlModel['tables'],
callback: (tableName: string, table: AbstractSqlTable) => T,
): T[] => {
const usedTableNames: { [tableName: string]: true } = {};
const result = [];
for (const key in model) {
if (model.hasOwnProperty(key)) {
const table = model[key];
if (
typeof table !== 'string' &&
!table.primitive &&
!usedTableNames[table.name]
) {
usedTableNames[table.name] = true;
result.push(callback(key, table));
}
}
}
return result;
};
export const generateODataMetadata = (
vocabulary: string,
abstractSqlModel: AbstractSqlModel,
) => {
const complexTypes: { [fieldType: string]: string } = {};
const resolveDataType = (fieldType: string): string => {
if (sbvrTypes[fieldType] == null) {
console.error('Could not resolve type', fieldType);
throw new Error('Could not resolve type' + fieldType);
}
const { complexType } = sbvrTypes[fieldType].types.odata;
if (complexType != null) {
complexTypes[fieldType] = complexType;
}
return sbvrTypes[fieldType].types.odata.name;
};
const model = abstractSqlModel.tables;
const associations: Array<{
name: string;
ends: Array<{
resourceName: string;
cardinality: '1' | '0..1' | '*';
}>;
}> = [];
forEachUniqueTable(model, (_key, { name: resourceName, fields }) => {
resourceName = getResourceName(resourceName);
for (const { dataType, required, references } of fields) {
if (dataType === 'ForeignKey' && references != null) {
const { resourceName: referencedResource } = references;
associations.push({
name: resourceName + referencedResource,
ends: [
{ resourceName, cardinality: required ? '1' : '0..1' },
{ resourceName: referencedResource, cardinality: '*' },
],
});
}
}
});
return (
`
<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="2.0">
<Schema Namespace="${vocabulary}"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
` +
forEachUniqueTable(
model,
(_key, { idField, name: resourceName, fields }) => {
resourceName = getResourceName(resourceName);
return (
`
<EntityType Name="${resourceName}">
<Key>
<PropertyRef Name="${idField}" />
</Key>
` +
fields
.filter(({ dataType }) => dataType !== 'ForeignKey')
.map(({ dataType, fieldName, required }) => {
dataType = resolveDataType(dataType);
fieldName = getResourceName(fieldName);
return `<Property Name="${fieldName}" Type="${dataType}" Nullable="${!required}" />`;
})
.join('\n') +
'\n' +
fields
.filter(
({ dataType, references }) =>
dataType === 'ForeignKey' && references != null,
)
.map(({ fieldName, references }) => {
const { resourceName: referencedResource } = references!;
fieldName = getResourceName(fieldName);
return `<NavigationProperty Name="${fieldName}" Relationship="${vocabulary}.${
resourceName + referencedResource
}" FromRole="${resourceName}" ToRole="${referencedResource}" />`;
})
.join('\n') +
'\n' +
`
</EntityType>`
);
},
).join('\n\n') +
associations
.map(({ name, ends }) => {
name = getResourceName(name);
return (
`<Association Name="${name}">` +
'\n\t' +
ends
.map(
({ resourceName, cardinality }) =>
`<End Role="${resourceName}" Type="${vocabulary}.${resourceName}" Multiplicity="${cardinality}" />`,
)
.join('\n\t') +
'\n' +
`</Association>`
);
})
.join('\n') +
`
<EntityContainer Name="${vocabulary}Service" m:IsDefaultEntityContainer="true">
` +
forEachUniqueTable(model, (_key, { name: resourceName }) => {
resourceName = getResourceName(resourceName);
return `<EntitySet Name="${resourceName}" EntityType="${vocabulary}.${resourceName}" />`;
}).join('\n') +
'\n' +
associations
.map(({ name, ends }) => {
name = getResourceName(name);
return (
`<AssociationSet Name="${name}" Association="${vocabulary}.${name}">` +
'\n\t' +
ends
.map(
({ resourceName }) =>
`<End Role="${resourceName}" EntitySet="${vocabulary}.${resourceName}" />`,
)
.join('\n\t') +
`
</AssociationSet>`
);
})
.join('\n') +
`
</EntityContainer>` +
Object.values(complexTypes).join('\n') +
`
</Schema>
</edmx:DataServices>
</edmx:Edmx>`
);
};
generateODataMetadata.version = version;