|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
import {
|
|
|
|
|
api,
|
|
|
|
|
api, ArrayIterable, AsyncCollection, AsyncPipe, Awaitable,
|
|
|
|
|
Builder,
|
|
|
|
|
collect,
|
|
|
|
|
collect, Collection,
|
|
|
|
|
Config,
|
|
|
|
|
Controller,
|
|
|
|
|
DatabaseService,
|
|
|
|
@ -10,14 +10,64 @@ import {
|
|
|
|
|
HTTPError,
|
|
|
|
|
HTTPStatus,
|
|
|
|
|
Inject,
|
|
|
|
|
Injectable,
|
|
|
|
|
Injectable, Iterable,
|
|
|
|
|
Maybe,
|
|
|
|
|
QueryRow,
|
|
|
|
|
} from '@extollo/lib'
|
|
|
|
|
import {FieldDefinition, FieldType, ResourceAction, ResourceConfiguration, SelectOptions} from '../../../cobalt'
|
|
|
|
|
import {
|
|
|
|
|
DataSource,
|
|
|
|
|
FieldDefinition,
|
|
|
|
|
FieldType,
|
|
|
|
|
ResourceAction,
|
|
|
|
|
ResourceConfiguration,
|
|
|
|
|
SelectOptions,
|
|
|
|
|
} from '../../../cobalt'
|
|
|
|
|
|
|
|
|
|
const parser = require('any-date-parser')
|
|
|
|
|
|
|
|
|
|
class AwaitableIterable<T> extends Iterable<T> {
|
|
|
|
|
public static lift<TInner>(source: Awaitable<Iterable<TInner>>): AwaitableIterable<TInner> {
|
|
|
|
|
return new AwaitableIterable<TInner>(source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resolved?: Iterable<T>
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
protected source: Awaitable<Iterable<T>>
|
|
|
|
|
) {
|
|
|
|
|
super()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
at(i: number): Promise<Maybe<T>> {
|
|
|
|
|
return this.resolve()
|
|
|
|
|
.then(x => x.at(i))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clone(): Iterable<T> {
|
|
|
|
|
return AwaitableIterable.lift(
|
|
|
|
|
this.resolve()
|
|
|
|
|
.then(x => x.clone())
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
range(start: number, end: number): Promise<Collection<T>> {
|
|
|
|
|
return this.resolve()
|
|
|
|
|
.then(x => x.range(start, end))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
count(): Promise<number> {
|
|
|
|
|
return this.resolve()
|
|
|
|
|
.then(x => x.count())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async resolve(): Promise<Iterable<T>> {
|
|
|
|
|
if ( !this.resolved ) {
|
|
|
|
|
this.resolved = await this.source
|
|
|
|
|
}
|
|
|
|
|
return this.resolved
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ResourceAPI extends Controller {
|
|
|
|
|
@Inject()
|
|
|
|
@ -26,8 +76,8 @@ export class ResourceAPI extends Controller {
|
|
|
|
|
@Inject()
|
|
|
|
|
protected readonly db!: DatabaseService
|
|
|
|
|
|
|
|
|
|
public configure(key: string) {
|
|
|
|
|
const config = this.getResourceConfig(key)
|
|
|
|
|
public async configure(key: string) {
|
|
|
|
|
const config = await this.getResourceConfig(key)
|
|
|
|
|
if ( config ) {
|
|
|
|
|
return api.one(config)
|
|
|
|
|
}
|
|
|
|
@ -36,54 +86,73 @@ export class ResourceAPI extends Controller {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async read(key: string) {
|
|
|
|
|
const config = this.getResourceConfigOrFail(key)
|
|
|
|
|
const config = await this.getResourceConfigOrFail(key)
|
|
|
|
|
this.checkAction(config, ResourceAction.read)
|
|
|
|
|
|
|
|
|
|
let result = await this.make<Builder>(Builder)
|
|
|
|
|
.select(config.primaryKey, ...config.fields.filter(x => x.queryable !== false).map(x => x.key))
|
|
|
|
|
.from(config.collection)
|
|
|
|
|
.limit(500)
|
|
|
|
|
.orderBy(config.orderField || config.primaryKey, config.orderDirection || 'asc')
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.get()
|
|
|
|
|
.all()
|
|
|
|
|
let result: QueryRow[]
|
|
|
|
|
if ( hasOwnProperty(config.source, 'controller') ) {
|
|
|
|
|
const controller = config.source.controller.apply(this.request)
|
|
|
|
|
result = await controller.read()
|
|
|
|
|
} else if ( hasOwnProperty(config.source, 'method') ) {
|
|
|
|
|
const method = config.source.method.apply(this.request)
|
|
|
|
|
result = await method()
|
|
|
|
|
} else {
|
|
|
|
|
result = await this.make<Builder>(Builder)
|
|
|
|
|
.select(config.primaryKey, ...config.fields.filter(x => x.queryable !== false).map(x => x.key))
|
|
|
|
|
.from(config.source.collection)
|
|
|
|
|
.limit(500)
|
|
|
|
|
.orderBy(config.orderField || config.primaryKey, config.orderDirection || 'asc')
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.get()
|
|
|
|
|
.all()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( config.process?.afterRead ) {
|
|
|
|
|
result = result.map(config.process.afterRead)
|
|
|
|
|
if ( config.processAfterRead ) {
|
|
|
|
|
result = await Promise.all(result.map(config.processAfterRead))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return api.many(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async readOne(key: string, id: number|string) {
|
|
|
|
|
const config = this.getResourceConfigOrFail(key)
|
|
|
|
|
const config = await this.getResourceConfigOrFail(key)
|
|
|
|
|
this.checkAction(config, ResourceAction.readOne)
|
|
|
|
|
|
|
|
|
|
let row = await this.make<Builder>(Builder)
|
|
|
|
|
.select(config.primaryKey, ...config.fields.filter(x => x.queryable !== false).map(x => x.key))
|
|
|
|
|
.from(config.collection)
|
|
|
|
|
.where(config.primaryKey, '=', id)
|
|
|
|
|
.limit(1)
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.first()
|
|
|
|
|
let row: Maybe<QueryRow>
|
|
|
|
|
if ( hasOwnProperty(config.source, 'controller') ) {
|
|
|
|
|
const controller = config.source.controller.apply(this.request)
|
|
|
|
|
row = await controller.readOne(id)
|
|
|
|
|
} else if ( hasOwnProperty(config.source, 'method') ) {
|
|
|
|
|
const method = config.source.method.apply(this.request)
|
|
|
|
|
row = collect(await method())
|
|
|
|
|
.firstWhere(config.primaryKey, '=', key)
|
|
|
|
|
} else {
|
|
|
|
|
row = await this.make<Builder>(Builder)
|
|
|
|
|
.select(config.primaryKey, ...config.fields.filter(x => x.queryable !== false).map(x => x.key))
|
|
|
|
|
.from(config.source.collection)
|
|
|
|
|
.where(config.primaryKey, '=', id)
|
|
|
|
|
.limit(1)
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.first()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !row ) {
|
|
|
|
|
throw new HTTPError(HTTPStatus.NOT_FOUND)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( config.process?.afterRead ) {
|
|
|
|
|
row = config.process.afterRead(row)
|
|
|
|
|
if ( config.processAfterRead ) {
|
|
|
|
|
row = await config.processAfterRead(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return api.one(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async create(key: string, dataContainer: DataContainer) {
|
|
|
|
|
const config = this.getResourceConfigOrFail(key)
|
|
|
|
|
const config = await this.getResourceConfigOrFail(key)
|
|
|
|
|
this.checkAction(config, ResourceAction.create)
|
|
|
|
|
|
|
|
|
|
// Load input values
|
|
|
|
|
const queryRow: QueryRow = {}
|
|
|
|
|
let queryRow: QueryRow = {}
|
|
|
|
|
for ( const field of config.fields ) {
|
|
|
|
|
const value = dataContainer.input(field.key)
|
|
|
|
|
if ( field.required && typeof value === 'undefined' ) {
|
|
|
|
@ -97,20 +166,32 @@ export class ResourceAPI extends Controller {
|
|
|
|
|
queryRow[config.primaryKey] = config.generateKeyOnInsert()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( config.processBeforeInsert ) {
|
|
|
|
|
queryRow = await config.processBeforeInsert(queryRow)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
let result: Maybe<QueryRow>
|
|
|
|
|
if ( hasOwnProperty(config.source, 'controller') ) {
|
|
|
|
|
const controller = config.source.controller.apply(this.request)
|
|
|
|
|
result = await controller.insert(queryRow)
|
|
|
|
|
} else if ( hasOwnProperty(config.source, 'method') ) {
|
|
|
|
|
throw new Error('The "method" source type does not support creating records.')
|
|
|
|
|
} else {
|
|
|
|
|
result = await this.make<Builder>(Builder)
|
|
|
|
|
.table(config.source.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)
|
|
|
|
|
const config = await this.getResourceConfigOrFail(key)
|
|
|
|
|
this.checkAction(config, ResourceAction.update)
|
|
|
|
|
|
|
|
|
|
// Load input values
|
|
|
|
@ -128,31 +209,46 @@ export class ResourceAPI extends Controller {
|
|
|
|
|
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())
|
|
|
|
|
let result: Maybe<QueryRow>
|
|
|
|
|
if ( hasOwnProperty(config.source, 'controller') ) {
|
|
|
|
|
const controller = config.source.controller.apply(this.request)
|
|
|
|
|
result = await controller.update(id, queryRow)
|
|
|
|
|
} else if ( hasOwnProperty(config.source, 'method') ) {
|
|
|
|
|
throw new Error('The "method" source type does not support updating records.')
|
|
|
|
|
} else {
|
|
|
|
|
result = await this.make<Builder>(Builder)
|
|
|
|
|
.table(config.source.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)
|
|
|
|
|
const config = await 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()
|
|
|
|
|
if ( hasOwnProperty(config.source, 'controller') ) {
|
|
|
|
|
const controller = config.source.controller.apply(this.request)
|
|
|
|
|
await controller.delete(id)
|
|
|
|
|
} else if ( hasOwnProperty(config.source, 'method') ) {
|
|
|
|
|
throw new Error('The "method" source type does not support deleting records.')
|
|
|
|
|
} else {
|
|
|
|
|
await this.make<Builder>(Builder)
|
|
|
|
|
.table(config.source.collection)
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.where(config.primaryKey, '=', id)
|
|
|
|
|
.delete()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
}
|
|
|
|
@ -224,20 +320,76 @@ export class ResourceAPI extends Controller {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected getResourceConfigOrFail(key: string): ResourceConfiguration {
|
|
|
|
|
const config = this.getResourceConfig(key)
|
|
|
|
|
protected async getResourceConfigOrFail(key: string): Promise<ResourceConfiguration> {
|
|
|
|
|
const config = await this.getResourceConfig(key)
|
|
|
|
|
if ( !config ) {
|
|
|
|
|
throw new HTTPError(HTTPStatus.NOT_FOUND)
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected getResourceConfig(key: string): Maybe<ResourceConfiguration> {
|
|
|
|
|
protected async getResourceConfig(key: string): Promise<Maybe<ResourceConfiguration>> {
|
|
|
|
|
const configs = this.config.get('cobalt.resources') as ResourceConfiguration[]
|
|
|
|
|
for ( const config of configs ) {
|
|
|
|
|
if ( config.key === key ) {
|
|
|
|
|
return config
|
|
|
|
|
let config: Maybe<ResourceConfiguration> = undefined
|
|
|
|
|
for ( const match of configs ) {
|
|
|
|
|
if ( match.key === key ) {
|
|
|
|
|
config = match
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !config ) {
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve `select` fields with remote sources
|
|
|
|
|
config.fields = await collect(config.fields)
|
|
|
|
|
.map(async field => {
|
|
|
|
|
if ( field.type === FieldType.select && hasOwnProperty(field, 'source') ) {
|
|
|
|
|
return {
|
|
|
|
|
...field,
|
|
|
|
|
options: await this.readSource(field.source)
|
|
|
|
|
.map(qr => ({
|
|
|
|
|
display: qr[field.displayFrom],
|
|
|
|
|
value: qr[field.valueFrom],
|
|
|
|
|
}))
|
|
|
|
|
.then(c => c.all())
|
|
|
|
|
} as FieldDefinition
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return field
|
|
|
|
|
})
|
|
|
|
|
.awaitAll()
|
|
|
|
|
.then(x => x.all())
|
|
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected readSource(source: DataSource): AsyncCollection<QueryRow> {
|
|
|
|
|
let iter: Iterable<QueryRow>
|
|
|
|
|
|
|
|
|
|
if ( hasOwnProperty(source, 'controller') ) {
|
|
|
|
|
iter = AwaitableIterable.lift(
|
|
|
|
|
AsyncPipe.wrap(source.controller.apply(this.request))
|
|
|
|
|
.tap(controller => controller.read())
|
|
|
|
|
.tap(rows => new ArrayIterable(rows))
|
|
|
|
|
.resolve()
|
|
|
|
|
)
|
|
|
|
|
} else if ( hasOwnProperty(source, 'method') ) {
|
|
|
|
|
iter = AwaitableIterable.lift(
|
|
|
|
|
AsyncPipe.wrap(source.method.apply(this.request))
|
|
|
|
|
.tap(method => method())
|
|
|
|
|
.tap(rows => new ArrayIterable(rows))
|
|
|
|
|
.resolve()
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
iter = this.make<Builder>(Builder)
|
|
|
|
|
.from(source.collection)
|
|
|
|
|
.limit(500)
|
|
|
|
|
.connection(this.db.get())
|
|
|
|
|
.getResultIterable()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new AsyncCollection(iter)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|