Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to infer field type from data #5485

Merged
merged 1 commit into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ra-core/src/inference/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import getElementsFromRecords from './getElementsFromRecords';
import InferredElement from './InferredElement';

export * from './inferTypeFromValues';

export { getElementsFromRecords, InferredElement };
169 changes: 169 additions & 0 deletions packages/ra-core/src/inference/inferTypeFromValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import inflection from 'inflection';

import getValuesFromRecords from './getValuesFromRecords';

import {
isObject,
valuesAreArray,
valuesAreBoolean,
valuesAreDate,
valuesAreDateString,
valuesAreHtml,
valuesAreInteger,
valuesAreNumeric,
valuesAreObject,
valuesAreString,
} from './assertions';

const types = [
'array',
'boolean',
'date',
'email',
'id',
'number',
'reference',
'referenceChild',
'referenceArray',
'referenceArrayChild',
'richText',
'string',
'url',
] as const;

export type PossibleInferredElementTypes = typeof types[number];

export interface InferredElementDescription {
type: PossibleInferredElementTypes;
props?: any;
children?: InferredElementDescription | InferredElementDescription[];
}

/**
* Guesses an element type based on an array of values
*
* @example
* inferElementFromValues(
* 'address',
* ['2 Baker Street', '1 Downing street'],
* );
* // { type: 'string', props: { source: 'address' } }
*
* @param {string} name Property name, e.g. 'date_of_birth'
* @param {any[]} values an array of values from which to determine the type, e.g. [12, 34.4, 43]
*/
export const inferTypeFromValues = (
name,
values = []
): InferredElementDescription => {
if (name === 'id') {
return { type: 'id', props: { source: name } };
}
if (name.substr(name.length - 3) === '_id') {
return {
type: 'reference',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 3)
),
},
children: { type: 'referenceChild' },
};
}
if (name.substr(name.length - 2) === 'Id') {
return {
type: 'reference',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 2)
),
},
children: { type: 'referenceChild' },
};
}
if (name.substr(name.length - 4) === '_ids') {
return {
type: 'referenceArray',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 4)
),
},
children: { type: 'referenceArrayChild' },
};
}
if (name.substr(name.length - 3) === 'Ids') {
return {
type: 'referenceArray',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 3)
),
},
children: { type: 'referenceArrayChild' },
};
}
if (values.length === 0) {
if (name === 'email') {
return { type: 'email', props: { source: name } };
}
if (name === 'url') {
return { type: 'url', props: { source: name } };
}
// FIXME introspect further using name
return { type: 'string', props: { source: name } };
}
if (valuesAreArray(values)) {
if (isObject(values[0][0])) {
const leafValues = getValuesFromRecords(
values.reduce((acc, vals) => acc.concat(vals), [])
);
// FIXME bad visual representation
return {
type: 'array',
props: { source: name },
children: Object.keys(leafValues).map(leafName =>
inferTypeFromValues(leafName, leafValues[leafName])
),
};
}
// FIXME introspect further
return { type: 'string', props: { source: name } };
}
if (valuesAreBoolean(values)) {
return { type: 'boolean', props: { source: name } };
}
if (valuesAreDate(values)) {
return { type: 'date', props: { source: name } };
}
if (valuesAreString(values)) {
if (name === 'email') {
return { type: 'email', props: { source: name } };
}
if (name === 'url') {
return { type: 'url', props: { source: name } };
}
if (valuesAreDateString(values)) {
return { type: 'date', props: { source: name } };
}
if (valuesAreHtml(values)) {
return { type: 'richText', props: { source: name } };
}
return { type: 'string', props: { source: name } };
}
if (valuesAreInteger(values) || valuesAreNumeric(values)) {
return { type: 'number', props: { source: name } };
}
if (valuesAreObject(values)) {
// we need to go deeper
// Arbitrarily, choose the first prop of the first object
const propName = Object.keys(values[0]).shift();
const leafValues = values.map(v => v[propName]);
return inferTypeFromValues(`${name}.${propName}`, leafValues);
}
return { type: 'string', props: { source: name } };
};
166 changes: 166 additions & 0 deletions packages/ra-core/src/inference/inferTypesFromValues.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import expect from 'expect';
import { inferTypeFromValues } from './inferTypeFromValues';

describe('inferTypeFromValues', () => {
it('should return an InferredElement', () => {
expect(inferTypeFromValues('id', ['foo'])).toEqual({
type: 'id',
props: { source: 'id' },
});
});
it('should return an id field for field named id', () => {
expect(inferTypeFromValues('id', ['foo', 'bar'])).toEqual({
type: 'id',
props: { source: 'id' },
});
});
it('should return a reference field for field named *_id', () => {
expect(inferTypeFromValues('foo_id', ['foo', 'bar'])).toEqual({
type: 'reference',
props: { source: 'foo_id', reference: 'foos' },
children: { type: 'referenceChild' },
});
});
it('should return a reference field for field named *Id', () => {
expect(inferTypeFromValues('fooId', ['foo', 'bar'])).toEqual({
type: 'reference',
props: { source: 'fooId', reference: 'foos' },
children: { type: 'referenceChild' },
});
});
it('should return a reference array field for field named *_ids', () => {
expect(inferTypeFromValues('foo_ids', ['foo', 'bar'])).toEqual({
type: 'referenceArray',
props: { source: 'foo_ids', reference: 'foos' },
children: { type: 'referenceArrayChild' },
});
});
it('should return a reference array field for field named *Ids', () => {
expect(inferTypeFromValues('fooIds', ['foo', 'bar'])).toEqual({
type: 'referenceArray',
props: { source: 'fooIds', reference: 'foos' },
children: { type: 'referenceArrayChild' },
});
});
it('should return a string field for no values', () => {
expect(inferTypeFromValues('foo', [])).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return an array field for array of object values', () => {
expect(
inferTypeFromValues('foo', [
[{ bar: 1 }, { bar: 2 }],
[{ bar: 3 }, { bar: 4 }],
])
).toEqual({
type: 'array',
props: { source: 'foo' },
children: [{ type: 'number', props: { source: 'bar' } }],
});
});
it('should return a string field for array of non-object values', () => {
expect(
inferTypeFromValues('foo', [
[1, 2],
[3, 4],
])
).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return a boolean field for boolean values', () => {
expect(inferTypeFromValues('foo', [true, false, true])).toEqual({
type: 'boolean',
props: { source: 'foo' },
});
});
it('should return a date field for date values', () => {
expect(
inferTypeFromValues('foo', [
new Date('2018-10-01'),
new Date('2018-12-03'),
])
).toEqual({
type: 'date',
props: { source: 'foo' },
});
});
it('should return an email field for email name', () => {
expect(inferTypeFromValues('email', ['whatever'])).toEqual({
type: 'email',
props: { source: 'email' },
});
});
it.skip('should return an email field for email string values', () => {
expect(
inferTypeFromValues('foo', ['me@example.com', 'you@foo.co.uk'])
).toEqual({
type: 'email',
props: { source: 'foo' },
});
});
it('should return an url field for url name', () => {
expect(inferTypeFromValues('url', ['whatever', 'whatever'])).toEqual({
type: 'url',
props: { source: 'url' },
});
});
it.skip('should return an url field for url string values', () => {
expect(
inferTypeFromValues('foo', [
'http://foo.com/bar',
'https://www.foo.com/index.html#foo',
])
).toEqual({
type: 'url',
props: { source: 'foo' },
});
});
it('should return a date field for date string values', () => {
expect(
inferTypeFromValues('foo', ['2018-10-01', '2018-12-03'])
).toEqual({
type: 'date',
props: { source: 'foo' },
});
});
it('should return a rich text field for HTML values', () => {
expect(
inferTypeFromValues('foo', [
'This is <h1>Good</h1>',
'<body><h1>hello</h1>World</body>',
])
).toEqual({
type: 'richText',
props: { source: 'foo' },
});
});
it('should return a string field for string values', () => {
expect(
inferTypeFromValues('foo', ['This is Good', 'hello, World!'])
).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return a number field for number values', () => {
expect(inferTypeFromValues('foo', [12, 1e23, 653.56])).toEqual({
type: 'number',
props: { source: 'foo' },
});
});
it('should return a typed field for object values', () => {
expect(
inferTypeFromValues('foo', [
{ bar: 1, baz: 2 },
{ bar: 3, baz: 4 },
])
).toEqual({
type: 'number',
props: { source: 'foo.bar' },
});
});
});