actual/packages/loot-core/src/server/aql/schema-helpers.js

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;
}