You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
8.0 KiB
230 lines
8.0 KiB
import {
|
|
api,
|
|
Builder,
|
|
collect,
|
|
Config,
|
|
Controller,
|
|
DatabaseService,
|
|
DataContainer,
|
|
hasOwnProperty,
|
|
HTTPError,
|
|
HTTPStatus,
|
|
Inject,
|
|
Injectable,
|
|
Maybe,
|
|
QueryRow,
|
|
} from '@extollo/lib'
|
|
import {FieldDefinition, FieldType, ResourceAction, ResourceConfiguration} from '../../../cobalt'
|
|
|
|
const parser = require('any-date-parser')
|
|
|
|
@Injectable()
|
|
export class ResourceAPI extends Controller {
|
|
@Inject()
|
|
protected readonly config!: Config
|
|
|
|
@Inject()
|
|
protected readonly db!: DatabaseService
|
|
|
|
public configure(key: string) {
|
|
const config = this.getResourceConfig(key)
|
|
if ( config ) {
|
|
return api.one(config)
|
|
}
|
|
|
|
throw new HTTPError(HTTPStatus.NOT_FOUND)
|
|
}
|
|
|
|
public async read(key: string) {
|
|
const config = this.getResourceConfigOrFail(key)
|
|
this.checkAction(config, ResourceAction.read)
|
|
|
|
const result = await this.make<Builder>(Builder)
|
|
.select(config.primaryKey, ...config.fields.map(x => x.key))
|
|
.from(config.collection)
|
|
.connection(this.db.get())
|
|
.get()
|
|
.all()
|
|
|
|
return api.many(result)
|
|
}
|
|
|
|
public async readOne(key: string, id: number|string) {
|
|
const config = this.getResourceConfigOrFail(key)
|
|
this.checkAction(config, ResourceAction.readOne)
|
|
|
|
const row = await this.make<Builder>(Builder)
|
|
.select(config.primaryKey, ...config.fields.map(x => x.key))
|
|
.from(config.collection)
|
|
.where(config.primaryKey, '=', id)
|
|
.limit(1)
|
|
.connection(this.db.get())
|
|
.first()
|
|
|
|
if ( !row ) {
|
|
throw new HTTPError(HTTPStatus.NOT_FOUND)
|
|
}
|
|
|
|
return api.one(row)
|
|
}
|
|
|
|
public async create(key: string, dataContainer: DataContainer) {
|
|
const config = this.getResourceConfigOrFail(key)
|
|
this.checkAction(config, ResourceAction.create)
|
|
|
|
// Load input values
|
|
const queryRow: QueryRow = {}
|
|
for ( const field of config.fields ) {
|
|
const value = dataContainer.input(field.key)
|
|
if ( field.required && typeof value === 'undefined' ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${field.key}`)
|
|
}
|
|
|
|
queryRow[field.key] = this.castValue(field, value)
|
|
}
|
|
|
|
// Create insert query
|
|
const result = await this.make<Builder>(Builder)
|
|
.table(config.collection)
|
|
.returning(config.primaryKey, ...config.fields.map(x => x.key))
|
|
.connection(this.db.get())
|
|
.insert(queryRow)
|
|
.then(x => x.rows.first())
|
|
|
|
// Return result
|
|
return api.one(result)
|
|
}
|
|
|
|
public async update(key: string, id: number|string, dataContainer: DataContainer) {
|
|
const config = this.getResourceConfigOrFail(key)
|
|
this.checkAction(config, ResourceAction.update)
|
|
|
|
// Load input values
|
|
const queryRow: QueryRow = { [config.primaryKey]: id }
|
|
for ( const field of config.fields ) {
|
|
const value = dataContainer.input(field.key)
|
|
if ( field.required && typeof value === 'undefined' ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${field.key}`)
|
|
}
|
|
|
|
queryRow[field.key] = this.castValue(field, value)
|
|
}
|
|
|
|
// Make sure the record already exists
|
|
await this.readOne(key, id)
|
|
|
|
// Create update query
|
|
const result = await this.make<Builder>(Builder)
|
|
.table(config.collection)
|
|
.returning(config.primaryKey, ...config.fields.map(x => x.key))
|
|
.connection(this.db.get())
|
|
.where(config.primaryKey, '=', id)
|
|
.update(queryRow)
|
|
.then(x => x.rows.first())
|
|
|
|
// Return the result
|
|
return api.one(result)
|
|
}
|
|
|
|
public async delete(key: string, id: number|string) {
|
|
const config = this.getResourceConfigOrFail(key)
|
|
this.checkAction(config, ResourceAction.delete)
|
|
|
|
// Make sure the row exists
|
|
await this.readOne(key, id)
|
|
|
|
// Execute the query
|
|
await this.make<Builder>(Builder)
|
|
.table(config.collection)
|
|
.connection(this.db.get())
|
|
.where(config.primaryKey, '=', id)
|
|
.delete()
|
|
|
|
return { success: true }
|
|
}
|
|
|
|
private checkAction(config: ResourceConfiguration, action: ResourceAction) {
|
|
if ( !config.supportedActions.includes(action) ) {
|
|
throw new HTTPError(HTTPStatus.METHOD_NOT_ALLOWED, `Unsupported action: ${action}`)
|
|
}
|
|
}
|
|
|
|
private castValue(fieldDef: FieldDefinition, value: any): any {
|
|
const { type, key, required } = fieldDef
|
|
if ( type === FieldType.text || type === FieldType.textarea || type === FieldType.html ) {
|
|
const cast = String(value || '')
|
|
if ( required && !cast ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
|
|
}
|
|
return cast
|
|
} else if ( type === FieldType.number ) {
|
|
const cast = parseFloat(String(value))
|
|
if ( required && !value && value !== 0 ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
|
|
}
|
|
|
|
if ( isNaN(cast) ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Field must be a number: ${key}`)
|
|
}
|
|
return cast
|
|
} else if ( type === FieldType.integer ) {
|
|
const cast = parseInt(String(value))
|
|
if ( required && !value && value !== 0 ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
|
|
}
|
|
|
|
if ( isNaN(cast) ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Field must be a number: ${key}`)
|
|
}
|
|
return cast
|
|
} else if ( type === FieldType.date ) {
|
|
if ( required && !value ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
|
|
} else if ( !value ) {
|
|
return
|
|
}
|
|
|
|
return parser.fromAny(value)
|
|
} else if ( type === FieldType.email ) {
|
|
const cast = String(value || '')
|
|
if ( required && !cast ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Missing required field: ${key}`)
|
|
}
|
|
|
|
const rex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
|
|
if ( !rex.test(cast) ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Invalid e-mail address format: ${key}`)
|
|
}
|
|
return cast
|
|
} else if ( type === FieldType.select && hasOwnProperty(fieldDef, 'options') ) {
|
|
const options = collect(fieldDef.options)
|
|
if ( options.pluck('value').includes(value) ) {
|
|
return value
|
|
}
|
|
|
|
if ( required ) {
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST, `Invalid or missing value for select: ${key}`)
|
|
}
|
|
} else if ( type === FieldType.bool ) {
|
|
return !!value
|
|
}
|
|
}
|
|
|
|
protected getResourceConfigOrFail(key: string): ResourceConfiguration {
|
|
const config = this.getResourceConfig(key)
|
|
if ( !config ) {
|
|
throw new HTTPError(HTTPStatus.NOT_FOUND)
|
|
}
|
|
return config
|
|
}
|
|
|
|
protected getResourceConfig(key: string): Maybe<ResourceConfiguration> {
|
|
const configs = this.config.get('cobalt.resources') as ResourceConfiguration[]
|
|
for ( const config of configs ) {
|
|
if ( config.key === key ) {
|
|
return config
|
|
}
|
|
}
|
|
}
|
|
}
|