Skip to content

Commit

Permalink
Add dataType option to Select fields (#2617)
Browse files Browse the repository at this point in the history
* Add dataType option to Select fields

* Adding changeset

* Filter test all select data types

* Fix filtering for string options in the Admin UI

* Improving helpful error messages for invalid Select option values

* Adding explicit enum dataType to select text-fixtures

Co-authored-by: Mike <mike@madebymike.com.au>
  • Loading branch information
JedWatson and MadeByMike committed Apr 3, 2020
1 parent 665741e commit d138736
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-snails-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/fields': minor
---

Added dataType support to Select fields, values can now be stored as enums, strings or integers
101 changes: 87 additions & 14 deletions packages/fields/src/types/Select/Implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,62 @@ function initOptions(options) {
});
}

const VALID_DATA_TYPES = ['enum', 'string', 'integer'];
const DOCS_URL = 'https://keystonejs.com/keystonejs/fields/src/types/select/';

function validateOptions({ options, dataType, listKey, path }) {
if (!VALID_DATA_TYPES.includes(dataType)) {
throw new Error(
`
🚫 The select field ${listKey}.${path} is not configured with a valid data type;
📖 see ${DOCS_URL}
`
);
}
options.forEach((option, i) => {
if (dataType === 'enum') {
if (!/^[a-zA-Z]\w*$/.test(option.value)) {
throw new Error(
`
🚫 The select field ${listKey}.${path} contains an invalid enum value ("${option.value}") in option ${i}
👉 You may want to use the "string" dataType
📖 see ${DOCS_URL}
`
);
}
} else if (dataType === 'string') {
if (typeof option.value !== 'string') {
const integerHint =
typeof option.value === 'number'
? `
👉 Did you mean to use the the "integer" dataType?`
: '';
throw new Error(
`
🚫 The select field ${listKey}.${path} contains an invalid value (type ${typeof option.value}) in option ${i}${integerHint}
📖 see ${DOCS_URL}
`
);
}
} else if (dataType === 'integer') {
if (!Number.isInteger(option.value)) {
throw new Error(
`
🚫 The select field ${listKey}.${path} contains an invalid integer value ("${option.value}") in option ${i}
📖 see ${DOCS_URL}
`
);
}
}
});
}

export class Select extends Implementation {
constructor(path, { options }) {
constructor(path, { options, dataType = 'enum' }) {
super(...arguments);
this.options = initOptions(options);
validateOptions({ options: this.options, dataType, listKey: this.listKey, path });
this.dataType = dataType;
this.isOrderable = true;
}
gqlOutputFields() {
Expand All @@ -26,25 +78,32 @@ export class Select extends Implementation {
}

getTypeName() {
return `${this.listKey}${inflection.classify(this.path)}Type`;
if (this.dataType === 'enum') {
return `${this.listKey}${inflection.classify(this.path)}Type`;
} else if (this.dataType === 'integer') {
return 'Int';
} else {
return 'String';
}
}
getGqlAuxTypes() {
// TODO: I'm really not sure it's safe to generate GraphQL Enums from
// whatever options people provide, this could easily break with spaces and
// special characters in values so may not be worth it...
return [
`
return this.dataType === 'enum'
? [
`
enum ${this.getTypeName()} {
${this.options.map(i => i.value).join('\n ')}
}
`,
];
]
: [];
}

extendAdminMeta(meta) {
return { ...meta, options: this.options };
const { options, dataType } = this;
return { ...meta, options, dataType };
}
gqlQueryInputFields() {
// TODO: This could be extended for Int type options with numeric filters
return [
...this.equalityInputFields(this.getTypeName()),
...this.inInputFields(this.getTypeName()),
Expand All @@ -70,7 +129,14 @@ const CommonSelectInterface = superclass =>

export class MongoSelectInterface extends CommonSelectInterface(MongooseFieldAdapter) {
addToMongooseSchema(schema) {
schema.add({ [this.path]: this.mergeSchemaOptions({ type: String }, this.config) });
const options =
this.field.dataType === 'integer'
? { type: Number }
: {
type: String,
enum: [...this.field.options.map(i => i.value), null],
};
schema.add({ [this.path]: this.mergeSchemaOptions(options, this.config) });
}
}

Expand All @@ -82,10 +148,17 @@ export class KnexSelectInterface extends CommonSelectInterface(KnexFieldAdapter)
}

addToTableSchema(table) {
const column = table.enu(
this.path,
this.field.options.map(({ value }) => value)
);
let column;
if (this.field.dataType === 'enum') {
column = table.enu(
this.path,
this.field.options.map(({ value }) => value)
);
} else if (this.field.dataType === 'integer') {
column = table.integer(this.path);
} else {
column = table.text(this.path);
}
if (this.isUnique) column.unique();
else if (this.isIndexed) column.index();
if (this.isNotNullable) column.notNullable();
Expand Down
76 changes: 71 additions & 5 deletions packages/fields/src/types/Select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ keystone.createList('Orders', {

## Config

| Option | Type | Default | Description |
| ------------ | --------- | ------- | ----------------------------------------------------------------------- |
| `options` | \* | `null` | Defines the values (and labels) that can be be selected from, see below |
| `isRequired` | `Boolean` | `false` | Does this field require a value? |
| `isUnique` | `Boolean` | `false` | Adds a unique index that allows only unique values to be stored |
| Option | Type | Default | Description |
| ------------ | --------- | ------- | ------------------------------------------------------------------------------------------- |
| `options` | \* | `null` | Defines the values (and labels) that can be be selected from, see below |
| `dataType` | `String` | `enum` | Controls the data type stored in the database, and defined in the GraphQL schema, see below |
| `isRequired` | `Boolean` | `false` | Does this field require a value? |
| `isUnique` | `Boolean` | `false` | Adds a unique index that allows only unique values to be stored |

### `options`

Expand All @@ -48,3 +49,68 @@ keystone.createList('Rsvp', {
},
});
```

### `dataType`

The Select field can store its value as any of the three following types:

- `enum` (stored as a string in MongoDB)
- `string`
- `integer`

The dataType will also affect the type definition for the GraphQL Schema.

While `enum` is the default, and allows GraphQL to hint and validate input for the field, it also has a limited format for option values. If you want to store values starting with a number, or containing spaces, dashes and other special characters, consider using the `string` dataType.

The following example is not valid:

```js
// invalid enum options
const options = [
{ value: 'just one', label: 'Just One' },
{ value: '<10', label: 'Less than Ten' },
{ value: '100s', label: 'Hundreds' },
];

keystone.createList('Things', {
fields: {
scale: { type: Select, options },
},
});
```

Specifying the `string` dataType will work:

```js
keystone.createList('Things', {
fields: {
scale: { type: Select, options, dataType: 'string' },
},
});
```

If you use the `integer` dataType, options must be provided as integers, so you can't use the shorthand syntax for defining options.

```js
keystone.createList('Things', {
fields: {
number: { type: Select, options: '1, 2, 3', dataType: 'integer' },
},
});
```

Use the full syntax instead, and provide the values as numbers:

```js
const options = [
{ value: 1, label: 'One' },
{ value: 2, label: 'Two' },
{ value: 3, label: 'Three' },
];

keystone.createList('Things', {
fields: {
number: { type: Select, options: options, dataType: 'integer' },
},
});
```
Loading

0 comments on commit d138736

Please sign in to comment.