Transform default values for sqlite, add casting for boolean

This commit is contained in:
rijkvanzanten
2020-07-28 17:56:08 -04:00
parent a7de4c37d8
commit 937f9409bb
6 changed files with 574 additions and 438 deletions

View File

@@ -563,6 +563,7 @@ rows:
# @todo update this to be a versioned link
note: '[Learn More](https://docs.directus.io/guides/collections.html#hidden).'
sort: 3
special: boolean
width: half
- collection: directus_collections
field: singleton
@@ -571,6 +572,7 @@ rows:
# @todo update this to be a versioned link
note: '[Learn More](https://docs.directus.io/guides/collections.html#single).'
sort: 4
special: boolean
width: half
- collection: directus_collections
field: icon
@@ -578,7 +580,7 @@ rows:
locked: true
sort: 5
- collection: directus_collections
field: translations
field: translation
interface: repeater
locked: true
options:
@@ -593,52 +595,10 @@ rows:
type: string
interface: text-input
width: half
special: json
sort: 6
width: full
- collection: directus_folders
field: id
interface: text-input
locked: true
special: uuid
- collection: directus_files
field: id
hidden: true
interface: text-input
locked: true
special: uuid
- collection: directus_files
field: title
interface: text-input
locked: true
options:
iconRight: title
sort: 1
width: full
- collection: directus_files
field: description
interface: wysiwyg
locked: true
sort: 2
width: full
- collection: directus_files
field: tags
interface: tags
locked: true
options:
iconRight: local_offer
sort: 3
width: half
- collection: directus_files
field: location
interface: text-input
locked: true
options:
iconRight: place
sort: 4
width: half
- collection: directus_roles
field: id
hidden: true
@@ -668,6 +628,7 @@ rows:
interface: toggle
locked: true
sort: 4
special: boolean
width: full
- collection: directus_roles
field: module_list
@@ -698,6 +659,7 @@ rows:
options:
iconRight: link
placeholder: Relative or absolute URL...
special: json
sort: 5
width: full
- collection: directus_roles
@@ -743,215 +705,24 @@ rows:
type: string
interface: collections
width: full
special: json
sort: 6
width: full
- collection: directus_roles
field: users
interface: one-to-many
locked: true
sort: 7
width: full
special: o2m
- collection: directus_settings
field: project_name
interface: text-input
locked: true
options:
iconRight: title
placeholder: My project...
sort: 1
translation:
locale: en-US
translation: Name
width: half
- collection: directus_settings
field: project_url
interface: text-input
locked: true
options:
iconRight: link
placeholder: https://example.com
sort: 2
translation:
locale: en-US
translation: Website
width: half
- collection: directus_settings
field: project_logo
interface: file
locked: true
note: White 40x40 SVG/PNG
sort: 3
translation:
locale: en-US
translation: Brand Logo
width: half
- collection: directus_settings
field: project_color
interface: color
locked: true
note: Login & Logo Background
sort: 4
translation:
locale: en-US
translation: Brand Color
width: half
- collection: directus_settings
field: project_foreground
interface: image
locked: true
sort: 5
translation:
locale: en-US
translation: Login Foreground
width: half
- collection: directus_settings
field: project_background
interface: image
locked: true
sort: 6
translation:
locale: en-US
translation: Login Background
width: half
- collection: directus_settings
field: project_note
interface: text-input
locked: true
options:
placeholder: A short, public message that supports markdown formatting...
sort: 7
translation:
locale: en-US
translation: Login Message
width: full
- collection: directus_settings
field: project_telemetry
- collection: directus_roles
field: admin
interface: toggle
locked: true
options:
label: Send Anonymous Diagnostics
special: boolean
sort: 8
width: half
- collection: directus_settings
field: security_divider
interface: divider
locked: true
options:
icon: security
title: Security
color: '#2f80ed'
sort: 9
width: full
- collection: directus_settings
field: auth_password_policy
interface: dropdown
locked: true
options:
choices:
- value: null
text: None  Not Recommended
- value: "/^.{8,}$/"
text: Weak Minimum 8 Characters
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
text: Strong Upper / Lowercase / Numbers / Special
sort: 10
width: half
- collection: directus_settings
field: auth_idle_timeout
interface: numeric
locked: true
options:
iconRight: timer
sort: 11
width: half
- collection: directus_settings
field: auth_login_attempts
interface: numeric
locked: true
options:
iconRight: lock
sort: 12
width: half
- collection: directus_settings
field: files_divider
interface: divider
locked: true
options:
icon: storage
title: Files & Thumbnails
color: '#2f80ed'
sort: 13
width: full
- collection: directus_settings
field: storage_asset_presets
interface: repeater
locked: true
options:
fields:
- field: key
interface: slug
name: Key
options:
onlyOnCreate: false
required: true
type: string
width: half
- field: fit
interface: dropdown
name: Fit
options:
choices:
- value: contain
text: Contain (preserve aspect ratio)
- value: crop
text: Crop (forces exact size)
required: true
type: string
width: half
- field: width
interface: numeric
name: Width
required: true
type: integer
width: half
- field: height
interface: numeric
name: Height
required: true
type: integer
width: half
- field: quality
default_value: 80
interface: slider
name: Quality
options:
max: 100
min: 0
step: 1
required: true
type: integer
width: full
template: "{{key}}"
sort: 13
width: full
- collection: directus_settings
field: storage_asset_transform
interface: dropdown
locked: true
options:
choices:
- value: all
text: All
- value: none
text: None
- value: presets
text: Presets Only\
sort: 14
width: half
- collection: directus_users
field: id
hidden: true
@@ -1467,6 +1238,335 @@ rows:
sort: 16
width: full
# directus_fields isn't surfaced in the app
- collection: directus_fields
field: options
hidden: true
locked: true
special: json
- collection: directus_fields
field: display_options
hidden: true
locked: true
special: json
- collection: directus_fields
field: locked
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: readonly
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: hidden
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: translation
hidden: true
locked: true
special: json
# directus_activity isn't surfaced in the app
- collection: directus_folders
field: id
interface: text-input
locked: true
special: uuid
- collection: directus_files
field: id
hidden: true
interface: text-input
locked: true
special: uuid
- collection: directus_files
field: title
interface: text-input
locked: true
options:
iconRight: title
sort: 1
width: full
- collection: directus_files
field: description
interface: wysiwyg
locked: true
sort: 2
width: full
- collection: directus_files
field: tags
interface: tags
locked: true
options:
iconRight: local_offer
sort: 3
width: half
- collection: directus_files
field: location
interface: text-input
locked: true
options:
iconRight: place
sort: 4
width: half
- collection: directus_files
field: metadata
hidden: true
locked: true
special: json
# directus_permissions isn't surfaced in the app
- collection: directus_permissions
field: permissions
hidden: true
locked: true
special: json
- collection: directus_permissions
field: presets
hidden: true
locked: true
special: json
# directus_presets isn't surfaced in the app
- collection: directus_presets
field: filters
hidden: true
locked: true
special: json
- collection: directus_presets
field: view_query
hidden: true
locked: true
special: json
- collection: directus_presets
field: view_options
hidden: true
locked: true
special: json
# directus_relations isn't surfaced in the app
# directus_revisions isn't surfaced in the app
- collection: directus_revisions
field: data
hidden: true
locked: true
special: json
- collection: directus_revisions
field: delta
hidden: true
locked: true
special: json
# diretus_sessions isn't surfaced in the app
- collection: directus_settings
field: project_name
interface: text-input
locked: true
options:
iconRight: title
placeholder: My project...
sort: 1
translation:
locale: en-US
translation: Name
width: half
- collection: directus_settings
field: project_url
interface: text-input
locked: true
options:
iconRight: link
placeholder: https://example.com
sort: 2
translation:
locale: en-US
translation: Website
width: half
- collection: directus_settings
field: project_logo
interface: file
locked: true
note: White 40x40 SVG/PNG
sort: 3
translation:
locale: en-US
translation: Brand Logo
width: half
- collection: directus_settings
field: project_color
interface: color
locked: true
note: Login & Logo Background
sort: 4
translation:
locale: en-US
translation: Brand Color
width: half
- collection: directus_settings
field: project_foreground
interface: image
locked: true
sort: 5
translation:
locale: en-US
translation: Login Foreground
width: half
- collection: directus_settings
field: project_background
interface: image
locked: true
sort: 6
translation:
locale: en-US
translation: Login Background
width: half
- collection: directus_settings
field: project_note
interface: text-input
locked: true
options:
placeholder: A short, public message that supports markdown formatting...
sort: 7
translation:
locale: en-US
translation: Login Message
width: full
- collection: directus_settings
field: project_telemetry
interface: toggle
locked: true
options:
label: Send Anonymous Diagnostics
special: boolean
sort: 8
width: half
- collection: directus_settings
field: security_divider
interface: divider
locked: true
options:
icon: security
title: Security
color: '#2f80ed'
sort: 9
width: full
- collection: directus_settings
field: auth_password_policy
interface: dropdown
locked: true
options:
choices:
- value: null
text: None  Not Recommended
- value: "/^.{8,}$/"
text: Weak Minimum 8 Characters
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
text: Strong Upper / Lowercase / Numbers / Special
sort: 10
width: half
- collection: directus_settings
field: auth_idle_timeout
interface: numeric
locked: true
options:
iconRight: timer
sort: 11
width: half
- collection: directus_settings
field: auth_login_attempts
interface: numeric
locked: true
options:
iconRight: lock
sort: 12
width: half
- collection: directus_settings
field: files_divider
interface: divider
locked: true
options:
icon: storage
title: Files & Thumbnails
color: '#2f80ed'
sort: 13
width: full
- collection: directus_settings
field: storage_asset_presets
interface: repeater
locked: true
options:
fields:
- field: key
interface: slug
name: Key
options:
onlyOnCreate: false
required: true
type: string
width: half
- field: fit
interface: dropdown
name: Fit
options:
choices:
- value: contain
text: Contain (preserve aspect ratio)
- value: crop
text: Crop (forces exact size)
required: true
type: string
width: half
- field: width
interface: numeric
name: Width
required: true
type: integer
width: half
- field: height
interface: numeric
name: Height
required: true
type: integer
width: half
- field: quality
default_value: 80
interface: slider
name: Quality
options:
max: 100
min: 0
step: 1
required: true
type: integer
width: full
template: "{{key}}"
special: json
sort: 13
width: full
- collection: directus_settings
field: storage_asset_transform
interface: dropdown
locked: true
options:
choices:
- value: all
text: All
- value: none
text: None
- value: presets
text: Presets Only\
sort: 14
width: half
# directus_webhooks TBD
directus_permissions:
defaults:
id: null

View File

@@ -1,6 +1,6 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import * as FieldsService from '../services/fields';
import FieldsService from '../services/fields';
import validateCollection from '../middleware/collection-exists';
import { schemaInspector } from '../database';
import { FieldNotFoundException, InvalidPayloadException } from '../exceptions';
@@ -21,7 +21,9 @@ router.get(
'/',
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
const fields = await FieldsService.readAll();
const service = new FieldsService({ accountability: req.accountability });
const fields = await service.readAll();
return res.json({ data: fields || null });
})
);
@@ -31,7 +33,9 @@ router.get(
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
const fields = await FieldsService.readAll(req.collection);
const service = new FieldsService({ accountability: req.accountability });
const fields = await service.readAll(req.collection);
return res.json({ data: fields || null });
})
);
@@ -41,10 +45,12 @@ router.get(
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
const service = new FieldsService({ accountability: req.accountability });
const exists = await schemaInspector.hasColumn(req.collection, req.params.field);
if (exists === false) throw new FieldNotFoundException(req.collection, req.params.field);
const field = await FieldsService.readOne(req.collection, req.params.field);
const field = await service.readOne(req.collection, req.params.field);
return res.json({ data: field || null });
})
);
@@ -70,6 +76,8 @@ router.post(
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
const service = new FieldsService({ accountability: req.accountability });
const { error } = newFieldSchema.validate(req.body);
if (error) {
@@ -78,9 +86,9 @@ router.post(
const field: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
await FieldsService.createField(req.params.collection, field, req.accountability);
await service.createField(req.params.collection, field, req.accountability);
const createdField = await FieldsService.readOne(
const createdField = await service.readOne(
req.params.collection,
field.field,
req.accountability
@@ -95,15 +103,17 @@ router.patch(
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
const service = new FieldsService({ accountability: req.accountability });
if (Array.isArray(req.body) === false)
throw new InvalidPayloadException('Submitted body has to be an array.');
let results: any = [];
for (const field of req.body) {
await FieldsService.updateField(req.params.collection, field, req.accountability);
await service.updateField(req.params.collection, field, req.accountability);
const updatedField = await FieldsService.readOne(
const updatedField = await service.readOne(
req.params.collection,
field.field,
req.accountability
@@ -122,13 +132,15 @@ router.patch(
useCollection('directus_fields'),
// @todo: validate field
asyncHandler(async (req, res) => {
const service = new FieldsService({ accountability: req.accountability });
const fieldData: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
if (!fieldData.field) fieldData.field = req.params.field;
await FieldsService.updateField(req.params.collection, fieldData, req.accountability);
await service.updateField(req.params.collection, fieldData, req.accountability);
const updatedField = await FieldsService.readOne(
const updatedField = await service.readOne(
req.params.collection,
req.params.field,
req.accountability
@@ -143,11 +155,9 @@ router.delete(
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
await FieldsService.deleteField(
req.params.collection,
req.params.field,
req.accountability
);
const service = new FieldsService({ accountability: req.accountability });
await service.deleteField(req.params.collection, req.params.field, req.accountability);
res.status(200).end();
})

View File

@@ -1,206 +1,198 @@
import database, { schemaInspector } from '../database';
import { Field } from '../types/field';
import { uniq } from 'lodash';
import { Accountability } from '../types';
import { Accountability, AbstractServiceOptions } from '../types';
import ItemsService from '../services/items';
import { ColumnBuilder } from 'knex';
import getLocalType from '../utils/get-local-type';
import { types } from '../types';
import { InvalidPayloadException, FieldNotFoundException } from '../exceptions';
/**
* @TODO turn into class
*/
export const fieldsInCollection = async (collection: string) => {
const [fields, columns] = await Promise.all([
database.select('field').from('directus_fields').where({ collection }),
schemaInspector.columns(collection),
]);
return uniq([...fields.map(({ field }) => field), ...columns.map(({ column }) => column)]);
};
/**
* @TODO
* update read to use ItemsService instead of direct to db
*/
export const readAll = async (collection?: string) => {
const fieldsQuery = database.select('*').from('directus_fields');
if (collection) {
fieldsQuery.where({ collection });
}
const [columns, fields] = await Promise.all([
schemaInspector.columnInfo(collection),
fieldsQuery,
]);
return columns.map((column) => {
const field = fields.find(
(field) => field.field === column.name && field.collection === column.table
);
const data = {
collection: column.table,
field: column.name,
type: column ? getLocalType(column.type) : 'alias',
database: column,
system: field || null,
};
return data;
});
};
/** @todo add accountability */
export const readOne = async (
collection: string,
field: string,
accountability?: Accountability
) => {
let column;
const fieldInfo = await database
.select('*')
.from('directus_fields')
.where({ collection, field })
.first();
try {
column = await schemaInspector.columnInfo(collection, field);
} catch {}
const data = {
collection,
field,
type: column ? getLocalType(column.type) : 'alias',
database: column || null,
system: fieldInfo || null,
};
return data;
};
export const createField = async (
collection: string,
field: Partial<Field> & { field: string; type: typeof types[number] },
accountability?: Accountability
) => {
const itemsService = new ItemsService('directus_fields', { accountability });
/**
* @todo
* Check if table / directus_fields row already exists
*/
if (field.database) {
await database.schema.alterTable(collection, (table) => {
let column: ColumnBuilder;
if (!field.database) return;
if (field.type === 'string') {
column = table.string(
field.field,
field.database.max_length !== null ? field.database.max_length : undefined
);
} else if (['float', 'decimal'].includes(field.type)) {
const type = field.type as 'float' | 'decimal';
/** @todo add precision and scale support */
column = table[type](field.field /* precision, scale */);
} else {
column = table[field.type](field.field);
}
if (field.database.default_value) {
column.defaultTo(field.database.default_value);
}
if (field.database.is_nullable && field.database.is_nullable === true) {
column.nullable();
} else {
column.notNullable();
}
});
}
if (field.system) {
await itemsService.create({
...field.system,
collection: collection,
field: field.field,
});
}
};
/** @todo research how to make this happen in SQLite / Redshift */
import { FieldNotFoundException } from '../exceptions';
import Knex from 'knex';
type RawField = Partial<Field> & { field: string; type: typeof types[number] };
export const updateField = async (
collection: string,
field: RawField,
accountability?: Accountability
) => {
if (field.database) {
await database.schema.alterTable(collection, (table) => {
let column: ColumnBuilder;
export default class FieldsService {
knex: Knex;
accountability: Accountability | null;
service: ItemsService;
if (!field.database) return;
constructor(options?: AbstractServiceOptions) {
this.knex = options?.knex || database;
this.accountability = options?.accountability || null;
this.service = new ItemsService('directus_fields', options);
}
if (field.type === 'string') {
column = table.string(
field.field,
field.database.max_length !== null ? field.database.max_length : undefined
);
} else if (['float', 'decimal'].includes(field.type)) {
const type = field.type as 'float' | 'decimal';
/** @todo add precision and scale support */
column = table[type](field.field /* precision, scale */);
} else {
column = table[field.type](field.field);
}
async fieldsInCollection(collection: string) {
const [fields, columns] = await Promise.all([
this.service.readByQuery({ filter: { collection: { _eq: collection } } }),
schemaInspector.columns(collection),
]);
if (field.database.default_value) {
column.defaultTo(field.database.default_value);
}
return uniq([...fields.map(({ field }) => field), ...columns.map(({ column }) => column)]);
}
if (field.database.is_nullable && field.database.is_nullable === true) {
column.nullable();
} else {
column.notNullable();
}
async readAll(collection?: string) {
const fieldsQuery = this.knex.select('*').from('directus_fields');
column.alter();
if (collection) {
fieldsQuery.where({ collection });
}
let [columns, fields] = await Promise.all([
schemaInspector.columnInfo(collection),
fieldsQuery,
]);
return columns.map((column) => {
const field = fields.find(
(field) => field.field === column.name && field.collection === column.table
);
const data = {
collection: column.table,
field: column.name,
type: column ? getLocalType(column.type) : 'alias',
database: column,
system: field || null,
};
return data;
});
}
if (field.system) {
const record = await database
.select<{ id: number }>('id')
/** @todo add accountability */
async readOne(collection: string, field: string, accountability?: Accountability) {
let column;
const fieldInfo = await this.knex
.select('*')
.from('directus_fields')
.where({ collection, field: field.field })
.where({ collection, field })
.first();
if (!record) throw new FieldNotFoundException(collection, field.field);
await database('directus_fields')
.update(field.system)
.where({ collection, field: field.field });
try {
column = await schemaInspector.columnInfo(collection, field);
} catch {}
const data = {
collection,
field,
type: column ? getLocalType(column.type) : 'alias',
database: column || null,
system: fieldInfo || null,
};
return data;
}
return field.field;
};
async createField(
collection: string,
field: Partial<Field> & { field: string; type: typeof types[number] },
accountability?: Accountability
) {
const itemsService = new ItemsService('directus_fields', { accountability });
/** @todo save accountability */
export const deleteField = async (
collection: string,
field: string,
accountability?: Accountability
) => {
await database('directus_fields').delete().where({ collection, field });
/**
* @todo
* Check if table / directus_fields row already exists
*/
await database.schema.table(collection, (table) => {
table.dropColumn(field);
});
};
if (field.database) {
await database.schema.alterTable(collection, (table) => {
let column: ColumnBuilder;
if (!field.database) return;
if (field.type === 'string') {
column = table.string(
field.field,
field.database.max_length !== null ? field.database.max_length : undefined
);
} else if (['float', 'decimal'].includes(field.type)) {
const type = field.type as 'float' | 'decimal';
/** @todo add precision and scale support */
column = table[type](field.field /* precision, scale */);
} else {
column = table[field.type](field.field);
}
if (field.database.default_value) {
column.defaultTo(field.database.default_value);
}
if (field.database.is_nullable && field.database.is_nullable === true) {
column.nullable();
} else {
column.notNullable();
}
});
}
if (field.system) {
await itemsService.create({
...field.system,
collection: collection,
field: field.field,
});
}
}
/** @todo research how to make this happen in SQLite / Redshift */
async updateField(collection: string, field: RawField, accountability?: Accountability) {
if (field.database) {
await database.schema.alterTable(collection, (table) => {
let column: ColumnBuilder;
if (!field.database) return;
if (field.type === 'string') {
column = table.string(
field.field,
field.database.max_length !== null ? field.database.max_length : undefined
);
} else if (['float', 'decimal'].includes(field.type)) {
const type = field.type as 'float' | 'decimal';
/** @todo add precision and scale support */
column = table[type](field.field /* precision, scale */);
} else {
column = table[field.type](field.field);
}
if (field.database.default_value) {
column.defaultTo(field.database.default_value);
}
if (field.database.is_nullable && field.database.is_nullable === true) {
column.nullable();
} else {
column.notNullable();
}
column.alter();
});
}
if (field.system) {
const record = await database
.select<{ id: number }>('id')
.from('directus_fields')
.where({ collection, field: field.field })
.first();
if (!record) throw new FieldNotFoundException(collection, field.field);
await database('directus_fields')
.update(field.system)
.where({ collection, field: field.field });
}
return field.field;
}
/** @todo save accountability */
async deleteField(collection: string, field: string, accountability?: Accountability) {
await database('directus_fields').delete().where({ collection, field });
await database.schema.table(collection, (table) => {
table.dropColumn(field);
});
}
}

View File

@@ -15,9 +15,9 @@ import Knex from 'knex';
import PayloadService from './payload';
import AuthService from './auth';
import ActivityService from './activity';
import { pick, clone } from 'lodash';
import getDefaultValue from '../utils/get-default-value';
export default class ItemsService implements AbstractService {
collection: string;
@@ -202,7 +202,7 @@ export default class ItemsService implements AbstractService {
const defaults: Record<string, any> = {};
for (const column of columns) {
defaults[column.name] = column.default_value;
defaults[column.name] = getDefaultValue(column);
}
return defaults;

View File

@@ -67,6 +67,13 @@ export default class PayloadService {
// This is an non-existing column, so there isn't any data to save
return undefined;
},
async boolean(operation, value) {
if (operation === 'read') {
return value === true || value === 1 || value === '1';
}
return value;
},
};
processValues(operation: Operation, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;

View File

@@ -0,0 +1,27 @@
import { Column } from 'knex-schema-inspector/dist/types/column';
import getLocalType from './get-local-type';
export default function getDefaultValue(column: Column) {
const type = getLocalType(column.type);
let defaultValue = column.default_value || null;
if (defaultValue === null) return null;
// Check if the default is wrapped in an extra pair of quotes, this happens in SQLite
if (defaultValue.startsWith(`'`) && defaultValue.endsWith(`'`)) {
defaultValue = defaultValue.slice(1, -1);
}
switch (type) {
case 'bigInteger':
case 'integer':
case 'decimal':
case 'float':
return Number(defaultValue);
case 'boolean':
return !!Number(defaultValue);
default:
return defaultValue;
}
}