202 lines
5.4 KiB
JavaScript
202 lines
5.4 KiB
JavaScript
import { dayFromDate } from '../../shared/months';
|
|
import { toDateRepr, fromDateRepr } from '../models';
|
|
|
|
function isRequired(name, fieldDesc) {
|
|
return fieldDesc.required || name === 'id';
|
|
}
|
|
|
|
// TODO: All of the data type needs to check the input value. This
|
|
// doesn't just convert, it casts. See integer handling.
|
|
export function convertInputType(value, type) {
|
|
if (value === undefined) {
|
|
throw new Error('Query value cannot be undefined');
|
|
} else if (value === null) {
|
|
if (type === 'boolean') {
|
|
return 0;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'date':
|
|
if (value instanceof Date) {
|
|
return toDateRepr(dayFromDate(value));
|
|
} else if (
|
|
value.match(/^\d{4}-\d{2}-\d{2}$/) == null ||
|
|
value.date < '2000-01-01'
|
|
) {
|
|
throw new Error('Invalid date: ' + value);
|
|
}
|
|
|
|
return toDateRepr(value);
|
|
case 'date-month':
|
|
return toDateRepr(value.slice(0, 7));
|
|
case 'date-year':
|
|
return toDateRepr(value.slice(0, 4));
|
|
case 'boolean':
|
|
return value ? 1 : 0;
|
|
case 'id':
|
|
if (typeof value !== 'string' && value !== null) {
|
|
throw new Error('Invalid id, must be string: ' + value);
|
|
}
|
|
return value;
|
|
case 'integer':
|
|
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
return value;
|
|
} else {
|
|
throw new Error("Can't convert to integer: " + JSON.stringify(value));
|
|
}
|
|
case 'json':
|
|
return JSON.stringify(value);
|
|
default:
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function convertOutputType(value, type) {
|
|
if (value === null) {
|
|
if (type === 'boolean') {
|
|
return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'date':
|
|
return fromDateRepr(value);
|
|
case 'date-month':
|
|
return fromDateRepr(value).slice(0, 7);
|
|
case 'date-year':
|
|
return fromDateRepr(value).slice(0, 4);
|
|
case 'boolean':
|
|
return value === 1;
|
|
case 'json':
|
|
case 'json/fallback':
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (e) {
|
|
return type === 'json/fallback' ? value : null;
|
|
}
|
|
default:
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export function conform(schema, schemaConfig, table, obj, { skipNull } = {}) {
|
|
let tableSchema = schema[table];
|
|
if (tableSchema == null) {
|
|
throw new Error(`Table "${table}" does not exist`);
|
|
}
|
|
|
|
let views = schemaConfig.views || {};
|
|
|
|
// Rename fields if necessary
|
|
let fieldRef = field => {
|
|
if (views[table] && views[table].fields) {
|
|
return views[table].fields[field] || field;
|
|
}
|
|
return field;
|
|
};
|
|
|
|
return Object.fromEntries(
|
|
Object.keys(obj)
|
|
.map(field => {
|
|
// Fields that start with an underscore are ignored
|
|
if (field[0] === '_') {
|
|
return null;
|
|
}
|
|
|
|
let fieldDesc = tableSchema[field];
|
|
if (fieldDesc == null) {
|
|
throw new Error(
|
|
`Field "${field}" does not exist on table ${table}: ${JSON.stringify(
|
|
obj
|
|
)}`
|
|
);
|
|
}
|
|
|
|
if (isRequired(field, fieldDesc) && obj[field] == null) {
|
|
throw new Error(
|
|
`"${field}" is required for table "${table}": ${JSON.stringify(
|
|
obj
|
|
)}`
|
|
);
|
|
}
|
|
|
|
// This option removes null values (see `convertForInsert`)
|
|
if (skipNull && obj[field] == null) {
|
|
return null;
|
|
}
|
|
|
|
return [fieldRef(field), convertInputType(obj[field], fieldDesc.type)];
|
|
})
|
|
.filter(Boolean)
|
|
);
|
|
}
|
|
|
|
export function convertForInsert(schema, schemaConfig, table, rawObj) {
|
|
let obj = { ...rawObj };
|
|
|
|
let tableSchema = schema[table];
|
|
if (tableSchema == null) {
|
|
throw new Error(`Error inserting: table "${table}" does not exist`);
|
|
}
|
|
|
|
// Inserting checks all the fields in the table and adds any default
|
|
// values necessary
|
|
Object.keys(tableSchema).forEach(field => {
|
|
let fieldDesc = tableSchema[field];
|
|
|
|
if (obj[field] == null) {
|
|
if (fieldDesc.default !== undefined) {
|
|
obj[field] =
|
|
typeof fieldDesc.default === 'function'
|
|
? fieldDesc.default()
|
|
: fieldDesc.default;
|
|
} else if (isRequired(field, fieldDesc)) {
|
|
// Although this check is also done in `conform`, it only
|
|
// checks the fields in `obj`. For insert, we need to do it
|
|
// here to check that all required fields in the table exist
|
|
throw new Error(
|
|
`"${field}" is required for table "${table}": ${JSON.stringify(obj)}`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// We use `skipNull` to remove any null values. There's no need to
|
|
// set those when inserting, that will be the default and it reduces
|
|
// the amount of messages generated to sync
|
|
return conform(schema, schemaConfig, table, obj, { skipNull: true });
|
|
}
|
|
|
|
export function convertForUpdate(schema, schemaConfig, table, rawObj) {
|
|
let obj = { ...rawObj };
|
|
|
|
let tableSchema = schema[table];
|
|
if (tableSchema == null) {
|
|
throw new Error(`Error updating: table "${table}" does not exist`);
|
|
}
|
|
|
|
return conform(schema, schemaConfig, table, obj);
|
|
}
|
|
|
|
export function convertFromSelect(schema, schemaConfig, table, obj) {
|
|
let tableSchema = schema[table];
|
|
if (tableSchema == null) {
|
|
throw new Error(`Table "${table}" does not exist`);
|
|
}
|
|
|
|
let fields = Object.keys(tableSchema);
|
|
let result = {};
|
|
for (let i = 0; i < fields.length; i++) {
|
|
let fieldName = fields[i];
|
|
let fieldDesc = tableSchema[fieldName];
|
|
|
|
result[fieldName] = convertOutputType(obj[fieldName], fieldDesc.type);
|
|
}
|
|
return result;
|
|
}
|