@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
203 lines (186 loc) • 4.94 kB
text/typescript
declare module '@resin/abstract-sql-compiler' {
interface AbstractSqlTable {
fetchProcessingFields?: {
[field: string]: (field: any) => Bluebird<any>;
};
localFields?: {
[odataName: string]: true;
};
}
}
import type {
AbstractSqlModel,
AbstractSqlTable,
} from '@resin/abstract-sql-compiler';
import type { Result, Row } from '../database-layer/db';
import { sqlNameToODataName } from '@resin/odata-to-abstract-sql';
import * as sbvrTypes from '@resin/sbvr-types';
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { resolveNavigationResource, resolveSynonym } from './sbvr-utils';
const checkForExpansion = async (
vocab: string,
abstractSqlModel: AbstractSqlModel,
parentResourceName: string,
fieldName: string,
instance: Row,
) => {
let field = instance[fieldName];
if (field == null) {
return;
}
if (typeof field === 'string') {
try {
field = JSON.parse(field);
} catch (_e) {
// If we can't JSON.parse the field then we use it directly.
}
}
if (Array.isArray(field)) {
const mappingResourceName = resolveNavigationResource(
{
abstractSqlModel,
vocabulary: vocab,
resourceName: parentResourceName,
},
fieldName,
);
const expandedField = await process(
vocab,
abstractSqlModel,
mappingResourceName,
field,
);
instance[fieldName] = expandedField;
} else {
const mappingResourceName = resolveNavigationResource(
{
abstractSqlModel,
vocabulary: vocab,
resourceName: parentResourceName,
},
fieldName,
);
instance[fieldName] = {
__deferred: {
uri: '/' + vocab + '/' + mappingResourceName + '(' + field + ')',
},
__id: field,
};
}
};
export const resourceURI = (
vocab: string,
resourceName: string,
id: string | number,
): string | undefined => {
if (id == null) {
return;
}
if (typeof id === 'string') {
id = "'" + encodeURIComponent(id) + "'";
}
return `/${vocab}/${resourceName}(@id)?@id=${id}`;
};
const getLocalFields = (table: AbstractSqlTable) => {
if (table.localFields == null) {
table.localFields = {};
for (const { fieldName, dataType } of table.fields) {
if (dataType !== 'ForeignKey') {
const odataName = sqlNameToODataName(fieldName);
table.localFields[odataName] = true;
}
}
}
return table.localFields;
};
const getFetchProcessingFields = (table: AbstractSqlTable) => {
if (table.fetchProcessingFields == null) {
table.fetchProcessingFields = _(table.fields)
.filter(
({ dataType }) =>
sbvrTypes[dataType] != null &&
sbvrTypes[dataType].fetchProcessing != null,
)
.map(({ fieldName, dataType }) => {
const odataName = sqlNameToODataName(fieldName);
return [odataName, sbvrTypes[dataType].fetchProcessing];
})
.fromPairs()
.value();
}
return table.fetchProcessingFields!;
};
export const process = async (
vocab: string,
abstractSqlModel: AbstractSqlModel,
resourceName: string,
rows: Result['rows'],
): Promise<number | Row[]> => {
if (rows.length === 0) {
return [];
}
if (rows.length === 1) {
if (rows[0].$count != null) {
const count = parseInt(rows[0].$count, 10);
return count;
}
}
const sqlResourceName = resolveSynonym({
abstractSqlModel,
vocabulary: vocab,
resourceName,
});
const table = abstractSqlModel.tables[sqlResourceName];
const odataIdField = sqlNameToODataName(table.idField);
const instances = rows.map((instance) => {
instance.__metadata = {
uri: resourceURI(vocab, resourceName, instance[odataIdField]),
};
return instance;
});
const instanceKeys = Object.keys(instances[0]);
const localFields = getLocalFields(table);
// We check that it's not a local field, rather than that it is a foreign key because of the case where the foreign key is on the other resource
// and hence not known to this resource
const expandableFields = instanceKeys.filter(
(fieldName) =>
!fieldName.startsWith('__') && !localFields.hasOwnProperty(fieldName),
);
if (expandableFields.length > 0) {
await Bluebird.map(instances, (instance) =>
Bluebird.map(expandableFields, (fieldName) =>
checkForExpansion(
vocab,
abstractSqlModel,
sqlResourceName,
fieldName,
instance,
),
),
);
}
const fetchProcessingFields = getFetchProcessingFields(table);
const processedFields = instanceKeys.filter(
(fieldName) =>
!fieldName.startsWith('__') &&
fetchProcessingFields.hasOwnProperty(fieldName),
);
if (processedFields.length > 0) {
await Bluebird.map(instances, (instance) =>
Bluebird.map(processedFields, async (fieldName) => {
const result = await fetchProcessingFields[fieldName](
instance[fieldName],
);
instance[fieldName] = result;
}),
);
}
return instances;
};
export const prepareModel = (abstractSqlModel: AbstractSqlModel) => {
_.forEach(abstractSqlModel.tables, (table) => {
getLocalFields(table);
getFetchProcessingFields(table);
});
};